算法 - 常用模板(一)(Java)
很多情况下,一些基础的代码在求解复杂问题时可以进行复用,而无需重复造轮子。因此,本篇对一些常用的基础算法进行了总结。由于笔者目前仍在学习阶段,因此本篇将保持长期更新状态,目前并不代表最终的完全版本。
一、链表相关
1.1 链表数据结构
来自lc。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
1.1 翻转链表
使用前、中、后3个指针,分别命名为pre,cur,nxt。依次进行滚动,最终返回pre。
迭代法实现:
private ListNode reverse(ListNode head) {
ListNode cur = head, pre = null;
while (cur != null) {
ListNode nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
1.2 快慢指针
用于解决找中点,寻找环等问题。快指针移动速度快于慢指针。
public ListNode FindMid(ListNode head) {
ListNode fast = head, slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
二、字符串相关
2.1 字符串匹配(KMP)
字符串匹配,可以使用暴力求解,但复杂度为
O
(
n
2
)
O(n^2)
O(n2),因此,通常使用复杂度更低的
KMP
\textit{KMP}
KMP 算法。
K
M
P
KMP
KMP 算法的核心在于求模式串
p
a
t
t
e
r
n
pattern
pattern 的
n
e
x
t
next
next 数组,
n
e
x
t
next
next 数组有两种理解方式:
1)
n
e
x
t
[
i
]
next[i]
next[i] 表示位置
i
i
i 前面的子串前缀和后缀相同的最大长度。
2)
n
e
x
t
[
i
]
next[i]
next[i] 表示位置
i
i
i 匹配失败后,
p
a
t
t
e
r
n
pattern
pattern 串中下一个与主串当前位置字符进行匹配的字符位置。
模板代码如下:
public int strStr(String mainStr, String pattern) {
int n = mainStr.length(), m = pattern.length();
if (m == 0) {
return 0;
}
int[] next = new int[m];
// 求pattern串的next数组
for (int i = 1, j = 0; i < m; 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;
}
// 字符串匹配
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && mainStr.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
if (mainStr.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == m) {
return i - m + 1;
}
}
return -1;
}
三、二叉树相关
3.1 二叉树数据结构
来自lc。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
3.2 二叉树层序遍历
使用队列来依次存储每一层的节点进行遍历即可。
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
}
3.3 二叉树最近公共祖先(LCA)
求解 LCA(Least Common Ancestor) 问题时:
1)可以使用两次深度搜索进行求解,第一次搜索第一个节点时使用 哈希表 + 回溯 的方法存储所有路过的节点。然后第二次深搜搜索第二个节点时记录最后一个可以从哈希表中查到的节点,即为LCA。
2)使用递归的方法,对于一个节点
n
o
d
e
node
node ,分别从它的左子树和右子树中搜索,只要找到两个节点中的其中一个,就返回。如果在左子树和右子树中分别找到了不同的节点,那么,
n
o
d
e
node
node 即为LCA; 如果在左子树中找到了节点
p
p
p ,而右子树中没有找到节点,那么,节点
p
p
p 为最近公共祖先;如果只在右子树中找到节点,同理。模板代码如下:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
TreeNode l = lowestCommonAncestor(root.left, p, q);
TreeNode r = lowestCommonAncestor(root.right, p, q);
if (l != null && r != null) {
return root;
}
if (l != null) {
return l;
}
return r;
}
3.4 根据二叉树遍历数组中恢复二叉树
3.4.1 前提知识
- 根据中序和前序遍历数组恢复二叉树(唯一)
- 根据中序和后序遍历数组恢复二叉树(唯一)
- 根据前序和后序遍历数组恢复二叉树(只有当所有非叶子节点拥有两个孩子节点时,才能确定唯一的二叉树)
3.4.2 根据前序和中序遍历数组恢复二叉树
- 1)将中序遍历数组的值和索引存在 哈希表 中,方便递归时确定参数。
- 2)因为前序遍历先遍历根节点,然后遍历左子树,最后遍历右子树。因此,前序遍历数组中的值为: [ 根节点值,左子树全部节点值,右子树全部节点值 ] [根节点值,左子树全部节点值,右子树全部节点值] [根节点值,左子树全部节点值,右子树全部节点值] 。
- 3)中序遍历先遍历左子树,然后遍历根节点,最后遍历右子树。因此,中序遍历数组中的值为: [ 左子树全部节点值,根节点值,右子树全部节点值 ] [左子树全部节点值,根节点值,右子树全部节点值] [左子树全部节点值,根节点值,右子树全部节点值]
- 4)所以,递归函数的入参为前序遍历数组的索引
p
r
e
I
d
x
preIdx
preIdx , 以
p
r
e
o
r
d
e
r
[
p
r
e
I
d
x
]
preorder[preIdx]
preorder[preIdx] 为根的左子树或右子树在中序遍历数组中的起始索引和结束索引。
模板代码如下:
class Solution {
Map<Integer, Integer> dict;
public TreeNode buildTree(int[] preorder, int[] inorder) {
dict = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
dict.put(inorder[i], i);
}
return buildTree(preorder, inorder, 0, 0, inorder.length - 1);
}
public TreeNode buildTree(int[] preorder, int[] inorder, int preIdx, int inStart, int inEnd) {
if (inStart > inEnd || preIdx > preorder.length - 1) {
return null;
}
TreeNode root = new TreeNode(preorder[preIdx]);
int inIdx = dict.get(preorder[preIdx]);
root.left = buildTree(preorder, inorder, preIdx + 1, inStart, inIdx - 1);
root.right = buildTree(preorder, inorder, preIdx + inIdx - inStart + 1, inIdx + 1, inEnd);
return root;
}
}
3.5 平衡二叉树(BST)
3.5.1 验证平衡二叉树
分别可以使用前序遍历、中序遍历、后序遍历的方法验证BST,其中后序遍历验证二叉树的方法运用到的递归中的“归”的思想在很多BST的衍生问题中有大量的使用。
3.5.1.1 前序遍历验证BST
从上往下传传值,对于任意节点
n
o
d
e
node
node ,
n
o
d
e
.
v
a
l
node.val
node.val 必须大于传下来的最小值
m
n
mn
mn,小于传下来的最大值
m
x
mx
mx。同时,递归左子树时,将最大值修改为
n
o
d
e
.
v
a
l
node.val
node.val 传下去;递归右子树时,将最小值修改为
n
o
d
e
.
v
a
l
node.val
node.val 传下去。
如果
n
o
d
e
.
v
a
l
node.val
node.val 大于最大值或者小于最小值,则不是一棵BST。
public boolean isValidBST(TreeNode root) {
return isBST(root, Integer.MIN_VALUE, Integer.MAX_VALUE);
}
public boolean isBST(TreeNode root, int mn, int mx) {
if (root == null) {
return true;
}
if (root.val < mn || root.val > mx) {
return false;
}
return root.val > mn && root.val < mx && isBST(root.left, mn, root.val) && isBST(root.right, root.val, mx);
}
3.5.1.2 中序遍历验证BST
因为BST中序遍历的结果是严格非递减的,因此,我们只需要用一个全局变量 p r e pre pre 记录当前节点 n o d e node node 的上一个节点的值,并与当前节点值 n o d e . v a l node.val node.val 进行比较即可进行验证。
private long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
return isBST(root);
}
public boolean isBST(TreeNode root) {
if (root == null) {
return true;
}
if (!isBST(root.left)) {
return false;
}
if (root.val <= pre) {
return false;
}
pre = root.val;
return isBST(root.right);
}
3.5.1.3 后序遍历验证BST
使用后序遍历验证BST是自下而上递归,从叶子节点向上层返回信息,返回的信息为子树中所有结点的最小值和最大值,每个节点必须满足大于左子树的最大值,小于右子树的最小值。否则不是BST。
public boolean isValidBST(TreeNode root) {
return isBST(root)[0] != Long.MIN_VALUE;
}
public long[] isBST(TreeNode root) {
if (root == null) {
return new long[]{Long.MAX_VALUE, Long.MIN_VALUE};
}
long[] l = isBST(root.left);
long[] r = isBST(root.right);
int v = root.val;
if (v <= l[1] || v >= r[0]) {
return new long[]{Long.MIN_VALUE, Long.MAX_VALUE};
}
return new long[]{Math.min(l[0], v), Math.max(r[1], v)};
}
四、数学基础相关
4.1 最大公约数
特别地,两个数的乘积等于两个数的最大公约数和最小公倍数的乘积。
public int gcd(int a, int b) {
int mn = Math.min(a, b), mx = Math.max(a, b),;
while (mn != 0) {
int tmp = mn;
mn = mx % mn;
mx = tmp;
}
return mx;
}
4.2 int类型除法向上取整
4.2.1 Math.ceil()方法
int dividend = 10;
int divisor = 3;
int result = (int) Math.ceil((double) dividend / divisor);
4.2.2 使用加法和整除运算
int dividend = 10;
int divisor = 3;
int result = (dividend + divisor - 1) / divisor;
4.3 快速幂
4.3.1 快速幂-递归实现
public double quickMul(double x, long N) {
if (N == 0) {
return 1.0;
}
double y = quickMul(x, N / 2);
return N % 2 == 0 ? y * y : y * y * x;
}
4.3.2 快速幂-迭代实现
public double quickMul(double x, long n) {
double res = 1.0;
while (n > 0) {
if (n % 2 == 1) {
res *= x;
}
x *= x;
n /= 2;
}
return res;
}
4.4 确定质数
boolean[] np = new boolean[n];
np[1] = true;
for (int i = 2; i * i <= MX; i++) {
if (!np[i]) {
for (int j = i * i; j <= MX; j += i) {
np[j] = true;
}
}
}
五、暂未编写
暂无内容
N、其它
N.1 二分查找
二分查找,有好几种不同区间的写法,我最多使用的是闭区间的写法。这里给出闭区间的二分模板。
tips:
- 1)关于二分确定边界的问题,只需要看清楚最终要的结果是什么就很好确定。
- 2)关于取中点的问题,最好使用
m = l + (r - l) / 2
的写法,防止数值溢出。 - 3)缩小范围时千万不要粗心写成
l++
和r--
。这样就失去了二分的意义。
下面给出的二分可以用来查找第一个大于 target
的数。如果要查找第一个小于 target
的数,只需要返回 r
即可;如果要查找第一个大于等于 target
的数,只需要把 arr[m] <= target
修改为 arr[m] < target
即可。这是因为,我们可以站在结果的角度来看,以查找第一个大于 target
的数为例,当 arr[m] <= target
时,l
就会等于m + 1
,那么,结束时,l
要么等于arr.length
,要么满足 arr[l] > target
。
// arr 严格非递减
private int binarySearch(int[] arr, int target) {
int l = 0, r = arr.length - 1;
while (l <= r) {
int m = l + (r - l) / 2;
if (arr[m] <= target) {
l = m + 1;
} else {
r = m - 1;
}
}
return l;
}