算法模板
2020-07-20 14:50:02 # leetcode # 算法 # 总结

链表

题目链接:826. 单链表

快速排序

quick sort的一道题目,关于字符串排序的,剑指offer上的。剑指 Offer 45. 把数组排成最小的数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void quickSort(int[] arr, int l, int r){
if (l >= r) {
return;
}

int pivot = arr[l], i = l - 1, j = r + 1;

while (i < j) {
do i++; while(arr[i] < pivot);
do j--; while(arr[j] > pivot);
// 注意:重点在这里,要i < j的时候才需要swap,否则不需要!!!
if (i < j) swap(arr, i, j);
}

// 注意,这里我们要用j来写下面的递归,不能用i,具体原因y总在第二堂课开头里有讲。主要是边界问题。反之,如果我们前面的pivot用得是arr[r],那么这里就不能用j,只能用i.
quickSort(arr, l, j);
quickSort(arr, j + 1, r);
}

// 这种写法应该更加亲民一点,只是while循环多了一点内容。
public static void quickSort(int[] arr, int l, int r){
if (l >= r) {
return;
}
int i = l, j = r;
while (i < j) {
// 注意,这里一定得是j在前,i在后
while (i < j && arr[j] >= arr[l]) j--;
while (i < j && arr[i] <= arr[l]) i++;
swap(arr, i, j);
}
swap(arr, i, l);
quickSort(arr, l, i - 1);
quickSort(arr, i + 1, r);
}

Quick Select

和quick sort基本一致吧,就是找出top k元素。其实对于top k元素,找到k或者k-1均可,这里方便起见,直接80行就不换乘 i < k - 1了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void quickSelect(int[] nums, int l, int r, int k){
Map<Integer, Integer> temp = map;
if (l >= r) {
return;
}

int pivot = nums[l], i = l, j = r;
while (i < j) {
while (i < j && nums[j] <= pivot) j--;
while (i < j && nums[i] >= pivot) i++;
swap(nums, i, j);
}
swap(nums, i, l);

if (i > k) {
quickSelect(nums, l, i - 1, k);
} else if (i < k) {
quickSelect(nums, i + 1, r, k);
}
}

归并排序

上面的题目一样可以用mergeSort来做,在submission里面可以看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

public static void mergeSort(int[] arr, int l, int r) {
if (l >= r) {
return;
}

int mid = l + r >> 1;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);

int[] temp = new int[r - l + 1];
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}

while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= r) {
temp[k++] = arr[j++];
}

for (int i = 0; i < temp.length; i++) {
arr[l + i] = temp[i];
}
}

二分法

二分查找的边界问题一直很困扰人额,有两种二分查找,一种是求左边界,另一种是求右边界。最直接的题目就是Leetcode 34。既要求左边界,也要求右边界。

注意,这里左边界和右边界只是一种象征,实际的题目不会这么直白。例如LeetCode 875.

回到正题,网上有很多二分查找的模板,这里只写我自己的模板,以下所有的区间全都都是闭区间,思路参考acwing的yxc。

这里有两种情况

区间被分为[l, mid]和[mid + 1, r]

这种情况的意思就是,当我们做check(mid)的时候,mid是否在我们希望的区间内。换句话说,当mid满足条件时,我们是把它放到左区间还是右区间。比如{1,2,2,2,3,4,5,5},我们要找到2的左边界,那么当我们找到1个“2”的时候,这个2有可能就是我们要找的值,对吧?那么我们就应该让r = mid,把右边不要的部分去掉,再去遍历左边。反之亦然。

这里的终止条件都是l==r!!!!

1
2
3
4
5
6
7
8
9
// 找左边界
public static void binarySearch1(int l, int r){
int mid = l + r >> 1;
if (check(mid)){
r = mid;
} else {
l = mid + 1;
}
}

区间被分为[l, mid - 1]和[mid, r]

道理和上面类似,只是此时需要mid = (l + r + 1)/2。原因在于除法是向下取整,存在精读缺失,有可能无限死循环。

