1、回溯+剪枝
当一个问题可以通过枚举解决时,可以采用 回溯+剪枝 解决。
剪枝的方式往往是利用已知的一些条件来提前终止程序对选择树的深搜 或者 记忆化等技巧。
回溯解决问题的关键是,找清楚所有需要的 状态,以及 这些状态在回溯前后的变化,最好提前将回溯选择树画出来并标注好状态变化,再下手。
function backTrack() {
if (conditon) doSth(res);
for (i of arr) {
reduce(...arguments,res);
backTrack(...arguments,res);
increase(...arguments,res);
}
return res;
}
2、滑动窗口
滑动窗口可以用来解决一维数组中的连续子序列问题。
思想是用左右指针p、q界定的滑动窗口,依照已知的筛选条件去不断扩大窗口直至最大,找到最大窗口的最值,然后移动窗口(2种移动情况),最终滑完整个序列,取得结果。
移动窗口有两种情况,假定滑动窗口由 left 与 right 界定:
一是 窗口重新收缩到最小,再从right的后一位置开始寻找, 简称“缩移”,代码表现为
left = ++right;
二是 窗口左边界left不断向前移动至整个窗口满足条件,再移动右边界right直至不满足条件,求最值,再移动左边界…周而复始,直到滑完整个窗口,简称"螃蟹移动",代码表现为
function getMax(arr) {
left = 0;
right = 1;
res = 0;
while(right < arr.length) {
while(left < right && condition) left++;
while(!conditon) right++;
}
return res;
}
3、贪心
当一个问题可以分割成俄罗斯套娃般的子问题时,可以用局部最优解获取到全局最优解。
使用贪心的很重要一步是证明该贪心解法的正确性。
4、分治
当一个问题可以分割成多个且相互独立的子问题时,可以divide成若干个子片段conquer,然后将所有子片段的解整合,取得整体的最优解。
整合的过程往往有可观察到的数学关系,利用这个关系,可得最优解。
function dc(left, mid, right) {
if(left == right) return res;
return dc(left, mid) + dc(mid, right);
}
5、双指针
双指针技巧常用在 一维数组、(单/双/有环/无环)链表
第一种用法:同向快慢指针,可以用于求中点,判断链表是否有环等
第二种用法:相向指针,快排就是一个完美的体现,往往配合pivot一起用。
6、随机算法
第一种 洗牌算法 shuffle
- 抽牌法:从原牌堆抽出,组成新牌 ( 时间O(n²) 空间O(n) )
const shuffle = arr => {
let copy = [], n = arr.length;
while(n) {
let j = parseInt(Math.random() * n--);
copy.push(...arr.splice(j,1));
}
return copy;
};
- 换牌法:从牌堆抽出一张,与牌堆最后一张交换,再从未抽出的剩余牌堆中继续抽牌,以此类推。 (时间O(n) 空间O(1) )
const shuffle = arr => {
for(let i=arr.length-1; i>0; i--){
let j = parseInt(Math.random()*(i+1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
};
- 对于长度在动态增加或不知长度的情况,可以对换牌法进行优化:从前往后扫,随机插入新牌堆
const shuffle = arr => {
let copy = [], i = 0;
while(!!arr[i]) {
let j = parseInt(Math.random()*(copy.length+1));
if(!!copy[j]) {
let tmp = copy[j];
copy[j] = arr[i];
copy.push(tmp);
}else {
copy[j] = arr[i];
}
i++;
}
return copy;
};
第二种 蓄水池抽样 直接贴板子吧
const reservoir = (data_stream, m) => {
let n = data_stream.length;
let reservoir = new Array(m);
for(let i=0;i<reservoir.length;i++)
reservoir[i] = data_stream[i];
for(let i=m;i<n;i++){
let j = parseInt(Math.random()*(i+1));
if(j>=0 && j<m) reservoir[j] = data_stream[i];
}
return reservoir;
};
7、动态规划
当一个问题可以分割成多个子问题,且该问题的解可以通过反复调用并比较子问题的解最终求出大问题的解。
线性DP:单串(无/有维度)、双串、矩阵
前缀和DP:区间和、前缀和、前缀积、前缀异或、差分
区间DP:回文、区间
背包DP
状压DP
计数问题
矩阵快速幂
数位DP
(暂无板子,占个位)
8、搜索
深搜:拿最具一般性的图来举例子,dfs某图G的某结点v,假定G用邻接矩阵表示
[
[0, 1, 0, 1],
[1, 0, 0, 0],
[0, 0, 1, 0]
]
需要用visted = [] 来标注已遍历过的结点,也可以理解为用于剪枝的手段。
DFS可以用递归写法,也可以用递归等效的栈表示法
BFS可以用队列,也可以用栈
9、高斯消元
占坑
01010010101001010010100 数位反转,异或
[[010100010], …, [00100000101]] 得增广矩阵,高斯消元
10、位运算
占坑
异或^ 按位与& 按位或|
11、二分查找
精确查找
二分找界限(上界)
二分找界限(下界)
二分找区间
占坑
12、排序
快排
堆排
归并
选择
冒泡
桶排
…
13、模拟
栈:出栈顺序+入栈顺序
二叉树:前(后)遍历 + 中序遍历
——————————————
最后,记录递归与尾递归的板子
递归:
尾递归: