• 数据结构的选择比算法优化更重要

    1. 逻辑结构栈 队列 树(理论模型)
    2. 物理结构数组 链表 对象(代码表现)
  • 算法三大思维贪心(递归) 二分 动态规划

  • Traverse 遍历

    1. 深度优先栈 递归
      • 递归遍历recursion
      • 迭代遍历iteration
    2. 广度优先队列
      • 迭代遍历iteration
  • 有序数据考虑用二分

  • 双指针 和 Map 可以避免嵌套循环

  • 数组查找快O(1) 增删慢O(n)

  • 单向链表查找慢O(n) 增删快O(1)

  • BST二叉搜索树时间复杂度 O(logn)

  • 平衡二叉树BBST增删改查都快 O(log(n)) 树的高度

  • 常见的时间复杂度

    1. 常数阶O(1) 无循环
    2. 对数阶O(logn) 二分法
    3. 线性阶O(n) 普通循环
    4. 线性对数阶O(nlogn) 一层循环 & 一层二分
    5. 平方阶O(n^2) 嵌套循环
    6. 指数阶O(C^n)
  • 常见的空间复杂度

    1. O(1)
    2. O(n)

位运算

运算符 描述 例子 类似于 结果 十进制
& AND 5 & 1 0101 & 0001 0001 1
| OR 5 | 1 0101 | 0001 0101 5
~ 取反 ~ 5 ~ 0101 -6
^ 异或 5 ^ 1 0101 ^ 0001 0100 4
<< 左移 5 << 1 101 << 1 1010 10
>> 右移 5 >> 1 101 >> 1 10 2
>>> 无符号右移 2 >>> 1 0010 >>> 0001 0001 1

二进制

二进制 十进制 计算
1000 8 2^3 = 8
1010 10 2^3 + 2^1 = 10
1111 15 2^3 + 2^2 + 2^1 + 2^0 = 15
10000 16 2^4 = 16

按位与 &

每一位都为 1,结果才为 1

1
8 & 7 === 0 // 1000 & 0111 -> 0000 -> 0

按位或 |

其中一位为 1,结果就是 1

1
8 | 7 === 15 // 1000 | 0111 -> 1111 -> 15

按位异或 ^

每一位都不同,结果才为 1不进位的加法

1
2
3
4
5
6
7
8
9
8 ^ 8 === 0 // 1000 ^ 1000 -> 0000 -> 0
8 ^ 7 === 15 // 1000 ^ 0111 -> 1111 -> 15
8 ^ 6 === 14 // 1000 ^ 0110 -> 1110 -> 14

let a = 102, b = 324
a = a ^ b
b = a ^ b
a = a ^ b
console.log(a, b) // 两次异或相当于取消

左移 <<

公式a * (2 ^ b)

1
2
3
3 << 2 === 12 // 11 -> 1100 -> 12

11 << 1 === 22 // 1011 -> 10110 -> 22

右移 >>

大概公式a / (2 ^ b)

1
2
3
4
5
6
10 >> 1 === 5 // 1010 -> 101 -> 5
10 >> 2 === 2 // 1010 -> 10 -> 3
10 >> 3 === 1 // 1010 -> 1 -> 1

/* 二分法 取中间值 👍 */
11 >> 1 === 5 // 1011 -> 101 -> 5

递归

递归 --> 栈 --> 栈溢出stackoverflow

  • 递归逻辑性更加清晰
  • 迭代效率高也更复杂

尾递归

在严格模式下, ES6 才实现了尾递归优化

