排序
桶排序
是计数排序的升级版,利用了函数的映射关系
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的N个数据均匀分配到K个桶中
最快:输入的数据可以均匀分配到每一个桶中
最慢:输入的数据被分配到了一个桶中
构建 大/小 根堆
class Heap {
constructor(comparator) {
this.size = 0
this.values = []
// 如果是 (a - b) -> 小根堆
// 如果是 (b - a) -> 大根堆
this.comparator = comparator || Heap.minComparator
}
add(val) {
this.values[this.size++] = val
this.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] = end
this.bubbleDown()
}
return ret
}
// 上浮
bubbleUp() {
let index = this.values.length - 1
let parent = (index - 1) >> 1
while (this.comparator(this.values[index], this.values[parent]) < 0) {
this.swap(parent, index)
index = parent
parent = (index - 1) >> 1
}
}
bubbleDown() {
let index = 0,
len = this.values.length
while (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) break
this.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 - b
Heap.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 + 2
while ((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) / 2
while (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++] = v
swim(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)
// 根据范围继续partition
if (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 + 1
let offset = num % 5 === 0 ? 0 : 1
let len = Math.floor(num / 5) + offset
let 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) + begin
return 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 - 1
let cur = begin
let big = end + 1
while (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.length
if (n === 0) return ''
let newStr = '#'
// 构建新字符串,每个字符串被#包夹
// #a#b#c#
for (let i = 0; i < n; i++) {
newStr += s[i]
newStr += '#'
}
// 马拉车
const N = newStr.length
let 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 - curArmLen
end = 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.length
if (m === 0) return 0
const next = getNext(str2)
let i = 0,
j = 0
while (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] = -1
next[1] = 0
let cn = 0
let i = 2
while (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]) + next
next = 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]] = i
t.pop()
}
t.push(i)
}
while (t.length) {
ans[t[t.length - 1]] = -1
t.pop()
}
return ans
}
FIFO 队列与单调队列
循环队列
取模技巧
- index = i 的后一个 == (i + 1) % capacity
index = i 的前一个 == (i - 1 + capacity) % capacity
class MyCircularQueue {
constructor(k) {
this.capacity = k + 1
this.a = Array(k + 1)
this.front = 0
this.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.capacity
return true
}
Front() {
return this.isEmpty() ? -1 : this.a[this.front]
}
Rear() {
let tail = (this.rear - 1 + this.capacity) % this.capacity
return 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 = null
while (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 = root
root = 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 = null
let count = 0
let Cnt = null
function init(n) {
F = Array(n)
Cnt = Array(n)
for (let i = 0; i < n; i++) {
F[i] = i
Cnt[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] = ypar
Cnt[ypar] += Cnt[xpar]
count--
}
}
function size(i) {
return Cnt[find(i)]
}
二分
左闭右开原则
function lowerBound(arr, target) {
let l = 0, r = arr.length
while (l < r) {
let m = (l + r) >> 1
if (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;
}