数据结构——数组相关题目

一、二分法

二分法可以归为两端向中心的双指针
链接

二、双指针法

leetcode 27.移除元素

leetcode 27.移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O ( 1 ) O(1) O(1) 额外空间并原地修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。

示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。

你不需要考虑数组中超出新长度后面的元素。

1.暴力解法

两层for循环,一个for循环遍历数组元素 ,找到需要移除的元素;第二个for循环更新数组,让目标元素以后的数值都向前移动一位,覆盖目标移除的元素。

时间复杂度: O ( n 2 ) O(n^2) O(n2)

class Solution {
    public int removeElement(int[] nums, int val) {
        int size = nums.length;
        for (int i=0;i<size;i++) {
            if (nums[i]==val) { // 发现需要移除的元素,就将数组集体向前移动一位
                for (int j=i;j<size-1;j++) {
                    nums[j] = nums[j+1];
                }
                i--; // 下标i以后的数值都向前移动了一位,所以i也向前移动一位
                size--;
            }
        }
        return size;
    }
}

2.双指针法(快慢指针法)

通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
双指针法(快慢指针法) 在数组和链表的操作中非常常见,经常用于考察数组、链表、字符串

定义快慢两个指针,快指针指向当前将要处理的元素,慢指针指向下一个将要赋值的位置。在遇到目标值前,快慢指针相等,共同前移遍历;遇到目标值,快指针继续自增,慢指针停留,在之后将快指针指向的值赋给慢指针指向的值,实现覆盖目标值,整个数组前移。遍历完成后,慢指针最后停留的位置即是数组末尾的位置,即数组现在的长度。nums[0…slow] 就是不重复元素。
图片来自代码随想录
图片来自代码随想录

时间复杂度: O ( n ) O(n) O(n)
在最坏情况下(输入数组中没有元素等于val),左右指针各遍历了数组一次。需要遍历该序列至多两次

class Solution {
    public int removeElement(int[] nums, int val) {
        int slowIndex = 0;
        for (int fastIndex=0;fastIndex<nums.length;fastIndex++) {
            if (nums[fastIndex] != val) {//不等于 \textit{val}val,它一定是输出数组的一个元素
                nums[slowIndex] = nums[fastIndex];//将右指针指向的元素复制到左指针位置
                slowIndex++; //将左右指针同时右移
            }
            //等于val,不能在输出数组里,此时左指针不动,右指针右移一位
        }
        return slowIndex;
    }
}

3.双指针优化

如果要移除的元素恰好在数组的开头,例如序列 [1,2,3,4,5],当 val 为1时,我们需要把每一个元素都左移一位。题目中要求要原地移除,但元素的顺序可以改变实际上我们可以直接将最后一个元素 5 移动到序列开头,取代元素 1,得到序列 [5,2,3,4],同样满足题目要求。这个优化在序列中val元素的数量较少时非常有效。

使用双指针,两个指针初始时分别位于数组的首尾,向中间移动遍历该序列

如果左指针left 指向的元素等于val,此时将右指针right 指向的元素复制到左指针left 的位置,然后右指针right 左移一位。如果赋值过来的元素恰好也等于val,可以继续把右指针right 指向的元素的值赋值过来(左指针 left 指向的等于val 的元素的位置继续被覆盖),直到左指针指向的元素的值不等于val 为止。

当左指针 left 和右指针right 重合的时候,左右指针遍历完数组中所有的元素。此时数组前半段是有效部分,存储的是不等于 val 的元素;后半段(末尾部分)是无效部分,存储的是等于 val 的元素。

这样的方法两个指针在最坏的情况下合起来只遍历了数组一次。避免了需要保留的元素的重复赋值操作。

时间复杂度: O ( n ) O(n) O(n)
只需要遍历该序列至多一次。

class Solution {
    public int removeElement(int[] nums, int val) {
        int left = 0;
        int right = nums.length;
        while (left < right) {
            if (nums[left] == val) {
                nums[left] = nums[right - 1];
                right--;
            } else {
                left++;
            }
        }
        return left;
    }
}

leetcode 26.删除有序数组中的重复项

leetcode 26.删除有序数组中的重复项

给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

示例 1:

输入:nums = [1,1,2]
输出:2, nums = [1,2]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:

输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

时间复杂度: O ( n ) O(n) O(n)

双指针法

