Skip to content

AHABHGK

Algorithm

记录

2020-07-142020-07-172020-07-192020-07-23
283. 移动零
11. 盛最多水的容器
1. 两数之和
15. 三数之和

16 号 ✌️ 跟 LJ 去修电脑,吃了小吃街嘻嘻

2020-07-152020-07-172020-07-202020-07-27
206. 反转链表
92. 反转链表 II
24. 两两交换链表中的节点
141. 环形链表
2020-07-172020-07-192020-07-222020-07-26
142. 环形链表 II
25. K 个一组翻转链表
20. 有效的括号
155. 最小栈
2020-07-182020-07-202020-07-232020-07-27
622. 设计循环队列
641. 设计循环双端队列
84. 柱状图中最大的矩形
239. 滑动窗口最大值
2020-07-192020-07-212020-07-242020-08-09
242. 有效的字母异位词
49. 字母异位词分组
94. 二叉树的中序遍历
144. 二叉树的前序遍历
2020-07-202020-07-222020-07-252020-08-09
590. N 叉树的后序遍历
589. N 叉树的前序遍历
429. N 叉树的层序遍历
22. 括号生成
2020-07-212020-07-232020-07-262020-07-30
226. 翻转二叉树
98. 验证二叉搜索树
104. 二叉树的最大深度
111. 二叉树的最小深度

22 号 ✌️ 去拿电脑了,生产力回来了,LJ 去超市跟零食告别 👋

开始写项目了,后面随缘刷题了,等写好项目再好好刷

2020-07-282020-08-092020-10-142020-2020-
297. 二叉树的序列化与反序列化
235. 二叉搜索树的最近公共祖先
236. 二叉树的最近公共祖先
2020-07-292020-08-092020-2020-2020-
105. 从前序与中序遍历序列构造二叉树
77. 组合
46. 全排列
47. 全排列 II
2020-08-192020-10-152020-2020-2020-
78. 子集
50. Pow(x, n)
2020-08-202020-10-162020-2020-2020-
169. 多数元素
51. N 皇后
17. 电话号码的字母组合
2020-08-212020-2020-2020-2020-
102. 二叉树的层序遍历
515. 在每个树行中找最大值

Vue3 源码看完了,继续刷题,准备面试

2020-10-112020-2020-2020-2020-
752. 打开转盘锁
433. 最小基因变化
127. 单词接龙
2020-10-122020-2020-2020-2020-
200. 岛屿数量
529. 扫雷游戏
2020-10-132020-2020-2020-2020-
455. 分发饼干
860. 柠檬水找零
2020-10-152020-2020-2020-2020-
121. 买卖股票的最佳时机
122. 买卖股票的最佳时机 II
2020-10-162020-2020-2020-2020-
55. 跳跃游戏
45. 跳跃游戏 II
2020-10-172020-2020-2020-2020-
69. x 的平方根
367. 有效的完全平方数
2020-10-202020-2020-2020-2020-
33. 搜索旋转排序数组
34. 在排序数组中查找元素的第一个和最后一个位置
74. 搜索二维矩阵
2020-10-222020-2020-2020-2020-
62. 不同路径
63. 不同路径 II
1143. 最长公共子序列
2020-10-232020-2020-2020-2020-
120. 三角形最小路径和
53. 最大子序和
152. 乘积最大子数组
2020-10-242020-2020-2020-2020-
198. 打家劫舍
337. 打家劫舍 III
2020-10-252020-2020-2020-2020-
714. 买卖股票的最佳时机含手续费
309. 最佳买卖股票时机含冷冻期
2020-10-262020-2020-2020-2020-
208. 实现 Trie (前缀树)
212. 单词搜索 II
79. 单词搜索
2020-10-272020-2020-2020-2020-
547. 朋友圈
130. 被围绕的区域
2020-10-292020-2020-2020-2020-
146. LRU 缓存机制
2020-11-102020-2020-2020-2020-
1122. 数组的相对排序
2020-11-112020-2020-2020-2020-
56. 合并区间
2020-11-142020-2020-2020-2020-
771. 宝石与石头
8. 字符串转换整数 (atoi)
2020-11-162020-2020-2020-2020-
541. 反转字符串 II
917. 仅仅反转字母
125. 验证回文串
680. 验证回文字符串 Ⅱ
5. 最长回文子串