1
2
3
4
5
6
7
8
9
/* 尾调用: 在一个函数执行的最后一步, 是调用函数 */
function fn() {
console.log('👍')
fn1()
}
function fn() {
console.log('👎')
return fn1() + 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 尾递归: 一个函数执行的最后一步, 是调用自己 */

// 阶乘 7 * 6 * 5 * 4 * 3 * 2 * 1

/* 普通递归 */
function fn(n) {
if (n === 1) {
return 1
}
return n * fn(n - 1)
}
/* 尾递归 */
function fn(n, total = 1) {
if (n === 1) {
return total
}
return fn(n - 1, n * total)
}

二分查找

分治法的一种特例减治法

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
42
43
function binarySearch(arr, target) {

function binarySearchEx(start, end) {
if (start > end) return -1

let midIndex = Math.floor((start + end) / 2)
let midValue = arr[midIndex]

if (target < midValue) {
return binarySearchEx(start, midIndex - 1)
} else if (target > midValue) {
return binarySearchEx(midIndex + 1, end)
} else {
return midIndex
}
}
return binarySearchEx(0, arr.length - 1)
}

function binarySearch(arr, target) {
const length = arr.length

let start = 0
let end = length - 1

while (start > end) {

const midIndex = Math.floor((start + end) / 2)
const midValue = arr[midIndex]

if (target < midValue) {
end = midIndex - 1
} else if (target > midValue) {
start = midIndex + 1
} else {
return midIndex
}
}

return -1
}

binarySearch([1, 2, 3, 4, 5, 6, 7, 8, 9], 7)

双指针

寻找和为 n 的两个数

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
function findTowNumbers(arr, n) {
const res = []

const length = arr.length
for (let i = 0; i < length - 1; i++) {
const n1 = arr[i]
let flag = false
// O(n^2) 👎
for (let j = i + 1; j < length; j++) {
const n2 = arr[j]

if (n1 + n2 === n) {
res.push(n1)
res.push(n2)
flag = true
break
}
}

if (flag) break
}

return res
}

function findTowNumbers(arr, n) {
const res = []

const length = arr.length
let i = 0 // 头
let j = length - 1 // 尾

// O(n) 👍
while (i < j) {
const n1 = arr[i]
const n2 = arr[j]
const sum = n1 + n2

if (sum > n) {
j--
} else if (sum < n) {
i++
} else {
res.push(n1)
res.push(n2)
break
}
}

return res
}

function findTowNumbers(arr, n) {
const res = []
const length = arr.length

const map = {} /* Map 映射 👍 */
for (let i = 0; i < length; i++) {
map[arr[i]] = i
}

for (let i = 0; i < length; i++) {
if (map[n - arr[i]]) {
res.push(arr[i], n - arr[i])
break
}
}

return res
}

findTowNumbers([1, 2, 4, 7, 11, 15], 15)

移动 0 到数组末尾

将数组中所有的 0 都移动到末尾,例如输入 [1, 0, 3, 0, 11, 0] 输出 [1, 3, 11, 0, 0, 0]

  • 时间复杂度 O(n)
  • 空间复杂度 O(1)
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
function moveZero(arr) {
const length = arr.length
let i
let j = -1 // 指向第一个 0

for (i = 0; i < length; i++) {
if (arr[i] === 0 && j < 0) {
// 第一个 0
j = i
}
if (arr[i] !== 0 && j >= 0) {
[arr[i], arr[j]] = [arr[j], arr[i]]
j++
}
}
}

function moveZero(arr) {
const length = arr.length
let zeroCount = 0
/* O(n^2) */
for (let i = 0; i < length - zeroCount; i++) {
if (arr[i] === 0) {
arr.push(0)
arr.splice(i, 1) // 👎 O(n)
i--
zeroCount++
}
}
}

连续最多的字符

给一个字符串,找出连续最多的字符,以及次数
例如字符串 aabbcccddeeee11223 连续最多的是 e 4 次

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
function findContinuousChar(str) {
const res = {
char: '',
length: 0
}

const length = str.length

let tempLength = 0 // 临时记录当前连续字符的长度

for (let i = 0; i < length; i++) {
tempLength = 0
/* 嵌套循环 时间复杂度 O(n) */
for (let j = i; j < length; j++) {
if (str[i] === str[j]) {
tempLength++
}

if (str[i] !== str[j] || j === length - 1) {
// 不相等,或者已经到了最后一个元素
if (tempLength > res.length) {
res.char = str[i]
res.length = tempLength
}

if (i !== length - 1) {
i = j - 1 // 跳步
}

break
}
}
}

return res
}

function findContinuousChar(str) {
const res = {
char: '',
length: 0
}

const length = str.length

let tempLength = 0
let i = 0
let j = 0

for (; i < length; i++) {
if (str[i] === str[j]) {
tempLength++
}

if (str[i] !== str[j] || i === length - 1) {
if (tempLength > res.length) {
res.char = str[j]
res.length = tempLength
}
tempLength = 0

if (i !== length - 1) {
j = i
i--
}
}
}

return res
}

动态规划

青蛙跳台阶

一只青蛙,一次可以跳 1 个台阶,也可以跳 2 个台阶,问青蛙跳上 n 级台阶,总共有多少种方式,也称:爬楼梯问题

  • f(1) = 1 跳 1 级台阶,只有一种方式
  • f(2) = 2 跳 2 级台阶,有两种方式
  • f(n) = f(n - 1) + f(n - 2) 跳 n 级,可拆分为两个问题
    • 第一次跳,要么 1 级,要么 2 级,只有这两种
    • 第一次跳 1 级,剩下有 f(n - 1) 种方式
    • 第一次跳 2 级,剩下有 f(n - 2) 种方式
1
2
3
4
5
6
7
8
9
function climbStairs(n) {
const f = []
f[1] = 1
f[2] = 2
for (let i = 3; i <= n; i++) {
f[i] = f[i-2] + f[i-1]
}
return f[n]
}

斐波那契数列

  • 递归自顶向下
  • 动态规划自底向上
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
function fibonacci(n) {// 和 climbStairs 一样
let arr = [0, 1]
let i = 2
while (i <= n) {
arr[i] = arr[i - 1] + arr[i - 2]
i++
}
return arr[n]
}
/* 时间复杂度 O(n) */
function fibonacci(n) {
let n2 = 0, n1 = 1, res = 1
for (let i = 1; i < n; i++) {
res = n2 + n1
n2 = n1
n1 = res
}
return res
}

/* 递归 */
function fibonacci(n) {
if (n <= 2) return 1
return fibonacci(n - 1) + fibonacci(n - 2)
}

最少硬币找零

给出需要找零的钱数,你可以用指定面额的硬币来完成找零,问达成找零所需要的最少硬币个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function coinChange(coins, amount) {
const f = []
// 定义已知情况
f[0] = 0
// 遍历 [1, amount] 金额区间
for (let i = 1; i <= amount; i++) {
f[i] = Infinity
for (let j = 0; j < coins.length; j++) {
if (i - coins[j] >= 0) {
/* 状态转移方程 */
f[i] = Math.min(f[i], f[i - coins[j]] + 1)
}
}
}
return f[amount] === Infinity ? : -1 : f[amount]
}