class Solution {
    public int removeDuplicates(int[] nums) {
        int slow = 0;
        for (int fast=0;fast<nums.length;fast++) {
            if (nums[fast]!=nums[slow]) {
                slow++; //先自增slow
                nums[slow] = nums[fast]; //再赋值
            }
        }
        return slow + 1; //实际数组区间[0,slow],长度slow+1
    }
}

或单独讨论数组长度为0的情况,指针从1取起

class Solution {
    public int removeDuplicates(int[] nums) {
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        int fast = 1, slow = 1;
        while (fast < n) {
            if (nums[fast] != nums[fast - 1]) {
                nums[slow] = nums[fast]; //先赋值
                ++slow; //再自增slow
            }
            ++fast;
        }
        return slow; //[0,slow-1],长度slow
    }
}

leetcode 80.删除有序数组中的重复项 II

leetcode 80.删除有序数组中的重复项 II

1.双指针法

因为本题要求相同元素最多出现两次而非一次,所以我们需要检查上上个应该被保留的元素nums[slow−2] 是否和当前待检查元素nums[fast] 相同。当且仅当nums[slow−2]=nums[fast] 时,当前待检查元素 nums[fast] 不应该被保留(因为此时必然有nums[slow−2]=nums[slow−1]=nums[fast])。最后,slow 即为处理好的数组的长度。

特别地,数组的前两个数必然可以被保留,因此对于长度不超过 2 的数组,我们无需进行任何处理,对于长度超过 2 的数组,我们直接将双指针的初始值设为 2 即可。

时间复杂度: O ( n ) O(n) O(n)

class Solution {
    public int removeDuplicates(int[] nums) {
        if (nums.length<=2) {
            return nums.length;
        }
        int slow = 2;
        for (int fast=2;fast<nums.length;fast++) {
            if (nums[fast]!=nums[slow-2]) {
                nums[slow] = nums[fast];
                slow++;
            }
        }
        return slow;
    }
}

2.有序数组去重保留k位重复数的通法

由于是保留 k 个相同数字,对于前 k 个数字,我们可以直接保留

对于后面的任意数字,能够保留的前提是:与当前写入的位置前面的第 k 个元素进行比较,不相同则保留

class Solution {
    public int removeDuplicates(int[] nums) {   
        return process(nums, 2); //传入不同k值
    }
    int process(int[] nums, int k) {
        int idx = 0; 
        for (int x : nums) {
            if (idx < k || nums[idx - k] != x)
            	nums[idx++] = x;
        }
        return idx;
    }
}

leetcode 283.移动零

leetcode 283.移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
1.必须在原数组上操作,不能拷贝额外的数组。
2.尽量减少操作次数。

双指针法

相当于对整个数组用双指针法移除元素0,然后slow之后都是移除元素0的冗余元素,最后把这些元素都赋值为0就可以了。

class Solution {
    public void moveZeroes(int[] nums) {
        int slow = 0;
        for (int fast=0;fast<nums.length;fast++) {
            if (nums[fast]!=0) {
                nums[slow++] = nums[fast];
            }
        }
        for (int i=slow;i<nums.length;i++) {
            nums[i]=0; //从slow以后都赋值为0
        }
    }
}

leetcode 977.有序数组的平方

leetcode 977.有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1: 输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100]

示例 2: 输入:nums = [-7,-3,2,3,11] 输出:[4,9,9,49,121]

1.暴力排序解法

将数组中每个数平方,再排序
时间复杂度: O ( n log ⁡ n ) O(n\log n) O(nlogn)

2.双指针法(左右指针法)

数组是有序的, 负数平方后可能成为最大数。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。

可以考虑双指针法,i指向起始位置,j指向终止位置。

定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。

如果A[i] * A[i] < A[j] * A[j] 那么result[k–] = A[j] * A[j]; 。

如果A[i] * A[i] >= A[j] * A[j] 那么result[k–] = A[i] * A[i]; 。

图片来自代码随想录
图片来自代码随想录

时间复杂度: O ( n ) O(n) O(n)

class Solution {
    public int[] sortedSquares(int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        int[] result = new int[nums.length];
        int idx = result.length - 1;
        while (left<=right) {
            if (nums[left]*nums[left]>nums[right]*nums[right]) {
                result[idx--] = nums[left]*nums[left];
                left++;
            } else {
                result[idx--] = nums[right]*nums[right];
                right--;
            }
        }
        return result;
    }
}

18. 四数之和

18. 四数之和

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

注意:

答案中不可以包含重复的四元组。

示例: 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ]

双指针法

和15.三数之和是一个思路,都是使用双指针法。一样的道理,五数之和、六数之和等等都采用这种解法。

