排序

桶排序

是计数排序的升级版,利用了函数的映射关系

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的N个数据均匀分配到K个桶中

最快:输入的数据可以均匀分配到每一个桶中
最慢:输入的数据被分配到了一个桶中

构建 大/小 根堆

  1. class Heap {
  2. constructor(comparator) {
  3. this.size = 0
  4. this.values = []
  5. // 如果是 (a - b) -> 小根堆
  6. // 如果是 (b - a) -> 大根堆
  7. this.comparator = comparator || Heap.minComparator
  8. }
  9. add(val) {
  10. this.values[this.size++] = val
  11. this.bubbleUp()
  12. }
  13. peak() {
  14. return this.values[0] || null
  15. }
  16. poll() {
  17. const ret = this.values[0]
  18. const end = this.values.pop()
  19. this.size--
  20. if (this.values.length) {
  21. this.values[0] = end
  22. this.bubbleDown()
  23. }
  24. return ret
  25. }
  26. // 上浮
  27. bubbleUp() {
  28. let index = this.values.length - 1
  29. let parent = (index - 1) >> 1
  30. while (this.comparator(this.values[index], this.values[parent]) < 0) {
  31. this.swap(parent, index)
  32. index = parent
  33. parent = (index - 1) >> 1
  34. }
  35. }
  36. bubbleDown() {
  37. let index = 0,
  38. len = this.values.length
  39. while (true) {
  40. let left = null,
  41. right = null,
  42. swap = null,
  43. leftIndex = index * 2 + 1, // 左子节点
  44. rightIndex = index * 2 + 2 // 右子节点
  45. if (leftIndex < len) {
  46. left = this.values[leftIndex]
  47. if (this.comparator(left, this.values[index]) < 0) {
  48. swap = leftIndex
  49. }
  50. }
  51. if (rightIndex < len) {
  52. right = this.values[rightIndex]
  53. if (swap !== null && this.comparator(right, left) < 0) {
  54. swap = rightIndex
  55. }
  56. }
  57. if (swap === null) break
  58. this.swap(index, swap)
  59. index = swap
  60. }
  61. }
  62. swap(i, j) {
  63. ;[this.values[i], this.values[j]] = [this.values[j], this.values[i]]
  64. }
  65. size() {
  66. return this.size
  67. }
  68. }
  69. Heap.minComparator = (a, b) => a - b
  70. Heap.maxComparator = (a, b) => b - a

前 k 小的数

  1. let a = [] // 堆
  2. let n = 0 // 当前堆中的个数
  3. // 下沉
  4. const sink = function (i) {
  5. let t = a[i] // 要下沉的元素
  6. let j = 0 // 左子节点,i 节点的左子节点:(2 * i) + 1,右子节点:2 * i + 2
  7. while ((j = i * 2 + 1) < n) {
  8. // 找到 i 节点的左子节点
  9. // 比插入排序多出来一步,需要在两个后继中找到最大的
  10. // j < n - 1 表示存在右子节点
  11. // 如果存在并且右子节点更大
  12. // 就指向右子节点
  13. if (j < n - 1 && a[j] < a[j + 1]) {
  14. // 小根堆这里要改成大于
  15. j = j + 1
  16. }
  17. // 如果子节点比t大
  18. // t还需要向后排,把这个大的子节点移上去
  19. if (a[j] > t) {
  20. // 如果是小根堆,这里改成 小于 即可
  21. a[i] = a[j]
  22. i = j
  23. } else {
  24. // 找到了t的位置
  25. // 此时t的位置是大于所有子节点的
  26. break
  27. }
  28. }
  29. // 将t的位置放在找到的位置那里
  30. a[i] = t
  31. }
  32. // 上浮
  33. const swim = function (i) {
  34. let t = a[i]
  35. let par = 0 // 父节点, i 节点的父节点 (i - 1) / 2
  36. while (i > 0 && (par = Math.floor((i - 1) / 2)) !== i) {
  37. if (a[par] < t) {
  38. // 如果是小根堆,这里改成 大于 即可
  39. a[i] = a[par]
  40. i = par
  41. } else {
  42. break
  43. }
  44. }
  45. a[i] = t
  46. }
  47. // 新来的元素先上浮,然后再执行上浮操作
  48. const heapPush = function (v) {
  49. a[n++] = v
  50. swim(n - 1)
  51. }
  52. // 返回a[0],因为是队列
  53. // 将最后一个元素放到第一个位置
  54. // 然后下沉
  55. const heapPop = function () {
  56. let ret = a[0]
  57. a[0] = a[--n]
  58. sink(0)
  59. return ret
  60. }
  61. const size = () => {
  62. return n
  63. }

