LeetCode 02 - 数组类型题目

1. 两数之和#

给定一个数组和一个目标值,返回数组中和为目标值的两数的索引。

分析:使用一个哈希表在遍历数组的同时记录已访问的数,同时在访问每个数时检查哈希表中是否存在 target-nums[i] ,存在则将当前数和表中的数的索引返回。这样只需要一次遍历,时间和空间复杂度都为 O(N)

int[] twoSum(int[] nums, int target) {
    // 键为数值,值为索引
    HashMap<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        int complement = target - nums[i];
        if (map.containsKey(complement)) {
            return new int[] {map.get(complement), i};
        }
    }
    return new int[0];
}

167. 两数之和 II - 输入有序数组#

给定有序数组和一个目标值,返回数组中和为目标值的两数的索引。

分析:因为数组有序,所以可以使用二分法,也可以使用双指针法,当然,上面的借助哈希表的方法同样很好。

二分查找:首先固定一个数 num,然后用二分查找寻找另一个数 target-num

// 二分法
int[] twoSum(int[] nums, int target) {
    for(int i = 0; i < nums.length; i++) {
        // 固定 nums[i],寻找 target-nums[i]
        int left = i+1, right = nums.length-1;
        int finding = target-nums[i];
        while(left <= right) {
            int mid = left + (right - left)/2;
            if(nums[mid] == finding)
                return new int[] {i, mid};
            else if(nums[mid] < finding)
                left = mid+1;
            else
                right = mid-1;
        }
    }
    return new int[0];
}

双指针:用两个指针分别指向第一个和最后一个数,如果两数之和大于目标值,则右指针左移,反之左指针右移,直到两数之和刚好等于目标值。但这样缩小查找范围会不会错过可能的解?答案是不会。假如两数分别在 i,j ,那么在找到答案之前,左指针肯定在 [0,i] 范围内,右指针肯定在 [j,len-1] 范围内,且除非 [0, len-1] 就是答案,否则一定是左指针先到 i 或者是右指针先到 j ,而且一个指针先到之后,它就不会再移动了,只有另一个指针不停移动直到找到答案。

int[] twoSum(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while(left < right) {
        if (nums[left] + nums[right] == target)
            return new int[] {left, right};
        else if (nums[left] + nums[right] < target)
            left++;
        else
            right--;
    }
    return new int[0];
}

454. 四数相加 II#

给你四个整数数组 nums1nums2nums3nums4,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

首先遍历数组 nums1, nums2 的每一个两两元素的组合,将其和存入一个哈希表,然后遍历 nums3, nums4 两两元素的组合,查看其和的相反数是否存在于哈希表中,如果存在则统计其出现次数,这个次数就是符合条件的四元组的个数。(将问题变成了两数相加问题)

public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
    HashMap<Integer, Integer> map = new HashMap<>();
    int result = 0;
    for (int num1 : nums1) {
        for (int num2 : nums2) {
            map.put(num1 + num2, map.getOrDefault(num1 + num2, 0) + 1);
        }
    }
    for (int num3 : nums3) {
        for (int num4 : nums4) {
            if (map.containsKey(-(num3 + num4)))
                result += map.get(-(num3 + num4));
        }
    }
    return result;
}

15. 三数之和#

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a+b+c=0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

方法:双指针

首先将数组排序,然后从头开始遍历,对于当前元素 nums[i] ,在右侧使用双指针寻找两个和为 -nums[i] 的元素(两数之和)。

因为元素可能重复出现,且答案中不能包含重复的三元组,所以:

  • 在遍历时对于重复出现元素只在第一次出现时进行处理,后面直接跳过;
  • 然后再在后续子数组中解决两数之和问题,找出所有符合条件的二元组;
  • 用双指针找出两个符合条件的值之后,如果这两个数也重复出现,则让左指针移动到第一个数最后一次出现的位置,右指针移动到第二个数第一次出现的位置。然后继续搜索符合条件的二元组。