数组、链表、跳表

栈、队列

哈希表

回溯

DFS、BFS

贪心

二分查找

动态规划

  • 62. 不同路径

  • 63. 不同路径 II

  • 1143. 最长公共子序列

  • 120. 三角形最小路径和

  • 53. 最大子序和

  • 152. 乘积最大子数组

  • 198. 打家劫舍

  • 337. 打家劫舍 III

  • 714. 买卖股票的最佳时机含手续费

  • 309. 最佳买卖股票时机含冷冻期

  • 322. 零钱兑换

    递归法:自上而下,会有重复的计算

    dp-rec

    1/**
    2 * @param {number[]} coins
    3 * @param {number} amount
    4 * @return {number}
    5 */
    6var coinChange = function(coins, amount) {
    7 let memo = new Map() // memo 进行优化
    8 function dp(n) {
    9 if (memo.has(n)) return memo.get(n)
    10 if (n === 0) return 0
    11 if (n < 0) return -1
    12 let res = Infinity
    13 for (let coin of coins) {
    14 let sub = dp(n - coin) // 子问题
    15 if (sub === -1) continue
    16 res = Math.min(res, 1 + sub) // 找最优解
    17 }
    18 memo.set(n, res)
    19 return res === Infinity ? -1 : res // 无解就返回 -1
    20 }
    21 return dp(amount)
    22};

    迭代法:自下而上

    1/**
    2 * @param {number[]} coins
    3 * @param {number} amount
    4 * @return {number}
    5 */
    6var coinChange = function(coins, amount) {
    7 let dp = new Array(amount + 1).fill(amount + 1)
    8 dp[0] = 0 // amount 为 0 时解为 0
    9 for (let i = 0; i < dp.length; i++) { // 从 amount 为 0 开始计算,自下而上计算
    10 for (let coin of coins) { // 遍历 coins
    11 let left = i - coin // 子问题的 amount
    12 if (left < 0) continue
    13 dp[i] = Math.min(dp[i], 1 + dp[left] /* 子问题的解 */) // 找最优解
    14 }
    15 }
    16 return (dp[amount] === amount + 1) ? -1 : dp[amount]
    17};
  • 96. 不同的二叉搜索树

    假设 n 个节点存在的二叉搜索树有 G(n) 种,f(i) 为以 i 为根的二叉搜索树的个数

    G(n)=f(1)+f(2)+f(3)+...+f(n)G(n)=f(1)+f(2)+f(3)+...+f(n)

    f(i)=G(i1)G(ni)f(i)=G(i-1)*G(n-i)

    得到:

    G(n)=i=1nG(i1)G(ni)G(n) = \sum_{i=1}^n G(i-1) * G(n-i)

    递归法:

    1/**
    2 * @param {number} n
    3 * @return {number}
    4 */
    5let memo = [1, 1]
    6var numTrees = function(n) {
    7 if (m = memo[n]) return m
    8 let res = 0
    9 for (let i = 1; i <= n; i++) {
    10 res += numTrees(i - 1) * numTrees(n - i) // 子问题
    11 }
    12 memo[n] = res
    13 return res
    14};

    迭代法:

    1/**
    2 * @param {number} n
    3 * @return {number}
    4 */
    5var numTrees = function(n) {
    6 let dp = new Array(n + 1).fill(0)
    7 dp[0] = 1, dp[1] = 1
    8 for (let m = 2; m <= n; m++) { // 2 ~ n 的解
    9 for (let i = 1; i <= m; i++) {
    10 dp[m] += dp[i - 1] * dp[m - i] // 子问题
    11 }
    12 }
    13 return dp[n]
    14};
  • 剑指 Offer 14- II. 剪绳子 II

    先看不需要求模的版本

    1/**
    2 * @param {number} n
    3 * @return {number}
    4 */
    5var cuttingRope = function(n) {
    6 let dp = new Array(n + 1).fill(0)
    7 ;[dp[0], dp[1]] = [0, 1]
    8 for (let i = 2; i <= n; i++) { // 自下而上,先求 0 1 2... 的结果,求上去得到 n 的
    9 for (let j = 1; j <= i; j++) { // 剪多长
    10 dp[i] = Math.max(dp[i], dp[i - j] * j, (i - j) * j) // 剪了还剪、剪了就不剪了
    11 }
    12 }
    13 return dp[n]
    14};

    但是在需要求模时就不行了,因为 Math.max 不能正确比较出经过求模后的原来的最大值,如果先比较后求模又会溢出

    所以可以用贪心:因为 1 和任何一个更大的数就有更大的,2 可以,3 可以,4 拆成两个 2 效果一样,5 可以拆成 2 * 3

    1/**
    2 * @param {number} n
    3 * @return {number}
    4 */
    5var cuttingRope = function(n) {
    6 if (n < 3) return 1
    7 if (n === 3) return 2
    8 let res = 1
    9 let mod = 1000000007
    10 while (n > 4) {
    11 res = (res * 3) % mod
    12 n -= 3
    13 }
    14 return (res * n) % mod
    15};

    对此就是求 3 的幂,可以使用快速幂进行进一步优化

    因为 (xy)modp=[(xmodp)(ymodp)]modp(xy) \mod p = [(x \mod p) * (y \mod p)] \mod p,所以可以同时取余防止溢出

    1// 递归快速幂
    2function qpow(a, n) { // a ** n
    3 if (n === 1) return 1
    4 if (n % 2 === 1) return qpow(a, n - 1) * a % MOD
    5 let tmp = qpow(a, Math.floor(n / 2) % MOD)
    6 return tmp * tmp % MOD
    7}
    1// 迭代快速幂,7 ^ 10 = 7 ^ 0b1010 = 7 ^ 0b1000 * 7 ^ 0b10
    2function qpow(a, n) {
    3 let res = 1
    4 while (n > 0) {
    5 if ((n & 1) === 1) {
    6 res *= a
    7 res = res % MOD
    8 }
    9 a *= a // 相当于左移
    10 a = a % MOD
    11 n = n >> 1
    12 }
    13 return res
    14}

    qpow

    因为取余是为了防溢出,所以 JS 也可以使用 BigInt,最后取余(具体类似于前两个代码块版本,只不过 number 全都变为 BigInt,最后再取余)

