史上最全面的八种排序算法总结
排序包括内部排序和外部排序。内部排序是对内存中的数据记录进行排序,而外部排序是因为排序后的数据非常大,无法一次性容纳所有已排序的记录。在排序过程中,需要访问外部存储器。
我们这里所说的八大排名都是内部排名。
当n很大时,应该使用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。
快速排序:目前认为是基于比较的内部排序中最好的方法。当待排序关键字随机分布时,快速排序的平均时间最短;
1.插入排序——直接插入排序
基本理念:
向已排序的有序列表中插入一条记录,得到一个新的有序列表,记录数加1。即:先将序列的第一条记录视为有序子序列,然后逐条插入第二条记录,直到整个序列是有序的。
要点:设置哨兵,用于临时存储和判断数组边界。
直接插入排序示例:
如果遇到与插入元素相等的元素,则插入元素将要插入的元素放在相等元素之后。因此,相等元素的顺序没有改变。原来的无序序列的顺序就是排序后的顺序,所以插入排序是稳定的。
算法的实现:
[cpp] 查看普通副本打印?
无效打印(int a [],int n,int i){
库蒂':';
for(int j=0; j8; j++){
库塔[j] ' ';
}
库滕德尔;
}
void InsertSort(int a[], int n)
{
for(int i=1; in; i++){
if(a[i] a[i-1]){ //如果第i个元素大于第i-1个元素,则直接插入。如果小于,则移动已排序的列表并插入。
整数j=i-1;
a[i]=a[i-1]; //将一个元素逐个移动
while(x a[j]){ //在有序列表中查找插入位置
a[j+1]=a[j];
j——; //将元素向后移动
}
a[j+1]=x; //插入到正确的位置
}
打印(a,n,i); //打印每次排序的结果
}
}
int main(){
int a[8]={3,1,5,7,2,4,9,6};
插入排序(a,8);
打印(a,8,8);
}
效率:
时间复杂度:O(n^2)。
其他插入排序包括二元插入排序和2路插入排序。
2.插入排序——希尔排序(Shell`s Sort)
希尔排序法是由D.L. 提出的。 Shell于1959年提出,是对直接排序的很大改进。希尔排序也称为减少增量排序。
基本思想:
首先将整个待排序记录序列分成若干子序列进行直接插入排序。当整个序列中的记录“基本有序”时,则直接对所有记录进行插入排序。
操作方法:
选择增量序列t1,t2,tk,其中titj,tk=1;
根据增量序列数k对序列进行k次排序;
在每次排序过程中,将待排序列根据对应的增量ti分成若干个长度为m的子序列,对每个子表进行直接插入排序。只有当增量因子为1时,整个序列才被当作一个表来处理,表的长度就是整个序列的长度。
希尔排序示例:
算法实现:
我们简单处理增量序列:增量序列d={n/2,n/4, n/8.1} n 为要排序的数字个数
即:首先将一组待排序记录按照一定的增量d(n/2,n为待排序数的个数)划分为若干组子序列,每组记录的下标相差d.对于每组中的所有记录元素通过直接插入进行排序,然后按更小的增量(d/2)进行分组,并在每组内再次排序。继续减少增量,直到为1,最后使用直接插入排序完成排序。
[cpp] 查看普通副本打印?
无效打印(int a [],int n,int i){
库蒂':';
for(int j=0; j8; j++){
库塔[j] ' ';
}
库滕德尔;
}
/**
* 直接插入排序的一般形式
*
* @param int dk 减少增量。如果是直接插入排序,dk=1
*
*/
void ShellInsertSort(int a[], int n, int dk)
{
for(int i=dk; in; ++i){
if(a[i] a[i-dk]){ //如果第i个元素大于第i-1个元素,则直接插入。如果小于,则移动已排序的列表并插入。
int j=i-dk;
a[i]=a[i-dk]; //首先向后移动一个元素
while(x a[j]){ //在有序列表中查找插入位置
a[j+dk]=a[j];
j-=dk; //将元素向后移动
}
a[j+dk]=x; //插入到正确的位置
}
打印(a,n,i);
}
}
/**
* 首先按照增量d进行希尔排序(n/2,n为要排序的数字个数)
*
*/
void shellSort(int a[], int n){
int dk=n/2;
而(dk=1){
ShellInsertSort(a, n, dk);
dk=dk/2;
}
}
int main(){
int a[8]={3,1,5,7,2,4,9,6};
//ShellInsertSort(a,8,1); //直接插入排序
shellSort(a,8); //希尔插入排序
打印(a,8,8);
}
希尔排序时效性分析比较困难。键码比较的次数和记录的移动的次数取决于增量因子序列d的选择。在某些情况下,可以准确估计键码比较次数和记录的走法次数。目前还没有人给出一种选择最佳增量因子序列的方法。增量因子序列可以采用多种方式获取,包括奇数和素数。但需要注意的是,增量因子之间除1外没有其他公共因子,最后的增量因子必须为1。希尔排序方法是一种不稳定的排序方法。
3. 选择排序——简单选择排序
基本思想:
在一组待排序的数字中,选择最小(或最大)的数字,与第一个位置的数字交换;然后找到剩余数字中最小(或最大)的数字,并将其与第二位置的数字交换。交换,依此类推,直到第n-1 个元素(倒数第二个数字)和第n 个元素(最后一个数字)进行比较。
简单选择排序的示例:
操作方法:
第一遍,找到n条记录中关键码最小的记录,与第一条记录交换;
第二遍,从第二条记录开始的n-1条记录中选择关键码最小的记录,与第二条记录交换;
等等.
对于第i遍,从第i条记录开始的n-i+1条记录中选择具有最小密钥码的记录并与第i条记录交换。
直到整个序列按key排序。
算法实现:
[cpp] 查看普通副本打印?
无效打印(int a [],int n,int i){
cout'第'i+1'行:';
for(int j=0; j8; j++){
库塔[j] ' ';
}
库滕德尔;
}
/**
* 数组的最小值
*
* @return int 数组的键值
*/
int SelectMinKey(int a[], int n, int i)
{
整数k=i;
for(int j=i+1;j n;++j) {
if(a[k] a[j]) k=j;
}
返回k;
}
/**
* 选择排序
*
*/
无效selectSort(int a[], int n){
int 键,tmp;
for(int i=0; i n; ++i) {
key=SelectMinKey(a, n,i); //选择最小的元素
如果(键!=我){
tmp=a[i]; a[i]=a[键]; a[键]=tmp; //最小元素与第i个元素互换
}
打印(a,n,i);
}
}
int main(){
int a[8]={3,1,5,7,2,4,9,6};
cout'初始值:';
for(int j=0; j8; j++){
库塔[j] ' ';
}
库滕德伦德尔;
选择排序(a,8);
打印(a,8,8);
}
简单选择排序的改进—— 二元选择排序
在简单选择排序中,每次循环只能确定排序后一个元素的位置。我们可以考虑改进每次循环对两个元素(当前最大和最小记录)的定位,从而减少排序所需的循环次数。改进后,排序n个数据最多只需要[n/2]次循环。具体实现如下:
[cpp] 查看普通副本打印?
void SelectSort(int r[],int n) {
int i,j,最小值,最大值,tmp;
for (i=1;i=n/2;i++) {
//进行不超过n/2 个选择排序操作
最小值=我;最大值=我; //分别记录最大和最小关键字记录位置
for (j=i+1; j=n-i; j++) {
if (r[j] r[max]) {
最大值=j ;继续;
}
if (r[j] r[min]) {
最小值=j ;
}
}
//这个交换操作也可以具体情况具体讨论,提高效率。
tmp=r[i-1]; r[i-1]=r[最小值]; r[分钟]=tmp;
tmp=r[n-i]; r[n-i]=r[最大值]; r[最大值]=tmp;
}
}
4.选择排序——堆排序
堆排序是一种树选择排序,是对直接选择排序的有效改进。
基本思想:
堆的定义如下:n个元素的序列(k1,k2,kn),当且仅当
它被称为堆。从堆的定义可以看出,堆顶元素(即第一个元素)一定是最小项(小顶堆)。
如果将堆存储为一维数组,则堆对应于一棵完全二叉树,所有非叶子节点的值不大于(或不小于)其子节点的值,根节点(堆顶元素)的值最小(或最大)。喜欢:
(a) 大顶堆序列:(96,83,27,38,11,09)
(b) 最小堆序列:(12, 36, 24, 85, 47, 30, 53, 91)
最初,将待排序的n个数的序列视为一个顺序存储的二叉树(一维数组存储二叉树),调整它们的存储顺序,使其成为一个堆,输出堆顶元素即可得到n 个元素。堆中最小(或最大)的元素,则堆的根节点数最小(或最大)。然后重新调整前(n-1)个元素组成堆,输出堆顶元素,得到n个元素中第二小(或第二大)的元素。以此类推,直到出现一个只有两个节点的堆,将它们交换,最后得到n个节点的有序序列。将此过程称为堆排序。
因此,实现堆排序需要解决两个问题:
1.如何构建n个要排序的数字到堆中;
2、输出堆顶元素后,如何调整剩余的n-1个元素,使其成为新的堆。
我们先讨论第二个问题:输出堆顶元素后,用剩余的n-1个元素重建堆的调整过程。
如何调整小顶堆:
1)有一个有m个元素的堆。输出堆顶元素后,还剩下m-1个元素。堆底的元素被发送到堆顶((最后一个元素与堆顶交换),堆被销毁。唯一的原因是根节点不满足属性堆的。
2)将根节点与左右子树中较小的元素交换。
3)如果与左子树交换:如果左子树堆被破坏,即左子树的根节点不满足堆的属性,则重复方法(2)。
4)如果与右子树交换,如果右子树堆被破坏,即右子树的根节点不满足堆的性质。然后重复方法(2)。
5)继续对不满足堆属性的子树进行上述交换操作,直到叶子节点和堆构建完毕。
这种从根节点到叶节点的调整过程称为筛选。如图所示:
我们来讨论一下最初构建一个包含n 个元素的堆的过程。
堆建法:初始序列建堆的过程是一个反复筛选的过程。
1)一棵有n个节点的完全二叉树,那么最后一个节点就是
节点的子树。
2) 过滤
从以节点为根的子树开始,子树变成堆。
3)然后,向前过滤以每个节点为根的子树,形成堆,直到根节点。
如图,构建堆的初始过程:无序序列:(49,38,65,97,76,13,27,49)
算法的实现:
从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶和堆最后一个元素之间交换位置。所以堆排序由两个函数组成。一是构建堆的穿透函数,二是重复调用穿透函数实现排序的函数。
[cpp] 查看普通副本打印?
无效打印(int a [],int n){
for(int j=0; jn; j++){
库塔[j] ' ';
}
库滕德尔;
}
/**
* 已知H[s.m]除H[s]外均满足堆的定义
* 调整H[s]使其成为一个大的顶堆。即过滤以第s个节点为根的子树,
*
* @param H 为要调整的堆数组
* @param s 为要调整的数组元素的位置
* @param length 是数组的长度
*
*/
void HeapAdjust(int H[],int s, int 长度)
{
int tmp=H[s];
int 子级=2*s+1; //左子节点的位置。 (i+1为当前调整节点右子节点的位置)
while(子长度){
if(child+1 length H[child]H[child+1]) { //如果右孩子大于左孩子(找到比当前节点大的子节点进行调整)
++孩子;
}
if(H[s]H[child]) { //如果较大的子节点比父节点大
H[s]=H[子]; //然后将较大的子节点向上移动并替换其父节点
s=孩子; //重置s,即下一个要调整的节点的位置
孩子=2*s+1;
} else { //如果当前要调整的节点大于其左右子节点,则无需调整,直接退出
休息;
}
H[s]=tmp; //将当前要调整的节点放置到比它大的子节点的位置。
}
打印(高度,长度);
}
/**
* 调整初始堆
* 将H[0.length-1] 构建成堆
* 调整后第一个元素为序列中最小的元素
*/
void BuildingHeap(int H[], int length)
{
//最后一个有子节点的位置i=(length -1)/2
for (int i=(长度-1)/2 ; i=0; --i)
HeapAdjust(H,i,长度);
}
/**
* 堆排序算法
*/
void HeapSort(int H[],int 长度)
{
//初始堆
BuildingHeap(H, 长度);
//调整从最后一个元素开始的顺序
for (int i=长度- 1; i 0; --i)
{
//交换堆顶元素H[0]和堆最后一个元素
int 温度=H[i]; H[i]=H[0]; H[0]=温度;
//每次交换堆顶元素和堆最后一个元素后,必须对堆进行调整
堆调整(H,0,i);
}
}
int main(){
int H[10]={3,1,5,7,2,4,9,6,10,8};
cout'初始值:';
打印(H,10);
堆排序(H,10);
//选择排序(a, 8);
cout'结果:';
打印(H,10);
}
分析:
设树深度为k。从根到叶子过滤时,元素比较次数最多为2(k-1)次,记录最多交换k次。因此,堆建好后,排序过程中的筛选次数不超过以下公式:
构建堆时的比较次数不超过4n,因此堆排序最坏情况的时间复杂度为:O(nlogn)。
5.交换排序——冒泡排序(Bubble Sort)
基本思想:
在一组待排序的数字中,对范围内所有尚未排序的数字,从上到下依次比较和调整相邻的两个数字,使较大的数字向下沉,较大的数字向下沉。小的向上升起。即:每当比较两个相邻的数字,发现其顺序与排序要求相反时,就将它们交换。
冒泡排序示例:
算法的实现:
[cpp] 查看普通副本打印?
void bubbleSort(int a[], int n){
for(int i=0; i n-1; ++i) {
for(int j=0; j n-i-1; ++j) {
if(a[j] a[j+1])
{
int tmp=a[j] ; a[j]=a[j+1] ; a[j+1]=tmp;
}
}
}
}
冒泡排序算法的改进
1. 设置一个地标变量pos 来记录每次排序过程中最后一次交换的位置。由于pos 位置之后的记录已就位交换,因此在下一次排序过程中仅扫描pos 位置。
改进后的算法如下:
[cpp] 查看普通副本打印?
void Bubble_1 (int r[], int n) {
整数i=n -1; //最初,最后一个位置保持不变
而(我0){
整数位置=0; //每趟开始时,不进行记录交换
for (int j=0; j i; j++)
如果(r[j] r[j+1]){
位置=j; //记录兑换位置
int tmp=r[j]; r[j]=r[j+1]; r[j+1]=tmp;
}
我=位置; //为下一步排序做准备
}
}
2.传统冒泡排序中,每次排序操作只能找到一个最大值或最小值。我们考虑采用在每次排序操作中进行正向冒泡和反向冒泡的方法,一次性获得两个最终值(最大的一个)。和最小),从而将排序次数减少了近一半。
改进后的算法实现为:
[cpp] 查看普通副本打印?
void Bubble_2 (int r[], int n){
int 低=0;
int 高=n -1; //设置变量的初始值
int tmp,j;
而(低高){
for (j=low; j high; ++j) //正向冒泡,找到最大的
如果(r[j] r[j+1]){
tmp=r[j]; r[j]=r[j+1]; r[j+1]=tmp;
}
- 高的; //修改高位值并向前移动一位
for (j=high; jlow; --j) //反转气泡,找到最小的一个
如果(r[j]r[j-1]){
tmp=r[j]; r[j]=r[j-1]; r[j-1]=tmp;
}
++低; //修改低位值并向后移动一位
}
}
6.交换排序——快速排序
基本思想:
1)选择一个基本元素,通常是第一个元素或最后一个元素,
2)通过一轮排序,将待排序的记录分为两个独立的部分,并且其中一部分记录的元素值小于参考元素值。另一部分记录的元素值大于基值。
3)此时,参考元素排序后已处于正确位置。
4)然后继续用同样的方式对两部分记录进行排序,直到整个序列有序。
快速排序示例:
(a) 一次排序过程:
(b) 排序全过程
算法的实现:
递归实现:
[cpp] 查看普通副本打印?
无效打印(int a [],int n){
for(int j=0; jn; j++){
库塔[j] ' ';
}
库滕德尔;
}
无效交换(int *a,int *b)
{
int tmp=*a;
*a=*b;
*b=tmp;
}
int 分区(int a[], int low, int high)
{
int privotKey=a[低]; //基本元素
while(low high){ //从表格两端向中间交替扫描
while(low high a[high]=privotKey) --high; //从high指向的位置向前查找,一直到low+1的位置。将小于底端的元素交换到下端
交换(a[低],a[高]);
while(low high a[low]=privotKey ) ++low;
交换(a[低],a[高]);
}
打印(a,10);
返回低位;
}
void fastSort(int a[], int low, int high){
如果(低高){
int privotLoc=分区(a, 低, 高); //将表分成两部分
快速排序(a,低,privotLoc -1); //对低位子表递归排序
QuickSort(a, privotLoc + 1, 高); //对高位子表递归排序
}
}
int main(){
int a[10]={3,1,5,7,2,4,9,6,10,8};
cout'初始值:';
打印(a,10);
快速排序(a,0,9);
cout'结果:';
打印(a,10);
}
分析:
快速排序通常被认为是同数量级排序方法中平均性能最好的(O(nlog2n))。但如果初始序列是按键排序或者基本排序的话,快速排序就会退化为冒泡排序。为了改进它,通常采用“三合一法”来选择基准记录,即将以两个端点为中心的三个记录键和排序区间的中点调整为支点记录。快速排序是一种不稳定的排序方法。
快速排序改进
在该改进算法中,仅对长度大于k的子序列递归调用快速排序,使原始序列基本有序,然后使用插入排序算法对整个基本有序序列进行排序。实践证明,改进算法的时间复杂度有所降低,且当k取8左右时,改进算法性能最佳。算法思想如下:
[cpp] 查看普通副本打印?
无效打印(int a [],int n){
for(int j=0; jn; j++){
库塔[j] ' ';
}
库滕德尔;
}
无效交换(int *a,int *b)
{
int tmp=*a;
*a=*b;
*b=TM
p; } int partition(int a[], int low, int high) { int privotKey = a[low]; //基准元素 while(low < high){ //从表的两端交替地向中间扫描 while(low < high && a[high] >= privotKey) --high; //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端 swap(&a[low], &a[high]); while(low < high && a[low] <= privotKey ) ++low; swap(&a[low], &a[high]); } print(a,10); return low; } void qsort_improve(int r[ ],int low,int high, int k){ if( high -low > k ) { //长度大于k时递归, k为指定的数 int pivot = partition(r, low, high); // 调用的Partition算法保持不变 qsort_improve(r, low, pivot - 1,k); qsort_improve(r, pivot + 1, high,k); } } void quickSort(int r[], int n, int k){ qsort_improve(r,0,n,k);//先调用改进算法Qsort使之基本有序 //再用插入排序对基本有序序列排序 for(int i=1; i<=n;i ++){ int tmp = r[i]; int j=i-1; while(tmp < r[j]){ r[j+1]=r[j]; j=j-1; } r[j+1] = tmp; } } int main(){ int a[10] = {3,1,5,7,2,4,9,6,10,8}; cout<<"初始值:"; print(a,10); quickSort(a,9,4); cout<<"结果:"; print(a,10); } 7. 归并排序(Merge Sort) 基本思想: 归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。 归并排序示例: 合并方法: 设r[i…n]由两个有序子表r[i…m]和r[m+1…n]组成,两个子表长度分别为n-i +1、n-m。 j=m+1;k=i;i=i; //置两个子表的起始下标及辅助数组的起始下标 若i>m 或j>n,转⑷ //其中一个子表已合并完,比较选取结束 //选取r[i]和r[j]较小的存入辅助数组rf 如果r[i]<r[j],rf[k]=r[i]; i++; k++; 转⑵ 否则,rf[k]=r[j]; j++; k++; 转⑵ //将尚未处理完的子表中元素存入rf 如果i<=m,将r[i…m]存入rf[k…n] //前一子表非空 如果j<=n , 将r[j…n] 存入rf[k…n] //后一子表非空 合并结束。 [cpp] view plain copy print? //将r[i…m]和r[m +1 …n]归并到辅助数组rf[i…n] void Merge(ElemType *r,ElemType *rf, int i, int m, int n) { int j,k; for(j=m+1,k=i; i<=m && j <=n ; ++k){ if(r[j] < r[i]) rf[k] = r[j++]; else rf[k] = r[i++]; } while(i <= m) rf[k++] = r[i++]; while(j <= n) rf[k++] = r[j++]; } 归并的迭代算法 1 个元素的表总是有序的。所以对n 个元素的待排序列,每个元素可看成1 个有序子表。对子表两两合并生成n/2个子表,所得子表除最后一个子表长度可能为1 外,其余子表长度均为2。再进行两两合并,直到生成n 个元素按关键码有序的表。 [cpp] view plain copy print? void print(int a[], int n){ for(int j= 0; j<n; j++){ cout<<a[j] <<" "; } cout<<endl; } //将r[i…m]和r[m +1 …n]归并到辅助数组rf[i…n] void Merge(ElemType *r,ElemType *rf, int i, int m, int n) { int j,k; for(j=m+1,k=i; i<=m && j <=n ; ++k){ if(r[j] < r[i]) rf[k] = r[j++]; else rf[k] = r[i++]; } while(i <= m) rf[k++] = r[i++]; while(j <= n) rf[k++] = r[j++]; print(rf,n+1); } void MergeSort(ElemType *r, ElemType *rf, int lenght) { int len = 1; ElemType *q = r ; ElemType *tmp ; while(len < lenght) { int s = len; len = 2 * s ; int i = 0; while(i+ len <lenght){ Merge(q, rf, i, i+ s-1, i+ len-1 ); //对等长的两个子表合并 i = i+ len; } if(i + s < lenght){ Merge(q, rf, i, i+ s -1, lenght -1); //对不等长的两个子表合并 } tmp = q; q = rf; rf = tmp; //交换q,rf,以保证下一趟归并时,仍从q 归并到rf } } int main(){ int a[10] = {3,1,5,7,2,4,9,6,10,8}; int b[10]; MergeSort(a, b, 10); print(b,10); cout<<"结果:"; print(a,10); } 两路归并的递归算法 [cpp] view plain copy print? void MSort(ElemType *r, ElemType *rf,int s, int t) { ElemType *rf2; if(s==t) r[s] = rf[s]; else { int m=(s+t)/2; /*平分*p 表*/ MSort(r, rf2, s, m); /*递归地将p[s…m]归并为有序的p2[s…m]*/ MSort(r, rf2, m+1, t); /*递归地将p[m+1…t]归并为有序的p2[m+1…t]*/ Merge(rf2, rf, s, m+1,t); /*将p2[s…m]和p2[m+1…t]归并到p1[s…t]*/ } } void MergeSort_recursive(ElemType *r, ElemType *rf, int n) { /*对顺序表*p 作归并排序*/ MSort(r, rf,0, n-1); } 8. 桶排序/基数排序(Radix Sort) 说基数排序之前,我们先说桶排序: 基本思想:是 将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。 当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。 简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。 例如要对大小为[1..1000]范围内的n个整数A[1..n]排序 首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集 合B[2]存储 (10..20]的整数,……集合B[i]存储( (i-1)*10, i*10]的整数,i = 1,2,..100。总共有 100个桶。 然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。 最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。 假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果 对每个桶中的数字采用快速排序,那么整个算法的复杂度是 O(n + m * n/m*log(n/m)) = O(n + nlogn - nlogm) 从上式看出,当m接近n的时候,桶排序复杂度接近O(n) 当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的 ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。 前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是: 1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。 2)其次待排序的元素都要在一定的范围内等等。 桶式排序是一种分配排序。分配排序的特定是不需要进行关键码的比较,但前提是要知道待排序列的一些具体情况。 分配排序的基本思想:说白了就是进行多次的桶式排序。 基数排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序。它们的时间复杂度可达到线性阶:O(n)。 实例: 扑克牌中52 张牌,可按花色和面值分成两个字段,其大小关系为: 花色: 梅花< 方块< 红心< 黑心 面值: 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < J < Q < K < A 若对扑克牌按花色、面值进行升序排序,得到如下序列: 即两张牌,若花色不同,不论面值怎样,花色低的那张牌小于花色高的,只有在同花色情况下,大小关系才由面值的大小确定。这就是多关键码排序。 为得到排序结果,我们讨论两种排序方法。 方法1:先对花色排序,将其分为4 个组,即梅花组、方块组、红心组、黑心组。再对每个组分别按面值进行排序,最后,将4 个组连接起来即可。 方法2:先按13 个面值给出13 个编号组(2 号,3 号,...,A 号),将牌按面值依次放入对应的编号组,分成13 堆。再按花色给出4 个编号组(梅花、方块、红心、黑心),将2号组中牌取出分别放入对应花色组,再将3 号组中牌取出分别放入对应花色组,……,这样,4 个花色组中均按面值有序,然后,将4 个花色组依次连接起来即可。 设n 个元素的待排序列包含d 个关键码{k1,k2,…,kd},则称序列对关键码{k1,k2,…,kd}有序是指:对于序列中任两个记录r[i]和r[j](1≤i≤j≤n)都满足下列有序关系: 其中k1 称为最主位关键码,kd 称为最次位关键码 。 两种多关键码排序方法: 多关键码排序按照从最主位关键码到最次位关键码或从最次位到最主位关键码的顺序逐次排序,分两种方法: 最高位优先(Most Significant Digit first)法,简称MSD 法: 1)先按k1 排序分组,将序列分成若干子序列,同一组序列的记录中,关键码k1 相等。 2)再对各组按k2 排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd 对各子组排序后。 3)再将各组连接起来,便得到一个有序序列。扑克牌按花色、面值排序中介绍的方法一即是MSD 法。 最低位优先(Least Significant Digit first)法,简称LSD 法: 1) 先从kd 开始排序,再对kd-1进行排序,依次重复,直到按k1排序分组分成最小的子序列后。 2) 最后将各个子序列连接起来,便可得到一个有序的序列, 扑克牌按花色、面值排序中介绍的方法二即是LSD 法。 基于LSD方法的链式基数排序的基本思想 “多关键字排序”的思想实现“单关键字排序”。对数字型或字符型的单关键字,可以看作由多个数位或多个字符构成的多关键字,此时可以采用“分配 -收集”的方法进行排序,这一过程称作基数排序法,其中每个数字或字符可能的取值个数称为基数。比如,扑克牌的花色基数为4,面值基数为13。在整理扑克 牌时,既可以先按花色整理,也可以先按面值整理。按花色整理时,先按红、黑、方、花的顺序分成4摞(分配),再按此顺序再叠放在一起(收集),然后按面值 的顺序分成13摞(分配),再按此顺序叠放在一起(收集),如此进行二次分配和收集即可将扑克牌排列有序。 基数排序: 是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。 算法实现: [cpp] view plain copy print? Void RadixSort(Node L[],length,maxradix) { int m,n,k,lsp; k=1;m=1; int temp[10][length-1]; Empty(temp); //清空临时空间 while(k<maxradix) //遍历所有关键字 { for(int i=0;i<length;i++) //分配过程 { if(L[i]<m) Temp[0][n]=L[i]; else Lsp=(L[i]/m)%10; //确定关键字 Temp[lsp][n]=L[i]; n++; } CollectElement(L,Temp); //收集 n=0; m=m*10; k++; } } 总结 各种排序的稳定性,时间复杂度和空间复杂度总结: 我们比较时间复杂度函数的情况: 时间复杂度函数O(n)的增长情况 所以对n较大的排序记录。一般的选择都是时间复杂度为O(nlog2n)的排序方法。 时间复杂度来说: (1)平方阶(O(n2))排序 各类简单排序:直接插入、直接选择和冒泡排序; (2)线性对数阶(O(nlog2n))排序 快速排序、堆排序和归并排序; (3)O(n1+§))排序,§是介于0和1之间的常数。 希尔排序 (4)线性阶(O(n))排序 基数排序,此外还有桶、箱排序。 说明: 当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n); 而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2); 原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。 稳定性: 排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。 稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较; 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序 选择排序算法准则: 每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。 选择排序算法的依据 影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点: 1.待排序的记录数目n的大小; 2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小; 3.关键字的结构及其分布情况; 4.对排序稳定性的要求。 设待排序元素的个数为n. 1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。 快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短; 堆排序 : 如果内存空间允许且要求稳定性的, 归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。 2) 当n较大,内存空间允许,且要求稳定性 =》归并排序 3)当n较小,可采用直接插入或直接选择排序。 直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。 直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序 5)一般不使用或不直接使用传统的冒泡排序。 6)基数排序 它是一种稳定的排序算法,但有一定的局限性: 1、关键字可分解。 2、记录的关键字位数较少,如果密集更好
用户评论
这个“史上最全八大排序算法汇总”的游戏让我眼前一亮,通过轻松的方式深入学习了许多排序方法。
有7位网友表示赞同!
这款游戏真的太适合编程新手了!它能以寓教于乐的方式解释各种排序算法的概念。
有5位网友表示赞同!
"史上最全八大排序算法汇总"不仅包含了常见的算法如冒泡、快速排序,还介绍了更高效的斯特林和归并排序。
有12位网友表示赞同!
这款游戏的互动演示部分让我对复杂算法的理解有了直观的认识,不再是死记硬背的公式。
有13位网友表示赞同!
玩了这个”史上最全八大排序算法“游戏后,我感觉编程中解决大问题变得更具体例化了。
有18位网友表示赞同!
"史上最全"这几个字让人不禁期待,果然在游戏里包含了从简单到复杂的经典排序策略。
有15位网友表示赞同!
这个游戏完美地结合了理论和实践,在操作各种排序算法的环节让我收获满满。
有15位网友表示赞同!
我特别喜欢这个游戏里对每种算法背后的逻辑解释和比较,使我对排序技术有了全新的认识。
有17位网友表示赞同!
"史上最全八大"游戏不仅教学效果一流,还增加了不少挑战性模式,让学习过程充满乐趣。
有13位网友表示赞同!
通过”史上最全八大排序算法汇总“这款游戏,我重新发现了自己对数字处理的兴趣。
有12位网友表示赞同!
这个游戏不仅详细讲解了不同算法的步骤和优缺点,还有案例分析,非常实用。
有19位网友表示赞同!
"八"这个数是游戏设计中的亮点,从基础的插入排序到高阶的堆排序,每一种都深入浅出。
有19位网友表示赞同!
玩完”史上最全八大排序算法“后,感觉自己对编程逻辑的理解更加流畅自然了。
有8位网友表示赞同!
这款游戏把复杂的排序算法变得容易接受,特别是那些直观示例让我印象深刻。
有20位网友表示赞同!
"史上最全"让我对其抱有期望,这款游戏确实集合了所有的排序算法,从初学到进阶都有。
有18位网友表示赞同!
“史上最全八大”不仅展示算法,还教我们如何在代码中实现这些算法步骤。
有16位网友表示赞同!
我对游戏中的互动体验印象特别深刻,仿佛亲自操作每一种算法,让学习过程变成了一次奇妙旅行。
有19位网友表示赞同!
"这"是指游戏的制作,无论是清晰的界面还是精心设计的教程环节,都体现了制作者的专业。
有12位网友表示赞同!
通过这个”史上最全八大排序算法“游戏,我找到了自己解决问题的新工具包和角度。
有8位网友表示赞同!