返回顶部

算法刷题:数组指针、前缀和/差分/树状数组、滑窗/滚动哈希/单调队列、二分(8.13,持续更)

算法刷题系列:


指针迭代技巧 - 力扣链接:

删除有序数组中的重复项
删除排序链表中的重复元素
移除元素
移除链表元素
两数之和
反转字符串
反转链表
验证回文串
验证回文串 II

前缀和 - 力扣链接:

区域和检索 - 数组不可变
二维区域和检索 - 矩阵不可变

差分数组 - 力扣链接:

航班预定统计
拼车

树状数组 / 线段树 - 力扣链接:

区域和检索 - 数组可修改

滑动窗口 - 力扣链接:

  • 定长滑窗、字符排列

字符串的排列
找到字符串中所有字母异位词


目录


技巧总结:

指针技巧

同向移动:

  • 同步移动:
    1. 速度倍数 sf:
    2. 相对速度 sf:
    3. 相邻先后 pcn:
    4. 齐头并进:是否需要齐头,之后再并进
  • 异步移动:
    1. 慢指针符合条件再动

异向移动:

  • 左右指针
    • 边向中
    • 中向边

指针指向:

  • 空位(快速排序)
  • 处理点

矩阵(二维数组)

前缀和 / 差分

数组的效率:

  • 数组随机访问:\(O(1)\)
  • 数组修改下标位置的值:\(O(1)\)
  • 数组修改一个大小为 n 的区域:\(O(N)\)
  • 数组的大小为 n 的区域的累加和:\(O(N)\)
  • 数组插入 1 个元素:\(O(N)\)
  • 数组删除 1 个元素:\(O(N)\)

前缀数组:

求 an:s[n] - s[n-1]

  • 访问已 1 2 3 4 5 -- 1 3 6 10 15
  • 修改难 1 (2+2) 3 4 5 -- 1 (3+2) (6+2) (10+2) (15+2)

前缀和数组的效率:

  • 随机访问:\(O(1)\)
  • 修改下标位置值:\(O(N)\) // 修改的效率下降
  • 修改一个大小为 n 的区域:\(O(N^2)\)
  • 大小为 n 的区域的累加和:\(O(1)\)
  • 插入 1 个元素:\(O(N)\)
  • 删除 1 个元素:\(O(N)\)

差分数组(适合 修改连续区域,不适合 求累加和):

求an:a[2] - a[1] + a[3] - a[2] + ... ... + a[n] - a[n-1]

  • 访问难 1 2 3 4 5 -- 1 1 1 1 1
  • 修改易 1 (2+2) 3 4 5 -- 1 (1+2) (1-2) 1 1

差分数组的效率:

  • 随机访问:\(O(N)\)
  • 修改下标位置值:\(O(1)\)
  • 修改一个大小为 n 的区域:\(O(1)\)
  • 大小为 n 的区域的累加和:\(O(N^2)\)
  • 插入 1 个元素:\(O(N)\)

针对不同的题目,我们有不同的方案可以选择(假设我们有一个数组):

  • 数组不变,求区间和:「前缀和」、「树状数组」、「线段树」
  • 多次修改某个数(单点),求区间和:「树状数组」、「线段树」
  • 多次修改某个区间,输出最终结果:「差分」
  • 多次修改某个区间,求区间和:「线段树」、「树状数组」(看修改区间范围大小)

多次将某个区间变成同一个数,求区间和:「线段树」、「树状数组」(看修改区间范围大小)

这样看来,「线段树」能解决的问题是最多的,那我们是不是无论什么情况都写「线段树」呢?

答案并不是,而且恰好相反,只有在我们遇到第 4 类问题,不得不写「线段树」的时候,我们才考虑线段树。

因为「线段树」代码很长,而且常数很大,实际表现不算很好。我们只有在不得不用的时候才考虑「线段树」。

总结一下,我们应该按这样的优先级进行考虑:

  • 简单求区间和,用「前缀和」
  • 多次将某个区间变成同一个数,用「线段树」
  • 多次修改某个区间,返回结果数组,用「差分」
  • 其他情况,用「树状数组」

树状数组 / 线段树

树状数组和线段树很相似:

  1. 线段树 在功能上 包含 树状数组
    • 树状数组能有的操作,线段树一定有
    • 线段树有的操作,树状数组不一定有
  2. 树状数组的 常数复杂度更低
    • 树状数组的代码要比线段树短,思维更清晰,速度也更快,在解决一些单点修改的问题时,树状数组是不二之选。

整体的时间复杂度\(O(nlogn)\),空间复杂度\(O(n)\)

树状数组数据结构实现:

原理(图解)

查改影响的传递
自己做了个图展示数状数组原理:
求树状数组图解