四数之和的双指针解法是两层for循环nums[j] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[j] + nums[i] + nums[left] + nums[right] == target的情况。四数之和的双指针解法就是将原本暴力 O ( n 4 ) O(n^4) O(n4)的解法,降为 O ( n 3 ) O(n^3) O(n3)的解法

时间复杂度: O ( n 3 ) O(n^3) O(n3)

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        for (int i=0;i<nums.length;i++) {
            if (i>0&&nums[i]==nums[i-1]) continue;//去重

            for (int j=i+1;j<nums.length;j++) {
                if(j>i+1&&nums[j]==nums[j-1]) continue;//去重

                int left =j+1;
                int right = nums.length-1;
                while (right>left) {
                    //nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出
                    if (nums[i]+nums[j]>target-(nums[left]+nums[right])) {
                        right--;
                    } else if (nums[i]+nums[j]<target-(nums[left]+nums[right])) {
                        left++;
                    } else {
                        res.add(Arrays.asList(nums[i],nums[j],nums[left],nums[right]));
                        while (right>left&&nums[right]==nums[right-1]) right--;
                        while (right>left&&nums[left]==nums[left+1]) left++;
                        left++;
                        right--;
                    }
                }
            }
        }
        return res;
    }
}

三、滑动窗口

leetcode 209.长度最小的子数组

leetcode 209.长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

示例:

输入:s = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。

1.暴力解法

时间复杂度: O ( n 2 ) O(n^2) O(n2)

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int result = nums.length+1;
        int sum = 0; // 子序列的数值之和
        int length = 0; // 子序列的长度
        for (int i=0;i<nums.length;i++) { // 设置子序列起点为i
            sum = 0;
            for (int j=i;j<nums.length;j++) {// 设置子序列终止位置为j
                sum += nums[j];
                if (sum>=target) {
                    length = j-i+1;
                    result = result<length ? result:length;
                    break;
                }
            }
        }
         // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
        return result==nums.length+1?0:result;
    }
}

2.滑动窗口

所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。滑动窗口也可以理解为双指针法的一种。

在这里插入图片描述

实现滑动窗口,主要确定如下三点:

  • 窗口内是什么?
  • 如何移动窗口的起始位置?
  • 如何移动窗口的结束位置?

窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。

窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。

窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。

滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。

时间复杂度: O ( n ) O(n) O(n)
每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被被操作两次,所以时间复杂度是 2 × n 也就是 O ( n ) O(n) O(n)

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0;
        int sum = 0;
        int result = Integer.MAX_VALUE;
        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];
            // 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
            while (sum >= target) {
                result = Math.min(result, right - left + 1);//更新结果
                sum -= nums[left++];// 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
            }
        }
        return result == Integer.MAX_VALUE ? 0 : result;
    }
}

leetcode 904.水果成篮

leetcode 904.水果成篮

四、模拟行为

leetcode 59.螺旋矩阵 II

leetcode 59.螺旋矩阵 II
给定一个正整数 n,生成一个包含 1 到 n 2 n^2 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。

示例:

输入: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]
在这里插入图片描述

思路

本题并不涉及到什么算法,就是模拟过程,模拟顺时针画矩阵的过程:

填充上行从左到右
填充右列从上到下
填充下行从右到左
填充左列从下到上
由外向内一圈一圈这么画下去。坚持循环不变量原则
每圈要画四条边,每画一条边都要坚持一致的左闭右开,或者左开又闭的原则,这样这一圈才能按照统一的规则画下来
在这里插入图片描述
这里每一种颜色,代表一条边遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。坚持了每条边左闭右开的原则。

时间复杂度: O ( n 2 ) O(n^2) O(n2)

class Solution {
    public int[][] generateMatrix(int n) {
        int[][] result = new int[n][n];
// 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
        int loop = n/2;
        int startX = 0,startY = 0; // 定义每循环一个圈的起始位置
        int offset = 1;//偏移量。每次循环,控制每一条边遍历的长度
        int count = 1;// 定义填充数字
        int mid = n/2;// 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1)

        while (loop--) {
            int i = startX;
            int j = startY;
			// 下面开始的四个for就是模拟转了一圈
            // 模拟填充上行从左到右(左闭右开)
            for (;j<startY+n-offset;j++) {
                result[i][j] = count++;
            }
            // 模拟填充右列从上到下
            for (;i<startX+n-offset;i++) {
                result[i][j] = count++;
            }
            // 模拟填充下行从右到左
            for (;j>startY;j--) {
                result[i][j] = count++;
            }
            // 模拟填充左列从下到上
            for (;i>startX;i--) {
                result[i][j] = count++;
            }
            // 第二圈开始的时候,起始位置要各自加1
            startX++;
            startY++;
            //每循环一次,外圈两侧完成填充,遍历长度-2
            offset += 2;
        }
         // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
        if (n%2==1) {
            result[mid][mid] = count;
        }
        return result;
    }
}

