排序中的通用函数。
function checkArray(array) {
if (!array || array.length < 2) return
}
function swap(array, left, right) {
let rightValue = array[right]
array[right] = array[left]
array[left] = rightValue
}
冒泡排序:
原理如下:从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要在比较最后一个元素,只需要比较到length - 1 的位置。
function bubble(array) {
checkArray(array);
let len = array.length;
for(let i = len - 1; i > 0; i--){
for(let j = 0; j < i; j++){
if(array[j] > array[j+1]) {
swap(array, j, j + 1);
}
}
}
return array;
}
当数组正序时候,复杂度为O(n),当逆序的时候,复杂度:O(n*n);
时间复杂度:O(n*n),空间复杂度O(1)
插入排序:
原理如下:第一个元素默认是已排序元素,取出下一个元素和当前元素比较,如果当前元素大就交换位置。那么此时第一个元素就是当前的最小数,所以下次去除操作从第三个元素开始,向前对比,重复之前的操作。
function insertion(array) {
checkArray(array);
let len = array.length;
for(let i = 1; i < len; i++) {
for(let j = i - 1; j >= 0 && array[j] > array[j+1]; j--){
swap(array, j, j+1);
}
}
return array;
}
时间复杂度:O(n*n),空间复杂度O(1)
选择排序:
原理如下:遍历数组,设置最小值的索引为0,如果取出的值比当前最小值小,就替换最小值索引,遍历完成后,将第一个元素和最小值索引上的值交换。如上操作后,第一个元素就是数组中的最小值,下次遍历的就可以从索引1开始重复上述操作。
function selection(array) {
checkArray(array);
let len = array.length;
for(let i = 0; i < len - 1; i++) {
let minIndex = i;
for(let j = i + 1; j < len; j++) {
minIndex = array[j] < array[minIndex] ? j : minIndex;
}
swap(array, i, minIndex);
}
return array;
}
时间复杂度:O(n*n),空间复杂度O(1);
归并排序:
原理如下:递归的将数组两两分开直到最多包含两个元素,然后将数组排序合并,最终合并为排序好的数组。假设我有一组数组【3,1,2,8,9,7,6】中间索引是3,先排序数组【3,1,2,8】。在这个左边数组上,继续拆分直到变成数组包含两个元素(如果数组是奇数的话,会有一个拆分数组只包含一个元素)。然后排序数组【3,1】和【2,8】,然后再排序数组【1,3,2,8】,这样左边数组就排序完成,然后按照以上思路排序右边数组,最后将数组【1,2,3,8】和【6,7,9】排序。
function mergeSort(array, left, right) {
if(left === right) {
return;
}
let mid = parseInt(left + ((right - left) >> 1))
mergeSort(array, left, mid);
mergeSort(array, mid + 1, right)
let help = [];
let i = 0;
let p1 = left;
let p2 = mid + 1;
while(p1 <= mid && p2 <= right) {
help[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++]
}
while(p1 <= mid) {
help[i++] = array[p1++];
}
while(p2 <= right) {
help[i++] = array[p2++];
}
let len = help.length;
for(let i = 0; i < len; i++) {
array[left + i] = help[i];
}
return array;
}
function sort(array) {
checkArray(array);
mergeSort(array, 0, array.length - 1);
return array;
}
sort([3,1,2,8,9,7,6])
以上算法使用了递归的思想,递归的本质就是压栈,每递归执行一次函数,就将该函数的信息,比如(参数,内部的变量,执行到的行数)压栈,直到遇到终止条件,然后出站并继续执行函数。对于以上递归函数的调用轨迹如下:
mergeSort(data, 0, 6) // mid = 3
mergeSort(data, 0, 3) // mid = 1
mergeSort(data, 0, 1) // mid = 0
mergeSort(data, 0, 0) // 遇到终止,回退到上一步
mergeSort(data, 1, 1) // 遇到终止,回退到上一步
// 排序 p1 = 0, p2 = mid + 1 = 1
// 回退到 `mergeSort(data, 0, 3)` 执行下一个递归
mergeSort(2, 3) // mid = 2
mergeSort(2, 2) // 遇到终止,回退到上一步
mergeSort(3, 3) // 遇到终止,回退到上一步
// 排序 p1 = 2, p2 = mid + 1 = 3
// 回退到 `mergeSort(data, 0, 3)` 执行合并逻辑
// 排序 p1 = 0, p2 = mid + 1 = 2
// 执行完毕回退
// 左边数组排序完毕,右边也是如上轨迹
时间复杂度:O(n*logn),空间复杂度O(1);
快速排序:
原理如下:随机选取一个数组中的值作为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。然后将数组以基准值的位置分为两部分,继续递归以上操作。
时间复杂度:最坏情况,O(n*n),最好情况,O(n*logn),空间复杂度O(logn);
//方法一:
function quickSort(array) {
if (array.length <= 1) { return array; }
let pivotIndex = Math.floor(array.length/2);
let pivot = array.splice(pivotIndex, 1)[0];
let left = [], right = [], len = array.length;
for(let i = 0; i < len; i++) {
if(array[i] < pivot) {
left.push(array[i])
}
else{
right.push(array[i])
}
}
return quickSort(left).concat([pivot], quickSort(right))
}
//方法二:
function quickSort(arr,low,high){
var key=arr[low];
var start=low;
var end=high;
while(end>start){
while(end>start&&arr[end]>=key) end--;
if(arr[end]<=key){
var temp = arr[end];
arr[end]=arr[start];
arr[start] = temp;
}
while(end>start&&arr[start]<=key) start++;
if(arr[start]>=key){
var temp = arr[start];
arr[start]=arr[end];
arr[end]=temp;
}
}
if(start>low) quickSort(arr,low,start-1);
if(end<high) quickSort(arr,end+1,high);
return arr;
}
//方法三:
function part(array, left, right) {
if (arr.length <= 1) { return; }
let less = left - 1;
let more = right;
while (left < more) {
if (array[left] < array[right]) {
// 当前值比基准值小,`less` 和 `left` 都加一
++less;
++left;
} else if (array[left] > array[right]) {
// 当前值比基准值大,将当前值和右边的值交换
// 并且不改变 `left`,因为当前换过来的值还没有判断过大小
swap(array, --more, left);
} else {
// 和基准值相同,只移动下标
left++;
}
}
// 将基准值和比基准值大的第一个值交换位置
// 这样数组就变成 `[比基准值小, 基准值, 比基准值大]`
swap(array, right, more);
return more;
}
let arr = [23, 43, 43, 5, 2, 32, 42, 556, 27, 34];
// console.log(part(arr, 0 ,9));
function swap(array, left, right) {
[array[left], array[right]] = [array[right], array[left]];
}
function quickSort(arr, left, right) {
let random = left;
swap(arr, random, right);
if(left<right) {
let index = part(arr, left, right);
if(index > left)
quickSort(arr, left, index-1);
if(index < right)
quickSort(arr, index + 1, right);
}
return arr;
}
//方法四:
function quickSort(arr, from,to) {
let i = from, j = to, key = arr[from];
if(from >= to) return;
while(i<j) {
while(i<j && arr[j]>=key)
j--;
while(i<j && arr[i]<=key)
i++;
if(i<j) {
swap(arr, i, j);
}
}
arr[from] = arr[i]
arr[i] = key;
quickSort(arr, from, i-1);
quickSort(arr, i+1, to);
return arr;
}
堆排序:
原理如下:堆排序利用了二叉堆特性来做,二叉堆通常用数组表示,并且为一颗完全二叉树,二叉堆又分为大根堆和小根堆。
堆排序的原理就是组成一个大根堆或者小跟堆。一小跟堆为例,某个节点的左边子节点索引是i * 2 + 1,右边是i * 2 + 2, 父节点是(i - 1)>> 1。
- 首先遍历数组,判断该节点的父节点是否比他小,如果小就交换位置并继续判断,知道他的父节点比他大。
- 重复以上操作,直到数组首位是最大值。
- 然后将首位和末尾交换位置并将数组长度减一,表示数组末尾已是最大值,不需要在比较大小。
- 对比左右节点那个大,然后记住大的节点的索引并且和父节点对比大小,如果子节点大舅交换位置。
- 重复以上操作3- 4 直到整个数组都是小根堆。如图
//堆排序
function heap(array) {
checkArray(array);
for(let i = 1; i < array.length; i++){
heapInsert(array, i);
}
let size = array.length;
swap(array, 0, --size);
while(size > 0){
heapify(array, 0, size)
swap(array, 0, --size)
}
return array;
}
//当前节点大于父节点就交换
function heapInsert(array, index){
while(array[index] > array[(index - 1) >> 1]) {
swap(array, index, (index - 1) >> 1)
index = (index - 1) >> 1;
}
}
//
function heapify(array, index, size) {
let left = index * 2 + 1;
while(left < size) {
let largest = left + 1 < size && array[left] < array[left + 1] ? left + 1 : left;
largest = array[index] < array[largest] ? largest : index;
if(largest === index) break
swap(array, index, largest);
index = largest;
left = index * 2 + 1;
}
}
时间复杂度:O(n*logn),空间复杂度O(logn);
系统自带排序:
每个语言的排序内部实现都是不同的。
对于JS来说,数组长度大于10会采用快排,否则使用插入排序。源码实现
选择插入排序是因为虽然时间复杂度很差,但是在数据量很小的情况下和O(N*logN)相差无几,然而插入排序需要的常数时间很小,所以相对别的排序来说更快。
对于JAVA来说,还会考虑内部元素的类型。对于存储对象的数组来说,会采用稳定性好的算法。稳定性的意思就是对于想同志来说,相对顺序不能改变。
时间复杂度:O(n*logn),空间复杂度O(1);
希尔排序(插入排序的一种):
原理如下:希尔排序(Shell's Sort)在插入排序算法的基础上进行了改进,算法的时间复杂度与前面几种算法相比有较大的改进。其算法的基本思想是:先将待排记录序列分割成为若干子序列分别进行插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序。
//https://blog.csdn.net/qq_39207948/article/details/80006224
//https://blog.csdn.net/lhjuejiang/article/details/80505127
function shellSort(arr) {
let len = arr.length,
temp,
gap = 1;
while(gap<len/3){
gap = gap * 3 + 1;
}
let i, j;
for(gap; gap > 0; gap = Math.floor(gap/3)){
for(i = gap; i < len; i++){
temp = arr[i];
for(j = i - gap; j >= 0 && arr[j] > temp; j-=gap){
arr[j+gap] = arr[j];
}
arr[j+gap] = temp;
}
}
return arr;
}
复杂度:
希尔排序的复杂度和增量序列是相关的
{1,2,4,8,...}这种序列并不是很好的增量序列,使用这个增量序列的时间复杂度(最坏情形)是O(n^2)
Hibbard提出了另一个增量序列{1,3,7,...,2^k-1},这种序列的时间复杂度(最坏情形)为O(n^1.5)
Sedgewick提出了几种增量序列,其最坏情形运行时间为O(n^1.3),其中最好的一个序列是{1,5,19,41,109,...}
来自:https://yuchengkai.cn/docs/cs/algorithm.html#%E6%8E%92%E5%BA%8F