求出树状数组:

  1. 确定当前下标索引存储的区间大小:lowbit(idx)
    • 区间大小和 lowbit 息息相关
  2. 复用累加和:sum(idx)
    • 复用就是将大区间叠加到小区间

单点修改树状数组:update(idx, val)

找到下一个下标的区间覆盖当前下标的
关键代码:
i += lowbit(i)
边界是:小于最大下标

取出区间累加和:

类似从子树到树根
本质是:跳过区间

  • lowbit 表示当前下标存储的区间大小,下一个下标需要跳过区间长度
  • 同时每次跳过一次区间,下次需要跳过的区间就翻一倍(少一半需要处理的区间,\(O(logN)\) 的时间复杂度

跳过区间的关键代码:

i -= lowbit(i)

最低位 1 的越靠左,表示的区间就会大一倍
时间复杂度:\(O(logN)\)

代码实现

class NumArray {
    int[] tree;
    int[] nums;
    int n;
	
    public NumArray(int[] nums) {
        this.nums = nums;
        n = nums.length;
        tree = new int[n + 10];
        for (int i = 0; i < n; i++) add(i + 1, nums[i]);
    }
    int lowbit(int x) {
        return x & -x;
    }
    // 单点修改
    void add(int x, int u) {
        for (int i = x; i <= n; i += lowbit(i)) tree[i] += u;
    }
    public void update(int index, int val) {
        add(index + 1, val - nums[index]); // 修改树状数组
        nums[index] = val; // 修改原数组
    }
    // 查询,tr1100101 = a1100101 + tr1100100 + 
    //        tr1100000 + tr1000000 + tr0
    //   i -= lowbit(i)
    int query(int x) { // 查询 [0, x] 区间的累加和
        int ans = 0; // answer
        for (int i = x; i > 0; i -= lowbit(i)) ans += tree[i];
        return ans;
    }
    public int sumRange(int left, int right) {
        return query(right + 1) - query(left); // 容斥原理
    }
}

滑动窗口 - 子串(最优、全解)问题

子串问题的关键是,理解左或右端点确定后,子串的可能性才能被确定这一思想:

  • 扩大窗口 - 比较模版(需求),从无到有
  • 缩小窗口 - 比较模版,从有到无,找到当前右界的最优解
  • 更新最优解 / 加入可解集合

定长滑动窗口

解决固定长度连续子串的覆盖问题,是否覆盖 取决于 当前窗口比较器是否匹配模板

变长滑动窗口

解决连续子数组/连续子串的最优解
关键在于:

  • 比较器和模板:
  • 移入元素和移出元素
  • 右边界决定是否覆盖(存在额外的多余部分),左边界决定是否最小覆盖(不存在多余的部分)

交集滑动窗口(公共子串问题)

分为三段:

  • 短串 进入 长串
  • 短串 完全进入 长串中
  • 短串 移出 长串

伪代码图示:

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        // 两个串的长短
        int [] minArr = nums1.length < nums2.length ? nums1 : nums2;
        int [] maxArr = minArr == nums1 ? nums2 : nums1;
        int minLen = minArr.length, maxLen = maxArr.length;

        // 1 :
        // |*|*|*|*|
        //       |*|*|*|*|*|*|
        // ---
        // |*|*|*|*|
        //   |*|*|*|*|*|*|
        for(int i = 0; i < minArr.length; i++){
            minLen - i;
            i;
            // 窗口:
            // [0, i] - maxArr         ||| j
            // [minLen - i, minLen] - minArr 
            // ||| minLen - i + j
        }

        // 2 :
        // |*|*|*|*|
        // |*|*|*|*|*|*|
        // ---
        //     |*|*|*|*|
        // |*|*|*|*|*|*|
        int l = 0, r = minLen;
        while(r < maxLen){
            l++;
            r++;
            // 窗口:
            // [l, r] - maxArr ||| l + j
            // all - minArr   ||| j
        }

        // 3:
        //     |*|*|*|*|
        // |*|*|*|*|*|*|
        // ---
        //           |*|*|*|*|
        // |*|*|*|*|*|*|
        for(r = minLen - 1; r >= 0; r--){
            l++;
            // 窗口:
            // [l, maxLen] - maxArr  
            //||| l + j / maxLen - i + j
            // [0, r] - minArr   ||| j
        }
    }
}

二分搜索

注意事项

效率注意点