也可设定4个位置进行模拟

class Solution {
    public int[][] generateMatrix(int n) {
        int l = 0, r = n - 1, t = 0, b = n - 1;
        int[][] mat = new int[n][n];
        int num = 1, tar = n * n;
        while(num <= tar){
            for(int i = l; i <= r; i++) mat[t][i] = num++; // left to right.
            t++;
            for(int i = t; i <= b; i++) mat[i][r] = num++; // top to bottom.
            r--;
            for(int i = r; i >= l; i--) mat[b][i] = num++; // right to left.
            b--;
            for(int i = b; i >= t; i--) mat[i][l] = num++; // bottom to top.
            l++;
        }
        return mat;
    }
}

leetcode 54.螺旋矩阵

leetcode 54.螺旋矩阵与剑指Offer 29.顺时针打印矩阵相同

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
在这里插入图片描述
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

在这里插入图片描述
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]

解法

class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> res = new ArrayList<>();
        int count = 0, m = matrix.length, n = matrix[0].length;
        int total = m * n;
        int up = 0, down = m - 1, left = 0, right = n - 1;
        while(count < total){
            for(int i = left; i <= right && count < total; i++){
                res.add(matrix[up][i]);
                count++;
            }
            up++;
            for(int i = up; i <= down && count < total; i++){
                res.add(matrix[i][right]);
                count++;
            }
            right--;
            for(int i = right; i >= left && count < total; i--){
                res.add(matrix[down][i]);
                count++;
            }
            down--;
            for(int i = down; i >= up && count < total; i--){
                res.add(matrix[i][left]);
                count++;
            }
            left++;
        }
        return res;
    }
}

leetcode 48.旋转图像

leetcode 48.旋转图像
在这里插入图片描述

思路

旋转二维矩阵的难点在于将「行」变成「列」,将「列」变成「行」,而只有按照对角线的对称操作是可以轻松完成这一点的,对称操作之后就很容易发现规律了。

我们可以先将 n*n 矩阵 matrix 按照左上到右下的对角线进行镜像对称:
在这里插入图片描述
然后再对矩阵的每一行进行反转(按竖直中轴线反转):
在这里插入图片描述
发现结果就是 matrix 顺时针旋转 90 度的结果:
在这里插入图片描述
先按水平中轴线反转,再按对角线反转也可得到相同结果

时间复杂度: O ( n 2 ) O(n^2) O(n2)

class Solution {
    public void rotate(int[][] matrix) {
        int n = matrix.length;
        // 先沿对角线镜像对称二维矩阵
        for (int i = 0; i < n; i++) {
            for (int j = i; j < n; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }
        // 然后反转二维矩阵的每一行
        for (int[] arr : matrix) {
            // 反转一维数组
            int i = 0, j = arr.length - 1;
            while (j > i) {
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
                i++;
                j--;
            }
        }
    }
}

同理可以将矩阵逆时针旋转 90 度
在这里插入图片描述

五、前缀和数组

前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和

leetcode 303.区域和检索 - 数组不可变

leetcode 303.区域和检索 - 数组不可变
在这里插入图片描述

1.暴力解法

效率较低,因为 sumRange 方法会被频繁调用
时间复杂度:每次查询 O ( n ) O(n) O(n),n为数组长度

class NumArray {
    private int[] nums;
    public NumArray(int[] nums) {
        this.nums = nums;
    }
    
    public int sumRange(int left, int right) {
        int res = 0;
        for (int i=left;i<=right;i++) {
            res += nums[i];
        }
        return res;
    }
}

2.前缀和

new 一个新的数组 preSum 出来,preSum[i] 记录 nums[0…i-1] 的累加和
在这里插入图片描述
求索引区间 [left, right] 内的所有元素之和,就可以通过 preSum[right + 1] - preSum[left] 得出

sumRange 函数仅仅需要做一次减法运算,避免了每次进行 for 循环调用,从而降低了检索的时间复杂度

时间复杂度:初始化 O ( n ) O(n) O(n),每次检索 O ( 1 ) O(1) O(1)
空间复杂度: O ( n ) O(n) O(n)。需要创建一个长度为 n+1 的前缀和数组

class NumArray {
    private int[] preSum;
    private int[] nums;
    // 输入一个数组,构造前缀和 
    public NumArray(int[] nums) {
        preSum = new int[nums.length + 1];
        for (int i=1;i<preSum.length;i++) {
            preSum[i] = preSum[i-1] + nums[i-1];
        }
    }
    