public List<List<Integer>> threeSum(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    Arrays.sort(nums);
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] > 0) return result;
                // 重复元素只处理第一个
        if (i > 0 && nums[i] == nums[i-1])
            continue;
        int cur = nums[i];
        int target = -cur;
        int left = i + 1, right = nums.length - 1;
        while (left < right) {
            int sum = nums[left] + nums[right];
            if (sum == target) {
                result.add(Arrays.asList(cur, nums[left], nums[right]));
                                // 如果 left、right 是一个答案,则跳过重复部分
                while (left < right && nums[left] == nums[left+1]) left++;
                while (left < right && nums[right] == nums[right-1]) right--;
                  // 别忘了移动指针,开始寻找不同于 left、right 的答案
                                left++;
                right--;
            } else if (sum < target) {
                left++;
            } else {
                right--;
            }
        }
    }
    return result;
}

18. 四数之和#

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

思路和三数之和一样,只是在三数之和上又套了一层循环。

注意几点不同:

  • 不能根据 nums[i] > target 来提前返回,因为 target 可能是负数,元素也可能是负数。可以用 nums[i] > target && (nums[i] > 0 || target > 0) 替代,作为剪枝条件。
public List<List<Integer>> fourSum(int[] nums, int target) {
    List<List<Integer>> result = new ArrayList<>();
    int n = nums.length;
    Arrays.sort(nums);
    for (int i = 0; i < n; i++) {
        if (i > 0 && nums[i] == nums[i-1]) continue;
                // 三数之和
        for (int j = i +1; j < n; j++) {
            if (j > i + 1 && nums[j] == nums[j-1]) continue;
            int left = j +1, right = n - 1;
            while (left < right) {
                long sum = (long)nums[i] + nums[j] + nums[left] + nums[right];
                if (sum < target) left++;
                else if (sum > target) right--;
                else {
                    result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                    while (left < right && nums[left] == nums[left+1]) left++;
                    while (left < right && nums[right] == nums[right-1]) right--;
                    left++;
                    right--;
                }
            }
        }
    }
    return result;
}

53. 最大子数组和#

找出数组中和最大的连续子数组。

分析:可以用动态规划来做。**dp[i] 定义为以第 i 个数结尾的所有子数组中和最大的那个子数组的和**。那么状态转移为:dp[i] = max(nums[i], dp[i-1]+nums[i])

int maxSubArray(int[] nums) {
    int len = nums.length;
    int[] dp = new int[len];
    dp[0] = nums[0];
    int maxSum = nums[0];
    for (int i = 1; i < len; i++) {
        dp[i] = Math.max(dp[i-1]+nums[i], nums[i]);
        maxSum = Math.max(maxSum, dp[i]);
    }
    return maxSum;
}

因为每次状态转移只需要知道前一个 dp[i-1] ,所以不需要使用整个数组,只用两个变量就够了,这样可以将空间复杂度优化到常数级别。

int maxSubArray(int[] nums) {
    int len = nums.length;
    int pre = nums[0], maxSum = nums[0];
    for (int num : nums) {
        pre = Math.max(pre + num, num);
        maxSum = Math.max(pre, maxSum);
    }
    return maxSum;
}

189. 轮转数组#

将数组整体向右轮转,最后面移出的元素会转到开头,返回轮转后的结果。

分析:先将整个数组翻转,然后依次将前 k 个和剩余子数组翻转,即可得到结果。

// 原始:"----->-->"
// 整体翻转:"<--<-----"
// 部分翻转:"-->----->"
int[] rotateArray(int[] nums, int k) {
    // 注意将 k 取余
    k = k % nums.length;
    reverse(nums, 0, nums.length-1);
    reverse(nums, 0, k-1);
    reverse(nums, k, nums.length-1);
}

void reverse(int[] nums, int start, int end) {
    while(start < end) {
        int temp = nums[start];
        nums[start] = nums[end];
        nums[end] = temp;
        start++;
        end--;
    }
}

977. 有序数组的平方#

给定有序数组(有正有负),将每个数求平方后返回平方值的有序数组。

分析:直接求平方后进行排序(Arrays.sort()),时间复杂度会是 O(nlogn)。更好的方法是使用 双指针:首先找到正负数分界,然后用两个指针分别指向最后一个负数和第一个非负数,然后执行类似于链表合并的操作,每次迭代中将较小的平方值放入结果数组,然后移动对应指针。