if(str.charAt(l) != str.charAt(r)){

不如

char lval = str.charAt(l);
char rval = str.charAt(r);
if(lval != rval){

的效率高

细节

  1. 迭代方向相反:\([xx - j] == [yy + j]\) --> \([xx - i + j] == [yy + j]\)
  2. (dp) 起始条件产生少的结果时,这个起始条件不一定是从小位置/头开始,还有可能从尾开始
  3. 滑动窗口不能用 for(; r < chrs.length; r++){...}
    而应该用 while(r < chrs.length){...},防止 移动 L 指针(缩小窗口)时,r 进行本不该移动的移动

指针技巧题

异步移动

删除有序数组的重复项

代码实现

public int removeDuplicates(int [] nums){
	int slow = 0;
	for(int fast = 0; fast < nums.length; ++fast){
		if(nums[fast] != nums[slow]){
			nums[++slow] = nums[fast];
		}
	}
	return slow + 1;
}

对比链表的删除重复项代码

很类似:

public ListNode deleteDuplicates(ListNode head){
	if(head == null) return null;

	ListNode slow = head;
	for(ListNode fast = head; fast != null; fast = fast.next){
		if(fast.val != slow.val){
			slow.next = fast;
			slow = slow.next;
		}
	}
	slow.next = null;
	return head;
}

移除元素

代码实现

int removeElement(int [] nums, int val){
	int sl = 0;
	for(int fa = 0; fa < nums.length; ++fa){
		if(nums[fa] != val){
			nums[sl] = nums[fa];
			sl++;
		}
	}
	return sl;
}

对比移除链表元素

ListNode removeElements(ListNode head, int val){
	ListNode dummy = new ListNode();
	
	ListNode sl = dummy;
	for(ListNode fa = head; fa != null; fa = fa.next){
		if(fa.val != val){
			sl.next = fa;
			sl = sl.next;
		}
	}
	return dummy.next;
}

异向而行

两数之和

暴力解法(\(O(N^2)\),56ms)

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int [] res = new int [2];
        for(int i = 0; i < nums.length; i++){
            for(int j = i + 1; j < nums.length; j++){
                int sum = nums[i] + nums[j];
                if(sum == target) {
                    res[0] = i;
                    res[1] = j;
                    return res;
                }
            }
        }
        return res;
    }
}

快排 + 左右指针(\(O(NlogN)\),2ms)

但是排序之后,索引位置会发生变化
所以需要保存排序之前的数组

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;

        int [] src = new int [nums.length]; // 保存源数组
        for(int i = 0; i < nums.length; i++){
            src[i] = nums[i];
        }
        quickSort(nums, 0, right); // 排序

        int [] res = new int [2];

        int sum = 0;
        while(left < right){
            sum = nums[left] + nums[right];
            if(sum < target){
                left ++;
            } else if(sum > target){
                right --;
            } else {
                int i = 0;
                int lval = nums[left];
                int rval = nums[right];
                while(true){
                    if(src[i] == lval){
                        res[0] = i;
                        break;
                    } i++;
                } i = 0;
                while(true){
                    if(src[i] == rval){
                        if(i == res[0]){
                            i++;
                            continue;
                        }
                        res[1] = i;
                        break;
                    } i++;
                }
                break;
            }
        }
        return res;
	}
}

哈希表空间换时间(\(O(N)\)

单向遍历解法(2ms)

将两个变量中的一个变量,用空间(哈希表)转化成常量,空间换时间

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for(int l = 0; l < nums.length; l++){
            int rval = target - nums[l];
            if(map.containsKey(rval)){
                return new int []{map.get(rval), l};
            }
            map.put(nums[l], l);
        }
        // return new int []{0, 0};
        return new int [2];
    }
}
优化:边向中左右指针(0ms)

类似快速排序的左右指针从边界向中间迭代:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for(int l = 0, r = nums.length - 1; l <= r; l++, r--){
            int rval = target - nums[l];
            if(map.containsKey(rval)){
                return new int []{map.get(rval), l};
            }
            map.put(nums[l], l);

            int lval = target - nums[r];
            if(map.containsKey(lval)){
                return new int []{map.get(lval), r};
            }
            map.put(nums[r], r);
        }
        // return new int []{0, 0};
        return new int [2];
    }
}