    public int sumRange(int left, int right) {
        return preSum[right+1] - preSum[left];
    }
}

leetcode 304.二维区域和检索 - 矩阵不可变

leetcode 304.二维区域和检索 - 矩阵不可变
在这里插入图片描述
示例:
在这里插入图片描述
sumRegion([2,1,4,3]) 就是图中红色子矩阵,需要返回该子矩阵的元素和 8

思路

和一维数组中的前缀和非常类似
在这里插入图片描述
如果想计算红色的子矩阵元素之和,可以用绿色矩阵减去蓝色矩阵减去橙色矩阵最后加上粉色矩阵,而绿蓝橙粉这四个矩阵有一个共同的特点,就是左上角就是 (0, 0) 原点

那么我们可以维护一个二维 preSum 数组,记录以原点为顶点的矩阵的元素之和,就可以用几次加减运算算出任何一个子矩阵的元素和

时间复杂度:初始化 O(mn),每次检索 O(1),其中 m和 n 分别是矩阵 matrix 的行数和列数

class NumMatrix {
    int[][] matrix;
    int[][] preSum;
    public NumMatrix(int[][] matrix) {
    	//行列数+1保证不需对row1=0和col1=0的情况做特殊处理
        preSum = new int[matrix.length+1][matrix[0].length+1];
        for (int i=1;i<=matrix.length;i++) {
            for (int j=1;j<=matrix[0].length;j++) {
                // 计算每个矩阵 [0, 0, i, j] 的元素和
                preSum[i][j]=preSum[i-1][j]+preSum[i][j-1]
                +matrix[i-1][j-1]-preSum[i-1][j-1];
            }
        }
    }
    // 计算子矩阵 [row1,col1,row2,col2] 的元素和
    public int sumRegion(int row1, int col1, int row2, int col2) {
        // 目标矩阵之和由四个相邻矩阵运算获得
        return preSum[row2+1][col2+1] - preSum[row1][col2+1] 
        - preSum[row2+1][col1] + preSum[row1][col1];
    }
}

leetcode 560.和为 K 的子数组

leetcode 560.和为 K 的子数组

六、差分数组

差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减

给你输入一个数组 nums,然后又要求给区间 nums[2…6] 全部加 1,再给 nums[3…9] 全部减 3,再给 nums[0…4] 全部加 2,再给…最后 nums 数组的值是什么?

常规思路:for循环,时间复杂度为 O ( n ) O(n) O(n)。这个场景下对 nums 的修改非常频繁,所以效率会很低下

差分数组技巧:先对 nums 数组构造一个 diff 差分数组,diff[i] 就是 nums[i] 和 nums[i-1] 之差。通过这个 diff 差分数组可以反推出原始数组 nums
差分数组求前缀和即可得到原数组
在这里插入图片描述

如果你想对区间 nums[i…j] 的元素全部加 3,那么只需要让 diff[i] += 3,然后再让 diff[j+1] -= 3 即可
在这里插入图片描述

diff[i] += 3 意味着给 nums[i…] 所有的元素都加了 3,然后 diff[j+1] -= 3 又意味着对于 nums[j+1…] 所有元素再减 3,综合起来,就是对 nums[i…j] 中的所有元素都加 3
只要花费 O(1) 的时间修改 diff 数组,就相当于给 nums 的整个区间做了修改。多次修改 diff,然后通过 diff 数组反推,即可得到 nums 修改后的结果。

// 差分数组工具类
class Difference {
    // 差分数组
    private int[] diff;
    
    /* 输入一个初始数组,区间操作将在这个数组上进行 */
    public Difference(int[] nums) {
        assert nums.length > 0;
        diff = new int[nums.length];
        // 根据初始数组构造差分数组
        diff[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            diff[i] = nums[i] - nums[i - 1];
        }
    }

    /* 给闭区间 [i,j] 增加 val(可以是负数)*/
    public void increment(int i, int j, int val) {
        diff[i] += val;
        if (j + 1 < diff.length) {
            diff[j + 1] -= val;
        }
    }