BFPRT 算法

数组中第 k 小的数

  1. function getMinKthByBFPRT(arr, k) {
  2. let copyArr = arr.slice()
  3. return bfprt(copyArr, 0, copyArr.length - 1, copyArr.length - k)
  4. }
  5. // BFPRT 算法模板
  6. function bfprt(arr, begin, end, i) {
  7. if (begin === end) {
  8. return arr[begin]
  9. }
  10. // 找到划分依据
  11. let pivot = medianOfMedians(arr, begin, end)
  12. // partition根据这个值分,不是随机的
  13. let pivotRange = partition(arr, begin, end, pivot)
  14. // 根据范围继续partition
  15. if (i >= pivotRange[0] && i <= pivotRange[1]) {
  16. return arr[i]
  17. } else if (i < pivotRange[0]) {
  18. return bfprt(arr, begin, pivotRange[0] - 1, i)
  19. } else {
  20. return bfprt(arr, pivotRange[1] + 1, end, i)
  21. }
  22. }
  23. // 求每个分组后的中位数组成的数组的中位数
  24. function medianOfMedians(arr, begin, end) {
  25. // 5个分为一组,不足5个的单独为一组
  26. let num = end - begin + 1
  27. let offset = num % 5 === 0 ? 0 : 1
  28. let len = Math.floor(num / 5) + offset
  29. let medianArr = new Array(len).fill(null) // 中位数组成的数组
  30. for (let i = 0; i < medianArr.length; i++) {
  31. let beginI = begin + i * 5 // 分组后的每个数组的开头位置
  32. let endI = beginI + 4 // 分组后的每个数组的结尾位置
  33. medianArr[i] = getMedian(arr, beginI, Math.min(end, endI))
  34. }
  35. // 递归找到中位数数组里的中位数
  36. return bfprt(medianArr, 0, medianArr.length - 1, Math.floor(medianArr.length / 2))
  37. }
  38. // 求一个数组的中位数
  39. function getMedian(arr, begin, end) {
  40. insertionSort(arr, begin, end) // 排序
  41. const mid = Math.ceil((end - begin) / 2) + begin
  42. return arr[mid]
  43. }
  44. function insertionSort(arr, begin, end) {
  45. for (let i = begin + 1; i <= end; i++) {
  46. for (let j = i; j > begin; j--) {
  47. if (arr[j - 1] > arr[j]) {
  48. swap(arr, j, j - 1)
  49. } else {
  50. break
  51. }
  52. }
  53. }
  54. }
  55. function partition(arr, begin, end, pivotValue) {
  56. let small = begin - 1
  57. let cur = begin
  58. let big = end + 1
  59. while (cur !== big) {
  60. // 降序这里改成大于
  61. if (arr[cur] < pivotValue) {
  62. swap(arr, ++small, cur++)
  63. }
  64. // 降序这里改成小于
  65. else if (arr[cur] > pivotValue) {
  66. swap(arr, --big, cur)
  67. } else {
  68. cur++
  69. }
  70. }
  71. return [small + 1, big - 1]
  72. }
  73. function swap(arr, a, b) {
  74. ;[arr[a], arr[b]] = [arr[b], arr[a]]
  75. }

