文章目录
一,双指针
1.1链表
1、合并两个有序链表
2、链表的分解
3、合并 k 个有序链表(用到归并思想,每次合并两条链表)
4、寻找单链表的倒数第 k 个节点(前后两个指针,先走k步)
5、寻找单链表的中点(快慢指针,fast走两步,slow走一步,当 fast 走到链表末尾时,slow 就指向了链表中点)
6、判断单链表是否包含环
并找出环起点
(快慢指针,fast2步,slow1步,当fast.next为null,则无环,若fast与slow相遇,有环)
(快慢指针相遇时,fast回到起点,slow继续在相遇点,继续走,两指针每次走1步,再次相遇则是相遇点)
7、判断两个单链表是否相交
并找出交点
(链表A后面接链表B,链表B后面接链表A,两指针走,当走到两指针值一样,则是交点,且有交点)
1.2数组
左右指针
和快慢指针
【原地修改数组】(快慢指针
)
1、删除有序数组中的重复项(快慢指针,fast遇到重复项跳过,否则赋值给slow)
2、移动零,将数组中的所有值为 0 的元素移到数组末尾(思路与上题一样,fast遇到0跳过,最后把slow后面都赋值为0)
左右指针
1、二分查找(left与right指针)
2、升序数组,两数之和为目标值(左右指针,开始在两端,相向而行)
3、反转数组(左右指针,开始在两端,相向而行,交换值)
4、回文串判断(左右指针,开始在两端,相向而行)
5、最长回文子串(左右指针,从中间向两端,得到回文子串。原字符串每个i遍历一遍,每次都pal(i,i)与pal(i,i+1)。以 s[i] 为中心,以 s[i] 和 s[i+1] 为中心)
二,二叉树
思想
1、是否可以通过
遍历
一遍二叉树得到答案?
回溯算法「遍历」
如果可以,用一个 traverse 函数配合外部变量
来实现,这叫「遍历」的思维模式。
没有返回值的backtrack函数
2、是否可以定义一个
递归函数
,通过子问题(子树
)的答案推导出原问题的答案?
动态规划算法「分解子树」
如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值
,这叫「分解问题」的思维模式。
带有返回值的 dp 函数
层序遍历(BFS框架)
迭代遍历
用队列
实现,常用于求无权图的最短路径问题
三,动态规划
动态规划问题
一般形式就是求最值
(比如说:求最长递增子序列,最小编辑距离呀等等)
核心问题是穷举
列出正确的「状态转移方程
」
判断算法问题是否具备「最优子结构
」,是否能够通过子问题的最值得到原问题的最值
存在「重叠子问题
」,使用「备忘录
」或者「DP table
」来优化穷举过程,避免不必要的计算
思想
明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
# 方法1:自顶向下递归的动态规划
def dp(状态1, 状态2, ...):
for 选择 in 所有可能的选择:
# 此时的状态已经因为做了选择而改变
result = 求最值(result, dp(状态1, 状态2, ...))
return result
# 方法2:自底向上迭代的动态规划
# 初始化 base case
dp[0][0][...] = base case
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
1、斐波那契数列
方法1(暴力穷举)
方法2(带备忘录的递归解法)
造一个「备忘录
」(数组或哈希表),每次算出某个子问题的答案后别急着返回,先记
到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查
一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用
方法3(dp 数组的迭代解法)
把这个「备忘录」独立出来成为一张表,通常叫做 DP table
,在这张表上完成「自底向上」的推算
2、凑零钱问题,零钱兑换
四,回溯算法
回溯算法是在遍历「树枝」
,DFS 算法是在遍历「节点」
思想
1、路径
:也就是已经做出的选择。
2、选择列表
:也就是你当前可以做的选择。
3、结束条件
:也就是到达决策树底层,无法再做选择的条件。
回溯框架
核心
就是 for 循环里面的递归,在递归调用之前
「做选择
」,在递归调用之后
「撤销选择
」
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
1、全排列
2、N皇后。
本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择
是,在该行的任意一列放置一个皇后。
因为皇后是一行一行从上往下放,只检查了左上角,右上角和上方的格子
(排列
,组合
,子集
)
主要变体:
形式一、元素无重不可复选
,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式。
形式二、元素可重不可复选
,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次。
(如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历)
(需要先进行排序
,让相同的元素
靠在一起,如果发现 nums[i] == nums[i-1],则跳过
:)
形式三、元素无重可复选
,即 nums 中的元素都是唯一的,每个元素可以被使用若干次。
五,BFS算法
本质
:在一幅「图」中找到从起点 start 到终点 target 的最短路径
BFS框架:
(队列与 哈希表)
var BFS = function(start, target) {
var q = []; // 核心数据结构
var visited = new Set(); // 避免走回头路
q.push(start); // 将起点加入队列
visited.add(start);
var step = 0; // 记录扩散的步数
while (q.length != 0) {
var sz = q.length;
/* 将当前队列中的所有节点向四周扩散 */
for (var i = 0; i < sz; i++) {
var cur = q.shift();
/* 划重点:这里判断是否到达终点 */
if (cur === target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (var x in cur.adj()) {
if (!visited.has(x)) {
q.push(x);
visited.add(x);
}
}
}
/* 划重点:更新步数在这里 */
step++;
}
};
1、二叉树的最小高度
2、打开转盘锁(解开密码锁的最少次数,有死亡锁)
问题:
1、会走回头路。
2、终止条件。
3、对 deadends 的处理
解决方法:
1,用set记录需要跳过的死亡密码(剪枝),已经穷举过的密码
双向BFS
本质:
传统的 BFS 框架就是从起点开始向四周扩散,遇到终点时停止;
双向 BFS
则是从起点
和终点
同时开始扩散,当两边有交集
的时候停止
。(局限性
:必须知道终点在哪里)
六,二分搜索算法
寻找一个数
[left,right]
while(left<=right)
相等便返回
var binarySearch = function(nums, target) {
var left = 0; // 注意
var right = nums.length - 1; // 注意
while(left <= right) {
var mid = left + Math.floor((right - left) / 2);
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
};
寻找左侧边界
[left,right)
while(left<right)
相等后,在[left,mid-1]内搜索,即[left, mid)
var left_bound = function(nums, target) {
var left = 0;
var right = nums.length; // 注意
while (left < right) { // 注意
var mid = left + Math.floor((right - left) / 2);
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意
}
}
return left;
}
寻找右侧边界
[left,right)
while(left<right)
相等后,在[mid+1,right)内搜索,即[mid + 1, right)
var right_bound = function(nums, target) {
var left = 0, right = nums.length;
while (left < right) {
var mid = left + Math.floor((right - left) / 2);
if (nums[mid] === target) {
left = mid + 1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left - 1; // 注意
}
七,滑动窗口
框架
var slidingWindow = function(s) {
const window = new Map();
let left = 0, right = 0;
while (right < s.length) {
// c 是将移入窗口的字符
const c = s[right];
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
...
// (循环)判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
const d = s[left];
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
};
八,股票买卖
状态机
dp[i][k][0 or 1]
0 <= i <= n - 1, 1 <= k <= K
n 为天数,大 K 为交易数的上限,0 和 1 代表是否持有股票。
此问题共 n × K × 2 种状态,全部穷举就能搞定。
状态转移方程
base case:
dp[-1][...][0] = dp[...][0][0] = 0
dp[-1][...][1] = dp[...][0][1] = -infinity
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
九,并查集
let parent=new Array(n).fill(0).map((element,index)=>index)
const find=function(parent,index){
if(parent[index]!=index){
parent[index]=find(parent,parent[index])
}
return parent[index]
}
const union=function(parent,index1,index2){
parent[find(parent,index1)]=find(parent,index2)
}