int[] sortedSquares(int[] nums) {
    int len = nums.length;
    int[] result = new int[len];

    int i = 0;
    for (; i < len && nums[i] < 0; i++);
    // i, j 分别指向最后一个负数、第一个非负数
    int j = i;
    i--;
    
    int resultIndex = 0;
    while(i >= 0 && j <= len-1) {
        int negSquare = nums[i] * nums[i];
        int posSquare = nums[j] * nums[j];
        if (negSquare < posSquare) {
            result[resultIndex++] = negSquare;
            i--;
        } else {
            result[resultIndex++] = posSquare;
            j++;
        }
    }
    // 循环结束时,j, j 至多有一个没到头
    if (i >= 0)
        for (; i >= 0; i--)
            result[resultIndex++] = nums[i] * nums[i];
    if (j <= len-1)
        for (; j <= len-1; j++)
            result[resultIndex++] = nums[j] * nums[j];
    
    return result;
}

另外,还可以将两个指针分别指向 0n-1 ,每次选择较大的平方值逆序放入结果数组,同时移动对应指针。这种方法无需处理边界情况。

int[] sortedSquares(int[] nums) {
    int len = nums.length;
    int[] result = new int[len];
    int resultIndex = len-1;
    for (int i = 0, j = len-1; i <= j;) {
        int negSquare = nums[i] * nums[i];
        int posSquare = nums[j] * nums[j];
        if(negSquare > posSquare) {
            result[resultIndex--] = negSquare;
            i++;
        } else {
            result[resultIndex--] = posSquare;
            j--;
        }
    }
    return result;
}

使用双指针方法的时间复杂度只有 O(N)

169. 多数元素#

找出给定数组中的 majority 元素,即个数大于 nums.length/2 的元素。

分析:除了简单地利用哈希表在遍历数组的同时统计各个数出现的次数,还可以使用 Boyer-Moore 投票算法。

基本思想是,把 majority 元素记为 +1,把其他元素记为 -1,则所有元素的和会大于 0(因为题目保证一定存在 majority 元素)。具体算法过程为:

  • 维护一个候选元素 candidate 及其出现次数 count ,初始时候选元素任选,次数为 0。
  • 遍历数组,在处理每个元素 x 之前,如果 count==0 ,则将 x 作为 candidate ,这时候,
    • 如果 x 和原来的 candidate 相同,则 count++ (变成 1)。
    • 否则, count-- (变成 -1)。
  • 遍历结束后, candidate 即为整个数组的 majority 元素。

为什么这么做是对的呢?以 candidate 变成 0 的位置为界,前半部分数组中,majority 元素的个数 <= 其他数的总数,所以后半部分数组中,majority 元素的个数一定 > 其他所有数的总数,也就是说 majority 元素也是后半部分子数组的 majority 元素。总的来说, candidate=0 将数组分成若干段,每个 candidate=0 右边的子数组的 majority 元素一定是整个数组的 majority 元素。

int majority(int[] nums) {
    int candidate = -1, count = 0;
    for(int num : nums) {
        // 在分界点换人
        if(count == 0) candidate = num;
        if(num == candidate) count++;
        else count--;
    }
    return candidate;
}

338. 比特位计数#

计算 1~n 中每个数字的二进制表示的 1 的个数。

方法一:Brian Kernighan 算法

对于任意整数 x,令 x=x&(x1),该运算将 x 的二进制表示的最后一个 1 变成 0。因此,对 x 重复该操作,直到 x 变成 0,则操作次数即为 x 的「一比特数」。

image

计算每个数的「一比特数」时间复杂度为 O(logn),所以总的时间复杂度为 O(nlogn)

int countOnes(int num) {
    int count = 0;
    while(num > 0) {
        num = num & (num-1);
        count++;
    }
    return count;
}

方法二:动态规划——最高有效位

对于正整数 x,它的最高有效位是这样的正整数 yy 是 2 的整数幂,且最接近 xy 的二进制表示只有最高位是 1,所以 xy 的「一比特数」刚好比 x 的「一比特数」少 1。利用这个性质,可以使用动态规划, dp[x]=dp[xy]+1.