马拉车算法

最长回文子串
来到 i 位置

  1. 可能性 1:i 不在回文右边界里,暴力扩
  2. 可能性 2:i 在回文有边界里

    1. i 的对称位置的回文在 L,R 内
    2. i 的对称位置的回文在 L,R 外
    3. i 的对称位置的回文左边界与 L 压线,从 R 的右边开始扩

      1. var longestPalindrome = function (s) {
      2. const n = s.length
      3. if (n === 0) return ''
      4. let newStr = '#'
      5. // 构建新字符串,每个字符串被#包夹
      6. // #a#b#c#
      7. for (let i = 0; i < n; i++) {
      8. newStr += s[i]
      9. newStr += '#'
      10. }
      11. // 马拉车
      12. const N = newStr.length
      13. let arr = [] // 记录每个位置的回文最大半径
      14. let right = -1, // right->回文的最右边界
      15. j = -1 // j->最大回文的中心点
      16. let start = 0, // start->回文开始的位置
      17. end = -1 // end->回文结束的位置
      18. for (let i = 0; i < N; i++) {
      19. let curArmLen // 当前位置构成的回文的最大半径
      20. // i 在回文右边界内
      21. if (right >= i) {
      22. const curSymmetry = 2 * j - i // 当前位置关于上一个回文中心的对称点
      23. const minArmLen = Math.min(arr[curSymmetry], right - i)
      24. curArmLen = expand(newStr, i - minArmLen, i + minArmLen)
      25. }
      26. // i在回文右边界外
      27. else {
      28. curArmLen = expand(newStr, i, i)
      29. }
      30. arr.push(curArmLen)
      31. // 找到了更大的回文子串
      32. if (i + curArmLen > right) {
      33. j = i // 更新最大回文中心点
      34. right = i + curArmLen // 更新回文的最右边界
      35. }
      36. // 当前的回文长度大于了之前的回文长度
      37. // 更新回文开始的位置, 回文结束的位置
      38. if (curArmLen * 2 + 1 > end - start) {
      39. start = i - curArmLen
      40. end = i + curArmLen
      41. }
      42. }
      43. // 到这里我们得到的 [start, end] 就是最大回文字符串了
      44. let ans = ''
      45. for (let i = start; i <= end; i++) {
      46. if (newStr[i] !== '#') {
      47. ans += newStr[i]
      48. }
      49. }
      50. return ans
      51. // 找出回文最大半径
      52. // 暴力扩
      53. function expand(s, left, right) {
      54. while (left >= 0 && right < s.length && s[left] === s[right]) {
      55. --left
      56. ++right
      57. }
      58. return (right - left - 2) / 2
      59. }
      60. }

KMP 算法

实现 strStr

  1. var strStr = function (str1, str2) {
  2. const n = str1.length,
  3. m = str2.length
  4. if (m === 0) return 0
  5. const next = getNext(str2)
  6. let i = 0,
  7. j = 0
  8. while (i < str1.length && j < str2.length) {
  9. if (str1[i] == str2[j]) {
  10. i++
  11. j++
  12. } else {
  13. if (next[j] == -1) {
  14. i++
  15. } else {
  16. j = next[j]
  17. }
  18. }
  19. }
  20. return j == str2.length ? i - j : -1
  21. }
  22. const getNext = str => {
  23. if (str.length == 1) {
  24. return [-1]
  25. }
  26. const next = Array(str.length)
  27. next[0] = -1
  28. next[1] = 0
  29. let cn = 0
  30. let i = 2
  31. while (i < str.length) {
  32. if (str[i - 1] == str[cn]) {
  33. next[i++] = ++cn
  34. } else if (cn > 0) {
  35. cn = next[cn]
  36. } else {
  37. next[i++] = 0
  38. }
  39. }
  40. return next
  41. }

相加

大数相加

