算法概述
labuladong的算法小抄
第零章
学习算法和刷题的思路指南
数据结构的基本存储方式就是链式和顺序两种,基本操作就是增删查改,遍历方式无非迭代和递归。
回溯算法就是个 N 叉树的前后序遍历问题,没有例外
动态规划详解
首先,动态规划问题的一般形式就是求最值
重叠子问题、最优子结构、状态转移方程就是动态规划三要素。
动态规划中使用递归,需要解决重叠子问题
明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
由迭代完成的「自底向上」的动态规划 ——————>DP table
# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
要符合「最优子结构」,子问题间必须互相独立。
动态规划套路:
- 明确 dp 函数/数组的定义
- 找出关系数组元素间的关系式子:**列出正确的状态转移方程**
- 找出初始值(即不能用转移矩阵计算的值)
举例:322.凑零钱问题
dp[i]表示金额i所需要的最少硬币数:
public class Solution {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[amount + 1]; //给一个初始值,相当于白送
Arrays.fill(dp, max);//数组大小为 amount + 1,初始值也为 amount + 1
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1); //Math.min() : 用于在coins数组de遍历中不断更新dp[i]
}
}
}
return dp[amount] > amount ? -1 : dp[amount]; //如果dp[amount]的值没有变过,说明找不到硬币组合,返回-1
}
}
PS:为啥 dp 数组初始化为 amount + 1 呢,因为凑成 amount 金额的硬币数最多只可能等于 amount(全用 1 元面值的硬币),所以初始化为 amount + 1 就相当于初始化为正无穷,便于后续取最小值。
回溯算法详解(DFS)
你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
代码方面,回溯算法的框架:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」
for 选择 in 选择列表:
# 做选择
将该选择从选择列表移除
路径.add(选择)
backtrack(路径, 选择列表)
# 撤销选择
路径.remove(选择)
将该选择再加入选择列表
我们只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确得到每个节点的选择列表和路径。
46. 全排列
public class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
public void backtrack(int[] nums, LinkedList<Integer> track) {
// 触发结束条件
if (track.size() == nums.length) {
res.add(new LinkedList(track));
return;
}
for (int i = 0; i < nums.length; i++) {
// 排除不合法的选择
if (track.contains(nums[i]))
continue;
// 做选择
track.add(nums[i]);
// 进入下一层决策树
backtrack(nums, track);
// 取消选择
track.removeLast();
}
}
}
BFS算法框架套路详解
问题描述:问题的本质就是让你在一幅「图」中找到从起点 start 到终点 target 的最近距离,这个例子听起来很枯燥,但是 BFS 算法问题其实都是在干这个事儿
BFS 借助队列做到一次一步「齐头并进」,是可以在不遍历完整棵树的条件下找到最短距离的
形象点说,DFS 是线,BFS 是面;DFS 是单打独斗,BFS 是集体行动。
框架
// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
Queue<Node> q; // 核心数据结构
Set<Node> visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
int step = 0; // 记录扩散的步数
while (q not empty) {
int sz = q.size(); //实时记录更新遍历到的当前层的节点个数
for (int i = 0; i < sz; i++) { /* 将当前队列中的所有节点向四周扩散 */
Node cur = q.poll();
if (cur is target) /* 划重点:这里判断是否到达终点 */
return step;
for (Node x : cur.adj()) /* 将 cur 的相邻节点加入队列 */
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
step++; /* 划重点:更新步数在这里 */
}
}
二分查找详解
最基本的二分查找算法:
因为我们初始化 right = nums.length - 1
所以决定了我们的「搜索区间」是 [left, right]
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1
因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回
int binary_search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// 直接返回
return mid;
}
}
// 直接返回
return -1;
}
寻找左侧边界的二分查找:
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
//找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, right] 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
right = mid - 1;
}
}
////当要查找的目标元素不存在的时,分两种情况:(1)target 很大,left越右界(2)target 很小:right越左界
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
寻找右侧边界的二分查找:
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定右侧边界
left = mid + 1;
}
}
// 最后要检查 right 越界的情况
if (right < 0 || nums[right] != target)
return -1;
return right;
}
滑动窗口
滑动窗口的思想:
用i,j表示滑动窗口的左边界和右边界,通过改变i,j来扩展和收缩滑动窗口,可以想象成一个窗口在字符串上游走,当这个窗口包含的元素满足条件,即包含字符串T的所有元素,记录下这个滑动窗口的长度j-i+1,这些长度中的最小值就是要求的结果。步骤一
不断增加j使滑动窗口增大,直到窗口包含了T的所有元素步骤二
不断增加i使滑动窗口缩小,因为是要求最小字串,所以将不必要的元素排除在外,使长度减小,直到碰到一个必须包含的元素,这个时候不能再扔了,再扔就不满足条件了,记录此时滑动窗口的长度,并保存最小值步骤三
让i再增加一个位置,这个时候滑动窗口肯定不满足条件了,那么继续从步骤一开始执行,寻找新的满足条件的滑动窗口,如此反复,直到j超出了字符串S范围。

浙公网安备 33010602011771号