反转字符串(\(O(N)\)

代码实现

void reverseString(char [] s){
	int left = 0;
	int right = s.length - 1;
	while(left < right){
		swap(s, left, right);
		left++;
		right--;
	}
}

对比反转链表

ListNode reverse(ListNode head){
	if(head == null && head.next == null){
		return head;
	}
	ListNode pre = null; // 处理完成
	ListNode cur = head; // cur 是待处理的点
	ListNode nxt = null;
	while(cur != null){ // 没有需要处理的
		nxt = cur.next;
		
		cur.next = pre; // 处理内容:反转
		
		pre = cur; // 跟随
		cur = nxt;
	}
	return pre;
}

快速排序

原理

(自己懒得做图,图片来自别的博客)
下面序列:
快排00

把第一位57作为基准位,用变量把它存起来,因为一会就没了

  • 把所有比57小的数放在57的左面,把比57大的数放在57的右面
  • 两边同时进行,左边找大的,右边找小的,把小的放左边,大的放右边,具体操作如下:

第一趟:从指针j开始,24小于57,放到左边,把57覆盖掉
01

之后:指针i右移,指向68,68>57,放到右边
02

之后:指针j左移指向33,33<57,放到左边
03

之后:指针i右移指向59,59>57,放右边
04

之后:指针j左移指向96,96>57,j再左移指向28,28<57,放左边
05

之后:指针i右移指向52,52<57,i继续右移指向72,72>57,放右边
06

之后:指针j左移,与指针i重合指向NULL,这时放入57
07

这时发现左边的数都比57小,右边都比57大

然后再对57左边的数,即:0到i-1进行快速排序(同样操作,把24作为基准,左边小,右边大),对57右边的数,即:i+1到n进行快速排序(以72位基准,左边小,右边大)直到不能再进行排序为止;

代码实现

优化:扰动

int pvt = nums[mid];
swap(nums, left, mid);
static void quickSort(int[] nums, int left, int right){
    if(left > right) return;

    int mid = (left + right) >>> 1;
    int l = left;
    int r = right;
    int pvt = nums[mid]; // 基准元素
	
    swap(nums, left, mid);
    while(l < r){
        // 处理点 r 迭代到 小于 基准时停下
        while(r > l && nums[r] >= pvt) r--;
        // 将 r 的值给 l 指向的空位
        // 将处理点找到的 赋值给 空位
        if(r > l) nums[l] = nums[r]; // l 指向空位
        l++;
        // 处理点 l 迭代到 大于 基准时停下
        while(l < r && nums[l] < pvt) l++;
        // 将 l 的值给 r 指向的空位
        // 将处理点找到的 赋值给 空位
        if(l < r) nums[r] = nums[l]; // r 指向空位
        r--;
    } nums[l] = pvt; // 基准元素放入 lr 指向的空位
        
    quickSort(nums, left, l - 1);
    quickSort(nums, l + 1, right);
}

验证回文串(\(O(N)\),1ms击败100%)

这个本质也是相向而行,但是在相向而行中多处理了很多步骤

class Solution {
    public boolean isPalindrome(String s) {
        int left = 0;
        int right = s.length() - 1;
        while(left < right){
            // 大写转换为小写
            char lval = castChar(s.charAt(left));
            char rval = castChar(s.charAt(right));
            // 是否是小写字母 或数字
            if(lval > 'z' || lval < 'a' ){
                if(lval > '9' || lval < '0'){
                    left ++;
                    continue;
                }
            }
            if(rval > 'z' || rval < 'a' ){
                if(rval > '9' || rval < '0'){
                    right --;
                    continue;
                }
            }
            // 判断是否回文串
            if(lval != rval){
                return false;
            }
            // 迭代
            left ++;
            right --;
        }
        return true;
    }
    char castChar(char c){ // 将大写字母转换为小写字母
        if(c >= 'A' && c <= 'Z'){
            return (char)((int)c - ('A' - 'a'));
        }
        return c;
    }
}

验证回文串 II(\(O(N)\),4ms击败100%)

全是英文小写字母,重点是可以有一次机会修改,这个可以理解为左右指针指向了不一致的值可以删除左指针值或者右指针值,再看看是不是回文串

class Solution {
    public boolean validPalindrome(String s) {
        int l = 0;
        int r = s.length() - 1;
        while(l < r){
            char lval = s.charAt(l);
            char rval = s.charAt(r);
            if(lval != rval){
                boolean lok = palindrome(s, l + 1, r);
                if(lok) break;
                boolean rok = palindrome(s, l, r - 1);
                if(rok) break;
                return false;
            }
            l++; r--;
        }
        return true;
    }
    public boolean palindrome(String s, int l, int r){
        while(l < r){
            char lval = s.charAt(l);
            char rval = s.charAt(r);
            if(lval != rval){
                return false;
            }
            l++; r--;
        }
        return true;
    }
}

前缀和题

区间求和 / 不修改:前缀和

区域和检索 - 数组不可变

直接解法 56ms

class NumArray {
    private int [] nums;

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

原地前缀和解法(击败100%,7ms)

class NumArray {
    private int [] preSum;

    public NumArray(int[] nums) {
        int sum = 0;
        for(int i = 0; i < nums.length; i++){
            sum += nums[i];
            nums[i] = sum;
        }
        preSum = nums;
    }
    
    public int sumRange(int left, int right) {
        if(left == 0) return preSum[right];

        return preSum[right] - preSum[left - 1];
    }
}

新建前缀和数组

多了\(O(N)\)的空间复杂度,单独拿出一个数组做前缀和数组,同时保留原数组

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

直接解法 2149 ms

class NumMatrix {
    int [][] mtx;
    
    public NumMatrix(int[][] matrix) {
        mtx = matrix;
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        int sum = 0;
        for(int i = row1; i <= row2; i++){
            for(int j = col1; j <= col2; j++){
                sum += mtx[i][j];
            }
        }
        return sum;
    }
}

一维前缀和解法(\(O(N^2)\)

图解一维前缀和

矩阵的一维前缀和的解释(图解):
矩阵的一维前缀和

代码实现

创建一维前缀和矩阵代码:

for(int i = 0; i < matrix.length; i++){
    int sum = 0;
    for(int j = 0; j < matrix[0].length; j++){
        sum += matrix[i][j];
        matrix[i][j] = sum;
    }
} mtx = matrix;

查询子矩阵区间的累加和:

二维前缀和解法(\(O(N)\),96ms击败100%)

图解二维前缀和

矩阵的二维前缀和的解释(图解):
矩阵的二维前缀和

代码实现

创建二维前缀和矩阵代码:

public NumMatrix(int[][] matrix) {
    for(int i = 0; i < matrix.length; i++){
        int sum = 0;
        for(int j = 0; j < matrix[0].length; j++){
            sum += matrix[i][j];
            matrix[i][j] = sum;
        }
    }
    for(int j = 0; j < matrix[0].length; j++){
        int sum = 0;
        for(int i = 0; i < matrix.length; i++){
            sum += matrix[i][j];
            matrix[i][j] = sum;
        }
    }
    mtx = matrix;
}

查询子矩阵区间的累加和:

int rmin = row1 - 1;
int rmax = row2;
int cmin = col1 - 1;
int cmax = col2;
return mtx[rmax][cmax] + mtx[rmin][cmin]
		- mtx[rmin][cmax] - mtx[rmax][cmin];

边界处理

if(row1 + col1 == 0) return mtx[row2][col2];
if(row1 == 0) return mtx[row2][col2] - mtx[row2][col1 - 1];
if(col1 == 0) return mtx[row2][col2] - mtx[row1 - 1][col2];

完整代码:

class NumMatrix {
    int [][] mtx;
    public NumMatrix(int[][] matrix) {
        for(int i = 0; i < matrix.length; i++){
            int sum = 0;
            for(int j = 0; j < matrix[0].length; j++){
                sum += matrix[i][j];
                matrix[i][j] = sum;
            }
        }
        for(int j = 0; j < matrix[0].length; j++){
            int sum = 0;
            for(int i = 0; i < matrix.length; i++){
                sum += matrix[i][j];
                matrix[i][j] = sum;
            }
        }
        mtx = matrix;
    }
    public int sumRegion(int row1, int col1, int row2, int col2) {
        if(row1 + col1 == 0) return mtx[row2][col2];
        if(row1 == 0) return mtx[row2][col2] - mtx[row2][col1 - 1];
        if(col1 == 0) return mtx[row2][col2] - mtx[row1 - 1][col2];
        
        int rmin = row1 - 1;
        int rmax = row2;
        int cmin = col1 - 1;
        int cmax = col2;
        return mtx[rmax][cmax] - mtx[rmin][cmax] - mtx[rmax][cmin] + mtx[rmin][cmin];
    }
}

差分题

多次区间修改 + 求修改后数组:差分数组

航班预定统计

class Solution {
		public int[] corpFlightBookings(int[][] bookings, int n) {
				// 差分数组 处理 多次区间修改
				int [] diff = new int [n];
				for(int i = 0; i < bookings.length; i++){
						// 区间的左右界
						int l = bookings[i][0] - 1;
						int r = bookings[i][1];
						// 差分数组处理 区间修改
						diff[l] += bookings[i][2];
						if(r < n) diff[r] -= bookings[i][2];
				}
				// 若干次的区间修改 已经 修改完成
				// 此时  将 差分数组 还原为 原数组
				int [] src = diff;
				for(int i = 1; i < n; i++){
					src[i] = diff[i] + src[i - 1];
				}
				return src; 
		}
}

拼车

树状数组 / 线段树题

单点修改 + 区间求和:树状数组 + 线段树

区域和检索 - 数组可修改

差分+前缀和解法(超时,但是适合 区间修改)

class NumArray {
    private int [] src;
    private int [] diff;
    private int [] psum;

    private boolean toFlush;

    public NumArray(int[] nums) {
        // src
        src = nums;
        // diff | psum
        diff = new int[src.length];
        psum = new int[src.length];

        diff[0] = src[0];
        psum[0] = src[0];
        for(int i = 1; i < src.length; i++){
            diff[i] = src[i] - src[i - 1];
            psum[i] = psum[i - 1] + src[i];
        }
    }
    // 单点修改
    public void update(int index, int val) {
        if(!toFlush) toFlush = true;
        diff[index] += val;
        if(index < src.length - 1) diff[index + 1] -= val;
    }
		// 区间修改
		// 本题未涉及!!
    
    public int sumRange(int left, int right) {
        if(toFlush) flushArrays();
        return left != 0 ? psum[right] - psum[left - 1] : psum[right];
    }

    public void flushArrays(){
        for(int i = 1; i < src.length; i++){
            src[i] = src[i - 1] + diff[i];
            psum[i] = psum[i - 1] + src[i];
        }
        toFlush = false;
    }
}

处理内容:

  1. 单点修改
    • 可加入差分 - 区间修改:
  2. 差分 - 计算 还原 原数组:
  3. 原数组 - 计算 更新 前缀和:

前缀和解法(超时,单点修改 不适合 前缀和)

单点修改和前缀和交替进行时,会超时:

class NumArray {
    private int [] src;
    private int [] psum;

    private boolean toFlush;

    public NumArray(int[] nums) {
        src = nums;
        psum = new int[src.length];
        flushArrays();
    }
    
    public void update(int index, int val) {
        if(!toFlush) toFlush = true;
        src[index] = val;
    }
    
    public int sumRange(int left, int right) {
        if(toFlush) flushArrays();
        return left != 0 ? psum[right] - psum[left - 1] : psum[right];
    }

    public void flushArrays(){
        psum[0] = src[0];
        for(int i = 1; i < src.length; i++)
            psum[i] = psum[i - 1] + src[i];
        toFlush = false;
    }
}

树状数组解法 (\(O(NlogN)\))

时间复杂度:\(O(NlogN)\)
空间复杂度:\(O(N)\)

class NumArray {
    int[] tree;
    int[] nums;
    int n;
    public NumArray(int[] _nums) {
        nums = _nums;
        n = nums.length;
        tree = new int[n + 1];
        for (int i = 0; i < n; i++) add(i + 1, nums[i]);
    }
    void add(int x, int u) {
        for (int i = x; i <= n; i += i & -i) tree[i] += u;
    }
    int query(int x) {
        int ans = 0; // answer
        for (int i = x; i > 0; i -= i & -i) ans += tree[i];
        return ans;
    }
    public void update(int index, int val) {
        add(index + 1, val - nums[index]);
        nums[index] = val;
    }
    public int sumRange(int left, int right) {
        return query(right + 1) - query(left);
    }
}

定长滑窗题

字符排列

字符串的排列

/* 暴力解:
class Solution {
    // O(m*n^2)
    public boolean checkInclusion(String s1, String s2) {
        int r = s1.length() - 1;
        int l = 0;
        while(r < s2.length()){ // 移动窗口
            int ok = 0; // 当前窗口 是否符合要求
            for(int i = 0; i < s1.length(); i++){ // 遍历窗口内指针
                int a = 0; // 窗口内当前指针 是否是排列内 且不与上文重复的字符
                char c0 = s2.charAt(l + i);


                // 遍历排列
                for(int j = 0; j < s1.length(); j++){ // 当前窗口内指针是否是排列里的
                    char c = s1.charAt(j);
                    if(c == c0) { 
                        // 排列 abc - 窗口 abb 时也会 放行
                        // 所以需要集合,避免重复字母导致窗口内指针错误 放行
                        a = 1;
                        break;
                    }
                }
                if(a == 0) break;
                if(i == s1.length() - 1) return true;
            }
            r++;l++;
        }
        return false;
    }
}*/
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        // 固定窗口
        int ww = s1.length() - 1;
        int end = s2.length() - ww - 1;

        if(end < 0) return false;

        int l = 0;

        int [] cnt1 = new int [26];
        int [] cntw = new int [26];

        // 初始化窗口
        for(int i = 0; i <= ww; i++){
            cnt1[s1.charAt(i) - 'a'] ++;
            cntw[s2.charAt(i) - 'a'] ++;
        }
        //for(int i = 0; i < 26; i++){
        //    System.out.print(cntw[i] + " ");
        //} System.out.println();

        if(Arrays.equals(cnt1, cntw)){
            return true;
        }

        // 移动窗口 (固定窗口用 l 描述窗口位置, ww 描述窗口大小;
        // 变长窗口用 r 描述窗口位置,r-l 描述窗口大小)
        while(l < end){
            cntw[s2.charAt(l) - 'a'] --; // 移出窗口的左端点
            cntw[s2.charAt(l + ww + 1) - 'a'] ++; // 进入窗口的右端点
            
            //System.out.println(l);
            //for(int i = 0; i < 26; i++){
            //    System.out.print(cntw[i] + " ");
            //} System.out.println();

            if(Arrays.equals(cnt1, cntw)){
                return true;
            }
            l++;
        }
        return false;
    }
}

子串排列

串联所有单词的子串

需要做一个偏移:

for(int i = 0; i < alen; i++){
	// 初始化窗口 及 滑动窗口
}

用哈希表的话,意味着:hash表为空,就可以判断符合条件
同理,数组也可以用一个计数变量 isEmpty 记录当前计数数组是否为空

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        int n = words.length, alen = words[0].length();
        int tlen = n * alen, slen = s.length();

        List<Integer> ans = new ArrayList<Integer>();
        if(tlen > slen) return ans;

        for(int i = 0; i < alen; i++){
            if(slen < n * alen + i) return ans; // 如果不足以进入一个窗口,就直接退出
            // Map<--> cntt = new HashMap<-->(), cntw = new HashMap<-->();
            // 用差数组简化两个数组的比较
            HashMap<String, Integer> diff = new HashMap<String, Integer>();

            // 初始化窗口
            int l = i, r = i;
            for(; r < tlen; r += alen){
                // add(cntt, words[r/alen]);
                // add(cntw, s.substring(r, r + alen));
                add(diff, s.substring(r, r + alen));
            }
            // 初始化窗口与模版的差值
            for(String word : words){
                sub(diff, word);
            }
            // 窗口的比较器初始化后刚好匹配模版
            if(diff.isEmpty()) ans.add(l);

            // 滑动窗口
            while(r < slen - alen + 1){
                // 滑动
                add(diff, s.substring(r, r + alen));
                sub(diff, s.substring(l, l + alen));
                r += alen;
                l += alen;
                // 当前窗口的比较器 匹配了模版
                if(diff.isEmpty()) ans.add(l); // 
            }
        }
        return ans;
        
    }
    void add(HashMap<String, Integer> cnt, String key){
        // Integer vcnt = cnt.get(key);
        // if(vcnt != null) cnt.put(key, vcnt + 1);
        // else cnt.put(key, 1);
        cnt.put(key, cnt.getOrDefault(key, 0) + 1);
        if(cnt.get(key) == 0) cnt.remove(key);
    }
    void sub(HashMap<String, Integer> cnt, String key){
        cnt.put(key, cnt.getOrDefault(key, 0) - 1);
        if(cnt.get(key) == 0) cnt.remove(key);
    }
}