1
2
3
4
5
6
7
8
9
// 找右边界,注意l=mid的时候,要+1
public void binarySearch2(int l, int r){
int mid = (l + r + 1)/2;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}

取模

这样为了防止负数!!!一定要 +mod 再 % mod

1
2
3
private int mod(int a) {
return (a % mod + mod) % mod;
}

快速幂

快速幂就是求大数字a的大数字b次方对p取模。极其重要!!!

1
2
3
4
5
6
7
8
9
10
11
private static int process(int a, int b, int p) {
int ans = 1 % p;
while (b > 0) {
if ((b & 1) == 1) {
ans = (int)((ans * 1L * a) % p);
}
a = (int)((a * 1L * a) % p);
b = b >> 1;
}
return ans;
}

最大公约数

1
2
3
4
5
6
int gcd(int a, int b) {
if (b == 0) {
return a;
}
return gcd(b, a % b);
}

二维前缀和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

using namespace std;

const int N = 1010;

int a[N][N], s[N][N];

int main() {
int n, m, q;
cin >> n >> m >> q;

for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
scanf("%d", &a[i][j]);
s[i][j] = s[i][j - 1] + s[i - 1][j] - s[i - 1][j - 1] + a[i][j]; // 求前缀和
}

while (q--) {
int x1,y1,x2,y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
// 算子矩阵的和
printf("%d\n", s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]);
}

return 0;
}

单调栈

首先,从名字上来看,单调栈的意思就是,我们通过某些操作,使得栈里的元素都是单调的。

单调栈解决的问题是“给出一个无序数组,找到每个数字左(右)侧离他最近比它大(小)的数字”。如果一个问题能够抽象成这种类型,那么毫无疑问,马上用单调栈。或者说其他变种,类似于LeetCode 155. Min Stack,blog解析见LeetCode 456. 132 Pattern

举例:给出一个数组a[],若要找到 a[7]左侧比它大的数字,假设有a[3]和a[5],那么我们应该选择a[5]而不是a[3]。那么,换句话说,对于任意两个数a[i]和a[j],如果”a[i] <= a[j] && i < j,那么a[i]可以被抛弃掉,因为对于j后面的数字,a[j]一定是更优的解。”

这种做法的思想可以理解成保守式单调栈,要哪侧的“第一个最大/最小”,就从哪侧开始遍历,保证遍历到每个数的时候,其结果都能确定。

例题:LeetCode 496. Next Greater Element I,该题目的解析link传送门

这里我们写一个模板,是找到每个数字左侧第一个比它大的数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void montonousStack(int[] nums) {
Stack<Integer> stack = new Stack();

for (int num : nums) {
while (!stack.isEmpty() && stack.peek() <= num) {
stack.pop();
}

if (stack.isEmpty()) {
// 左侧没有数字比它更大
System.out.print("-1 ");
} else {
System.out.printf("%d ", stack.peek());
}

stack.push(num);
}
}
注意:单调栈并不一定是从左向右,或者从右向左!!!

如上面的左侧第一个更大数字,是从左向右遍历的。那么换种思想,我们从右向左遍历:如果当前数字比栈顶或者等于,则压入;若比栈顶大,则不断弹出,直到栈为空或者小于等于栈顶,然后压栈。最后栈底剩下的元素,是没有左侧更大元素的!

这种做法的思想可以理解成探索式单调栈,对于未知的情况,我们先保留,最后再一起处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void montonousStack(int[] nums) {
Stack<Integer> stack = new Stack();

for (int i = nums.length; i >= 0; i--) {
while (!stack.isEmpty() && stack.peek() < nums[i]) {
System.out.printf("index %d left greater element is %d\n", stack.pop(), nums[i]);
}
stack.push(i);
}
while (!stack.isEmpty()) {
System.out.printf("index %d left greater element is %d\n", stack.pop(), -1);
}
}

单调队列