字典树

并查集

LRU

数组和链表的不同

本质上是内存存储结构的不同,数组是一段连续的内存,链表是不连续的

由于数组是连续的,所以更适合通过下标访问:array[i]_address = base_address + i * data_type_size

而对于插入删除由于需要保证内存的连续性,就需要大量的移动,如果忽略其连续性,插入时直接把原来的元素移到最后面时间复杂度同样是 O(1),对于删除操作,在用到时再把多次删除集中到一起,移动一次

容器能否完全替代数组

容器的优势在于封装大量操作,支持动态扩容。由于动态扩容会有涉及内存申请和数据搬移,消耗性能,如果事先已知大小,那使用数组性能更好,对于底层框架开发性能更重要,对于业务开发,可维护性更重要

数组下标为什么从 0 开始

  1. 从内存上看,下标叫做 offset 更合适,如果从 1 开始,array[i]_address = base_address + (i - 1) * data_type_size 每次访问就需要多一个减法运算,CPU 就需要多一个减法指令

  2. 历史原因,C 从 0 开始,之后的语言都效仿 C

LRU 缓存淘汰算法

LRU(Least Recently Used,最近最少使用),如果数据最近被访问过,那么将来被访问的几率也更高

单链表实现

一个单向链表,最近访问的放到头部,越接近尾部就是越早访问的,当有一个新的数据访问时,从头遍历:

  • 如果有缓存,就删除缓存节点,并插入到头部

  • 如果没有缓存

    • 如果缓存未满,直接插到头部

    • 如果满了,就先删除最后一个尾节点再插入到头部

