常见的排序算法比较及总结
三种线性排序算法 计数排序、桶排序与基数排序
[非基于比较的排序]
在计算机科学中,排序是一门基础的算法技术,许多算法都要以此作为基础,不同的排序算法有着不同的时间开销和空间开销。排序算法有非常多种,如我们最常用的快速排序和堆排序等算法,这些算法需要对序列中的数据进行比较,因为被称为基于比较的排序。
基于比较的排序算法是不能突破O(NlogN)的。简单证明如下:
N个数有N!个可能的排列情况,也就是说基于比较的排序算法的判定树有N!个叶子结点,比较次数至少为log(N!)=O(NlogN)(斯特林公式)。
而非基于比较的排序,如计数排序,桶排序,和在此基础上的基数排序,则可以突破O(NlogN)时间下限。但要注意的是,非基于比较的排序算法的使用都是有条件限制的,例如元素的大小限制,相反,基于比较的排序则没有这种限制(在一定范围内)。但并非因为有条件限制就会使非基于比较的排序算法变得无用,对于特定场合有着特殊的性质数据,非基于比较的排序算法则能够非常巧妙地解决。
本文着重介绍三种线性的非基于比较的排序算法:计数排序、桶排序与基数排序。
[计数排序]
首先从计数排序(Counting Sort)开始介绍起,假设我们有一个待排序的整数序列A,其中元素的最小值不小于0,最大值不超过K。建立一个长度为K的线性表C,用来记录不大于每个值的元素的个数。
算法思路如下:
- 扫描序列A,以A中的每个元素的值为索引,把出现的个数填入C中。此时C[i]可以表示A中值为i的元素的个数。
- 对于C从头开始累加,使C[i]<-C[i]+C[i-1]。这样,C[i]就表示A中值不大于i的元素的个数。
- 按照统计出的值,输出结果。
由线性表C我们可以很方便地求出排序后的数据,定义B为目标的序列,Order[i]为排名第i的元素在A中的位置,则可以用以下方法统计。
显然地,计数排序的时间复杂度为O(N+K),空间复杂度为O(N+K)。当K不是很大时,这是一个很有效的线性排序算法。更重要的是,它是一种稳定排序算法,即排序后的相同值的元素原有的相对位置不会发生改变(表现在Order上),这是计数排序很重要的一个性质,就是根据这个性质,我们才能把它应用到基数排序。
[桶排序]
可能你会发现,计数排序似乎饶了点弯子,比如当我们刚刚统计出C,C[i]可以表示A中值为i的元素的个数,此时我们直接顺序地扫描C,就可以求出排序后的结果。的确是这样,不过这种方法不再是计数排序,而是桶排序(Bucket Sort),确切地说,是桶排序的一种特殊情况。
用这种方法,可以很容易写出程序,比计数排序还简单,只是不能求出稳定的Order。
这种特殊实现的方式时间复杂度为O(N+K),空间复杂度也为O(N+K),同样要求每个元素都要在K的范围内。更一般的,如果我们的K很大,无法直接开出O(K)的空间该如何呢?
首先定义桶,桶为一个数据容器,每个桶存储一个区间内的数。依然有一个待排序的整数序列A,元素的最小值不小于0,最大值不超过K。假设我们有M个桶,第i个桶Bucket[i]存储iK/M至(i+1)K/M之间的数,有如下桶排序的一般方法:
- 扫描序列A,根据每个元素的值所属的区间,放入指定的桶中(顺序放置)。
- 对每个桶中的元素进行排序,什么排序算法都可以,例如快速排序。
- 依次收集每个桶中的元素,顺序放置到输出序列中。
对该算法简单分析,如果数据是期望平均分布的,则每个桶中的元素平均个数为N/M。如果对每个桶中的元素排序使用的算法是快速排序,每次排序的时间复杂度为O(N/Mlog(N/M))。则总的时间复杂度为O(N)+O(M)O(N/Mlog(N/M)) = O(N+ Nlog(N/M)) =O(N + NlogN - NlogM)。当M接近于N是,桶排序的时间复杂度就可以近似认为是O(N)的。就是桶越多,时间效率就越高,而桶越多,空间却就越大,由此可见时间和空间是一个矛盾的两个方面。
桶中元素的顺序放入和顺序取出是有必要的,因为这样可以确定桶排序是一种稳定排序算法,配合基数排序是很好用的。
[基数排序]
下面说到我们的重头戏,基数排序(Radix Sort)。上述的基数排序和桶排序都只是在研究一个关键字的排序,现在我们来讨论有多个关键字的排序问题。
假设我们有一些二元组(a,b),要对它们进行以a为首要关键字,b的次要关键字的排序。我们可以先把它们先按照首要关键字排序,分成首要关键字相同的若干堆。然后,在按照次要关键值分别对每一堆进行单独排序。最后再把这些堆串连到一起,使首要关键字较小的一堆排在上面。按这种方式的基数排序称为MSD(Most Significant Dight)排序。
第二种方式是从最低有效关键字开始排序,称为LSD(Least Significant Dight)排序。首先对所有的数据按照次要关键字排序,然后对所有的数据按照首要关键字排序。要注意的是,使用的排序算法必须是稳定的,否则就会取消前一次排序的结果。由于不需要分堆对每堆单独排序,LSD方法往往比MSD简单而开销小。下文介绍的方法全部是基于LSD的。
通常,基数排序要用到计数排序或者桶排序。使用计数排序时,需要的是Order数组。使用桶排序时,可以用链表的方法直接求出排序后的顺序
基数排序是一种用在老式穿卡机上的算法。一张卡片有80列,每列可在12个位置中的任一处穿孔。排序器可被机械地"程序化"以检查每一迭卡片中的某一列,再根据穿孔的位置将它们分放12个盒子里。这样,操作员就可逐个地把它们收集起来。其中第一个位置穿孔的放在最上面,第二个位置穿孔的其次,等等。
对于一个位数有限的十进制数,我们可以把它看作一个多元组,从高位到低位关键字重要程度依次递减。可以使用基数排序对一些位数有限的十进制数排序。
[三种线性排序算法的比较]
从整体上来说,计数排序,桶排序都是非基于比较的排序算法,而其时间复杂度依赖于数据的范围,桶排序还依赖于空间的开销和数据的分布。而基数排序是一种对多元组排序的有效方法,具体实现要用到计数排序或桶排序。
相对于快速排序、堆排序等基于比较的排序算法,计数排序、桶排序和基数排序限制较多,不如快速排序、堆排序等算法灵活性好。但反过来讲,这三种线性排序算法之所以能够达到线性时间,是因为充分利用了待排序数据的特性,如果生硬得使用快速排序、堆排序等算法,就相当于浪费了这些特性,因而达不到更高的效率。
在实际应用中,基数排序可以用于后缀数组的倍增算法,使时间复杂度从O(NlogNlogN)降到O(N*logN)。线性排序算法使用最重要的是,充分利用数据特殊的性质,以达到最佳效果。
排序算法总结
插入排序
直接插入排序
原理:将数组分为无序区和有序区两个区,然后不断将无序区的第一个元素按大小顺序插入到有序区中去,最终将所有无序区元素都移动到有序区完成排序。
void insertSort(int a[], int N)
{for(int i = 1; i < N; i++){int temp = a[i];int j;for(j = i; j > 0 && temp < a[j-1]; j--)a[j] = a[j-1];a[j] = temp;}
}
希尔排序
原理:又称增量缩小排序。先将序列按增量划分为元素个数相同的若干组,使用直接插入排序法进行排序,然后不断缩小增量直至为1,最后使用直接插入排序完成排序。
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
void shellSort(int a[], int N)
{for(int incre = N / 2; incre > 0; incre /= 2){for(int i = incre; i < N; i++){int temp = a[i];int j;for(j = i; j >= incre && temp < a[j - incre]; j -= incre)a[j] = a[j - incre];a[j] = temp;}}
}
交换排序
冒泡排序
原理:将序列划分为无序和有序区,不断通过交换较大元素至无序区尾完成排序。
void bubbleSort(int a[], int N)
{for(int i = 0; i < N; i++){for(int j = i; j < N - i - 1; j++){if(a[j] > a[j+1]){int temp = a[j];a[j] = a[j+1];a[j+1] = temp;}}}
}
补充说明:使用didSwap=true/false可以避免重复的比较,使得最好情况的复杂度变为O(n)
快速排序
原理:不断寻找一个序列的中点,然后对中点左右的序列递归的进行排序,直至全部序列排序完成,使用了分治的思想。
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
实现:
void quickSortCore(int a[], int left, int right)
{if(left < right){int temp = a[left];int i = left, j = right;while(i < j){while(i < j && temp <= a[j])j--;if(i < j)a[i++] = a[j];while(i < j && temp >= a[i])i++;if(i < j)a[j--] = a[i];}a[i] = temp;quickSortCore(a, left, i-1);quickSortCore(a, i+1, right);}}void quickSort(int a[], int N)
{quickSortCore(a, 0, N-1);
}
选择排序
直接选择排序
原理:将序列划分为无序和有序区,寻找无序区中的最小值和无序区的首元素交换,有序区扩大一个,循环最终完成全部排序。
void selectSort(int a[], int N)
{for(int i = 0; i < N; i++){int k = i;for(int j = i + 1; j < N; j++){if(a[j] < a[k])k = j;}int temp = a[i];a[i] = a[k];a[k] = temp;}
}
堆排序
堆序性质: 堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]
称为大顶堆,满足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]
称为小顶堆。
原理:利用大顶堆或小顶堆思想,首先建立堆,然后将堆首与堆尾交换,堆尾之后为有序区。如从小到大排序,建立大顶堆,堆顶元素与堆尾不断交换,同时缩小堆的范围,最终得到排序结果。
void percDown(int a[], int i, int N)
{int temp = a[i], child;for(; 2 * i + 1 < N; i = child){child = 2 * i + 1;if(child + 1 < N && a[child] < a[child + 1])child++;if(temp < a[child])a[i] = a[child];elsebreak;}a[i] = temp;
}void heapSort(int a[], int N)
{for(int i = N / 2; i >= 0; i--)percDown(a, i, N);for(int i = N-1; i > 0; i--){int temp = a[i];a[i] = a[0];a[0] = temp;percDown(a, 0, i);}
}
归并排序
原理:将原序列划分为有序的两个序列,然后利用归并算法进行合并,合并之后即为有序序列。
void merge(int a[], int temp[], int left, int mid, int right)
{if(left < right){int lpos = left, lend = mid;int rpos = mid + 1, rend = right;int tpos = left;while(lpos <= lend && rpos <= rend){if(a[lpos] <= a[rpos])temp[tpos++] = a[lpos++];elsetemp[tpos++] = a[rpos++];}while(lpos <= lend)temp[tpos++] = a[lpos++];while(rpos <= rend)temp[tpos++] = a[rpos++];for(int i = 0; i <= right; i++)a[i] = temp[i];}
}
void mergeSortCore(int a[], int temp[], int left, int right)
{if(left < right){int mid = (left + right) / 2;mergeSortCore(a, temp, left, mid);mergeSortCore(a, temp, mid+1, right);merge(a, temp, left, mid, right);}
}
void mergeSort(int a[], int N)
{int *temp = new int[N];mergeSortCore(a, temp, 0, N-1);delete [] temp;
}
各种排序算法的复杂度稳定性分析
分类 | 名称 | 复杂度分析 | 稳定性 | 稳定性原因分析 |
---|---|---|---|---|
插入排序 | 简单插入 | 平均O(n^2),最好O(n),最坏O(n^2) | 稳定 | 没有跨元素交换 |
———- | 希尔排序 | 平均接近nlogn,最好O(n),最坏O(n^2) | 不稳定 | 增量分组,有跨元素交换 |
交换排序 | 冒泡排序 | 平均O(n^2),最好O(n),最坏O(n^2) | 稳定 | 没有跨元素交换 |
———- | 快速排序 | 平均nlogn,最好nlogn,最坏O(n^2) | 不稳定 | 有跨元素交换 |
选择排序 | 直接选择 | 平均O(n^2),最好O(n^2),最坏O(n^2) | 不稳定 | 5 8 5 2 |
———- | 堆排序 | 平均,最好,最坏nlogn | 不稳定 | 3 2 3 2 |
归并排序 | 归并排序 | 平均,最好,最坏nlogn,有O(n)空间复杂度 | 稳定 | 没有跨元素交换 |
扩展问题
- 单链表可以做快速排序吗?为什么?
可以。快速排序的核心函数partition,选择某个元素为枢纽元x(通常是第一个),一遍扫描之后使得比x小的在枢纽元左边,比x大的在枢纽元的右边。使用链表时,x指向链表头(枢纽元),扫描这个链表,小元素拼在链表头,大元素拼在链表尾部,从而完成一次partition函数的流程。
常见的排序算法比较及总结相关推荐
- access两字段同时升序排序_7 天时间,我整理并实现了这 9 种常见的排序算法
排序算法 回顾 我们前面已经介绍了 3 种最常见的排序算法: java 实现冒泡排序讲解 QuickSort 快速排序到底快在哪里? SelectionSort 选择排序算法详解(java 实现) 然 ...
- PHP面试题:请写出常见的排序算法,并用PHP实现冒泡排序,将数组$a = array()按照从小到大的方式进行排序。
常见的排序算法: 冒泡排序法.快速排序法.简单选择排序法.堆排序法.直接插入排序法.希尔排序法.合并排序法. 冒泡排序法的基本思想是:对待排序记录关键字从后往前(逆序)进行多遍扫描,当发现相邻两个关键 ...
- python常用算法有哪些_python常见的排序算法有哪些?
大家都知道,关于python的算法有很多,其中最为复杂的就是python的排序算法,因为它并不是单一的,而是复杂的,关于排序算法就有好几种不同的方式,大家可以根据以下内容,结合自己的项目需求,选择一个 ...
- JS 常见的排序算法
工作中算法不常用,但是排序经常用.因此在这里整理了几种JS中常见的排序算法. 冒泡排序 1.算法思想:判断两个相邻元素,大于则交换位置 2.算法步骤 从数组中第一个数开始,依次与下一个数比较并次交换比 ...
- 七种常见的排序算法总结
目录 引言 1.什么是排序? 2.排序算法的目的是什么? 3.常见的排序算法有哪些? 一,插入排序 1.基本思想 2.代码实现 3.性能分析 4.测试 二,希尔排序(缩小增量排序) 1.基本思想 2. ...
- 基于比较的常见的排序算法
目录 写在前面 排序 稳定性 排序的分类 常见的基于比较的排序 直接插入排序 代码 性能分析 总结 代码优化 折半插入 希尔排序 希尔排序 如何分组 代码 性能分析 选择排序 代码 性能分析 双向选择 ...
- 常见的排序算法与MSQL
常见的排序算法 1.常见的排序算法 冒泡排序法.快速排序法.简单选择排序法.堆排序法.直接插入排序法.希尔排序法.合并排序法. (1)冒泡排序法:对待排序记录关键字从后往前(逆序)进行多遍扫描,当发现 ...
- 常见的排序算法的稳定性
分析一下常见的排序算法的稳定性,每个都给出简单的理由. 冒泡排序 冒泡排序就是把小的元素往前调或者把大的元素往后调.比较是相邻的两个元素比较,交换也发生在这两个元素之间.所以,如果两个元素相等,我想你 ...
- 【数据结构---排序】庖丁解牛式剖析常见的排序算法
排序算法 一.常见的排序算法 二.常见排序算法的实现 1. 直接插入排序 2. 希尔排序 3. 直接选择排序 4. 堆排序 5. 冒泡排序 6. 快速排序 6.1 递归实现快速排序 思路一.hoare ...
- java 排序_Java中常见的排序算法有哪些?---选择排序
排序相关的的基本概念 排序: 将一组杂乱无章的数据按一定的规律顺次排列起来. 数据表( data list): 它是待排序数据对象的有限集合. 排序码(key):通常数据对象有多个属性域, 即多个数据 ...
最新文章
- TRY NOT TO SAY SO MUCH!
- Coursera自动驾驶课程第16讲:LIDAR Sensing
- 阿里云 Aliplayer高级功能介绍(四):直播时移
- ubuntu系统部署python3.6.4
- 【操作系统/OS笔记13】信号量、PV操作、管程、条件变量、生产者消费者问题
- Source Insight 中查看日文注释
- 用JavaScript做一个日历和用canvas做一个时钟
- 51单片机红外遥控小车
- CHIA币的本质认识
- mysql中的mysql数据库不见了
- UVA 12235 Help Bubu 状态压缩DP
- Python:实现pollard rho大数分解算法(附完整源码)
- 在KubeSphere中部署微服务(阡陌)+ DevOps
- cos(a+b)=cosa*cosb-sina*sinb的推导过程
- FTPClientUtil FTP客户端工具
- 微信公众号的用户运营?
- 计算机如何接两个屏幕,笔记本连接两个显示器的步骤_笔记本电脑怎么外接两个显示器做分屏-win7之家...
- 印光法师:《灵岩遗旨》壹、悲化有情
- 计算机网络英文简称名词解释
- ChibiOS系列:五、将STM32 USART与ChibiOS串行驱动程序配合使用