变长滑窗题

连续子串解题思想(窗口思想):

窗口一端确定,另一端的可解性才存在,不确定一端无法确定另一端

  • 扩大窗口 - 比较模版(需求),从无到有
  • 缩小窗口 - 比较模版,从有到无,找到当前右界的最优解
  • 更新最优解 / 加入可解集合

长度最小的子数组

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int cnt = 0;
        int l = 0, r = 0;
        int min = 7 << 60; // 最小值需求,存储一个极大值
        while(r < nums.length){
            cnt += nums[r]; // 子串问题要有
            if(cnt >= target){
                while(l <= r){
                    cnt -= nums[l];
                    if(cnt < target){
                        int len = r - l + 1;
                        if(min > len) min = len;

                        l++;
                        break;
                    }
                    l++;
                }
            }
            r++;
        }
        if(min == 7 << 60) return 0;
        return min;
    }
}

最小覆盖子串

class Solution {
    public String minWindow(String s, String t) {
        int tlen = t.length();
        int slen = s.length();
        // 在 s 里面找最小的模版 t
        if(tlen > slen){
            return "";
        }

        int l = 0;
        int r = tlen;
        // 初始化窗口的比较器(计数数组比较器)
        int [] cntt = new int [52];
        int [] cntw = new int [52];
        for(int i = 0; i < tlen; i++){
            ++cntt[getIdx(t.charAt(i))];
            ++cntw[getIdx(s.charAt(i))]; // 窗口最小也得是t串的长度
        }
        // 比较当前窗口是否符合目标
        if(Arrays.equals(cntt, cntw)){
            return s.substring(0, tlen);
        }
        
        String ans = null;
        // 滑动窗口
        while(r < slen){
            ++cntw[getIdx(s.charAt(r))]; // 移动窗口,更新比较器
            // 进行比较
            if(compare(cntt, cntw)){ // 窗口满足条件
                // 尝试缩小窗口,记录当前窗口的最小收缩值
                // 比较当前窗口最小值和历史最小值
                while(l <= r){
                    --cntw[getIdx(s.charAt(l))];
                    if(!compare(cntt, cntw)){
                        // 记录当前窗口最小值
                        if(ans == null || ans.length() > r - l + 1)
                            ans = s.substring(l, r + 1);
                        l ++;
                        break;
                    }
                    l++; // 符合条件,就继续缩小窗口
                }
            }
            r++;
        }
        return ans != null ? ans : "";
    }


