排序
桶排序
是计数排序的升级版,利用了函数的映射关系
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的N个数据均匀分配到K个桶中
最快:输入的数据可以均匀分配到每一个桶中
最慢:输入的数据被分配到了一个桶中
构建 大/小 根堆
class Heap {constructor(comparator) {this.size = 0this.values = []// 如果是 (a - b) -> 小根堆// 如果是 (b - a) -> 大根堆this.comparator = comparator || Heap.minComparator}add(val) {this.values[this.size++] = valthis.bubbleUp()}peak() {return this.values[0] || null}poll() {const ret = this.values[0]const end = this.values.pop()this.size--if (this.values.length) {this.values[0] = endthis.bubbleDown()}return ret}// 上浮bubbleUp() {let index = this.values.length - 1let parent = (index - 1) >> 1while (this.comparator(this.values[index], this.values[parent]) < 0) {this.swap(parent, index)index = parentparent = (index - 1) >> 1}}bubbleDown() {let index = 0,len = this.values.lengthwhile (true) {let left = null,right = null,swap = null,leftIndex = index * 2 + 1, // 左子节点rightIndex = index * 2 + 2 // 右子节点if (leftIndex < len) {left = this.values[leftIndex]if (this.comparator(left, this.values[index]) < 0) {swap = leftIndex}}if (rightIndex < len) {right = this.values[rightIndex]if (swap !== null && this.comparator(right, left) < 0) {swap = rightIndex}}if (swap === null) breakthis.swap(index, swap)index = swap}}swap(i, j) {;[this.values[i], this.values[j]] = [this.values[j], this.values[i]]}size() {return this.size}}Heap.minComparator = (a, b) => a - bHeap.maxComparator = (a, b) => b - a
let a = [] // 堆let n = 0 // 当前堆中的个数// 下沉const sink = function (i) {let t = a[i] // 要下沉的元素let j = 0 // 左子节点,i 节点的左子节点:(2 * i) + 1,右子节点:2 * i + 2while ((j = i * 2 + 1) < n) {// 找到 i 节点的左子节点// 比插入排序多出来一步,需要在两个后继中找到最大的// j < n - 1 表示存在右子节点// 如果存在并且右子节点更大// 就指向右子节点if (j < n - 1 && a[j] < a[j + 1]) {// 小根堆这里要改成大于j = j + 1}// 如果子节点比t大// t还需要向后排,把这个大的子节点移上去if (a[j] > t) {// 如果是小根堆,这里改成 小于 即可a[i] = a[j]i = j} else {// 找到了t的位置// 此时t的位置是大于所有子节点的break}}// 将t的位置放在找到的位置那里a[i] = t}// 上浮const swim = function (i) {let t = a[i]let par = 0 // 父节点, i 节点的父节点 (i - 1) / 2while (i > 0 && (par = Math.floor((i - 1) / 2)) !== i) {if (a[par] < t) {// 如果是小根堆,这里改成 大于 即可a[i] = a[par]i = par} else {break}}a[i] = t}// 新来的元素先上浮,然后再执行上浮操作const heapPush = function (v) {a[n++] = vswim(n - 1)}// 返回a[0],因为是队列// 将最后一个元素放到第一个位置// 然后下沉const heapPop = function () {let ret = a[0]a[0] = a[--n]sink(0)return ret}const size = () => {return n}
BFPRT 算法
function getMinKthByBFPRT(arr, k) {let copyArr = arr.slice()return bfprt(copyArr, 0, copyArr.length - 1, copyArr.length - k)}// BFPRT 算法模板function bfprt(arr, begin, end, i) {if (begin === end) {return arr[begin]}// 找到划分依据let pivot = medianOfMedians(arr, begin, end)// partition根据这个值分,不是随机的let pivotRange = partition(arr, begin, end, pivot)// 根据范围继续partitionif (i >= pivotRange[0] && i <= pivotRange[1]) {return arr[i]} else if (i < pivotRange[0]) {return bfprt(arr, begin, pivotRange[0] - 1, i)} else {return bfprt(arr, pivotRange[1] + 1, end, i)}}// 求每个分组后的中位数组成的数组的中位数function medianOfMedians(arr, begin, end) {// 5个分为一组,不足5个的单独为一组let num = end - begin + 1let offset = num % 5 === 0 ? 0 : 1let len = Math.floor(num / 5) + offsetlet medianArr = new Array(len).fill(null) // 中位数组成的数组for (let i = 0; i < medianArr.length; i++) {let beginI = begin + i * 5 // 分组后的每个数组的开头位置let endI = beginI + 4 // 分组后的每个数组的结尾位置medianArr[i] = getMedian(arr, beginI, Math.min(end, endI))}// 递归找到中位数数组里的中位数return bfprt(medianArr, 0, medianArr.length - 1, Math.floor(medianArr.length / 2))}// 求一个数组的中位数function getMedian(arr, begin, end) {insertionSort(arr, begin, end) // 排序const mid = Math.ceil((end - begin) / 2) + beginreturn arr[mid]}function insertionSort(arr, begin, end) {for (let i = begin + 1; i <= end; i++) {for (let j = i; j > begin; j--) {if (arr[j - 1] > arr[j]) {swap(arr, j, j - 1)} else {break}}}}function partition(arr, begin, end, pivotValue) {let small = begin - 1let cur = beginlet big = end + 1while (cur !== big) {// 降序这里改成大于if (arr[cur] < pivotValue) {swap(arr, ++small, cur++)}// 降序这里改成小于else if (arr[cur] > pivotValue) {swap(arr, --big, cur)} else {cur++}}return [small + 1, big - 1]}function swap(arr, a, b) {;[arr[a], arr[b]] = [arr[b], arr[a]]}
马拉车算法
最长回文子串
来到 i 位置
- 可能性 1:i 不在回文右边界里,暴力扩
可能性 2:i 在回文有边界里
- i 的对称位置的回文在 L,R 内
- i 的对称位置的回文在 L,R 外
i 的对称位置的回文左边界与 L 压线,从 R 的右边开始扩
var longestPalindrome = function (s) {const n = s.lengthif (n === 0) return ''let newStr = '#'// 构建新字符串,每个字符串被#包夹// #a#b#c#for (let i = 0; i < n; i++) {newStr += s[i]newStr += '#'}// 马拉车const N = newStr.lengthlet arr = [] // 记录每个位置的回文最大半径let right = -1, // right->回文的最右边界j = -1 // j->最大回文的中心点let start = 0, // start->回文开始的位置end = -1 // end->回文结束的位置for (let i = 0; i < N; i++) {let curArmLen // 当前位置构成的回文的最大半径// i 在回文右边界内if (right >= i) {const curSymmetry = 2 * j - i // 当前位置关于上一个回文中心的对称点const minArmLen = Math.min(arr[curSymmetry], right - i)curArmLen = expand(newStr, i - minArmLen, i + minArmLen)}// i在回文右边界外else {curArmLen = expand(newStr, i, i)}arr.push(curArmLen)// 找到了更大的回文子串if (i + curArmLen > right) {j = i // 更新最大回文中心点right = i + curArmLen // 更新回文的最右边界}// 当前的回文长度大于了之前的回文长度// 更新回文开始的位置, 回文结束的位置if (curArmLen * 2 + 1 > end - start) {start = i - curArmLenend = i + curArmLen}}// 到这里我们得到的 [start, end] 就是最大回文字符串了let ans = ''for (let i = start; i <= end; i++) {if (newStr[i] !== '#') {ans += newStr[i]}}return ans// 找出回文最大半径// 暴力扩function expand(s, left, right) {while (left >= 0 && right < s.length && s[left] === s[right]) {--left++right}return (right - left - 2) / 2}}
KMP 算法
var strStr = function (str1, str2) {const n = str1.length,m = str2.lengthif (m === 0) return 0const next = getNext(str2)let i = 0,j = 0while (i < str1.length && j < str2.length) {if (str1[i] == str2[j]) {i++j++} else {if (next[j] == -1) {i++} else {j = next[j]}}}return j == str2.length ? i - j : -1}const getNext = str => {if (str.length == 1) {return [-1]}const next = Array(str.length)next[0] = -1next[1] = 0let cn = 0let i = 2while (i < str.length) {if (str[i - 1] == str[cn]) {next[i++] = ++cn} else if (cn > 0) {cn = next[cn]} else {next[i++] = 0}}return next}
相加
大数相加
function add(num1, num2) {let maxlen = Math.max(num1.length, num2.length)let a = num1.padStart(maxlen, 0)let b = num2.padStart(maxlen, 0)let res = ''let next = 0 //用一个变量存每一次的进位for (let i = maxlen - 1; i >= 0; i--) {let acc = Number(a[i]) + Number(b[i]) + nextnext = Math.floor(acc / 10)res = (acc % 10) + res}if (next === 1) res = '1' + res //如果到最高位还有进位就再加一位return res}
小数相加
add(num1,num2){let len1 = (num1 + "").split(".")[1].length;let len2=(num2 + "").split(".")[1].length;let maxlen = Math.max(len1, len2);let a = Math.pow(10, maxlen);return (num1 * a + num2 * a) / a;}
栈
单调栈
function findRightSmall(arr) {let ans = []let t = [] // 存放下标for (let i = 0; i < arr.length; i++) {while (t.length && arr[t.length - 1] > arr[i]) {ans[t[t.length - 1]] = it.pop()}t.push(i)}while (t.length) {ans[t[t.length - 1]] = -1t.pop()}return ans}
FIFO 队列与单调队列
循环队列
取模技巧
- index = i 的后一个 == (i + 1) % capacity
index = i 的前一个 == (i - 1 + capacity) % capacity
class MyCircularQueue {constructor(k) {this.capacity = k + 1this.a = Array(k + 1)this.front = 0this.rear = 0}enQueue(value) {if (this.isFull()) {return false}this.a[this.rear] = value // 元素放在rear(队尾)位置this.rear = (this.rear + 1) % this.capacity // rear向后移动return true}deQueue() {if (this.isEmpty()) {return false}this.front = (this.front + 1) % this.capacityreturn true}Front() {return this.isEmpty() ? -1 : this.a[this.front]}Rear() {let tail = (this.rear - 1 + this.capacity) % this.capacityreturn this.isEmpty() ? -1 : this.a[tail]}isEmpty() {return this.front === this.rear}isFull() {return (this.rear + 1) % this.capacity === this.front}}
单调队列
class MonoMinusQueue {constructor() {this.Q = []}push(val) {while (this.Q.length && this.Q[this.Q.length - 1] < val) {this.Q.pop()}this.Q.push(val)}pop(val) {if (this.Q.length && this.Q[0] === val) {this.Q.shift()}}}
二叉树的遍历
前序遍历
function preorderTraversal(root) {const stack = []const ans = []while (root !== null || stack.length) {while (root !== null) {stack.push(root)ans.push(root.val)root = root.left}root = s.pop()root = root.right}return ans}
中序遍历
function inorderTraversal(root) {const stack = []const ans = []while (root !== null || stack.length) {while (root !== null) {stack.push(root)root = root.left}root = s.pop()ans.push(root.val)root = root.right}return ans}
后序遍历
function inorderTraversal(root) {const stack = []const ans = []let pre = nullwhile (stack.length && root !== null) {while (root !== null) {stack.push(root)root = root.left}root = stack[stack.length - 1]if (root.right === null || root.right === pre) {ans.push(root.val)stack.pop()pre = rootroot = null} else {root = root.right}}return ans}
Morris 遍历
- 如果 cur 无左孩子,cur 向右移动(cur = cur.right)
- 如果 cur 有左孩子,找到 cur 左子树上最右的节点,记为 mostright
- 如果 mostright 的 right 指针指向空,让其指向 cur,cur 向左移动(cur = cur.left)
- 如果 mostright 的 right 指针指向 cur,让其指向空,cur 向右移动(cur = cur.right)
并查集
let F = nulllet count = 0let Cnt = nullfunction init(n) {F = Array(n)Cnt = Array(n)for (let i = 0; i < n; i++) {F[i] = iCnt[i] = i}count = n}function find(x) {return x === F[x] ? x : find(F[x])}function union(x, y) {let xpar = find(x)let ypar = find(y)if (xpar !== ypar) {F[xpar] = yparCnt[ypar] += Cnt[xpar]count--}}function size(i) {return Cnt[find(i)]}
二分
左闭右开原则
function lowerBound(arr, target) {let l = 0, r = arr.lengthwhile (l < r) {let m = (l + r) >> 1if (arr[m] < target) {l = m + 1} else {r = m}}return l}
双指针
区间单调性
区间最优原则
从左向右遍历区间右端固定集合中的每个区间,找到一个满足条件的解即可
再添加一个左指针 left,指向区间的左边,与A[i] 元素构成区间 (left, i]。—— 开闭原则(左开右闭)
寻找A[i] 为右边界的最优解可以分为3步走:
- 将 A[i] 加到区间中,形成新区间 (left, i]
遍历 A[i] 的区间右端固定集合,直到找到以 A[i] 为右端点的最优解
while (left < i && (left, i] 区间不满足要求) {left++;}
(left, i] 满足要求
最长区间时双指针的结论:最长区间问题的最优解 =》只需要遍历每个元素 A[i] 的最优解
最长区间
题目具备如下特点:
- 给定一个条件
- 求最长区间/最长子串
- 题目给的区间需要具备单调性
解题:
- 两个指针:left 指针和 right 指针,两个指针形成的区间 (left, right]。左开右闭
- 惰性原则:left 直到条件不满足的时候才向右移动
无重复最长子串function maxLength(arr) {let n = arr.length;let left = -1;let ans = 0;for (let i = 0; i < n; i++) {// 1. 将 arr[i] 加入区间中,形成(left, i]// 2. 将 arr[i] 加入后,惰性原则while (check((left, i])/* 检查区间状态是否满足条件*/ ) {++left; // 如果不满足条件移动左指针// 修改区间的状态}ans = max(ans, i - left);}return ans;}
