数组
前缀和-数组
主要适⽤的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。
差分-数组
主要适⽤场景是频繁对原始数组的某个区间的元素进⾏增减
快慢指针
左右指针
滑动窗口
主要适用寻找子串的场景
代码框架
/**
* 在字符串 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++;
}
}
二分查找
常见场景有:寻找一个元素、寻找元素的右侧边界、寻找元素的左侧边界
代码框架
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的取值范围作为⼆分搜索的搜索区间,初始化left和right变量。3、在数组
[f(left), ... , f(right)]中二分查找target4、根据题⽬的要求,确定应该使⽤搜索左侧还是搜索右侧的⼆分搜索算法,写出解法代码。
代码框架
// 函数 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;
}
田忌赛马
从最快的⻢开始,⽐得过就⽐,⽐不过就送
代码框架
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扔来的值,再往前走
代码框架
int slow = 0, fast = 0;
for(; fast < nums.length; fast++) {
if (meetCondition(nums[fast])) { // nums[fast] 满足条件
// 将值扔给 slow,或者交换
nums[slow] = nums[fast];
// slow 前进
slow++;
}
}
链表
单链表
虚拟头结点:避免空结点判断
双/快慢指针:寻找结点(环形链表交点、链表中间结点、链表倒数第N个结点)
链表的递归思维
不要跳进递归,⽽是利⽤明确的定义来实现算法逻辑
处理看起来⽐较困难的问题,可以尝试化整为零,把⼀些简单的解法进⾏修改,解决困难的问题。
和迭代解法相⽐,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),⽽递归解法需要堆栈,空间复杂度是 O(N)。
考虑效率的话还是使⽤迭代算法更好