需要遍历链表,时间复杂度为 O(n)

散列表实现

// TODO: 散列表实现

完全二叉树与堆的关系

完全二叉树

完全二叉树按层序遍历的顺序通过数组存储,浪费很少,堆就是完全二叉树

二叉搜索树如何删除

二叉查找树的删除

  1. 无子节点,直接删除

  2. 有一个子节点,用该子节点替换

  3. 两个子节点,用右子树的最小节点替换

有重复数据的二叉搜索树

存储的数据往往是个对象,需要一个 key 作为唯一标识,重复数据表示 key 相同的情况

  1. 通过链表把 key 相同的数据都存储在同一个节点上

  2. 把新的数据当作大于这个节点数据来处理。插入时放到节点右子树;查找时找到相同的之后继续在右子树中查找,返回所有查找到的;删除时将每个找到的节点跟上面的方式一样删除即可

散列表与二叉搜索树比较

散列表插入删除查找都是 O(1),而二叉查找树在比较平衡时插入删除查找才是 O(logn),为什么还要用二叉查找树?

  1. 散列表无序,二叉树中序遍历是有序的

  2. 散列表扩容耗时,需处理散裂冲突,二叉树性能稳定

  3. 散列表处理冲突,hash 函数耗时不一定比平衡二叉查找树快

  4. 散列表需要考虑散列函数的设计、冲突解决办法、扩容、缩容等,平衡二叉查找树只需考虑平衡性

排序

