目录
排序相关概念
稳定性
不改变相同数值的顺序为稳定。例如在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的(记录的相对次序保持不变);否则称为不稳定的。
内部排序
指待排序列完全存放在内存中所进行的排序过程,适合不太大的元素序列。有冒泡、选择、插入、希尔、快速、堆排序(通常快速排序为最好的)。
外部排序
外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。有归并、计数、桶、基数排序。
十种排序算法特点总结
交换排序
冒泡排序(数组sort方法的原理)
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
图解
js实现
function swap(arr, x, y) {
let tmp = arr[x]
arr[x] = arr[y]
arr[y] = tmp
}
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len - 1; i++) {
//比较出一轮中的最大值放入最后
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1)
}
}
}
return arr;
}
特点
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
快速排序
1.以数组第一位为基准值,将小于的元素放在左边,大于的元素放在右边,分为左右两块。
2.在左右2块中重复第一步,不断循环,直到分区的大小为1则停止。
图解
注意左右分区,我们可以通过将基准元素和左边分区最右端元素交换实现
js实现
function swap(arr, x, y) {
let tmp = arr[x]
arr[x] = arr[y]
arr[y] = tmp
}
//用于一次以基准值进行排序
function partition(arr, left, right) {
//设定基准值pivot
let pivot = left
let index = pivot + 1
for (let i = index; i <= right; i++) {
//小于基准值的放左边,大于的不变
if (arr[i] < arr[pivot]) {
swap(arr, i, index)
index++
}
}
//和小于基准值的最右边一个交换位置,实现左边小于基准值,右边大于基准值
swap(arr, pivot, index - 1)
//返回基准值的位置
return index - 1
}
function quickSort(arr, left, right) {
let len = arr.length
left = typeof left != 'number' ? 0 : left
right = typeof right != 'number' ? len - 1 : right
if (left < right) {
//进行一次排序
let partitionIndex = partition(arr, left, right)
//递归对左边进行排序
quickSort(arr, left, partitionIndex - 1)
//递归对右边进行排序
quickSort(arr, partitionIndex + 1, right)
}
return arr
}
console.log(quickSort([3, 1, 2, 3134, 113, 342, 123412, 55, 111, 4, 234, 5, 5]))
特点
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
选择排序
直接选择排序
每一次遍历待排序的数据元素从中选出最小(或最大)的一个元素,存放在序列的起始(或者末尾)位置(和原存放位置上的元素交换顺序),直到全部待排序的数据元素排完。
图解
js实现
//交换数组下标x位和y位的元素
function swap(arr, x, y) {
arr[x] = arr[x] - arr[y]
arr[y] = arr[y] + arr[x]
arr[x] = arr[y] - arr[x]
return arr
}
function selectionSort(arr) {
let t = arr.length
let x = 0
while (x != t) {
let min = 0;
let tmp = Number.MAX_VALUE
//选出最小的元素的下标
for (let i = x; i < t; i++) {
if (tmp > arr[i]) {
tmp = arr[i]
min = i
}
}
//不是当前位置,则交换顺序
if (x != min) {
swap(arr, x, min)
}
x++
}
return arr
}
console.log(selectionSort([3, 1, 2, 3134, 113, 342, 123412, 55, 111, 4, 234, 5, 5]))
特点
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
堆排序
1.将元素排序为一个大(小)顶堆,则顶部元素为最大(小)元素,将其与元素尾(头)部元素交换顺序。
2.重新排序剩下元素,再次形成大(小)顶堆,重复1操作,直到只剩一个元素。
大(小)顶堆
每个节点的值都大于(小于)或等于其子节点的值,在堆排序算法中用于升(降)序排列;
代码中表示为:
大顶堆arr[i]>=arr[2i+1]和arr[i]>=arr[2i+2] 父节点大于左右子节点
小顶堆arr[i]<=arr[2i+1]和arr[i]<=arr[2i+2] 父节点小于左右子节点
非叶节点
有孩子节点的节点。最后一个非叶节点在数组中的序号为元素个数除以2向下取整。
js实现
// 建立大顶堆
function buildMaxHeap(arr) {
let len = arr.length;
//Math.floor(len/2)表示最后一个非叶节点(含有子节点的节点),
//从后往前调整每一个非叶子节点的顺序,使堆变为大顶堆
for (let i = Math.floor(len / 2); i >= 0; i--) {
heapify(arr, i, len);
}
}
// 调整堆
function heapify(arr, i, len) {
let left = 2 * i + 1,
right = 2 * i + 2,
largest = i;
//通过2次判断选出父节点和2个子节点中最大的一个
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
//将最大的节点和父节点交换顺序
if (largest != i) {
swap(arr, i, largest);
//递归对换顺序的节点进行调整,使其还是大顶堆
heapify(arr, largest, len);
}
}
function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function heapSort(arr) {
// 建立大顶堆
buildMaxHeap(arr);
let len = arr.length
//每次建立一个大顶堆,将最大的元素(堆顶元素)放入尾部达到排序效果
for (let i = len - 1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0, len);
}
return arr;
}
console.log(heapSort([3, 1, 2, 3134, 113, 342, 123412, 55, 111, 4, 234, 5, 5]))
特点
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
插入排序
直接插入排序
每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序(默认是从后向前比较)。
图解
蓝色部分为无序表,黄色部分为有序表:
js实现
使用的是一个数组,将数组下标为i之前的为有序,之后为无序。
注意用的是改变原数组的方法,可以不用返回。
//将数组下标为的x元素取出插入为y的
function popInsert(arr,x,y){
arr.splice(y,0,...arr.splice(x,1))
}
function insertSort(arr){
let t=arr.length
for (let i=1;i<t;i++){
let j=i-1
do{
if(arr[j]<arr[i]){
popInsert(arr,i,j+1)
break
}
j--
//比较到下标为0的时候直接插入
if(j<0){
popInsert(arr,i,0)
}
}while(j>=0)
}
return arr
}
console.log(insertSort([3,1,2,3134,113,342,123412,55,111,4,234,5,5]))
特点
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
希尔排序
希尔排序在直接排序之前,进行预排列,将某些极端数据更快的排列到数列前面,构成一个接近排列好的序列,最后再进行一次直接插入排序。
预排列的原理也是插入排列,只不过这里的将数组分成了gap组,分别对每一个小组进行插入排序。
图解
对于升序,当gap从5 – 2 – 1的过程中,排在后面的数值小的数能更快排到前面,当gap为1的时候实际上就是进行了一次插入排序
js实现
每次将下标位置差为gap的数进行直接排序,其中第一次gap=Math.floor(arr.length/2),之后每次gap=Math.floor(gap/2)。
function shellsSort(arr){
let t=arr.length
let gap=t
let tmp
//gap取不同值的排序
do{
//每次的分组
gap=Math.floor(gap/2)
for(let i=gap;i<t;i++){
//只和上一个间距为gap的元素进行比较,判断是否要插入,还是维持原顺序
if(arr[i-gap]>arr[i]){
tmp = arr[i]
let j=i-gap
//将插入位置之后的元素整体后移
do{
arr[j+gap]=arr[j]
j-=gap
}while(j>=0&&arr[j]>tmp)
//插入对应位置
arr[j+gap]=tmp
}
}
}while(gap>1)
return arr
}
console.log(shellsSort([3,1,2,3134,113,342,123412,55,111,4,234,5,5]))
特点
- 希尔排序是对直接插入排序的优化当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,一般来说为O(n^1.3)
- 稳定性:不稳定
其它排序
桶排序
选出待排序元素中的最大最小值,根据最大最小值划分多个区域(桶),通过映射函数将元素分到不同区域(桶)中,对区域(桶)中的元素进行排序后,依次取出即可。
图解
将排序的元素放入对应的桶中
对桶中的元素进行排序
js实现
//插入排序
function insertionSort(arr) {
let len = arr.length;
let preIndex, current;
for (let i = 1; i < len; i++) {
preIndex = i - 1;
current = arr[i];
while (preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
return arr;
}
function bucketSort(arr, bucketSize) {
let minValue = arr[0];
let maxValue = arr[0];
//找到数组中的最大和最小值
for (let i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i];
} else if (arr[i] > maxValue) {
maxValue = arr[i];
}
}
//桶的初始化
let DEFAULT_BUCKET_SIZE = 5;
// 设置一个桶能存储的默认数量为5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
//桶的个数
let bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
let buckets = new Array(bucketCount);
for (let i = 0; i < buckets.length; i++) {
buckets[i] = [];
}
//利用映射函数将元素分配到各个桶中
for (let i = 0; i < arr.length; i++) {
buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
}
//删除arr数组中的所有元素
arr.length = 0;
for (let i = 0; i < buckets.length; i++) {
// 对每个桶进行排序,这里使用了插入排序
insertionSort(buckets[i]);
// 将每个桶的数据从小到大拿出
for (let j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}
return arr;
}
console.log(bucketSort([3, 1, 2, 3134, 113, 342, 123412, 55, 111, 4, 234, 5, 5]))
特点
- 时间复杂度:O(N+K)
- 空间复杂度:O(N+K)
- 稳定性:稳定
基数排序
确定最大数的位数,先按第一位进行排序,在按第二位进行排序,直到按最后一位进行排序为止。
图解
js实现
function radixSort(arr) {
let counter = [];
//获取元素的最大位数
let maxDigit = `${Math.max(...arr)}`.length
let mod = 10;
let dev = 1;
//从小到大依次对元素的每一位进行循环排序
for (let i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for (let j = 0; j < arr.length; j++) {
//获取元素对应位上的数值
let bucket = parseInt((arr[j] % mod) / dev);
if (counter[bucket] == null) {
counter[bucket] = [];
}
//根据对应的数值,将元素放入对应的桶中
counter[bucket].push(arr[j]);
}
let pos = 0;
//从0~9的桶中将元素取出,完成一次排序
for (let j = 0; j < counter.length; j++) {
let value = null;
if (counter[j] != null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value;
}
}
}
}
return arr;
}
console.log(radixSort([3, 1, 2, 3134, 113, 342, 123412, 55, 111, 4, 234, 5, 5], 6))
特点
- 时间复杂度:O(N*K)
- 空间复杂度:O(N+K)
- 稳定性:稳定
归并排序
注意下面的数组理解为队列(先进先出)并且为递增排序。
a.创建2个数组(用于排序时作为存放待排序数列,里面都是排好序的,所以只用比较两个数组的头元素)
b.将前2个元素分别放入a的数组,然后通过比较元素的大小,将小的先拿出放入原数组。这样前2个元素已排好序了。
c.向后移2个元素并重复b操作。
d.此时b、c操作已产生2组已分别排好序的2个元素,然后将2组元素分别放入a的两个数组中,通过比较元素的大小,将小的元素先拿出放入原数组。此时前4个元素已排好序。
e.对后面未操作的4个元素重复b、c、d操作,此时原数组产生2组已分别排好序的4个元素,然后将2组元素分别放入a的两个数组中,通过比较元素的大小,将小的元素先拿出放入原数组。此时前8个元素已排好序。
f.重复上述操作直到所有元素已排好序。
图解
js实现
//和操作中的相反,递归实现
function mergeSort(arr) {
let len = arr.length;
if (len < 2) {
return arr;
}
//对应a操作
let middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
//递归对左右两边调用排序函数,并对返回的数组调用合并函数
return merge(mergeSort(left), mergeSort(right));
}
//合并两已排序的数组为一个排序的数组。
function merge(left, right) {
let result = [];
//通过比较元素的大小,将小的先拿出放入原数组
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
//当左或右边的数组空了时,直接将另一边数组直接放入有序数组中
while (left.length) {
result.push(left.shift());
}
// 也可以用concat直接拼接
// if (left.length) {
// result = result.concat(left)
// }
while (right.length) {
result.push(right.shift());
}
return result;
}
console.log(mergeSort([3, 1, 2, 3134, 113, 342, 123412, 55, 111, 4, 234, 5, 5]))
特点
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
计数排序
1.找出待排序的数组中最大和最小的元素。
2.统计数组汇总每个值为i的元素出现的次数,存入数组bucket的第i项。
3.反向填充目标数组:j从最小值开始递增到最大值,判断数组bucket的每位是否大于0,大于就存入数组。例如bucket[i]>0就将j存入arr中bucket[j]次。
图解
js实现
function countingSort(arr) {
let maxValue = Math.max(...arr)
minValue = Math.min(...arr)
// 用于存储统计待排序数组中每个值出现的次数
bucket = []
sortedIndex = 0;
arrLen = arr.length,
bucketLen = maxValue + 1;
// 对应操作中的第2步
for (let i = minValue; i < arrLen; i++) {
if (!bucket[arr[i]]) {
bucket[arr[i]] = 0;
}
bucket[arr[i]]++;
}
// 对应操作中的第3步,反向填充目标数组
for (let j = minValue; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}
console.log(countingSort([3, 1, 2, 3134, -113, 342, 123412, 55, 111, 4, 234, 5, 5]))
特点
- 时间复杂度:O(N+K)
- 空间复杂度:O(K)
- 稳定性:稳定