另外, y 是 2 的整数次幂 y&(y1)=0.

int[] countBits(int n) {
    int[] dp = new int[n+1];
    // 遍历过程中不断更新当前最高有效位
    int highBit = 0;
    for(int i = 1; i <= n; i++) {
        if((i & (i-1)) == 0) highBit = i; // & 的优先级低于 ==
        dp[i] = dp[i - highBit] + 1;
    }
    return dp;
}

448. 找到所有数组中消失的数字#

给定长度为 n 的数组,其中的数字在 1-n 范围内,但可能有一些该范围内的数字缺失,找出这些缺失的数字。

分析:当然可以用一个 Set 来记录出现的数字,然后再遍历一遍 [1,n] 来找出缺失的数字。但还可以不借助额外的空间,将空间复杂度优化到 O(1)

具体来说,遍历到数字 nums[i] 时,将 nums[nums[i]-1] 的值增加 n ,最后再遍历一遍 nums ,如果 nums[i]-1 处的数字大于 n,则说明 nums[i] 这个数字出现过。(用 x-1 这个位置来记录数字 x 是否出现过)因为数字 nums[i] 可能加了 n,所以每遇到一个数字,都对 n 取模来获取原本的数字。

List<Integer> findDisappearedNumbers(int[] nums) {
    int n = nums.length;
    List<Integer> result = new ArrayList<>();
    for(int num : nums) {
        int x = (num-1) % n;
        nums[x] += n;
    }
    for(int i = 0; i < n; i++) {
        if(nums[i] <= n) // nums[i] 表明数字 i+1 是否出现过 
            result.add(i+1);
    }
}

136. 只出现一次的数字#

数组中只有一个数出现一次,其他数都出现两次,找出这个出现一次的数。

分析:异或运算 有如下性质:

  • a0=a
  • aa=0
  • 异或运算满足交换律和结合律

所以数组中除了目标值,剩下的所有数做异或运算,结果为 0,再和目标值做异或运算即可得到目标值。

int singleNumber(int[] nums) {
  int single = 0;
  for (int num : nums) {
      single ^= num;
  }
  return single;
}

55. 跳跃游戏#

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。

方法:贪心

对于数组中的任意一个位置 y,我们如何判断它是否可以到达?根据题目的描述,只要存在一个位置 x,它本身可以到达,并且它跳跃的最大长度为 x+nums[x],这个值大于等于 y,即 x+nums[x]y,那么位置 y 也可以到达。

这样以来,我们依次遍历数组中的每一个位置,并实时维护 最远可以到达的位置。对于当前遍历到的位置 x如果它在最远可以到达的位置的范围内,那么我们就可以从起点通过若干次跳跃到达该位置,因此我们可以用 x+nums[x] 更新 最远可以到达的位置

boolean canJumpToEnd(int[] nums) {
    int len = nums.length;
  int rightmost = 0;
  for(int i = 0; i < len; i++) {
    if(i <= rightmost) {
      rightmost = Math.max(rightmost, i+nums[i]);
      if(rightmost >= len - 1)
        return true;
    }
  }
  return false;
}

121. 买卖股票的最佳时机#

给定一个数组,元素 i 表示第 i 天的股票价格。你可以在某一天买入,在以后的某一天卖出,求出最大的获利。

分析:实际上就是找出两数的最大差值。可以用动态规划方法来解。dp[i] 定义为在第 i 天卖出的最大获利,则状态转移为:

dp[i]=max{dp[i1]+(nums[i]nums[i1]),nums[i]nums[i1]}

int maxProfit(int[] nums) {
    int len = nums.length;
    int[] dp = new int[len];
    dp[0] = 0; // base case
    for(int i = 1; i < len; i++) { // 状态转移
        int minus = nums[i] - nums[i-1];
        dp[i] = Math.max(dp[i-1] + minus, minus);
    }
    int maxNum = 0;
    for(int num: dp)
        maxNum = Math.max(maxNum, num);
    return maxNum;
}

可以进一步优化空间复杂度。用 minPrice 表示以前的最低价格,同时用 maxProfit 表示之前的最大获利,则当前的最大获利为 max(nums[i] - minPrice, maxProfit)

