labuladong(一) 数组/链表

303 阅读4分钟

数组

前缀和-数组

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

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

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

560. 和为 K 的子数组

差分-数组

主要适⽤场景是频繁对原始数组的某个区间的元素进⾏增减

370. 区间加法

1109. 航班预订统计

1094. 拼车

快慢指针

左右指针

滑动窗口

主要适用寻找子串的场景

76. 最小覆盖子串

567. 字符串的排列

438. 找到字符串中所有字母异位词

3. 无重复字符的最长子串

代码框架

    /**
     * 在字符串 s 中,根据需求,寻找和 target 有关联的子串
     */
    <R> method(String target, String s) {
        // 窗口的左、右指针 
        int left = 0, right = 0;

        // 统计target
        Map<Character, Integer> need = new HashMap<>();
        // 统计窗口
        Map<Character, Integer> window = new HashMap<>();
   
        while(right < s.lenght) {
            // 更新 window map
            
            // 比较taget window,判断是否满足需求 
            ...
            
            // 窗口缩进
            while(
                // 判断窗口是否要缩进
                // 常见1:window内元素 >= 需求所需的元素
                // 常见2:window 达到某种限制
                //      (比如寻找固定长度子串,窗口长度恒定为所需子串长度,不得超出)
            ) {
                // 更新window map
                
                // 比较taget window,判断是否满足需求 
                ...
                left++;
            }
            right++;
        }
    }

二分查找

常见场景有:寻找一个元素、寻找元素的右侧边界、寻找元素的左侧边界

34. 在排序数组中查找元素的第一个和最后一个位置

代码框架

int l = 0, r = nums.length - 1;

while (l <= r) {
    int mid = l + (r - l) / 2;

    if (nums[mid] == target) {
        // 1) 寻找一个元素
        // return mid;
        
        // 2) 寻找左侧边界
        // 此处就缩进右侧边界
        // r = mid - 1;
        
        // 2) 寻找右侧边界
        // 此处就缩进左侧边界
        // l = mid + 1;       
    } else if (nums[mid] > target) {
        r = mid - 1;
    } else if (nums[mid] < target) {
        l = mid + 1;
    }
}

// 1) 寻找一个元素
// while内未找到,说明不存在
// return -1;

// 2) 寻找左侧边界
// if (l >= nums.length || nums[l] != target) { return -1; }  // 越界处理
// return l; 

// 3) 寻找右侧边界
// if (r < 0 || nums[r] != target) { return -1; }  // 越界处理
// return r; 

二分查找泛化

针对抽象的算法问题,从以下几点入手:

1、寻找单调(递增/递减)关系,确定 x, f(x), target 分别是什么,并写出单调函数 f(x) 的代码。

2、找到 x 的取值范围作为⼆分搜索的搜索区间,初始化 leftright 变量。

3、在数组[f(left), ... , f(right)]中二分查找 target

4、根据题⽬的要求,确定应该使⽤搜索左侧还是搜索右侧的⼆分搜索算法,写出解法代码。

875. 爱吃香蕉的珂珂

1011. 在 D 天内送达包裹的能力

410. 分割数组的最大值

代码框架

// 函数 f 是关于⾃变量 x 的单调函数
int f(int x) {
    // 就有序数组而言: return nums[x];
    // ...
}

// 主函数,在 f(x) == target 的约束下求 x 的最值
int solution(int[] nums, int target) {
    if (nums.length == 0) return -1;
    // 问⾃⼰:⾃变量 x 的最⼩值是多少?
    int left = ...;
    // 问⾃⼰:⾃变量 x 的最⼤值是多少?
    int right = ... + 1;

    while (left < right) {
        int mid = left + (right - left) / 2;
        if (f(mid) == target) {
            // 问⾃⼰:题⽬是求左边界还是右边界?
            // ...
        } else if (f(mid) < target) {
            // 问⾃⼰:怎么让 f(x) ⼤⼀点?
            // ...
        } else if (f(mid) > target) {
            // 问⾃⼰:怎么让 f(x) ⼩⼀点?
            // ...
        }
    }
    return left;
}

田忌赛马

从最快的⻢开始,⽐得过就⽐,⽐不过就送

870. 优势洗牌

代码框架

int n = nums1.length;

sort(nums1); // ⽥忌的⻢ 
sort(nums2); // ⻬王的⻢ 

// 从最快的⻢开始⽐ 
for (int i = n - 1; i >= 0; i--) {  
    if (nums1[i] > nums2[i]) {  
        // ⽐得过,跟他⽐  
    } else {
        // ⽐不过,换个垫底的来送⼈头
    }
}

原地修改数组

一般采用快慢(双)指针

slow = 0, fast = 0

fast在前面探路(不停往前),找到满足条件的值就扔给slow(或者交换)

slow收到fast扔来的值,再往前走

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

83. 删除排序链表中的重复元素

27. 移除元素

283. 移动零

代码框架

int slow = 0, fast = 0;

for(; fast < nums.length; fast++) {
    if (meetCondition(nums[fast])) { // nums[fast] 满足条件
        // 将值扔给 slow,或者交换
        nums[slow] = nums[fast];
        // slow 前进
        slow++;
    }
}

链表

单链表

虚拟头结点:避免空结点判断

双/快慢指针:寻找结点(环形链表交点、链表中间结点、链表倒数第N个结点)

21. 合并两个有序链表

23. 合并K个升序链表

141. 环形链表

142. 环形链表 II

876. 链表的中间结点

19. 删除链表的倒数第 N 个结点

160. 相交链表

链表的递归思维

不要跳进递归,⽽是利⽤明确的定义来实现算法逻辑

处理看起来⽐较困难的问题,可以尝试化整为零,把⼀些简单的解法进⾏修改,解决困难的问题。

和迭代解法相⽐,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),⽽递归解法需要堆栈,空间复杂度是 O(N)。

考虑效率的话还是使⽤迭代算法更好

206. 反转链表

92. 反转链表 II