数组
二分查找
注意边界条件的选择,常选用左闭右闭或者左闭右开,采用左闭右闭即[left,right]时,注意循环判断中采用<=;采用左闭右开时,注意循环判断中采用<。
双指针法
使用两个指针在一个for循环下完成两个for循环的工作
滑动窗口法
滑动窗口是双指针法的一种特殊形式,通常用于解决子数组或子字符串问题。
常用模板:
int left = 0, right = 0;
while (right < s.length()) {
// 扩大窗口
char c = s.charAt(right);
right++;
// 更新窗口状态
// 当窗口满足条件时,尝试缩小窗口
while (窗口满足条件) {
// 更新结果
char d = s.charAt(left);
left++;
// 更新窗口状态
}
}
链表
虚拟头节点
对于需要遍历所有节点的链表,可以添加一个虚拟头节点,使得头节点的便利不再特殊,简化代码。
哈希表
数组实现
如果要记录的键为字母,则可以采用数组来进行记录,相较于map节省了大量空间。
set实现
当要记录的键为范围较大的数字时,如果仍然采用数组,会造成较大的空间浪费,而set与map不会提前开辟所有可能键的空间,如果不需要记录每个键对应的值,只看是否重复,则采用set。
map实现
采用理由类似set,区别在于map需要记录的是键值对,当要保存键对应的元素时采用。
字符串
KMP算法
用于在一个主串(文本串)中查找一个模式串(子串)的出现位置
-
构建 next 数组:
-
next 数组记录了模式串中每个位置的最长相等前缀和后缀的长度。
-
通过 next 数组,可以在匹配失败时快速移动模式串的指针,避免重复匹配。
-
-
匹配过程:
-
使用双指针法,一个指针遍历主串,另一个指针遍历模式串。
-
当字符匹配时,两个指针同时后移。
-
当字符不匹配时,根据 next 数组移动模式串的指针,而不是回溯主串的指针。
-
private int[] getNext(String pattern) {
int n = pattern.length();
int[] next = new int[n];
next[0] = 0;
int j = 0;
for (int i = 1; i < n; i++) {
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
// KMP 匹配
public int kmp(String text, String pattern) {
int[] next = getNext(pattern);
int j = 0;
for (int i = 0; i < text.length(); i++) {
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
if (text.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == pattern.length()) {
return i - j + 1;
}
}
return -1;
}
二叉树
前序遍历
遍历顺序:根节点 -> 左子树 -> 右子树
应用场景
-
二叉树的构造:例如根据前序遍历和中序遍历构造二叉树。
-
深度优先搜索(DFS):前序遍历是 DFS 的一种实现方式。
-
复制二叉树:可以通过前序遍历复制一棵二叉树。
void preorder(TreeNode root) {
if (root == null) return;
System.out.print(root.val + " "); // 访问根节点
preorder(root.left); // 递归左子树
preorder(root.right); // 递归右子树
}
中序遍历
遍历顺序:左子树 -> 根节点 -> 右子树
应用场景
-
二叉搜索树(BST)的属性:中序遍历 BST 会得到一个升序序列。
-
表达式树:中序遍历可以还原中缀表达式。
-
验证二叉搜索树:通过中序遍历检查是否满足升序。
void inorder(TreeNode root) {
if (root == null) return;
inorder(root.left); // 递归左子树
System.out.print(root.val + " "); // 访问根节点
inorder(root.right); // 递归右子树
}
后序遍历
遍历顺序:左子树 -> 右子树 -> 根节点
应用场景
-
普通二叉树的属性:例如计算二叉树的高度、判断二叉树是否平衡等。
-
删除二叉树:后序遍历可以确保在删除根节点之前先删除子节点。
-
表达式树:后序遍历可以得到后缀表达式(逆波兰表达式)。
void postorder(TreeNode root) {
if (root == null) return;
postorder(root.left); // 递归左子树
postorder(root.right); // 递归右子树
System.out.print(root.val + " "); // 访问根节点
}
回溯法
采用递归进行纵向遍历,采用for循环进行横向遍历,常在横向时进行剪枝操作来提高性能。
模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
应用场景:
组合问题
切割问题
子集问题
排列问题
棋盘问题
贪心算法
通过局部最优来达到整体最优
动态规划
基本步骤:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
public class DynamicProgrammingTemplate {
public static void main(String[] args) {
// 确定dp数组以及下标的含义
// 例如:dp[i] 表示以i结尾的子数组的最大/最小值等
// 确定dp数组的长度
int n = /* 数组或问题的规模 */;
int[] dp = new int[n + 1]; // 有时候需要多一个空间,具体问题具体分析
// dp数组如何初始化
// 初始化dp数组的起始值,通常是dp[0]或者dp[1]
dp[0] = /* 初始化值 */;
// 确定遍历顺序
// 通常是根据问题的性质来确定是正序遍历还是倒序遍历
for (int i = 1; i <= n; i++) {
// 确定递推公式
// dp[i] = /* 根据dp[i-1], dp[i-2]...等之前的值来更新dp[i]的值 */;
// 举例推导dp数组
// 打印dp数组的值,用于调试和理解dp数组的更新过程
System.out.println("dp[" + i + "] = " + dp[i]);
}
// 根据dp数组的最终结果来返回答案
// int result = dp[n]; // 假设最终结果是dp数组的最后一个值
// return result;
}
}