排序
冒泡排序
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个。
- 对每一对相邻元素做比较。一轮结束后,最后的元素会是最大的数。
- 执行n-1轮,就可以完成排序
代码实现
function sortArray(nums) {
let len = nums.length
if (len <= 1) {
return nums
}
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
[nums[j], nums[j + 1]] = [nums[j + 1], nums[j]]
}
}
}
return nums
};
选择排序
1.找到数组0-n中的最小值,将它放在第一位
2.找到数组1-n中的最小值,将它放在第二位
3.依次类推
代码
function sortArray(nums) {
let len = nums.length;
if (len <= 1) {
return nums
}
for (let i = 0; i < nums.length - 1; i++) {
let minIndex = i
for (let j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[minIndex]) {
minIndex = j
}
}
if (minIndex !== i) {
[nums[minIndex], nums[i]] = [nums[i], nums[minIndex]]
}
}
return nums
};
插入排序
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素小于已经排序元素,将已经排序元素移到下一位置,继续向前扫描,否则插入当前位置
代码实现
function sortArray(nums) {
let len = nums.length;
if (len <= 1) {
return nums;
}
for (let i = 1; i < nums.length; i++) {
let curValue = nums[i]
let j = i - 1
while (curValue < nums[j] && j >= 0) {
nums[j + 1] = nums[j]
j--
}
nums[j + 1] = curValue
}
return nums
};
快速排序
快速排序使用分治法策略来把一个序列分为较小和较大的2个子序列,然后递归地排序两个子序列。
- 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot)
- 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成,
- 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列进行快速排序。
代码实现
function quickSort(arr, left = 0, right = arr.length) {
let partitionIndex;
if (left < right) {
partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, right);
}
return arr;
}
function partition(arr, left, right) {
//设定基准值位置,可以选择任意一个,此处选择最左边的位置
let pivot = left,
index = pivot + 1;//比基准值小的值放入的位置
for (let i = index; i <= right; i++) {
// 如果比基准值小,交换。
if (arr[i] < arr[pivot]) {
[arr[i], arr[index]] = [arr[index], arr[i]]
index++;
}
}
// 最后交换基准值和最后一位比基准值小的数
// 这样新的数组,以基准值为界分成小数和大数
[arr[pivot], arr[index - 1]] = [arr[index - 1], arr[pivot]]
return index - 1;
}
归并排序
1.分:把数组分成两半,递归子数组,进行分割操作,直到分成一个数
2.合:把两个有序子数组合并成一个有序数组
代码实现
function mergeSort(nums) {
let len = nums.length;
if (len <= 1) {
return nums
}
let middle = len >> 1
let left = nums.slice(0, middle);
let right = nums.slice(middle);
return mergeAry(mergeSort(left), mergeSort(right))
};
// 合并两个有序数组
function mergeAry(left, right) {
let ary = []
while (left.length && right.length) {
if (left[0] < right[0]) {
ary.push(left.shift())
} else {
ary.push(right.shift())
}
}
ary.push(...left, ...right)
return ary
}
计数排序
适合少量、数值集中的非负数排序。可实现优化以支持负数。非比较排序。
由于用来计数的数组count的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。
遍历数组nums,获取最大值max
-
定义数组count,长度为max+1,初始值均为0
因为js初始化数组时可以不设置长度,可以省略上面两步。
遍历数组nums,令count[nums[i] ] ++ 。
遍历count数组,把count[i]不是0的下标值逐个放入res数组中。
代码实现
function sortArray(nums) {
let len = nums.length
if (len <= 1) {
return nums
}
const count = []
for (let i = 0; i < len; i++) {
const j = nums[i]
if (count[j]) {
count[j]++
} else {
count[j] = 1
}
}
const res = []
for (let j = 0; j < count.length; j++) {
if (count[j]) {
while (count[j] > 0) {
res.push(j)
count[j]--
}
}
}
return res
};
基数排序
1.将所有待比较数值(非负整数)统一为同样的数位长度,数位较短的数前面补零。
2.从个位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数组就变成一个有序序列。
非比较排序因为不涉及比较,其基本操作的代价较小,所以在一定情况下,基数排序一般要快过基于比较的排序。
流程:
初始数组:[0, 50 , 5, 2 , 131]
补零:000 050 005 002 131
比较个位数
count = []
count[0] = [0, 50]
count[1] = [131]
count[2] = [2]
count[5] = [5]
合并:nums = [0, 50, 131, 2 , 5]
比较十位数
count=[]
count[0] = [0, 2, 5]
count[3] = [131]
count[5] = [50]
合并:nums = [0, 2, 5, 131, 50]
比较百位数
count= []
count[0] = [0, 2, 5, 50]
count[1] = [131]
合并:nums = [0, 2 ,5, 50, 131]
代码实现
function sortArray(nums) {
let len = nums.length
if (len <= 1) {
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 < maxDigit; i++) {
count = []
for (let j = 0; j < len; j++) {
let bucket = Math.floor((nums[j] % mod) / dev)
if (count[bucket]) {
count[bucket].push(nums[j])
} else {
count[bucket] = [nums[j]]
}
}
let pos = 0
for (let j = 0; j < count.length; j++) {
let value = null
if (count[j]) {
while ((value = count[j].shift()) != null) {
nums[pos++] = value
}
}
}
dev *= 10
mod *= 10
}
return nums
};
TimSort
Array.prototype.sort
v8文档关于排序的原文链接:https://v8.dev/features/stable-sort。
以前V8的排序算法是,对于数组长度小于或者等于10的时候,采用插入排序,否则采用快速排序。
但是从 V8 v7.0 / Chrome 70 之后采用稳定的TimSort。
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. 二分排序
当分区的长度较短时,相对来说二分插入排序会较快。