从单调队列这个名称来看,就是解决某些问题的时候,我们维护一个队列,使得队列里面的的数字呈现单调的情况。

单调队列解决的问题可以抽象为“求滑动窗口里的最大值和最小值”,最直接的题目就是LeetCode 239. Sliding Window Maximum

对于这种问题,我们举个例子,[1, 3, -1, -3, 5, 3, 6, 7],窗口大小为3,输出每个窗口里面的最小值(和239基本一致,只是有小区别)。 第一个窗口为[1, 3, -1],我们可以发现,当-1只要还在我们的窗口里的时候,在-1左侧的1和3就绝不可能为最小值。也就是说这两个数对于我们找窗口内最小值完全是冗余的!!!因此,我们需要一种数据结构,保证窗口的正常运转,要能够一边进,一边出,因此我们选择Java里的Deque,双端队列,逻辑如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    public int[] minSlidingWindow(int[] nums, int k) {
Deque<Integer> deque = new LinkedList();
int n = nums.length;
// 这里用res数组来存储结果
int[] res = new int[n - k + 1];

for (int i = 0; i < n; i++) {
// 如果当前队列里面存的数字过多,则将最前面的数字弹出,其实就是边界情况,当window里面数字全部都是升序的时候。注意判断是否为空!
if (!deque.isEmpty() && i - k + 1 > deque.peekFirst()) {
deque.pollFirst();
}
// 将队尾的所有比当前元素大的全部pop出去,因为它们不可能为解。注意,是队尾!!!不是队首!!!而且要注意有等于的情况!!!
while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
deque.pollLast();
}

// 这里pollLast以后,队列里要么为空,要么是一列升序的数字!!!!!!至于这里为什么要存index,是因为前面需要判断长度的时候需要用index判断窗口大小。

deque.offerLast(i);

// 此时,队首的数字就是当前队列里的最小值!!!
if (i >= k - 1) {
res[i - k + 1] = nums[deque.peekFirst()];
}
}

return res;
}
}

我们还可以使用数组来模拟双端队列。注意写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

// 这里用数组来模拟双端队列,q就是这个队列,里面存的也是index
int N = 100010, hh = 0, tt = -1;
int[] q = new int[N];

public int[] minSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];

for (int i = 0; i < nums.length; i++) {
//
if (hh <= tt && i - k + 1 > q[hh]) hh++;
while (hh <= tt && nums[q[tt]] >= nums[i]) tt--;

q[++tt] = i;

if (i >= k - 1) {
res[i - k + 1] = nums[q[hh]];
}
}

return res;
}

递归的写法没啥好说的,这里强调迭代的写法。

前序遍历

题目链接:前序遍历

这里提供两种遍历思路:

  1. 遍历时先压右子树,再压左子树。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<TreeNode>();
List<Integer> list = new ArrayList<Integer>();
TreeNode node = root;
stack.push(node);

while (!stack.isEmpty() && node != null) {
node = stack.pop();
list.add(node.val);
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}

return list;
}
}
  1. 压栈的目的只是为了回溯找右节点,不是压左右子树,而是访问过的节点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<TreeNode>();
List<Integer> list = new ArrayList<Integer>();
TreeNode node = root;

while (!stack.isEmpty() || node != null) {
if (node != null) {
list.add(node.val);
stack.push(node);
node = node.left;
} else {
node = stack.pop().right;
}
}

return list;
}
}

中序遍历

题目链接:中序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<TreeNode>();
List<Integer> list = new ArrayList<Integer>();
TreeNode node = root;

while (node != null) { // 下面的循环里保证了node在遍历未结束时不为null
if (node.left != null) {
stack.push(node);
node = node.left;
} else {
list.add(node.val);
while (node.right == null && !stack.isEmpty()) {
node = stack.pop();
list.add(node.val);
}
node = node.right;
}
}

return list;
}
}
  1. 判断节点是否为空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<TreeNode>();
