一、回顾
首先,让我们再回顾一遍,各种排序算法的特性
上一期,我们的推文已经讲述了冒泡排序、希尔排序、归并排序。本篇文章我们将继续讲述其他排序。
二、排序算法
1.插入排序
插入排序(Insertion Sort)的过程就像我们排序扑克牌一样(从左到右,从小到大)。开始时我们左手为空,然后我们从桌子上拿起一张牌并将它插入到左手中正确的位置,为了找到这个位置,我们将这张牌与左手中从右向左的每张牌进行比较,直到找到比它小或相等的牌的后面。
与排序扑克牌类似插入排序的原理是:将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素,接着取未排序区间中的元素(数组的第二个元素),在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序,重复这个过程,直到未排序区间中元素为空。
思路:
- 从第一个元素开始,该元素可以认为已经排序;
- 取出下一个元素,在已排序区间中倒序遍历;
- 如果已排序元素大于新元素,将已排序元素移动到下一个位置;
- 继续向前遍历,重复上一步骤直到找到已排序的元素小于或等于新元素,将新元素插入已排序元素的后面;
- 重复2-4步骤。
展示:
实现:
function insertSort(array) {
for (let i = 1; i < array.length; i++) {
let target = i;
for (let j = i - 1; j >= 0; j--) {
if (array[target] < array[j]) {
[array[target], array[j]] = [array[j], array[target]]
target = j;
} else {
break;
}
}
}
return array;
}
复杂度:
- 时间复杂度:
O(n2)
- 空间复杂度:
O(1)
2.快速排序
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。
思路:
- 选择一个基准元素
target
(一般选择第一个数) - 将比
target
小的元素移动到数组左边,比target
大的元素移动到数组右边 - 分别对
target
左侧和右侧的元素进行快速排序
展示:
从上面的步骤中我们可以看出,快速排序也利用了分治的思想(将问题分解成一些小问题递归求解)
下面是对序列6、1、2、7、9、3、4、5、10、8
排序的过程:
实现:
- 方法一
单独开辟两个存储空间left
和right
来存储每次递归比target
小和大的序列
每次递归直接返回left、target、right
拼接后的数组
(缺点:浪费大量存储空间;优点:写法简单)
function quickSort(array){
if(array.length < 2 ){
return array;
}
const target = array[0];
const left = [];
const right = [];
for(let i=0; i<array.length; i++){
if(array[i]<target){
left.push(array[i]);
}else{
right.push(array[i]);
}
}
return quickSort(left).concat([target],quickSort(right));
}
- 方法二
记录一个索引l
从数组最左侧开始,记录一个索引r
从数组右侧开始
在l<r
的条件下,找到右侧小于target
的值array[r]
,并将其赋值到array[l]
在l<r
的条件下,找到左侧大于target
的值array[l]
,并将其赋值到array[r]
这样让l=r
时,左侧的值全部小于target
,右侧的值全部小于target
,将target
放到该位置
(缺点:思路稍复杂;优点:节省存储空间)
function quickSort(array,start,end){
if(end - start < 1){
return;
}
const terget = array[start];
let l = start;
let r = end;
while(l < r){
while(l<r && array[r] >= target){
r--;
}
array[l] = array[r];
while(l<r && array[l] < target){
l++;
}
array[r] = array[l];
}
array[l] = target;
quickSort(array,start,l-1);
quickSort(array,l+1,end);
return array;
}
复杂度:
- 时间复杂度:平均
O(nlogn)
,最坏O(n2)
,实际上大多数情况下小于O(nlogn)
- 空间复杂度:
O(logn)
(递归调用消耗)
3.计数排序
计数排序是一种非基于比较的排序算法,其时间复杂度均为O(n+k),其中k是整数的范围。基于比较的排序算法时间复杂度最小是O(nlogn)的。该算法于1954年由 Harold H. Seward 提出。
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
思路:
- 花O(n)的时间扫描一下整个序列 A,获取最小值 min 和最大值 max
- 开辟一块新的空间创建新的数组 B,长度为 ( max - min + 1)
- 数组 B 中 index 的元素记录的值是 A 中某元素出现的次数
- 最后输出目标整数序列,具体的逻辑是遍历数组 B,输出相应元素以及对应的个数
展示:
所谓“计数”,就是数一数,统计每个元素重复出现的次数
实现:
function countingSort(array) {
let min = Infinity
for (let v of array) {
if (v < min) {
min = v
}
}
let counts = []
for (let v of array) {
counts[v-min] = (counts[v-min] || 0) + 1
}
let index = 0
for (let i = 0; i < counts.length; i++) {
let count = counts[i]
while(count > 0) {
array[index] = i + min
count--
index++
}
}
return array
}
复杂度:
- 时间复杂度:O(n+k)
- 空间复杂度:O(k)
三、总结
- 计数排序适合整数排序
- 快速排序适合大多数场景
- 插入排序适合大部分数据离他正确的位置很近,近乎有序的情况