十大常用排序算法详细分析,包括复杂度,原理实现如下:
上图中有个错误,关于归并排序的时间复杂度为O(n)。
补充: 桶排序、计数排序、基数排序都是非比较排序。
桶排序:对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:O(N)+O(M*(N/M)*log(N/M)) = O(N+N*logN-N*logM)
当N=M时,极限情况下每个桶只有一个数据时,桶排序的最好时间复杂度能够达到O(N)。空间复杂度为O(N+M)。桶排序是稳定的。
计数排序: 类似于桶排序的线性时间排序算法。该算法是对已知数量范围的数组进行排序。其时间复杂度为O(N),适合小范围集合的排序,计数排序是用来排序0-100之间数字的最好的算法。需要辅助数组O(N+M),其中M是数组的最大值。是稳定排序。
光看这张表格肯定记不住,要了解原理。先看看复杂度的计算方法:常用算法时间复杂度的计算。
算法稳定的含义: 排序算法的稳定性通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。
算法稳定的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。
稳定性分析参考文章
插冒归基计——稳定排序
快选希堆——不稳定排序
1、冒泡排序
1.1 算法原理
s1: 从待排序系列的起始位置开始,从前往后依次比较各个位置和其后一位置的大小并执行s2;
s2: 如果当前位置的值大于其后一位置的值,就把他两交换(完成一次全序列比较后,系列最后位置的值即此序列的最大值,所以其不需要再参与冒泡);
s3:将序列的最后位置从待排序序列中移除,若移除后的待排序序列不为空,则继续执行s1,否则冒泡结束。
1.2 算法实现
function bubbleSort(array){
let flag = true;
let len = array.length;
while(flag){
flag = false;
for(let i=0; i<len-1; i++){
if( array[i] > array[i+1] ){
let temp = array[i];
array[i] = array[i+1];
array[i+1] = temp;
flag = true; //如果flag为false,则说明那次冒泡全程无交换,即数组已是排序数组
}
}
len--; //数组每整体冒泡一次,数组末尾的数就以排序完毕。
}
}
let array = [23,4,12,8,5,26,3,4,1];
bubbleSort(array); //js是按值传递,此处传的值是array的地址,所以数组内array的变化,会自动反映到外面的array
console.log(array); //[1,3,4,4,5,8,12,23,26]
1.3 算法分析
T1: 未借用辅助数组,所以空间复杂度为:O(1);
T2: 在待排序数组已经是有序数组的情况下,时间复杂度有最好情况,只需数组整体的一次冒泡,最好时间复杂度为:O(n);
T3: 待排序数组均需比较的情况下,时间复杂度为(n+(n-1)+(n-2)+…+2+1)=n*(n+1)/2,所以最差情况下时间复杂度为:O(n^2);
T4: 平均时间复杂度为:O(n^2);(具体验证还是算了- -)
T5: 冒泡排序是稳定的排序。
2、快速排序
2.1 算法原理
快速排序是对冒泡排序的一种改进。
基本思想有:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按照此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此实现整个数据变成有序序列。
2.2 算法实现
s1: 额外数组递归(需要额外的辅助数组)
function quickSort(array){
if(array.length<=1){
return array;
}
let temp = array[0];
let left = [],right = [];
for(let i=1,len=array.length; i<len; i++){
if( array[i]<temp ){
left.push(array[i]);
}else{
right.push(array[i]);
}
}
return quickSort(left).concat([temp]).concat(quickSort(right));
}
console.log(quickSort([23,4,12,8,5,26,3,4,1])); //[1,3,4,4,5,8,12,23,26]
s2: 挖坑填数法,(不需要额外的辅助数组)
function quickSort(array,start,end){
if( start < end ){
let pivot = array[start];//哨兵
let low = start,high = end;
while(low<high){
while(low<high && array[high]>= pivot){
high--;
}
array[low] = array[high]; //high那个位置就挖了个坑
while(low<high && array[low]< pivot){
low++;
}
array[high] = array[low]; //high那个位置就挖了个坑
}
array[low] = pivot;
quickSort(array,start,low-1);
quickSort(array,low+1,end);
}
}
let array = [23,4,12,8,5,26,3,4,1];
quickSort(array,0,array.length-1);
console.log(array); //[1,3,4,4,5,8,12,23,26]
2.3 算法分析
T1: s2借用辅助数组时,空间复杂度为:O(nlog(n));
T2: 最好时间复杂度为:O(nlog(n));
T3: 在所有数都比哨兵大或小时,最差情况下时间复杂度为:O(n^2);
T4: 平均时间复杂度为:O(nlog(n))。
T5: 快速排序是非稳定排序。
3、直接插入排序
3.1 算法原理
插入排序的基本方法:每一步将一个待排序序列按顺序插入到前面已经排序的序列中的适当位置,直到全部数据插入完毕为止。
假设有一组无序序列,R0,R1,R2,…,Rn.
(1)、将这个序列的第一个元素R0视为有序序列,把R2插入到R1中;
(2)、将这个序列的R0、R1或R1、R0视为有序序列,把R3插入到这两个数组成的有序序列中;
(3)、将Ri插入到有序序列中时,前i-1个数是有序的,将Ri和Ri-1、…、R1从后往前比较,确定要插入的位置。
3.2 算法实现
function insertSort(array){
for(let i=1,len=array.length;i<len;i++){
if(array[i]<array[i-1]){
let temp = array[i];
let j=i-1;
for(;j>=0&&temp<array[j];j--){
array[j+1] = array[j];
}
array[j+1] = temp;
}
}
}
let array = [23,4,12,8,5,26,3,4,1];
insertSort(array);
console.log(array); //[1,3,4,4,5,8,12,23,26]
3.3 算法分析
T1: 未借用辅助数组,空间复杂度为:O(1);
T2: 数组有序的情况下,最好时间复杂度为:O(n);
T3: 最差情况下,时间复杂度为(n+(n-1)+(n-2)+…+2+1)=n*(n+1)/2,所以最差情况下时间复杂度为:O(n^2);
T4: 平均时间复杂度为:O(n^2)。
T5: 稳定排序。
4、Shell排序
4.1 算法原理
希尔排序是一种插入排序算法,又称作缩小增量排序。是对直接插入排序算法的改进,其基本思想是:
s1: 先取小于n的整数m(m一般取n/2)作为第一个增量,所有距离为m的倍数的记录放在同一个组中,把全部数据m个组。
s2: 先在各组内进行直接插入排序,然后取第二个增量(m/2)重复上述的分组和排序。
s3: 直至所取增量为1,即所有记录放在同一组中进行直接插入排序即可。
4.2 算法实现
function shellSort(array){
let len = array.length;
for(let h=Math.floor(len/2); h>0; h=Math.floor(h/2)){
for(let i=h; i<len; i++){
for(let j=i-h; j>=0; j-=h){ //在增量内的分组进行插入排序
if(array[j]>array[j+h]){
let temp = array[j];
array[j] = array[j+h];
array[j+h] = temp;
}
}
}
}
}
let array = [23,4,12,8,5,26,3,4,1];
shellSort(array);
console.log(array); //[1,3,4,4,5,8,12,23,26]
4.3 算法分析
T1: 未借用辅助数组,空间复杂度为:O(1);
T2: 数组有序的情况下,最好时间复杂度为:O(n);
T3: 最差情况下时间复杂度为:O(n^2);
T4: 平均时间复杂度为:O(n^1.3)。
T5: 不稳定排序(分组后打乱排序)。
5、直接选择排序
5.1 算法原理
其基本思想是:
s1: 第一次从R0,R1,R2,…,Rn中选取最小值,与R0交换;
s2: 第二次从R1,R2,R3,…,Rn中选取最小值,与R1交换;
。。。。
s3: 第n次从Rn-1,Rn中选取最小值,与Rn-1交换;
n+1个数,通过n次选择,得到一个从小到大的有序序列。
5.2 算法实现
function selectSort(array){
let len = array.length;
for(let i=0; i<len; i++){
let minIndex = i;
for(let j=i+1; j<len; j++){
if(array[minIndex]>array[j]){
minIndex = j;
}
}
if(i !== minIndex){
let temp = array[i];
array[i] = array[minIndex];
array[minIndex] = temp;
}
}
}
let array = [23,4,12,8,5,26,3,4,1];
selectSort(array);
console.log(array); //[1,3,4,4,5,8,12,23,26]
5.2 算法分析
T1: 未借用辅助数组,空间复杂度为:O(1);
T2: 时间复杂度统一为为:O(n^2);(无论array如何排列,都要比较那么多次)
T3: 由于在直接选择排序中存在着不相邻元素之间的互换,因此,直接选择排序是一种不稳定的排序方法。
6、堆排序
二叉堆: 二叉堆是完全二叉树或近似完全二叉树。二叉堆满足两个特性:
* 父节点的键值总是大于或等于(下于或等于)任何一个子节点的键值。
* 每个节点的左子树和右子树都是一个二叉堆。
当父结点的键值总是大于或等于任何一个子节点的键值时为大根堆。当父结点的键值总是小于或等于任何一个子节点的键值时为小根堆。
堆的存储: 一般就用数组来表示堆,i节点的父节点下标就为(i-1)/2,它的左右子节点下标分别为2*i+1和2*i+2。如第0个结点左右子结点下标分别为1和2。
6.1 算法原理
堆排序是:利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。利用大根堆排序的基本思想:
s1: 将初始数组建成一个大根堆,此堆为初始的无序区;
建堆:建堆是个不断调整堆的过程,从((Math.floor((len-1)/2))处(此处是第一个非叶子节点,len是(数组长度-1)。)开始调整,一直到第一个节点。建堆的过程是个线性的过程,时间复杂度O(n)。
s2: 将最大的元素(即堆顶)和无序区的最后一个记录交换,由此得到新的无序区和有序区,且满足<=关系;
s3: 由于交换后的新的根可能违反了堆性质,故应将当前无序区调整为堆。
调整堆: 调整堆在构建堆的过程中会用到,利用的思想是比较节点 i 和它的孩子节点 left(i) 、right(i);选出三者中的最大(或最小)者,如果最大值不是节点 i 而是它的一个孩子节点,那就交换节点 i 和该节点,若发生交换,就继续比较 i 和它的新的子节点的值,直到不需要发生交换为止,这是一个递归的过程。调整堆的过程时间复杂度与堆的深度有关系,是log(n)的操作。
n+1个数,通过n次交换堆的根与最后的叶子节点,得到一个从小到大的有序序列。
6.2 算法实现
function heapSort(array){
//1、创建最大堆:从最后一个节点的父节点开始
let len = array.length-1;
let lastIndex = Math.floor((len-1)/2);
for(let i=lastIndex; i>=0; i--){
maxHeap(array, len, i); //从最后一个父节点开始层层向上
}
for(let i=len; i>0; i--){
let temp = array[0];
array[0] = array[i];
array[i] = temp;
maxHeap(array, i, 0);//去掉已排好序的节点,剩余节点重新构堆。
}
}
function maxHeap(array, heapSize, index){ //从最后一个子节点到第index节点构堆
//左子节点
let leftChild = 2*index+1;
//右子节点
let rightChild = 2*index +2;
//最大元素下标
let largestIndex = index;
//分别比较当前假设的最大节点和它的左右节点,找出最大值
if(leftChild<heapSize && array[leftChild]>array[largestIndex]){
largestIndex = leftChild;
}
if(rightChild<heapSize && array[rightChild]>array[largestIndex]){
largestIndex = rightChild;
}
if(largestIndex !== index){
let temp = array[largestIndex];
array[largestIndex] = array[index];
array[index] = temp;
//交换后,其子节点可能就不是最大堆了,需要对交换后的子节点重新调整
maxHeap(array, heapSize, largestIndex)
}
}
let array = [23,4,12,8,5,26,3,4,1];
heapSort(array);
console.log(array); //[1,3,4,4,5,8,12,23,26]
6.3 算法分析
T1: 未借用辅助数组,空间复杂度为:O(1);
T2: 时间复杂度统一为为:O(nlogn);
T3: 不稳定排序。
7、归并排序
7.1 算法原理
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个典型的应用。将已有序的子序列合并,得到完全有序的序列;将二个有序表合并成一个有序表,称为二路归并。
归并过程:
s1: 将数组递归二分到最小;
s2: 每次递归后调用二路归并的子函数,确保合并后的序列有序;
s3: 二路归并做法:
* 申请空间,使其大小为两个合并序列的和;
* 比较两个子序列,循环取最小值放置于新数组中;
* 剩余未放完的那个子序列,直接放于新数组最后;
* 将新数组中的值依次赋值给原数组。
7.2 算法实现
function mergeSort(array, start, end){
let mid= Math.floor((start + end)/2);
if(start<end){
mergeSort(array, start, mid);
mergeSort(array, mid+1, end);
merge(array, start, mid, end);
}
}
function merge(array, start, mid, end){
let i = start, j = mid+1, arr=[];
while(i<=mid && j<=end){
if(array[i] < array[j]){
arr.push(array[i++]);
}else{
arr.push(array[j++]);
}
}
while(i<=mid){
arr.push(array[i++]);
}
while(j<=end){
arr.push(array[j++]);
}
for(let k=start; k<=end; k++){
array[k] = arr[k-start];
}
}
let array = [23,4,12,8,5,26,3,4,1];
mergeSort(array, 0, array.length-1);
console.log(array); //[1,3,4,4,5,8,12,23,26]
7.3 算法分析
T1: 借用辅助数组,空间复杂度为:O(n);
T2: 时间复杂度统一为为:O(nlogn);
T3: 稳定排序。
8、基数排序
8.1 算法原理
基数排序有两种方式(最高位优先和最低位优先),基于桶排序思想。
1、最低位优先:
s1: 从个位数开始将数组排序;
s2: 从十位数开始讲数组再次排序;
s3: 以此类推,知道最高位排序完毕,数组已是有序数组。
2、最高位优先:
s1: 从最高位数开始将数组分桶,最高位相同的数分同一个桶里;
s2: 数组排序后,再按照次高位分桶;
s3: 以此类推,每个桶排序完毕,再进行拼接,最后拼接完的数组是有序数组。
8.2 算法实现
最低位优先:
//桶排序代码思想
function bluketSort(array){
let start=0, temp = [], max = Math.max.apply(null, array);
for(let i=0,len=array.length; i<len; i++){
if(temp[array[i]] == undefined){
temp[array[i]] = 1;
}else{
temp[array[i]]++;
}
}
for(let j=0; j<=max; j++){
if(temp[j] !==undefined){
for(let k=1; k<=temp[j]; k++){
array[start++] = j;
}
}
}
}
let array = [23,4,12,8,5,26,3,4,1];
bluketSort(array);
console.log(array); //[1,3,4,4,5,8,12,23,26]
很明显,数组最大值与最小值的差值就为需要的桶数,极端情况,若数组只有两位数,但是却相差1000,那采用桶排序就十分不合理了,桶排序只适合范围较小,数据集中的数组。
//基于桶排序的基数排序,因为每一位的值都在0-9之间,所以利用桶排序比较合理
function radixSort(array){
let d = Math.max.apply(null, array).toString().length;
let temp = [];//定义为二维数组,第一维代表那个位数的数字,第二维代表数目,值代表数组的值
let order = [];//第二维
let n = 1; //定义一个数用于求array的数值得位数
for(let i=0; i<=9; i++){
temp[i] = [];
order[i] = 0;
}
while(d--){
for(var i=0,len=array.length; i<len; i++){
let index = (Math.floor(array[i]/n))%10;
temp[index][order[index]] = array[i];
order[index]++;
}
let k = 0;
for(let j=0; j<=9; j++){
if(order[j] !== 0){
for(let i=0; i<order[j]; i++){
array[k++] = temp[j][i];
}
order[j] = 0;
}
}
n *= 10; //往高位进阶
}
}
let array = [23,4,12,8,5,26,3,4,1];
radixSort(array);
console.log(array); //[1,3,4,4,5,8,12,23,26]
8.3 算法分析
T1: 稳定排序。
9、计数排序
9.1 算法原理
计数排序的基本思想:对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。
步骤:
s1: 扫描整个数组A,对每个ai属于A,找到A中小于等于ai的元素的个数,放在辅助数组B[ai]中;
s2: 扫描B,对B中元素bi进行累加,将A[index(bi)]放到C中的第bi的位置上,并将bi减一。
9.2 算法实现
function countSort(array){
let sortArr=[], resArr=[], len=array.length;
for(let i=0; i<100; i++){
sortArr[i] = 0;
}
for(let i=0; i<len; i++){
sortArr[array[i]]++;
}
for(let i=1; i<100; i++){
sortArr[i] += sortArr[i-1];
}
//逆向遍历源数组(保证稳定性),根据计数数组中对应的值填充到先的数组中
for(i = len; i>0; i--)
{
resArr[sortArr[array[i-1]]-1] = array[i-1];
sortArr[array[i-1]]--;
}
return resArr;
}
let array = [23,4,12,8,5,26,3,4,1];
console.log(countSort(array)); //[1,3,4,4,5,8,12,23,26]
博客参考来源:八大排序算法java实现