    int getIdx(char c){
        return c <= 'Z' && c >= 'A' ? c -'A' + 26: c - 'a';
    }
    boolean compare(int [] cntt, int [] cntw){
        for(int i = 0; i < 52; i++){
            if(cntt[i] != 0){
                if(cntw[i] < cntt[i]) return false;
            }
        }
        return true;
    }
}

无重复字符的最长子串

计数哈希解法(6ms)

class Solution {
    public int lengthOfLongestSubstring(String s) {
        char [] chrs = s.toCharArray();
        Map<Character, Integer> map = new HashMap();
        int maxLen = 0;
        int l = 0, r = 0;
        while(r < chrs.length){
            int cnt = map.getOrDefault(chrs[r], 0);

            if(cnt == 0) // 没有重复,扩大窗口
                map.put(chrs[r++], cnt + 1);
            else { // 有重复,缩小窗口
                int len = r - l;
                if(maxLen < len) maxLen = len;
                while(l < r){
                    map.put(chrs[l], map.get(chrs[l]) - 1);
                    if(chrs[l++] == chrs[r]) break;
                }
            }
        }
        int len = r - l;
        if(maxLen < len) return len;
        return maxLen;
    }
}

计数数组解法(1ms 击败 100%)

class Solution {
    public int lengthOfLongestSubstring(String s) {
        char [] chrs = s.toCharArray();
        int [] cntw = new int [128];
        int l = 0, r = 0;
        int ans = 0;
        while(r < chrs.length){
            if(cntw[chrs[r]] == 0) {
                ++ cntw[chrs[r++]];
                continue;
            }
            if(ans < r - l) ans = r - l;
            while(l < r) {
                -- cntw[chrs[l++]];
                if(chrs[l - 1] == chrs[r]) break;
            }
        }
        return ans < (r - l)? (r - l) : ans;
    }
}