int maxProfit(int[] nums) {
    int minPrice = nums[0], maxProfit = 0;
    for(int i = 1; i < nums.length; i++) {
        maxProfit = Math.max(maxProfit, nums[i]-minPrice);
        if(nums[i] < minPrice)
            minPrice = nums[i];
    }
    return maxProfit;
}

986. 区间列表的交集#

给定两个二维数组,分别表示两组区间,求两者的所有交集。

image

  • 两个区间如果有交集,则交集[两者较大的起点, 两者较小的终点]
  • 两组区间中终点最小的那个区间只可能与另外一组中的一个区间相交,则算出这个相交区间后,就可以不考虑这个终点最小的区间了。
int[][] intervalIntersection(int[][] A, int[][] B) {
    List<int[]> result = new ArrayList<>();
    int i = 0, j = 0;
    while(i < A.length && j < B.length) {
        // 找到相交区间
        int low = Math.max(A[i][0], B[j][0]);
        int high = Math.min(A[i][1], B[j][1]);
        if(low <= high) result.add(new int[] {low, high});
        // 移动指针
        if(A[i][1] < B[j][1]) i++;
        else  j++;
    }
    return result.toArray(new int[result.size()][]);
}

56. 合并区间#

示例:

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
class Solution {
    public int[][] merge(int[][] intervals) {
        Arrays.sort(intervals, new Comparator<int[]>() {
            public int compare(int[] a, int[] b) {
                return a[0] - b[0];
            }
        });
        List<int[]> result = new ArrayList<>();
        for(int i = 0; i < intervals.length; i++) {
            int left = intervals[i][0], right = intervals[i][1];
            if(result.size() > 0 && result.get(result.size() - 1)[1] >= left)
                result.get(result.size() - 1)[1] = Math.max(
                    result.get(result.size() - 1)[1], right);
            else
                result.add(new int[] {left, right});
        }
        return result.toArray(new int[result.size()][]);
    }
}

287. 寻找重复数#

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。假设 nums 只有一个重复的整数 ,返回这个重复的数 。

方法一:Set

用集合一次存储数组中的元素,如果当前元素已经存在于集合中,则该元素即为答案。

方法二:负号标记

遍历数组,对每一个数 num ,将索引为 num 处的数字取相反数。如果遇到的数字 num1 所对应索引处的数字已经是负数,则说明这个数字已经出现过。(用索引 x 标记数字 x 是否存在)

int findDuplicate(int[] nums) {
    int result = -1;
    for(int i = 0; i < nums.length; i++) {
        int cur = Math.abs(nums[i]);
        if(nums[cur] < 0) {
            result = cur;
            break;
        }    
        nums[cur] *= -1;
    }
    // 还原数组
    for(int i = 0; i < nums.length; i++)
        nums[i] = Math.abs(nums[i]);

    return result;
}

方法三:判断环形链表

根据 nums 数组建图,每个位置 i 连一条指向对应位置 nums[i] 的边,因为存在重复数字 target ,所以 target 这个索引处的入度至少为 2,所以图中一定有环,且 target 就是这个环的入口。

如何找到这个入口呢?使用快慢指针法——慢指针一次一步,快指针一次两步,这样的话,两指针在有环的情况下一定会相遇,在相遇时将慢指针指向起点,然后两指针每次移动一步,再次相遇处就是答案。

根据建图规则,走一步表示为 slow = nums[slow] ,走两步表示为 fast = nums[nums[fast]]

int findDuplicate(int[] nums) {
    int slow = 0, fast = 0;
    slow = nums[slow];
    fast = nums[nums[fast]];
    while(slow != fast) {
        slow = nums[slow];
        fast = nums[nums[fast]];
    }
    slow = 0;
    while(slow != fast) {
        slow = nums[slow];
        fast = nums[fast];
    }
    return slow;
}

这个方法的空间复杂度为 O(1)

128. 最长连续序列#

给定一个无序整数数组,求出最长的连续序列长度。例如 [1,3,2,5] 的最长连续序列为 [1,2,3]