字符串相加

  1. function add(num1, num2) {
  2. let maxlen = Math.max(num1.length, num2.length)
  3. let a = num1.padStart(maxlen, 0)
  4. let b = num2.padStart(maxlen, 0)
  5. let res = ''
  6. let next = 0 //用一个变量存每一次的进位
  7. for (let i = maxlen - 1; i >= 0; i--) {
  8. let acc = Number(a[i]) + Number(b[i]) + next
  9. next = Math.floor(acc / 10)
  10. res = (acc % 10) + res
  11. }
  12. if (next === 1) res = '1' + res //如果到最高位还有进位就再加一位
  13. return res
  14. }

小数相加

  1. add(num1,num2){
  2. let len1 = (num1 + "").split(".")[1].length;
  3. let len2=(num2 + "").split(".")[1].length;
  4. let maxlen = Math.max(len1, len2);
  5. let a = Math.pow(10, maxlen);
  6. return (num1 * a + num2 * a) / a;
  7. }

单调栈

  1. function findRightSmall(arr) {
  2. let ans = []
  3. let t = [] // 存放下标
  4. for (let i = 0; i < arr.length; i++) {
  5. while (t.length && arr[t.length - 1] > arr[i]) {
  6. ans[t[t.length - 1]] = i
  7. t.pop()
  8. }
  9. t.push(i)
  10. }
  11. while (t.length) {
  12. ans[t[t.length - 1]] = -1
  13. t.pop()
  14. }
  15. return ans
  16. }

FIFO 队列与单调队列

循环队列

取模技巧

  • index = i 的后一个 == (i + 1) % capacity
  • index = i 的前一个 == (i - 1 + capacity) % capacity

    1. class MyCircularQueue {
    2. constructor(k) {
    3. this.capacity = k + 1
    4. this.a = Array(k + 1)
    5. this.front = 0
    6. this.rear = 0
    7. }
    8. enQueue(value) {
    9. if (this.isFull()) {
    10. return false
    11. }
    12. this.a[this.rear] = value // 元素放在rear(队尾)位置
    13. this.rear = (this.rear + 1) % this.capacity // rear向后移动
    14. return true
    15. }
    16. deQueue() {
    17. if (this.isEmpty()) {
    18. return false
    19. }
    20. this.front = (this.front + 1) % this.capacity
    21. return true
    22. }
    23. Front() {
    24. return this.isEmpty() ? -1 : this.a[this.front]
    25. }
    26. Rear() {
    27. let tail = (this.rear - 1 + this.capacity) % this.capacity
    28. return this.isEmpty() ? -1 : this.a[tail]
    29. }
    30. isEmpty() {
    31. return this.front === this.rear
    32. }
    33. isFull() {
    34. return (this.rear + 1) % this.capacity === this.front
    35. }
    36. }

单调队列

  1. class MonoMinusQueue {
  2. constructor() {
  3. this.Q = []
  4. }
  5. push(val) {
  6. while (this.Q.length && this.Q[this.Q.length - 1] < val) {
  7. this.Q.pop()
  8. }
  9. this.Q.push(val)
  10. }
  11. pop(val) {
  12. if (this.Q.length && this.Q[0] === val) {
  13. this.Q.shift()
  14. }
  15. }
  16. }

二叉树的遍历

前序遍历

  1. function preorderTraversal(root) {
  2. const stack = []
  3. const ans = []
  4. while (root !== null || stack.length) {
  5. while (root !== null) {
  6. stack.push(root)
  7. ans.push(root.val)
  8. root = root.left
  9. }
  10. root = s.pop()
  11. root = root.right
  12. }
  13. return ans
  14. }

中序遍历

  1. function inorderTraversal(root) {
  2. const stack = []
  3. const ans = []
  4. while (root !== null || stack.length) {
  5. while (root !== null) {
  6. stack.push(root)
  7. root = root.left
  8. }
  9. root = s.pop()
  10. ans.push(root.val)
  11. root = root.right
  12. }
  13. return ans
  14. }

