三种线性排序算法 计数排序、桶排序与基数排序

[非基于比较的排序]

在计算机科学中,排序是一门基础的算法技术,许多算法都要以此作为基础,不同的排序算法有着不同的时间开销和空间开销。排序算法有非常多种,如我们最常用的快速排序和堆排序等算法,这些算法需要对序列中的数据进行比较,因为被称为基于比较的排序

基于比较的排序算法是不能突破O(NlogN)的。简单证明如下:

N个数有N!个可能的排列情况,也就是说基于比较的排序算法的判定树有N!个叶子结点,比较次数至少为log(N!)=O(NlogN)(斯特林公式)。

非基于比较的排序,如计数排序,桶排序,和在此基础上的基数排序,则可以突破O(NlogN)时间下限。但要注意的是,非基于比较的排序算法的使用都是有条件限制的,例如元素的大小限制,相反,基于比较的排序则没有这种限制(在一定范围内)。但并非因为有条件限制就会使非基于比较的排序算法变得无用,对于特定场合有着特殊的性质数据,非基于比较的排序算法则能够非常巧妙地解决。

本文着重介绍三种线性的非基于比较的排序算法:计数排序、桶排序与基数排序。

[计数排序]

首先从计数排序(Counting Sort)开始介绍起,假设我们有一个待排序的整数序列A,其中元素的最小值不小于0,最大值不超过K。建立一个长度为K的线性表C,用来记录不大于每个值的元素的个数。

算法思路如下:

  1. 扫描序列A,以A中的每个元素的值为索引,把出现的个数填入C中。此时C[i]可以表示A中值为i的元素的个数。
  2. 对于C从头开始累加,使C[i]<-C[i]+C[i-1]。这样,C[i]就表示A中值不大于i的元素的个数
  3. 按照统计出的值,输出结果。

由线性表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之间的数,有如下桶排序的一般方法:

  1. 扫描序列A,根据每个元素的值所属的区间,放入指定的桶中(顺序放置)。
  2. 对每个桶中的元素进行排序,什么排序算法都可以,例如快速排序。
  3. 依次收集每个桶中的元素,顺序放置到输出序列中。

对该算法简单分析,如果数据是期望平均分布的,则每个桶中的元素平均个数为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函数的流程。

常见的排序算法比较及总结相关推荐

  1. access两字段同时升序排序_7 天时间,我整理并实现了这 9 种常见的排序算法

    排序算法 回顾 我们前面已经介绍了 3 种最常见的排序算法: java 实现冒泡排序讲解 QuickSort 快速排序到底快在哪里? SelectionSort 选择排序算法详解(java 实现) 然 ...

  2. PHP面试题:请写出常见的排序算法,并用PHP实现冒泡排序,将数组$a = array()按照从小到大的方式进行排序。

    常见的排序算法: 冒泡排序法.快速排序法.简单选择排序法.堆排序法.直接插入排序法.希尔排序法.合并排序法. 冒泡排序法的基本思想是:对待排序记录关键字从后往前(逆序)进行多遍扫描,当发现相邻两个关键 ...

  3. python常用算法有哪些_python常见的排序算法有哪些?

    大家都知道,关于python的算法有很多,其中最为复杂的就是python的排序算法,因为它并不是单一的,而是复杂的,关于排序算法就有好几种不同的方式,大家可以根据以下内容,结合自己的项目需求,选择一个 ...

  4. JS 常见的排序算法

    工作中算法不常用,但是排序经常用.因此在这里整理了几种JS中常见的排序算法. 冒泡排序 1.算法思想:判断两个相邻元素,大于则交换位置 2.算法步骤 从数组中第一个数开始,依次与下一个数比较并次交换比 ...

  5. 七种常见的排序算法总结

    目录 引言 1.什么是排序? 2.排序算法的目的是什么? 3.常见的排序算法有哪些? 一,插入排序 1.基本思想 2.代码实现 3.性能分析 4.测试 二,希尔排序(缩小增量排序) 1.基本思想 2. ...

  6. 基于比较的常见的排序算法

    目录 写在前面 排序 稳定性 排序的分类 常见的基于比较的排序 直接插入排序 代码 性能分析 总结 代码优化 折半插入 希尔排序 希尔排序 如何分组 代码 性能分析 选择排序 代码 性能分析 双向选择 ...

  7. 常见的排序算法与MSQL

    常见的排序算法 1.常见的排序算法 冒泡排序法.快速排序法.简单选择排序法.堆排序法.直接插入排序法.希尔排序法.合并排序法. (1)冒泡排序法:对待排序记录关键字从后往前(逆序)进行多遍扫描,当发现 ...

  8. 常见的排序算法的稳定性

    分析一下常见的排序算法的稳定性,每个都给出简单的理由. 冒泡排序 冒泡排序就是把小的元素往前调或者把大的元素往后调.比较是相邻的两个元素比较,交换也发生在这两个元素之间.所以,如果两个元素相等,我想你 ...

  9. 【数据结构---排序】庖丁解牛式剖析常见的排序算法

    排序算法 一.常见的排序算法 二.常见排序算法的实现 1. 直接插入排序 2. 希尔排序 3. 直接选择排序 4. 堆排序 5. 冒泡排序 6. 快速排序 6.1 递归实现快速排序 思路一.hoare ...

  10. java 排序_Java中常见的排序算法有哪些?---选择排序

    排序相关的的基本概念 排序: 将一组杂乱无章的数据按一定的规律顺次排列起来. 数据表( data list): 它是待排序数据对象的有限集合. 排序码(key):通常数据对象有多个属性域, 即多个数据 ...

最新文章

  1. TRY NOT TO SAY SO MUCH!
  2. Coursera自动驾驶课程第16讲:LIDAR Sensing
  3. 阿里云 Aliplayer高级功能介绍(四):直播时移
  4. ubuntu系统部署python3.6.4
  5. 【操作系统/OS笔记13】信号量、PV操作、管程、条件变量、生产者消费者问题
  6. Source Insight 中查看日文注释
  7. 用JavaScript做一个日历和用canvas做一个时钟
  8. 51单片机红外遥控小车
  9. CHIA币的本质认识
  10. mysql中的mysql数据库不见了
  11. UVA 12235 Help Bubu 状态压缩DP
  12. Python:实现pollard rho大数分解算法(附完整源码)
  13. 在KubeSphere中部署微服务(阡陌)+ DevOps
  14. cos(a+b)=cosa*cosb-sina*sinb的推导过程
  15. FTPClientUtil FTP客户端工具
  16. 微信公众号的用户运营?
  17. 计算机如何接两个屏幕,笔记本连接两个显示器的步骤_笔记本电脑怎么外接两个显示器做分屏-win7之家...
  18. 印光法师:《灵岩遗旨》壹、悲化有情
  19. 计算机网络英文简称名词解释
  20. ChibiOS系列:五、将STM32 USART与ChibiOS串行驱动程序配合使用

热门文章

  1. 视频教程-Web前端从初级到高手之路-其他
  2. 项目实训--物流管理系统之调度中心
  3. UEFI原理与编程实践--Device Path
  4. Java是一种什么语言
  5. 数字货币正确的交易理念——第九章:关于杜绝锁单操作问题
  6. 软件缺陷分析-软件测试之犯罪心理学
  7. 基于java+jsp+mysql的酒店预订系统
  8. 基于Android中socket流的实时监控及遥控系统
  9. android 注册广播代码(备用)
  10. 微博互粉php,PHP+redis实现微博的推模型案例分析