    /* 返回结果数组 */
    public int[] result() {
        int[] res = new int[diff.length];
        // 根据差分数组构造结果数组
        res[0] = diff[0];
        for (int i = 1; i < diff.length; i++) {
            res[i] = res[i - 1] + diff[i];
        }
        return res;
    }
}

increment方法中当 j+1 >= diff.length 时,说明是对 nums[i] 及以后的整个数组都进行修改,那么就不需要再给 diff 数组减 val 了。

leetcode 307.区间加法

在这里插入图片描述
复用刚才实现的 Difference 类

int[] getModifiedArray(int n, int[][] updates) {
    // nums 初始化为全 0
    int[] nums = new int[n];
    // 构造差分解法
    Difference df = new Difference(nums);
    
    for (int[] update : updates) {
        int i = update[0];
        int j = update[1];
        int val = update[2];
        df.increment(i, j, val);
    }
    
    return df.result();
}

leetcode 1109.航班预订统计

leetcode 1109.航班预订统计
在这里插入图片描述
本质就是差分数组,复用刚才的类即可。
PS:n 是从 1 开始计数的,而数组索引从 0 开始,所以对于输入的三元组 (i,j,k),数组区间应该对应 [i-1,j-1]。

int[] corpFlightBookings(int[][] bookings, int n) {
    // nums 初始化为全 0
    int[] nums = new int[n];
    // 构造差分解法
    Difference df = new Difference(nums);

    for (int[] booking : bookings) {
        // 注意转成数组索引要减一
        int i = booking[0] - 1;
        int j = booking[1] - 1;
        int val = booking[2];
        // 对区间 nums[i..j] 增加 val
        df.increment(i, j, val);
    }
    // 返回最终的结果数组
    return df.result();
}
class Solution {
    public int[] corpFlightBookings(int[][] bookings, int n) {
        int[] nums = new int[n];
        for (int[] booking : bookings) {
            nums[booking[0] - 1] += booking[2];
            if (booking[1] < n) { //当右端点j==n时无需修改
                nums[booking[1]] -= booking[2];
            }
        }
        //求前缀和,得到结果数组
        for (int i = 1; i < n; i++) {
            nums[i] += nums[i - 1];
        }
        return nums;
    }
}

leetcode 1094.拼车

leetcode 1094.拼车

车上最初有 capacity 个空座位。车 只能 向一个方向行驶(也就是说,不允许掉头或改变方向)

给定整数 capacity 和一个数组 trips , trip[i] = [numPassengersi, fromi, toi] 表示第 i 次旅行有 numPassengersi 乘客,接他们和放他们的位置分别是 fromi 和 toi 。这些位置是从汽车的初始位置向东的公里数。

当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true,否则请返回 false。

思路

trips[i] 代表着一组区间操作,旅客的上车和下车就相当于数组的区间加减;只要结果数组中的元素都小于 capacity,就说明可以不超载运输所有旅客。

车站编号从 0 开始,最多到 1000,也就是最多有 1001 个车站,那么我们的差分数组长度可以直接设置为 1001,这样索引刚好能够涵盖所有车站的编号

复用Difference类:

class Solution {
    public boolean carPooling(int[][] trips, int capacity) {
    	// 最多有 1001 个车站
    	int[] nums = new int[1001];
    	// 构造差分解法
    	Difference df = new Difference(nums);
    
    	for (int[] trip : trips) {
        	// 乘客数量
        	int val = trip[0];
        	// 第 trip[1] 站乘客上车
        	int i = trip[1];
        	// 第 trip[2] 站乘客已经下车,
        	// 即乘客在车上的区间是 [trip[1], trip[2] - 1]
        	int j = trip[2] - 1;
        	// 进行区间操作
        	df.increment(i, j, val);
    	}
    
    	int[] res = df.result();
    
    	// 客车自始至终都不应该超载
    	for (int i = 0; i < res.length; i++) {
        	if (capacity < res[i]) {
            	return false;
        	}
    	}
    	return true;
	}
}
class Solution {
    public boolean carPooling(int[][] trips, int capacity) {
        int[] diff = new int[1001];
        for(int[] trip:trips) {
            diff[trip[1]] += trip[0];
            if(trip[2] < diff.length) {
                diff[trip[2]] -= trip[0];
            }
        }
        //先单独判断diff[0]是否超载
        if(diff[0] > capacity) return false;
        for(int i=1; i < diff.length; i++) {
            diff[i] += diff[i-1];
            if(diff[i] > capacity) {
                return false;
            }
        }
        return true;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值