后序遍历

  1. function inorderTraversal(root) {
  2. const stack = []
  3. const ans = []
  4. let pre = null
  5. while (stack.length && root !== null) {
  6. while (root !== null) {
  7. stack.push(root)
  8. root = root.left
  9. }
  10. root = stack[stack.length - 1]
  11. if (root.right === null || root.right === pre) {
  12. ans.push(root.val)
  13. stack.pop()
  14. pre = root
  15. root = null
  16. } else {
  17. root = root.right
  18. }
  19. }
  20. return ans
  21. }

Morris 遍历

  1. 如果 cur 无左孩子,cur 向右移动(cur = cur.right)
  2. 如果 cur 有左孩子,找到 cur 左子树上最右的节点,记为 mostright
    1. 如果 mostright 的 right 指针指向空,让其指向 cur,cur 向左移动(cur = cur.left)
    2. 如果 mostright 的 right 指针指向 cur,让其指向空,cur 向右移动(cur = cur.right)

并查集

  1. let F = null
  2. let count = 0
  3. let Cnt = null
  4. function init(n) {
  5. F = Array(n)
  6. Cnt = Array(n)
  7. for (let i = 0; i < n; i++) {
  8. F[i] = i
  9. Cnt[i] = i
  10. }
  11. count = n
  12. }
  13. function find(x) {
  14. return x === F[x] ? x : find(F[x])
  15. }
  16. function union(x, y) {
  17. let xpar = find(x)
  18. let ypar = find(y)
  19. if (xpar !== ypar) {
  20. F[xpar] = ypar
  21. Cnt[ypar] += Cnt[xpar]
  22. count--
  23. }
  24. }
  25. function size(i) {
  26. return Cnt[find(i)]
  27. }

二分

左闭右开原则

  1. function lowerBound(arr, target) {
  2. let l = 0, r = arr.length
  3. while (l < r) {
  4. let m = (l + r) >> 1
  5. if (arr[m] < target) {
  6. l = m + 1
  7. } else {
  8. r = m
  9. }
  10. }
  11. return l
  12. }

双指针

区间单调性

不允许出现 满足条件 =》不满足条件 =》满足条件 的情况

区间最优原则

从左向右遍历区间右端固定集合中的每个区间,找到一个满足条件的解即可
再添加一个左指针 left,指向区间的左边,与A[i] 元素构成区间 (left, i]。—— 开闭原则(左开右闭)
寻找A[i] 为右边界的最优解可以分为3步走:

  1. 将 A[i] 加到区间中,形成新区间 (left, i]
  2. 遍历 A[i] 的区间右端固定集合,直到找到以 A[i] 为右端点的最优解

    1. while (left < i && (left, i] 区间不满足要求) {
    2. left++;
    3. }
  3. (left, i] 满足要求

最长区间时双指针的结论:最长区间问题的最优解 =》只需要遍历每个元素 A[i] 的最优解

最长区间

题目具备如下特点:

  • 给定一个条件
  • 求最长区间/最长子串
  • 题目给的区间需要具备单调性

解题:

  1. 两个指针:left 指针和 right 指针,两个指针形成的区间 (left, right]。左开右闭
  2. 惰性原则:left 直到条件不满足的时候才向右移动
    1. function maxLength(arr) {
    2. let n = arr.length;
    3. let left = -1;
    4. let ans = 0;
    5. for (let i = 0; i < n; i++) {
    6. // 1. 将 arr[i] 加入区间中,形成(left, i]
    7. // 2. 将 arr[i] 加入后,惰性原则
    8. while (check((left, i])/* 检查区间状态是否满足条件*/ ) {
    9. ++left; // 如果不满足条件移动左指针
    10. // 修改区间的状态
    11. }
    12. ans = max(ans, i - left);
    13. }
    14. return ans;
    15. }
    无重复最长子串