利用 HashSet 存储每个数,在遍历原数组中的每个数时,进入内层循环,寻找以这个数开始的最长连续序列长度。但是如果有连续序列 x,x+1,x+2,x+3,那么在遍历到 x 时,得到的结果一定是比以 x+1 的结果长的,所以也就不必遍历 x+1 了。也就是说,如果一个数 x 的前一个自然数 x1 存在于数组中,那么就可以跳过这个数。

int longestConsecutive(int[] nums) {
    Set<Integer> nums_set = new HashSet<>();
    for(int num : nums) nums_set.add(num);
    int result = 0;
    for(int num : nums) {
        if(nums_set.contains(num-1)) continue;
        int cur = num;
        int length = 1;
        while(nums_set.continas(cur+1)) {
            cur++;
            length++;
        }
        result = Math.max(result, length);
    }
    return result;
}

238. 除自身以外数组的乘积#

不要使用除法,且在 O(n) 时间复杂度内完成此题。

方法一:空间复杂度 O(n)

算法过程如下:

  1. 初始化两个数组 LRl[i] 表示位置 i 左侧所有数字的乘积,R[i] 表示 i 右侧所有数字的乘积。
  2. 需要两个循环来分别填充这两个数组。
    1. 对于数组 LL[0]=1, L[i]=L[i-1] * nums[i-1]
    2. 对于数组 RR[len-1]=1, R[i]=R[i+1]*nums[i+1]
  3. 当这两个数组填充完成,对应位置的「其他元素乘积」为 L[i]*R[i]
int[] productExceptSelf(int[] nums) {
    int len = nums.length;
    int[] L = new int[len];
    int[] R = new int[len];
    int[] answer = new int[len];

    L[0] = 1;
    for(int i = 1; i < len; i++)
        L[i] = nums[i-1] * L[i-1];
    
    R[len-1] = 1;
    for(int i = len-2; i >= 0; i--)
        R[i] = nums[i+1] * R[i+1];
    
    for(int i = 0; i < len; i++)
        answer[i] = L[i] * R[i];
    
    return answer;
}

方法二:空间复杂度 O(1)

这个方法是对方法一的优化,我们不再显示构造 L,R 数组,而是先用 answer 数组记录 L 数组的内容,然后在从后往前的一遍循环中动态更新 R 的值(这里的 R 只是一个整数),同时据此计算 answer 数组。

int[] productExceptSelf(int[] nums) {
    int len = nums.length;
    int[] answer = new int[len];
    answer[0] = 1;
    for(int i = 1; i < len; i++)
        answer[i] = answer[i-1] * nums[i-1];
    int R = 1;
    for(int i = len-1; i >= 0; i--) {
        answer[i] = answer[i] * R;
        R = R * nums[i];
    }
    return answer;
}

406. 根据身高重建队列#

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高 大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人在这个队伍中的 真实属性,即第 j 个人前面真的刚好有 kj 个身高大于等于他的人。

方法一:从低到高考虑

将所有人按照身高从低到高排序,排序后身高依次为 h0,h1,,hn1,对应的 k 依次为 k0,,kn1,如果按照这个先后次序,依次将每个人放入最终的队列中,那么放入第 i 个人时:

  • 0,,i1 个人已经安排好了位置,且 他们的位置对第 i 个人没有影响,因为他们都比第 i 个人矮;(所谓没有影响,是指在安排 i 的位置之前不需要为这些身高小于 i 的人预留空位)
  • 而第 i+1,,n1 个人还没有开始分配位置,但 一旦他们排到了第 i 个人前面,就会对他产生影响,因为他们都较高。

如果初始化一个大小为 n 的空队列,那么放入第 i 个人时,需要给他安排一个 空位置,并且 这个位置之前还恰好有 ki 个空位置,用来预留给后面更高的人。也就是说,i 个人的位置,就是从左往右数的第 ki+1 个空位置

如果存在身高相同的人该怎么办?当 hi=hj 时,如果 ki>kj,那么

  • 在为 i 安排位置时,预留的其中一个空位是给 j 的;
  • 反过来,给 j 安排位置时,不需要考虑给 i 安排空位。
  • 所以 i 先选位置。(即在身高相同的几个人之间,按照 k 降序排序)