List<Integer> list = new ArrayList<Integer>();
TreeNode node = root;

while (node != null || !stack.isEmpty()) {
if (node != null) {
stack.push(node);
node = node.left;
} else {
node = stack.pop();
list.add(node.val);
node = node.right;
}
}

return list;
}
}

后序遍历

题目链接:后序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<TreeNode>();
List<Integer> list = new ArrayList<Integer>();
TreeNode node = root, lastNode = root;
stack.push(node);

while (!stack.isEmpty() && node != null) {
node = stack.pop();
if ((node.left == null && node.right == null) || node.left == lastNode || node.right == lastNode) {
// node为叶子节点或者访问返回到当前节点
list.add(node.val);
lastNode = node;
} else {
// 栈还没压到叶子节点
stack.push(node);
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
}

return list;
}
}

Morris中序遍历

其核心点在于,利用“左子树最右侧节点”来辅助判断是否已经遍历过左子树了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public List<Integer> inorderTraversal(TreeNode root) {
// Morris遍历
TreeNode cur = root;
List<Integer> list = new ArrayList<>();

while (cur != null) {
// 判断其有无左子树,如果有,找到它左子树的最右侧节点
TreeNode mostRight = cur.left;

if (mostRight != null) {
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}

if (mostRight.right == null) {
mostRight.right = cur;
cur = cur.left;
continue;
} else {
mostRight.right = null;
list.add(cur.val);
}
} else {
list.add(cur.val);
}
cur = cur.right;
}

return list;
}

并查集

并查集建议观看花花酱的并查集视频讲解

这里使用size来代替rank,也是可以的,复杂度上没有区别。LeetCode 547

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

class UF {
int[] parents;
int[] sizes;
int count;

public UF(int n) {
parents = new int[n];
sizes = new int[n];
count = n;

for (int i = 0; i < n; i++) {
parents[i] = i;
sizes[i] = 1;
}
}

public int find(int u) {
if (u == parents[u]) {
return u;
}
parents[u] = find(parents[u]);
return parents[u];
}

public void union(int u, int v) {
int pu = find(u), pv = find(v);
if (pu == pv) {
return;
}

if (sizes[pu] < sizes[pv]) {
parents[pu] = pv;
sizes[pv] += sizes[pu];
} else {
parents[pv] = pu;
sizes[pu] += sizes[pv];
}
}
}

Trie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TrieNode {
TrieNode[] sons;
boolean isWord;

TrieNode() {
sons = new TrieNode[26];
isWord = false;
}

public TrieNode get(char c) {
int idx = c - 'a';
return sons[idx];
}

public boolean containsKey(char c) {
int idx = c - 'a';
return sons[idx] != null;
}

public void add(char c) {
int idx = c - 'a';
sons[idx] = new TrieNode();
}
}

infra + cloud

front-side, visual team 25-30 people
Seattle, 20 engineers, backend engineer

3 teams:
bidding backend system,
SRE team

google cloud,Big Data,

  1. 确认输入输出,是否会有异常;确认输出输出。比如输入如果没有k个咋办。以及先说明定义的函数。
  2. 一边说一边说明每一段代码的逻辑,不用念代码;说代码是干啥的就行。
  3. 可以问面试官要一下hint,要一下example。写完以后主动.

Given the root of a binary tree and an integer targetSum, return all root-to-leaf paths where the sum of the node values in the path equals targetSum. Each path should be returned as a list of the node values, not node references.

A root-to-leaf path is a path starting from the root and ending at any leaf node. A leaf is a node with no children.

        5
    /      \
    4        8
  /        /    \
11       13       4

/ \ /
7 2 5 1

Input: root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
Output: [[5,4,11,2],[5,8,4,5]]

Given an integer array nums, return the length of the longest strictly increasing subsequence.

A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements. For example, [3,6,2,7] is a subsequence of the array [0,3,1,6,2,2,7].

反馈:

  1. 边界case没确认完全,比如root为null
  2. overflow没有确认