1.前端算法
前端中的算法与数据结构
全排序(数组、链表、二叉树、堆)
偏排序(数组、链表、堆)
查找与搜索(二叉树、平衡二叉树(包括红黑树)、哈希表)
动态规划(数组、链表、堆、二叉树)
React中的算法与数据结构:深度优先搜索、递归、动态规划、散列表、 数组、链表、二叉树、堆、栈等
Vue中的算法与数据结构:动态规划、递归、二分查找、散列表、LRU(最近最少使用) 、数组等
Vue3 种 diff 使用了 最长递增子序列实现。
使用这个算法主要用处是,当diff 算法已经比较剩下两个数组时候,原来[1,2,3,4,5,6]. 调整后 变成 [1,3,4,2,6,5] 这时候要找到最优的移动方案。
- 最差的方案就是每个元素都更新。
- 最优的移动方案就是 只移动 2和5 ,2移动到4后,5 移动到6后。
那么要只移动 2和5。就要确定 1,3,4,6 是固定不变的,也称为当前数组的最长的递增子序列。
因为原来就是使用数组为下标,所以已经是从小到大排列。只是局部乱了。所以找出局部错乱的是较优的策略。
https://leetcode.cn/problems/longest-increasing-subsequence/
leetcode只查询长度,当前vue3需要确定的数组 代码如下
var getSequence1 = function (nums) {
let result = []
for (let i = 0; i < nums.length; i++) {
let last = nums[result[result.length - 1]],
current = nums[i]
if (current > last || last === undefined) {
// 当前项大于最后一项
result.push(i)
} else {
// 当前项小于最后一项,二分查找+替换
let start = 0,
end = result.length - 1,
middle
while (start < end) {
middle = Math.floor((start + end) / 2)
if (nums[result[middle]] > current) {
end = middle
} else {
start = middle + 1
}
}
result[start] = i
}
}
return result
}
console.log( getSequence1([10,1,2,5,3,7,101,18]))
贪心算法
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
二分查找
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
二分查找之所以快是因为它只需检查很少几个条目(相对于数组的大小)就能够找到目标元素,或者是确认目标元素不存在。
const searchInsert = (nums, target) => {
let low = 0,
high = nums.length - 1,
mid;
while (low <= high) {
mid = (low + high) >> 1;
if (target < nums[mid]) {
high = mid - 1;
} else if (target > nums[mid]) {
low = mid + 1;
} else {
return mid;
}
}
};
实现VUE的VDOM DIFF
// vdom 虚拟dom
// old 老节点
// new 新节点
// old array a b c d e f g
// new array a b e c d h f g
// mountElement 新增元素 h
// patch 复用元素 a b c d e f g
// unmount 删除元素
// todo
// move 元素移动 ?
exports.diffArray = (c1, c2, { mountElement, patch, unmount, move }) => {
function isSameVnodeType(n1, n2) {
return n1.key === n2.key; // && n1.type === n2.type;
}
let l1 = c1.length;
let l2 = c2.length;
let i = 0;
let e1 = l1 - 1;
let e2 = l2 - 1;
// *1 从左边往右,如果元素可以复用就继续往右边,否则就停止循环
while (i <= e1 && i <= e2) {
const n1 = c1[i];
const n2 = c2[i];
if (isSameVnodeType(n1, n2)) {
patch(n1.key);
} else {
break;
}
i++;
}
// *2 从右边往左,如果元素可以复用就继续往左边,否则就停止循环
while (i <= e1 && i <= e2) {
const n1 = c1[e1];
const n2 = c2[e2];
if (isSameVnodeType(n1, n2)) {
patch(n1.key);
} else {
break;
}
e1--;
e2--;
}
// *3.1 老节点没了,新节点还有
if (i > e1) {
if (i <= e2) {
while (i <= e2) {
const n2 = c2[i];
mountElement(n2.key);
i++;
}
}
}
// *3.2 老节点还有,新节点没了
else if (i > e2) {
if (i <= e1) {
while (i <= e1) {
const n1 = c1[i];
unmount(n1.key);
i++;
}
}
} else {
// *4 新老节点都还有,但是顺序不稳定,有点乱
// * 4.1 把新元素做成Map,key:value(index)
const s1 = i;
const s2 = i;
const keyToNewIndexMap = new Map();
for (i = s2; i <= e2; i++) {
const nextChild = c2[i];
keyToNewIndexMap.set(nextChild.key, i);
}
// *4.2 记录一下新老元素的相对下标
const toBePatched = e2 - s2 + 1;
const newIndexToOldIndexMap = new Array(toBePatched);
// 数组的下标记录的是新元素的相对下标,
// value初始值是0
// todo 在4.3中做一件事:一旦元素可以被复用,value值更新成老元素的下标+1
// 数组的值如果还是0, 证明这个值在新元素中是要mount的
for (i = 0; i < toBePatched; i++) {
newIndexToOldIndexMap[i] = 0;
}
// * 4.3 去遍历老元素 (patch、unmount)
// old sdasjkjkkklll
// new ds
// 记录新节点要多少个还没处理
let patched = 0;
let moved = false;
let maxNewIndexSoFar = 0;
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
if (patched >= toBePatched) {
unmount(prevChild.key);
continue;
}
const newIndex = keyToNewIndexMap.get(prevChild.key);
if (newIndex === undefined) {
// 没有找到要复用它的节点,只能删除
unmount(prevChild.key);
} else {
// 节点要被复用
// 1 2 3 5 10
// maxNewIndexSoFar记录队伍最后一个元素的下标
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
// 插队
moved = true;
}
// newIndex - s2是相对下标
// i + 1老元素下标+1
newIndexToOldIndexMap[newIndex - s2] = i + 1;
patch(prevChild.key);
patched++;
}
}
// * 4.4 去遍新元素 mount、move
// e2 -> i 下标遍历
// toBePatched -> 0 相对下标
// [1, 2];
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: [];
console.log("increasingNewIndexSequence", increasingNewIndexSequence); //sy-log
let lastIndex = increasingNewIndexSequence.length - 1;
for (i = toBePatched - 1; i >= 0; i--) {
const nextChild = c2[s2 + i];
// 判断节点是mount还是move
if (newIndexToOldIndexMap[i] === 0) {
// nextChild要新增
mountElement(nextChild.key);
} else {
// 可能move
// i 是新元素的相对下标
// lastIndex是LIS的相对下标
if (lastIndex < 0 || i !== increasingNewIndexSequence[lastIndex]) {
console.log("ooo", nextChild.key); //sy-log
move(nextChild.key);
} else {
lastIndex--;
}
}
}
}
// function getSequence() {
// return [1, 2];
// }
// 1 2 5 [2]
function getSequence(arr) {
// return [1, 2];
// 返回的是LIS的路径
const lis = [0];
const len = arr.length;
const record = arr.slice();
for (let i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
const last = lis[lis.length - 1];
if (arr[last] < arrI) {
// 新来的元素比lis最后一个元素大,直接放到lis最后
// 1 3 5 10
record[i] = last;
lis.push(i);
continue;
}
// 二分替换
let left = 0,
right = lis.length - 1;
while (left < right) {
const mid = (left + right) >> 1;
if (arr[lis[mid]] < arrI) {
// 在右边
left = mid + 1;
} else {
right = mid;
}
}
// 从lis里找比arrI大的最小的元素,并且替换
if (arrI < arr[lis[left]]) {
if (left > 0) {
record[i] = lis[left - 1];
}
lis[left] = i;
}
}
}
let i = lis.length;
let last = lis[i - 1];
while (i-- > 0) {
lis[i] = last;
last = record[last];
}
console.log("l-----is", lis); //sy-log
return lis;
}
};
测试用例 vue-diff.spec.js
describe("数组Diff", () => {
it("1. 左边查找", () => {
const mountElement = jest.fn();
const patch = jest.fn();
const unmount = jest.fn();
const move = jest.fn();
const { diffArray } = require("../vue-diff");
diffArray(
[{ key: "a" }, { key: "b" }, { key: "c" }],
[{ key: "a" }, { key: "b" }, { key: "d" }, { key: "e" }],
{
mountElement,
patch,
unmount,
move,
}
);
// 第一次调用次数
expect(patch.mock.calls.length).toBe(2);
// 第一次调用的第一个参数
expect(patch.mock.calls[0][0]).toBe("a");
expect(patch.mock.calls[1][0]).toBe("b");
});
it("2. 右边边查找", () => {
const mountElement = jest.fn();
const patch = jest.fn();
const unmount = jest.fn();
const move = jest.fn();
const { diffArray } = require("../vue-diff");
diffArray(
[{ key: "a" }, { key: "b" }, { key: "c" }],
[{ key: "d" }, { key: "e" }, { key: "b" }, { key: "c" }],
{
mountElement,
patch,
unmount,
move,
}
);
expect(patch.mock.calls.length).toBe(2);
expect(patch.mock.calls[0][0]).toBe("c");
expect(patch.mock.calls[1][0]).toBe("b");
});
it("3. 老节点没了,新节点还有", () => {
const mountElement = jest.fn();
const patch = jest.fn();
const unmount = jest.fn();
const move = jest.fn();
const { diffArray } = require("../vue-diff");
diffArray(
[{ key: "a" }, { key: "b" }],
[{ key: "a" }, { key: "b" }, { key: "c" }],
{
mountElement,
patch,
unmount,
move,
}
);
expect(patch.mock.calls.length).toBe(2);
expect(patch.mock.calls[0][0]).toBe("a");
expect(patch.mock.calls[1][0]).toBe("b");
expect(mountElement.mock.calls[0][0]).toBe("c");
});
it("4. 老节点还有,新节点没了", () => {
const mountElement = jest.fn();
const patch = jest.fn();
const unmount = jest.fn();
const move = jest.fn();
const { diffArray } = require("../vue-diff");
diffArray(
[{ key: "a" }, { key: "b" }, { key: "c" }],
[{ key: "a" }, { key: "b" }],
{
mountElement,
patch,
unmount,
move,
}
);
// 第一次调用次数
expect(patch.mock.calls.length).toBe(2);
// 第一次调用的第一个参数
expect(patch.mock.calls[0][0]).toBe("a");
expect(patch.mock.calls[1][0]).toBe("b");
expect(unmount.mock.calls[0][0]).toBe("c");
});
it("5. 新老节点都有,但是顺序不稳定", () => {
const mountElement = jest.fn();
const patch = jest.fn();
const unmount = jest.fn();
const move = jest.fn();
const { diffArray } = require("../vue-diff");
diffArray(
[
{ key: "a" },
{ key: "b" },
{ key: "c" },
{ key: "d" },
{ key: "e" },
{ key: "f" },
{ key: "g" },
],
[
{ key: "a" },
{ key: "b" },
{ key: "e" },
{ key: "d" },
{ key: "c" },
{ key: "h" },
{ key: "f" },
{ key: "g" },
],
{
mountElement,
patch,
unmount,
move,
}
);
// 第一次调用次数
expect(patch.mock.calls.length).toBe(7);
// 第一次调用的第一个参数
expect(patch.mock.calls[0][0]).toBe("a");
expect(patch.mock.calls[1][0]).toBe("b");
expect(patch.mock.calls[2][0]).toBe("g");
expect(patch.mock.calls[3][0]).toBe("f");
expect(patch.mock.calls[4][0]).toBe("c");
expect(patch.mock.calls[5][0]).toBe("d");
expect(patch.mock.calls[6][0]).toBe("e");
expect(unmount.mock.calls.length).toBe(0);
// 0 1 2 3 4 5 6
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// 4 3 2 0
// [5 4 3 0]
// e d c
// e d c
// todo
// 1. mount
expect(mountElement.mock.calls[0][0]).toBe("h");
// // 2. move
expect(move.mock.calls[0][0]).toBe("d");
expect(move.mock.calls[1][0]).toBe("e");
});
it("6. 新老节点都有,但是顺序不稳定", () => {
const mountElement = jest.fn();
const patch = jest.fn();
const unmount = jest.fn();
const move = jest.fn();
const { diffArray } = require("../vue-diff");
diffArray(
[
{ key: "a" },
{ key: "b" },
{ key: "c" },
{ key: "d" },
{ key: "e" },
{ key: "f" },
{ key: "g" },
],
[
{ key: "a" },
{ key: "b" },
{ key: "d1" },
{ key: "e" },
{ key: "c" },
{ key: "d" },
{ key: "h" },
{ key: "f" },
{ key: "g" },
],
{
mountElement,
patch,
unmount,
move,
}
);
// 第一次调用次数
expect(patch.mock.calls.length).toBe(7);
// 第一次调用的第一个参数
expect(patch.mock.calls[0][0]).toBe("a");
expect(patch.mock.calls[1][0]).toBe("b");
expect(patch.mock.calls[2][0]).toBe("g");
expect(patch.mock.calls[3][0]).toBe("f");
expect(patch.mock.calls[4][0]).toBe("c");
expect(patch.mock.calls[5][0]).toBe("d");
expect(patch.mock.calls[6][0]).toBe("e");
expect(unmount.mock.calls.length).toBe(0);
// 0 1 2 3 4 5 6
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e c d h] f g
// 真实下标 0 1 2 3 4 5 6 7
// 相对下标 0 1 2 3
// 下标是新元素的相对下标,value是老元素的下标+1
// [5,3,4,0]
// todo
// 1. mount
expect(mountElement.mock.calls[0][0]).toBe("h");
expect(mountElement.mock.calls[1][0]).toBe("d1");
// 2. move
expect(move.mock.calls[0][0]).toBe("e");
});
});
React调度算法(最小堆)
最小堆参考:
https://juejin.cn/post/7048898549005680647/
排序(冒泡、快排、V8 sort)
代码
const increasingNewIndexSequece = moved
? getSequence(newIndexToOldIndexMap)
: [];
let lastIndex = increasingNewIndexSequece.length - 1;
// 相对下标
for (i = toBePatched - 1; i >= 0; i--) {
const nextChildIndex = s2 + i;
const nextChild = c2[nextChildIndex];
// 判断nextChild是mount还是move
// 在老元素中出现的元素可能要move,没有出现过的要mount
if (newIndexToOldIndexMap[i] === 0) {
mountElement(nextChild.key);
} else {
// 可能move
if (lastIndex < 0 || i !== increasingNewIndexSequece[lastIndex]) {
move(nextChild.key);
} else {
lastIndex--;
}
}
}
排序场景
商品排序(销量、好评等)
实现:后端排序、前端排序
要求:全排序OR偏排序?稳定性?时间复杂度?空间复杂度?
偏排序
在计算机科学里,偏排序是排序算法的一个放宽的变种。全排序返回的列表中,每个元素都按一定顺序出现,而偏排序返回的列表中,仅有 k 个最小(或 k 个最大)的元素是有序的。其他元素(第 k 个最小之外) 也可能被就地排序后存储,也可能被舍弃。偏排序最普遍的实例是计算某个列表的 "Top 100"。
LeetCode最小的k个数
Array.sort
下面排序结果是?
[30, 4, 1, 2, 5].sort()
默认情况下,sort会按照升序重新排列数组元素。为此,sort会在每一项上调用String()转型函数,然后比较字符串来决定顺序。即使数组的元素都是数值,也会先把数组转换为字符串再比较、排序。
因此,升序修改如下:
[30, 4, 1, 2, 5].sort((a, b) => a - b)
2.常见算法排序
插入排序 VS 快速排序 VS 归并排序
快速排序
相比较于 归并排序
,在整体性能上表现更好:
- 更高的计算效率。
快速排序
在实际计算机执行环境中比同等时间复杂度的其他排序算法更快(不命中最差组合的情况下) - 更低的空间成本。前者仅有O(㏒n)的空间复杂度,相比较后者O(n)的空间复杂度在运行时的内存消耗更少
实际应用:
如果数组很短,优先插入排序。(插入排序常数项低,长度小于60,选择插入排序)
如果数组很长,则检查元素是否是基础类型,如数字、字符串,因为这个时候和稳定性无关,可以选择快速排序。如果元素是对象,则稳定性一般情况下比较重要,这个时候可以选择归并排序。
冒泡排序
Bubble Sort,是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
var bubble_sort = function(nums) {
let len = nums.length
if(len<=1) {
return nums
}
for(let i=0; inums[j+1]) {
[nums[j], nums[j+1]] = [nums[j+1], nums[j]]
}
}
}
return nums
};
选择排序
Selection Sort, 它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
代码
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function (nums) {
let len = nums.length;
if (len === 1) {
return nums;
}
for(let i=0; i
插入排序
Insertion Sort,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
代码实现
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function (nums) {
let len = nums.length;
if (len === 1) {
return nums;
}
for(let i=1; i0 && (nums[j]< nums[j-1]); j--) {
[ nums[j], nums[j-1] ] = [ nums[j-1], nums[j] ]
}
}
return nums
};
希尔排序
Shell Sort,也称递减增量排序算法,是插入排序的一种更高效的改进版本。
h?(步长,gap)
h = 3
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
代码
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function (nums) {
let len = nums.length;
if (len === 1) {
return nums;
}
let h = 1
while(h=1) {
for(let i=h; i=h) && (nums[j]
快速排序
Quick Sort,又称分区交换排序(partition-exchange sort),最早由东尼·霍尔提出。
快速排序使用分治法(Divide and conquer)策略来把一个序列分为较小和较大的2个子序列,然后递归地排序两个子序列。(分而治之) [less] pivot [greater]
步骤为:
- 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot)
- 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成,
- 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。
举例:
[6, 2, 1, 5, 4, 3]
quicksort(nums, left, right)
[6, 2, 1, 5, 4, 3]
i=0, j = 6
pivot = 6
i 加加, 至 5
3 2 1 5 4 6
return 5
quick(nums, 0, 4)
—quick(nums, 6, 5)
pivot = 3
i=0, j=5
i=3, j = 2
1 2 3 5 4 6
return 2
quick(nums, 0, 1)
i=0, j = 2
pivot=1
i=1, j =0
1 2 3 5 4 6
return 0
quick(nums, 3, 5)
i=3, j=6
pivot=5
i=4, j = 4
1 2 3 4 5 6
伪代码
简版
缺点:需要额外O(n)空间
function quicksort(q)
{
var list less, pivotList, greater
if length(q) ≤ 1
return q
else
{
select a pivot value pivot from q
for each x in q except the pivot element
{
if x < pivot then add x to less
if x ≥ pivot then add x to greater
}
add pivot to pivotList
return concatenate(quicksort(less), pivotList, quicksort(greater))
}
}
代码实现
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
let len = nums.length
if(len===1) {
return nums
}
quickSort(nums, 0, len-1)
function quickSort(nums, left, right) {
if(left=j) {
break
}
[ nums[i], nums[j] ] = [ nums[j], nums[i] ]
}
[ nums[left], nums[j] ] = [ nums[j], nums[left] ]
return j
}
return nums
};
计数排序
Counting Sort,适合少量、数值集中的非负数排序。可实现优化以支持负数。
非比较排序。
由于用来计数的数组count的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。
c语言步骤如下:
- 遍历数组nums,获取最大值max
- 定义数组count,长度为max+1,初始值均为0
- 遍历数组count,令count[ nums[i] ] ++ ,即把nums[i]当做count的数组下标,记录nums[i]出现的次数
- 定义数组res存放待排序数组,定义初始值index=0
- 遍历count数组,把count[i]不是0的下标值逐个放入res数组中。注意:鉴于nums数组中可能会有相等的元素,因此count[i]可能会大于1
代码实现
var sortArray = function (nums) {
let len = nums.length
if(len<2) {
return nums
}
// 5 5 2 3 1
const count = []
for(let i=0; i0) {
res.push(j)
count[j] --
}
}
}
return res
};
如果数组的数字是10到99的数字,那么前10个空间会被浪费,因此可以优化如下:
var sortArray = function (nums) {
let len = nums.length
if(len<2) {
return nums
}
// 5 5 2 3 1
let min = nums[0]
let max = nums[0]
for(let i=1; imax) {
max = nums[i]
} else if(nums[i]0) {
res.push(j+min)
count[j] --
}
}
// 1 2 3 5 5
return res
};
不获取max
var sortArray = function (nums) {
let len = nums.length
if(len<2) {
return nums
}
// 100-999
// count.length = max+1
const min = Math.min.apply(null, nums)
const count = []
for(let i=0; i0) {
res.push(j+min)
count[j]--
}
}
}
return res
};
基数排序
Radix Sort,非比较型整数排序,将所有待比较数值(非负整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
非比较排序因为不涉及比较,其基本操作的代价较小,所以在一定情况下,基数排序一般要快过基于比较的排序,比如快速排序。
代码实现
var sortArray = function (nums) {
let len = nums.length
if(len<2) {
return nums
}
// nums的最大值
let max = Math.max.apply(null, nums)
// max的位数,如131就是3位
let maxDigit = 1
while(max = Math.floor(max/10)){
maxDigit ++
}
let count = []
let mod = 10
let dev = 1
for(let i=0; i
TimSort
V8引擎
V8 是 Google 发布的开源 JavaScript 引擎,采用 C++ 编写,在 Google 的 Chrome 浏览器中被使用。
如果想要查看chome的版本,可以再浏览器直接输入chrome://version/
。
Array.prototype.sort
v8文档关于排序的原文链接:https://v8.dev/features/stable-sort。
Array.prototype.sort
是 V8 中用 JavaScript 实现的一个内置函数之一。
以前V8的排序算法是,对于数组长度小于或者等于10的时候,采用插入排序,否则采用快速排序。但是从 V8 v7.0 / Chrome 70 之后采用。
很长一段时间以来,JavaScript 规范并不要求排序稳定性Array#sort
,而是将其留给了实现。换句话说,JavaScript 开发人员不能依赖排序稳定性。在实践中,因为一些 JavaScript 引擎会对短数组使用稳定排序,对大数组使用不稳定排序。这会造成比较困惑的结果,因为开发人员会测试他们的代码,看到稳定的结果,但是当数组稍大时,在生产中突然得到不稳定的结果。
好消息是,现在的V8已经接受了一个使Array#sort
稳定规范更改,所有主要的 JavaScript 引擎现在都实现了一个稳定的Array#sort
。作为 JavaScript 开发人员,就少了一件需要担心的事情。Nice!
TimSort
时间复杂度:O(n log n)。
稳定性:稳定。
TimSort主体是归并排序,但小片段的合并中 用了插入排序。
用上了二分搜索等算法
利用待排序数据可能部分有序
的事实,
依据待排序数据内容,动态改变排序策略——选择性进行归并
以及galloping
。
1. 分区(run)
在实际场景中,大部分的数组都是部分有序的,而TimSort就很好地利用了这一点:分区,分到的每个区都是有序的。如果分到的区是严格降序,那么就翻转(reverse)这个分区。最终得到若干个升序的分区。
如,对[1, 3, 5, 2, 4, 8, 7, 6]分区:
1, 3, 5
2, 4, 8
7, 6 --翻转--> 6, 7
2. 合并分区的顺序
下一步我们要两两合并分区,也就意味着要比较两个分区中的元素大小,但是如果长度为1000的分区和长度为1的分区合并,把长度为1001的分区和长度为2的分区合并,最后还需要把前面合并得到的长度为1001和长度为1003的分区合并,这显然不如先给分区长度排序,然后run1和run2合并,run1000和run1001合并。
TimSort维护了一个stack,在这个栈里,分区是按照分区长度升序存储的。
3. 合并分区
既然我们得到的每个分区都是升序的,那合并两个分区的时候可以去逐个去比较这两个分区中的元素,以此得到一个有序的分区。但是对于元素量较大的分区来说,这种做法显然是性能耗费过大。
这个时候,可以了解下Galloping(倍增搜寻法),即以2^n的递增,最终得到
run1[2^n-1] < run0[i] < run1[2^(n +1) - 1]
。那么这样我们就知道了,run0[i]在run1中的顺序我们就锁定了run1[2n-1]与run1[2(n +1) - 1]之间,这个时候我们可以再使用二分查找高效定位run0[i]在run1中的位置。
4. 二分排序
当分区的长度较短时,相对来说二分插入排序会较快。
v8源码中的sort实现
Vue DOM DIFF、React