例子

 原始序列:[[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
 排序后:[[4,4],[5,2],[5,0],[6,1],[7,1],[7,0]]
 
 第一步:[[   ],[   ],[   ],[   ],[4,4],[   ]]
 第二步:[[   ],[   ],[5,2],[   ],[4,4],[   ]]
 第三步:[[5,0],[   ],[5,2],[   ],[4,4],[   ]]
 ……
 int[][] reconstructQueue(int[][] people) {
     // 排序
     Arrays.sort(people, new Comparator<int[]>() {
         public int compare(int[] p1, int[] p2) {
             if(p1[0] != p2[0]) {
                 return p1[0] - p2[0]; // 先按身高升序
             } else {
                 return p2[1] - p1[1]; // 再按k降序
             }
         }
     });
     // 为每个人找到位置
     int n = people.length;
     int[][] result = new int[n][]; // 默认初始化 result[i] 为 null
     for(int[] p : people) {
         // 将 p 放到 result 中第 spaces 个空位置上
                 //(空位置是给更高的人预留的)
         int spaces = p[1] + 1;
         for(int i = 0; i < n; i++) {
             // 遇到空位置
             if(result[i] == null) {
                 spaces--;
                 // 找到第 spaces 个空位置
                 if(spaces == 0) {
                     result[i] = p;
                     break;
                 }
             }
         }
     }
     return result;
 }

方法二:从高到低考虑

将所有人按照身高从高到低排序,对于排序后序列中的任意一个人,因为后面的人身高较低,其位置靠前靠后不会对当前这个(较高的)人产生影响,所以直接将这个人插入结果队列,保证他的前面恰好有 ki 个人即可。

如果两个人身高相同怎么办?为了插入时按照先后位置顺序插入,按照 k 从小到大排序。

 int[][] reconstructQueue(int[][] people) {
     // 排序
     Arrays.sort(people,new Comparator<int[]>() {
         public int compare(int[] p1, int[] p2) {
             if (p1[0] != p2[0]) {
                 return p2[0] - p1[0];
             } else {
                 return p1[1] - p2[1];
             }
         }
     });
     List<int[]> result = new ArrayList<>();
     for(int[] p : people) {
         // 将p插入到位置 p[1],如果按 k 从大到小排列
         // 在这里就会出现问题,例如上例中,会先插入 [7,1]
         // 到位置 1,此时 list 是空的,会出现越界错误
         result.add(p[1], p);
     }
     return result.toArray(new int[result.size()][]);
 }

581. 最短无序连续子数组#

给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。请你找出符合题意的 最短 子数组,并输出它的长度。

可以将整个数组分成三段,前后两段各自有序,中间段就是问题的答案,它虽然无序,但是处于 minmax 之间。

image

可以发现:

  • 在右段中,每个元素都比左边的最大元素更大;
  • 在左段中,每个元素都比右边的最小元素更小。

那么就可以利用这两个特点来找到中段的左右端点:

  • 从左往右遍历,如果位置 i 的元素比它左边部分的最大值要小(它左边有更大的元素),说明这个位置需要重新排序,更新 right。这样遍历结束后 right 会指向右段左端点前一个位置,这也就是中段的右端点。
  • 从右往左遍历,如果位置 i 的元素比它右边部分的最小值要大,说明这个位置需要重新排序,更新 left。这样遍历结束后 left 会指向左段右端点后一个位置,这也就是中段的左端点。
class Solution {
    public int findUnsortedSubarray(int[] nums) {
        int n = nums.length;
        int left = -1, right = -1;
        int minNum = Integer.MAX_VALUE;
        int maxNum = Integer.MIN_VALUE;
        // 寻找右端点
        for(int i = 0; i < n; i++) {
            if(nums[i] < maxNum) {
                right = i;
            } else {
                maxNum = nums[i];
            }
        }
        // 寻找左端点
        for(int i = n-1; i >= 0; i--) {
            if(nums[i] > minNum) {
                left = i;
            } else {
                minNum = nums[i];
            }
        }

        return left == right ? 0 : right - left + 1;
    }
}
posted @   李志航  阅读(114)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示
主题色彩