1// min: O(n), max: O(n^2)
2// O(1) 原地排序
3// 稳定
4function bubbleSort(arr) {
5 for (let i = 0; i < arr.length; i++) {
6 let hasSwap = false
7 for (let j = 0; j < arr.length - i - 1; j++) {
8 if (arr[j] > arr[j + 1]) {
9 [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
10 hasSwap = true
11 }
12 }
13 if (!hasSwap) break
14 }
15 return arr
16}
17
18// min: O(n), max: O(n^2)
19// O(1) 原地排序
20// 稳定
21function insertSort(arr) {
22 for (let i = 1; i < arr.length; i++) {
23 let value = arr[i]
24 let j = i - 1
25 for (; j >= 0; j--) {
26 if (arr[j] > value) {
27 arr[j + 1] = arr[j]
28 } else break
29 }
30 arr[j + 1] = value
31 }
32 return arr
33}
34
35// min: O(n^2), max: O(n^2)
36// O(1)
37// 不稳定
38function selectSort(arr) {
39 for (let i = 0; i < arr.length - 1; i++) {
40 let index = i
41 for (let j = i + 1; j < arr.length; j++) {
42 if (arr[j] < arr[index]) {
43 index = j
44 }
45 }
46 [arr[index], arr[i]] = [arr[i], arr[index]]
47 }
48 return arr
49}
50
51// min: O(nlogn), max: O(nlogn)
52// O(n)
53// 稳定
54function mergeSort(arr) {
55 return slice(0, arr.length - 1)
56
57 function slice(left, right) {
58 if (left >= right) return
59 let mid = Math.floor((left + right) / 2)
60 slice(left, mid)
61 slice(mid + 1, right)
62 merge(left, mid, right)
63 }
64 function merge(left, mid, right) {
65 let tmp = []
66 let i = left, j = mid + 1
67 while (i <= mid && j <= right) {
68 tmp.push(arr[i] < arr[j] ? arr[i++] : arr[j++])
69 }
70 while (i <= mid) tmp.push(arr[i++])
71 while (j <= right) tmp.push(arr[j++])
72 for (let k = 0; k < tmp.length; k++) {
73 arr[left + k] = tmp[k]
74 }
75 }
76}
77
78// min: O(nlogn), max: O(n^2), ava: O(nlogn)
79// O(logn)
80// 不稳定
81function quickSort(arr) {
82 return sort(0, arr.length - 1)
83
84 function sort(begin, end) {
85 if (begin > end) return
86 let pivot = partition(begin, end)
87 sort(begin, pivot - 1)
88 sort(pivot + 1, end)
89 }
90 function partition(begin, end) {
91 let counter = begin, pivot = end
92 for (let i = begin; i < end; i++) {
93 if (arr[i] < arr[pivot]) {
94 ;[arr[counter], arr[i]] = [arr[i], arr[counter]]
95 counter++
96 }
97 }
98 ;[arr[counter], arr[pivot]] = [arr[pivot], arr[counter]]
99 return counter
100 }
101}
102
103// O(nlogn)
104// O(1)
105// not stable
106function heapSort(arr) {
107 if (arr.length === 0) return
108 for (let i = Math.floor(arr.length / 2) - 1; i >= 0; i--) {
109 heapify(arr, arr.length, i)
110 }
111 /**
112 * heapPush: O(logn)
113 * 8
114 * 5 6
115 * 1 3 0 2
116 */
117 for (let i = arr.length - 1; i > 0; i--) {
118 ;[arr[0], arr[i]] = [arr[i], arr[0]]
119 heapify(arr, i, 0)
120 }
121 /**
122 * heapPop: O(logn)
123 * 8 -> 2 | -> 6 |
124 * 5 6 -> 5 6| -> 5 2|
125 * 1 3 0 2 -> 1 3 0 |8 -> 1 3 0 |8
126 */
127 function heapify(arr, len, i) {
128 let left = 2 * i + 1, right = 2 * i + 2
129 let largest = i
130 if (left < len && arr[left] > arr[largest]) {
131 largest = left
132 }
133 if (right < len && arr[right] > arr[largest]) {
134 largest = right
135 }
136 if (largest !== i) {
137 ;[arr[largest], arr[i]] = [arr[i], arr[largest]]
138 heapify(arr, len, largest)
139 }
140 }
141}
142
143// max: O(nlogn) 一个桶, min: O(n) n 个桶
144// 可以处理大量数据的情况,分桶存入内存处理
145// 稳定性根据桶内的排序算法决定
146function bucketSort(arr, bucketSize) {
147 let max = Math.max(...arr)
148 let min = Math.min(...arr)
149 let bucketNum = Math.floor((max - min) / bucketSize) + 1
150 let buckets = Array.from({ length: bucketNum }, _ => [])
151 for (let i = 0; i < arr.length; i++) {
152 let index = Math.floor((arr[i] - min) / bucketSize)
153 buckets[index].push(arr[i])
154 }
155 let res = []
156 for (let i = 0; i < bucketNum; i++) {
157 res = res.concat(quickSort(buckets[i]))
158 }
159 return res
160}
161
162// 类似桶排序,保证 n 个桶
163// 负数和小数的情况下需要变为正数(+)和整数(*)
164function countSort(arr) {
165 let min = Math.min(...arr)
166 let max = Math.max(...arr)
167 let bucketNum = Math.floor((max - min) / 1) + 1
168 let buckets = Array.from({ length: bucketNum }, _ => 0)
169 for (let i = 0; i < arr.length; i++) {
170 buckets[arr[i] - min] += 1
171 }
172 let res = []
173 for (let i = 0; i < bucketNum; i++) {
174 for (let j = 0; j < buckets[i]; j++) {
175 res.push(i + min)
176 }
177 }
178 return res
179}
180
181// O(n)
182// 适合位数很大的情况,比如手机号排序
183function radixSort(arr) {
184 let digit = `${Math.max(...arr)}`.length
185 let start = 1
186 while (digit) {
187 start *= 10
188 let buckets = []
189 for(let i = 0; i < arr.length; i++) {
190 const index = arr[i] % start
191 !buckets[index] && (buckets[index] = [])
192 buckets[index].push(arr[i])
193 }
194 arr = []
195 for(let i = 0; i < buckets.length; i++) {
196 buckets[i] && (arr = arr.concat(buckets[i]))
197 }
198 digit -= 1
199 }
200 return arr
201}

插入排序为什么比冒泡排序性能好

时间复杂度都是最差 O(n^2),最好 O(n),平均 O(n^2),但是冒泡是通过交换,交换需要三步,而插入是通过移动,移动需要一步

如何用快排思想在 O(n) 内查找第 K 大元素

通过 pivot 得到三个部分,小于 arr[pivot] 的,arr[pivot],大于 arr[pivot] 的,判断 pivot + 1 === K,是则返回 arr[pivot],大于则说明第 K 大的在 arr[0...pivot - 1] 中,递归继续查找,小于同理

二分查找变体

1// 查找第一个值等于给定值的元素
2function binSearchFindFirst(arr, target) {
3 let left = 0
4 let right = arr.length - 1
5 while (left <= right) {
6 let mid = Math.floor((left + right) / 2)
7 if (target > arr[mid]) left = mid + 1
8 else if (target < arr[mid]) right = mid - 1
9 else {
10 if (mid === 0 || arr[mid - 1] !== target) return mid
11 else right = mid - 1
12 }
13 }
14 return -1
15}
16
17console.log(binSearchFindFirst([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 8))
18
19// 查找最后一个值等于给定值的元素
20function binSearchFindLatest(arr, target) {
21 let left = 0
22 let right = arr.length - 1
23 while (left <= right) {
24 let mid = Math.floor((left + right) / 2)
25 if (target > arr[mid]) left = mid + 1
26 else if (target < arr[mid]) right = mid - 1
27 else {
28 if (mid === arr.length - 1 || arr[mid + 1] !== target) return mid
29 else left = mid + 1
30 }
31 }
32 return -1
33}
34
35console.log(binSearchFindLatest([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 8))
36
37// 查找第一个大于等于给定值的元素
38function binSearchFindFirstGreaterEqual(arr, target) {
39 let left = 0
40 let right = arr.length - 1
41 while (left <= right) {
42 let mid = Math.floor((left + right) / 2)
43 if (arr[mid] >= target) {
44 if (mid === 0 || arr[mid - 1] < target) return mid
45 else right = mid - 1
46 } else left = mid + 1
47 }
48 return -1
49}
50
51console.log(binSearchFindFirstGreaterEqual([3, 4, 6, 7, 10], 5))
52
53// 查找最后一个小于等于给定值的元素
54function binSearchFindLatestLessEqual(arr, target) {
55 let left = 0
56 let right = arr.length - 1
57 while (left <= right) {
58 let mid = Math.floor((left + right) / 2)
59 if (arr[mid] <= target) {
60 if (mid === arr.length - 1 || arr[mid + 1] > target) return mid
61 else left = mid + 1
62 } else right = mid - 1
63 }
64 return -1
65}
66
67console.log(binSearchFindLatestLessEqual([3, 4, 6, 8, 9, 10], 7))

堆排序

1// i 到 n 进行堆化
2function heapify(arr, n, i) {
3 while (true) {
4 let maxPos = i
5 if (i * 2 <= n && arr[i] < arr[i * 2]) maxPos = i * 2
6 if (i * 2 + 1 <= n && arr[maxPos] < arr[i * 2 + 1]) maxPos = i * 2 + 1
7 if (maxPos === i) break
8 ;[arr[i], arr[maxPos]] = [arr[maxPos], arr[i]]
9 i = maxPos
10 }
11}
12
13function heapSort(arr) {
14 function buildHeap(arr) {
15 for (let i = Math.floor(arr.length / 2); i > 0; i--) {
16 heapify(arr, arr.length, i)
17 }
18 }
19 buildHeap(arr)
20 let k = arr.length - 1
21 while (k > 1) {
22 ;[arr[1], arr[k]] = [arr[k], arr[1]]
23 k -= 1
24 heapify(arr, k, 1)
25 }
26 return arr
27}
28
29// arr[0] 没用,只是从 1 开始方便计算
30console.log(heapSort([0, 1, 3, 2, 5, 4, 2, 1, 0]))