排序算法总结
冒泡排序
原理
顾名思义:冒泡排序每一轮将乱序数组中最大的数沉至数组尾部(或将最小的数上浮至数组头部)
冒泡排序需要两个嵌套的循环. 其中, 外层循环移动游标; 内层循环遍历游标及之后(或之前)的元素, 通过两两交换的方式, 每次只确保该内循环结束位置排序正确, 然后内层循环周期结束, 交由外层循环往后(或前)移动游标, 随即开始下一轮内层循环, 以此类推, 直至循环结束.
Tips: 由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法.
JavaScript 代码实现
function bubbleSort(array){
for(var i = 0; i < array.length - 1; i++){
for(var j = 0; j < array.length - i - 1; j++){
if(array[j] > array[j+1]){
var temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
return array;
}
选择排序
原理
每轮在乱序数组中选择最大的数放至数组尾部(或选择最小的数放至数组头部)
两层循环.内层循环每执行一遍, 将选出本次待排序的元素中最小(或最大)的一个, 存放在数组的起始位置. 而 外层循环则确保扫描数组,保证每个元素执行内层操作
Tips: 选择排序每次交换的元素都有可能不是相邻的, 因此它有可能打破原来值为相同的元素之间的顺序. 比如数组[2,2,1,3], 正向排序时, 第一个数字2将与数字1交换, 那么两个数字2之间的顺序将和原来的顺序不一致, 虽然它们的值相同, 但它们相对的顺序却发生了变化. 我们将这种现象称作 不稳定性 .
JavaScript 代码实现
function selectSort(array){
for(let i = 0; i < array.length; i++){
let min = array[i];
let minIndex = i;
for(let j = i; j < array.length ; j++){
if(array[j] < min){
min = array[j];
minIndex = j;
}
}
swap(i, minIndex, array);
}
return array;
}
直接插入排序
原理
将待排序的元素按照大小顺序, 依次插入到一个已经排好序的数组之中, 直到所有的元素都插入进去.
Tips: 由于直接插入排序每次只移动一个元素的位置, 并不会改变值相同的元素之间的排序, 因此它是一种稳定排序.
function directInsertSort(array){
var length = array.length, index, current;
for(var i = 1; i < length; i++){
index = i - 1;
current = array[i];
while(index >= 0 && array[index] > current){
array[index + 1] = array[index];
index--;
}
if(index + 1 != i){
array[index + 1] = current;
}
}
return array;
}
折半插入排序
原理
折半插入排序是直接插入排序的升级版. 鉴于插入排序第一部分为已排好序的数组, 我们不必按顺序依次寻找插入点, 只需比较它们的中间值与待插入元素的大小即可.
Tips: 同直接插入排序类似, 折半插入排序每次交换的是相邻的且值为不同的元素, 它并不会改变值相同的元素之间的顺序. 因此它是稳定的.
function halfInsertSort(array){
for(var i = 1; i < array.length; i++) {
var tail = i - 1;
var head = 0;
var current = array[i];
while (head <= tail) {
var half = (head + tail) >> 1;
array[i] >= array[half] ? head = half + 1 : tail = half - 1;
}
for (var j = i; j > head; j--) {
array[j] = array[j-1];
}
array[head] = current;
}
return array;
}
希尔排序
原理
- 将数组拆分为若干个子分组, 每个分组由相距一定”增量”的元素组成. 比方说将[0,1,2,3,4,5,6,7,8,9,10]的数组拆分为”增量”为5的分组, 那么子分组分别为 [0,5], [1,6], [2,7], [3,8], [4,9] 和 [5,10].
- 然后对每个子分组应用直接插入排序.
- 逐步减小”增量”, 重复步骤1,2.
- 直至”增量”为1, 这是最后一个排序, 此时的排序, 也就是对全数组进行直接插入排序
Tips: 我们知道, 单次直接插入排序是稳定的, 它不会改变相同元素之间的相对顺序, 但在多次不同的插入排序过程中, 相同的元素可能在各自的插入排序中移动, 可能导致相同元素相对顺序发生变化. 因此, 希尔排序并不稳定
function shellInsertSort(array, gap){
gap = (gap == undefined) ? 1 : gap;
var length = array.length, index, current;
for(var i = gap; i < length; i++){
index = i - gap;
current = array[i];
while(index >= 0 && array[index] > current){
array[index + gap] = array[index];//组内使用插入排序
index -= gap;
}
if(index + gap != i){
array[index + gap] = current;
}
}
return array;
}
function shellSort(array){
var length = array.length, gap = length >> 1;
while(gap > 0){
shellInsertSort(array, gap);
gap = gap >> 1;
}
return array;
}
归并排序
原理
将数组拆分为两个子数组, 分别排序, 最后才将两个子数组合并; 拆分的两个子数组, 再继续递归拆分为更小的子数组, 进而分别排序, 直到数组长度为1, 直接返回该数组为止.
(通常,开始组数为 数组长度/2,以保证每组元素不超过3个,之后组数依次递减,直至组数为1)
Tips: 归并排序严格按照从左往右(或从右往左)的顺序去合并子数组, 它并不会改变相同元素之间的相对顺序, 因此它也是一种稳定的排序算法.
function merge(left, right){
var result = [];
while(left.length && right.length){
var item = left[0] <= right[0] ? left.shift() : right.shift();
result.push(item);
}
return result.concat(left.length ? left : right);
}
function mergeSort(array){
var length = array.length;
if(length < 2){
return array;
}
var m = (length >> 1),
left = array.slice(0, m),
right = array.slice(m);
return merge(mergeSort(left), )
}
快速排序
原理
将数组拆分为两个子数组, 其中一个子数组的所有元素都比另一个子数组的元素小, 然后对这两个子数组再重复进行上述操作, 直到数组不可拆分, 排序完成.
Tips: 同选择排序相似, 快速排序每次交换的元素都有可能不是相邻的, 因此它有可能打破原来值为相同的元素之间的顺序. 因此, 快速排序并不稳定.
function quickSort(array){
if(!array || Object.prototype.toString.call(array).toLowerCase().indexOf('array') < 0){
throw new Error('quickSort(): First arguments must be an Array.')
}
if(array.length <= 1){
return array;
}
var length = array.length,
pivotIndex = Math.floor(array.length / 2),
pivot = array.splice(pivotIndex, 1)[0],
left = [],
right = [],
i = 0;
length -= 1;
for(; i < length; i++) {
if (array[i] < pivot) {
left.push(array[i]);
} else {
right.push(array[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
}
堆排序
预备知识
堆:可以将堆看作完全二叉树, k(i) 相当于是二叉树的非叶子节点, k(2i) 则是左子节点, k(2i+1)是右子节点.
大根堆:要求每个子节点的值都不大于其父节点的值, 即array[childIndex] <= array[parentIndex], 最大的值一定在堆顶.
小根堆:每个子节点的值都不小于其父节点的值, 最小的值一定在堆顶
原理
算法的基本思想(以大根堆为例):
1. 先将初始序列K[1..n]建成一个大根堆, 此堆为初始的无序区.
2. 再将关键字最大的记录K1 (即堆顶)和无序区的最后一个记录K[n]交换, 由此得到新的无序区K[1..n-1]和有序区K[n], 且满足K[1..n-1].keys≤K[n].key
3. 交换K1 和 K[n] 后, 堆顶可能违反堆性质, 因此需将K[1..n-1]调整为堆. 然后重复步骤2, 直到无序区只有一个元素时停止.
Tips: 由于堆排序中初始化堆的过程比较次数较多, 因此它不太适用于小序列. 同时由于多次任意下标相互交换位置, 相同元素之间原本相对的顺序被破坏了, 因此, 它是不稳定的排序.
function make_heap(array, x, length){
var lchild = 2 * x + 1,
rchild = 2 * x + 2,
largest = x,
temp;
if(lchild < length && array[lchild] > array[largest]){
largest = lchild;
}
if(rchild < length && array[rchild] > array[largest]){
largest = rchild;
}
if(largest != x){
temp = array[x];
array[x] = array[largest];
array[largest] = temp;
make_heap(array, largest, length);
}
}
function heapSort(array) {
var heapSize = array.length, temp;
//建堆
for(var i = Math.floor(heapSize / 2) - 1; i >= 0; i--){
make_heap(array, i, heapSize);
}
//堆排序
for(var j = heapSize - 1; j >= 1; j--){
temp = array[0];
array[0] = array[j];
array[j] = temp;
console.log(array);
make_heap(array, 0, --heapSize);
}
return array;
}
总结:排序算法比较
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
直接插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
折半插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(nlogn) | O(n²) | O(1) | 不稳定 |
归并排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(1) | 稳定 |
快速排序 | O(nlog₂n) | O(nlog₂n) | O(n²) | O(nlog₂n) | 不稳定 |
堆排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(1) | 不稳定 |