交集滑窗题(公共子串)

最长重复子数组

解法1:暴力

解法2:交集滑窗 (33ms,\(O(N^2)\)

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        // 两个串的长短
        int [] minArr = nums1.length < nums2.length ? nums1 : nums2;
        int [] maxArr = minArr == nums1 ? nums2 : nums1;
        int minLen = minArr.length, maxLen = maxArr.length;
        int ans = 0; // 记录 最长公共子串 的 历史记录(长度)

        int res = 0;
        for(int i = 1; i <= minLen; i++){ // 短入长
            for(int j = 0; j < i; j++){
                while(j < i && minArr[minLen - i + j] == maxArr[j]){
                    // minArr[minLen - j - 1] -- {(-j) != (j)}
                    // 滑动窗口 是 同向移动,所以 两个Arr / 左右指针 必须 同时 +或-
                    res++; j++;
                }
                if(res > ans) {
                    ans = res;
                    res = 0;
                }
                if(res != 0) res = 0;
            }
        }

        int l = 0, r = minLen;
        while(r < maxLen){ // r 负责判断固定窗口是否还能滑动 
            for(int j = 0; j < minLen; j++){ // l 负责固定窗口迭代
                while(j < minLen && minArr[j] == maxArr[l + j]){
                    res++; j++;
                }
                if(res > ans) {
                    ans = res;
                    res = 0;
                }
                if(res != 0) res = 0;
            }
            l++; r++;
        }
        for(int i = minLen; i > 0; i--){ // 短出长
            for(int j = 0; j < i; j++){
                while(j < i && minArr[j] == maxArr[maxLen - i + j]){
                    res++; j++;
                }
                if(res > ans) {
                    ans = res;
                    res = 0;
                }
                if(res != 0) res = 0;
            }
        }
        return ans;
    }
}

解法3:动态规划(33ms,\(O(N^2)\)

动态规划的解法很简洁

class Solution {
    public int findLength(int[] A, int[] B) {
        int alen = A.length, blen = B.length;
        int [][] dp = new int [alen + 1][blen + 1];
        int ans = 0;
        for(int i = 1; i <= alen; i++){
            for(int j = 1; j <= blen; j++){
                dp[i][j] = A[alen - i] == B[blen - j] ? dp[i - 1][j - 1] + 1 : 0;
                if(dp[i][j] > ans) ans = dp[i][j];//System.out.print(dp[i][j]+" ");
            }//System.out.println();
        } return ans;
    }
}

解法4:二分搜索+哈希表(8ms,\(O(NlogN)\)

posted @ 2023-08-13 12:26  你好,一多  阅读(13)  评论(0编辑  收藏  举报