传说时间复杂度为0(n)的排序

2024-03-18 04:18
文章标签 复杂度 时间 排序 传说

本文主要是介绍传说时间复杂度为0(n)的排序,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

线性时间的排序算法

   前面已经介绍了几种排序算法,像插入排序(直接插入排序,折半插入排序,希尔排序)、交换排序(冒泡排序,快速排序)、选择排序(简单选择排序,堆排序)、2-路归并排序(见我的另一篇文章:各种内部排序算法的实现)等,这些排序算法都有一个共同的特点,就是基于比较。本文将介绍三种非比较的排序算法:计数排序,基数排序,桶排序。它们将突破比较排序的Ω(nlgn)下界,以线性时间运行。

一、比较排序算法的时间下界

所谓的比较排序是指通过比较来决定元素间的相对次序。

“定理:对于含n个元素的一个输入序列,任何比较排序算法在最坏情况下,都需要做Ω(nlgn)次比较。

也就是说,比较排序算法的运行速度不会快于nlgn,这就是基于比较的排序算法的时间下界

通过决策树(Decision-Tree)可以证明这个定理,关于决策树的定义以及证明过程在这里就不赘述了。你可以自己去查找资料,推荐观看《MIT公开课:线性时间排序》。

根据上面的定理,我们知道任何比较排序算法的运行时间不会快于nlgn。那么我们是否可以突破这个限制呢?当然可以,接下来我们将介绍三种线性时间的排序算法,它们都不是通过比较来排序的,因此,下界Ω(nlgn)对它们不适用。

二、计数排序(Counting Sort)

计数排序的基本思想就是对每一个输入元素x,确定小于x的元素的个数,这样就可以把x直接放在它在最终输出数组的位置上,例如:

算法的步骤大致如下:

  • 找出待排序的数组中最大和最小的元素

  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项

  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)

  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

C++代码

  1. /************************************************************************* 
  2.     > File Name: CountingSort.cpp 
  3.     > Author: SongLee 
  4.     > E-mail: lisong.shine@qq.com 
  5.     > Created Time: 2014年06月11日 星期三 00时08分55秒 
  6.     > Personal Blog: http://songlee24.github.io 
  7.  ************************************************************************/  
  8. #include<iostream>  
  9. using namespace std;  
  10.   
  11. /* 
  12.  *计数排序:A和B为待排和目标数组,k为数组中最大值,len为数组长度 
  13.  */  
  14. void CountingSort(int A[], int B[], int k, int len)  
  15. {  
  16.     int C[k+1];  
  17.     for(int i=0; i<k+1; ++i)  
  18.         C[i] = 0;  
  19.     for(int i=0; i<len; ++i)  
  20.         C[A[i]] += 1;  
  21.     for(int i=1; i<k+1; ++i)  
  22.         C[i] = C[i] + C[i-1];  
  23.     for(int i=len-1; i>=0; --i)  
  24.     {  
  25.         B[C[A[i]]-1] = A[i];  
  26.         C[A[i]] -= 1;  
  27.     }  
  28. }  
  29.   
  30. /* 输出数组 */  
  31. void print(int arr[], int len)  
  32. {  
  33.     for(int i=0; i<len; ++i)  
  34.         cout << arr[i] << " ";  
  35.     cout << endl;  
  36. }  
  37.   
  38. /* 测试 */  
  39. int main()  
  40. {  
  41.     int origin[8] = {4,5,3,0,2,1,15,6};  
  42.     int result[8];  
  43.     print(origin, 8);  
  44.     CountingSort(origin, result, 15, 8);  
  45.     print(result, 8);  
  46.     return 0;  
  47. }  
当输入的元素是0到k之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k)。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。计数排序是一个稳定的排序算法。

可能你会发现,计数排序似乎饶了点弯子,比如当我们刚刚统计出C,C[i]可以表示A中值为i的元素的个数,此时我们直接顺序地扫描C,就可以求出排序后的结果。的确是这样,不过这种方法不再是计数排序,而是桶排序,确切地说,是桶排序的一种特殊情况。

三、桶排序(Bucket Sort)

桶排序(Bucket Sort)的思想是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法)。当要被排序的数组内的数值是均匀分配的时候,桶排序可以以线性时间运行。桶排序过程动画演示:Bucket Sort,桶排序原理图如下:

C++代码

  1. /************************************************************************* 
  2.     > File Name: BucketSort.cpp 
  3.     > Author: SongLee 
  4.     > E-mail: lisong.shine@qq.com 
  5.     > Created Time: 2014年06月11日 星期三 09时17分32秒 
  6.     > Personal Blog: http://songlee24.github.io 
  7.  ************************************************************************/  
  8. #include<iostream>  
  9. using namespace std;  
  10.   
  11. /* 节点 */  
  12. struct node  
  13. {  
  14.     int value;  
  15.     node* next;  
  16. };  
  17.   
  18. /* 桶排序 */  
  19. void BucketSort(int A[], int max, int len)  
  20. {  
  21.     node bucket[len];  
  22.     int count=0;  
  23.     for(int i=0; i<len; ++i)  
  24.     {  
  25.         bucket[i].value = 0;  
  26.         bucket[i].next = NULL;  
  27.     }  
  28.       
  29.     for(int i=0; i<len; ++i)  
  30.     {  
  31.         node *ist = new node();  
  32.         ist->value = A[i];  
  33.         ist->next = NULL;  
  34.         int idx = A[i]*len/(max+1); // 计算索引  
  35.         if(bucket[idx].next == NULL)  
  36.         {  
  37.             bucket[idx].next = ist;  
  38.         }  
  39.         else /* 按大小顺序插入链表相应位置 */  
  40.         {  
  41.             node *p = &bucket[idx];  
  42.             node *q = p->next;  
  43.             while(q!=NULL && q->value <= A[i])  
  44.             {  
  45.                 p = q;  
  46.                 q = p->next;  
  47.             }  
  48.             ist->next = q;  
  49.             p->next = ist;  
  50.         }  
  51.     }  
  52.   
  53.     for(int i=0; i<len; ++i)  
  54.     {  
  55.         node *p = bucket[i].next;  
  56.         if(p == NULL)  
  57.             continue;  
  58.         while(p!= NULL)  
  59.         {  
  60.             A[count++] = p->value;  
  61.             p = p->next;  
  62.         }  
  63.     }  
  64. }  
  65.   
  66. /* 输出数组 */  
  67. void print(int A[], int len)  
  68. {  
  69.     for(int i=0; i<len; ++i)  
  70.         cout << A[i] << " ";  
  71.     cout << endl;  
  72. }  
  73.   
  74. /* 测试 */  
  75. int main()  
  76. {  
  77.     int row[11] = {24,37,44,12,89,93,77,61,58,3,100};  
  78.     print(row, 11);  
  79.     BucketSort(row, 235, 11);  
  80.     print(row, 11);  
  81.     return 0;  
  82. }  

四、基数排序(Radix Sort)

基数排序(Radix Sort)是一种非比较型排序算法,它将整数按位数切割成不同的数字,然后按每个位分别进行排序。基数排序的方式可以采用MSD(Most significant digital)或LSD(Least significant digital),MSD是从最高有效位开始排序,而LSD是从最低有效位开始排序。

当然我们可以采用MSD方式排序,按最高有效位进行排序,将最高有效位相同的放到一堆,然后再按下一个有效位对每个堆中的数递归地排序,最后再将结果合并起来。但是,这样会产生很多中间堆。所以,通常基数排序采用的是LSD方式。

LSD基数排序实现的基本思路是将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。需要注意的是,对每一个数位进行排序的算法必须是稳定的,否则就会取消前一次排序的结果。通常我们使用计数排序或者桶排序作为基数排序的辅助算法。基数排序过程动画演示:Radix Sort

C++实现(使用计数排序)

  1. /************************************************************************* 
  2.     > File Name: RadixSort.cpp 
  3.     > Author: SongLee 
  4.     > E-mail: lisong.shine@qq.com 
  5.     > Created Time: 2014年06月22日 星期日 12时04分37秒 
  6.     > Personal Blog: http://songlee24.github.io 
  7.  ************************************************************************/  
  8. #include<iostream>  
  9. using namespace std;  
  10.   
  11. // 找出整数num第n位的数字  
  12. int findIt(int num, int n)  
  13. {  
  14.     int power = 1;  
  15.     for (int i = 0; i < n; i++)  
  16.     {  
  17.         power *= 10;  
  18.     }  
  19.     return (num % power) * 10 / power;  
  20. }  
  21.   
  22. // 基数排序(使用计数排序作为辅助)  
  23. void RadixSort(int A[], int len, int k)  
  24. {  
  25.     for(int i=1; i<=k; ++i)  
  26.     {  
  27.         int C[10] = {0};   // 计数数组  
  28.         int B[len];        // 结果数组  
  29.   
  30.         for(int j=0; j<len; ++j)  
  31.         {  
  32.             int d = findIt(A[j], i);  
  33.             C[d] += 1;  
  34.         }  
  35.   
  36.         for(int j=1; j<10; ++j)  
  37.             C[j] = C[j] + C[j-1];  
  38.   
  39.         for(int j=len-1; j>=0; --j)  
  40.         {  
  41.             int d = findIt(A[j], i);  
  42.             C[d] -= 1;  
  43.             B[C[d]] = A[j];  
  44.         }  
  45.           
  46.         // 将B中排好序的拷贝到A中  
  47.         for(int j=0; j<len; ++j)  
  48.             A[j] = B[j];  
  49.     }  
  50. }  
  51.   
  52. // 输出数组  
  53. void print(int A[], int len)  
  54. {  
  55.     for(int i=0; i<len; ++i)  
  56.         cout << A[i] << " ";  
  57.     cout << endl;  
  58. }  
  59.   
  60. // 测试  
  61. int main()  
  62. {  
  63.     int A[8] = {332, 653, 632, 5, 755, 433, 722, 48};  
  64.     print(A, 8);  
  65.     RadixSort(A, 8, 3);  
  66.     print(A, 8);  
  67.     return 0;  
  68. }  
基数排序的时间复杂度是 O(k·n),其中n是排序元素个数,k是数字位数。注意这不是说这个时间复杂度一定优于O(nlgn),因为n可能具有比较大的系数k。

另外,基数排序不仅可以对整数排序,也可以对有多个关键字域的记录进行排序。例如,根据三个关键字年、月、日来对日期进行排序。


这篇关于传说时间复杂度为0(n)的排序的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/821148

相关文章

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

usaco 1.3 Mixing Milk (结构体排序 qsort) and hdu 2020(sort)

到了这题学会了结构体排序 于是回去修改了 1.2 milking cows 的算法~ 结构体排序核心: 1.结构体定义 struct Milk{int price;int milks;}milk[5000]; 2.自定义的比较函数,若返回值为正,qsort 函数判定a>b ;为负,a<b;为0,a==b; int milkcmp(const void *va,c

hdu 1285(拓扑排序)

题意: 给各个队间的胜负关系,让排名次,名词相同按从小到大排。 解析: 拓扑排序是应用于有向无回路图(Direct Acyclic Graph,简称DAG)上的一种排序方式,对一个有向无回路图进行拓扑排序后,所有的顶点形成一个序列,对所有边(u,v),满足u 在v 的前面。该序列说明了顶点表示的事件或状态发生的整体顺序。比较经典的是在工程活动上,某些工程完成后,另一些工程才能继续,此时

《数据结构(C语言版)第二版》第八章-排序(8.3-交换排序、8.4-选择排序)

8.3 交换排序 8.3.1 冒泡排序 【算法特点】 (1) 稳定排序。 (2) 可用于链式存储结构。 (3) 移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时, 此算法不宜采用。 #include <stdio.h>#include <stdlib.h>#define MAXSIZE 26typedef int KeyType;typedef char In

MiniGPT-3D, 首个高效的3D点云大语言模型,仅需一张RTX3090显卡,训练一天时间,已开源

项目主页:https://tangyuan96.github.io/minigpt_3d_project_page/ 代码:https://github.com/TangYuan96/MiniGPT-3D 论文:https://arxiv.org/pdf/2405.01413 MiniGPT-3D在多个任务上取得了SoTA,被ACM MM2024接收,只拥有47.8M的可训练参数,在一张RTX

【软考】希尔排序算法分析

目录 1. c代码2. 运行截图3. 运行解析 1. c代码 #include <stdio.h>#include <stdlib.h> void shellSort(int data[], int n){// 划分的数组,例如8个数则为[4, 2, 1]int *delta;int k;// i控制delta的轮次int i;// 临时变量,换值int temp;in

批处理以当前时间为文件名创建文件

批处理以当前时间为文件名创建文件 批处理创建空文件 有时候,需要创建以当前时间命名的文件,手动输入当然可以,但是有更省心的方法吗? 假设我是 windows 操作系统,打开命令行。 输入以下命令试试: echo %date:~0,4%_%date:~5,2%_%date:~8,2%_%time:~0,2%_%time:~3,2%_%time:~6,2% 输出类似: 2019_06

学习记录:js算法(二十八):删除排序链表中的重复元素、删除排序链表中的重复元素II

文章目录 删除排序链表中的重复元素我的思路解法一:循环解法二:递归 网上思路 删除排序链表中的重复元素 II我的思路网上思路 总结 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 图一 图二 示例 1:(图一)输入:head = [1,1,2]输出:[1,2]示例 2:(图

【MRI基础】TR 和 TE 时间概念

重复时间 (TR) 磁共振成像 (MRI) 中的 TR(重复时间,repetition time)是施加于同一切片的连续脉冲序列之间的时间间隔。具体而言,TR 是施加一个 RF(射频)脉冲与施加下一个 RF 脉冲之间的持续时间。TR 以毫秒 (ms) 为单位,主要控制后续脉冲之前的纵向弛豫程度(T1 弛豫),使其成为显著影响 MRI 中的图像对比度和信号特性的重要参数。 回声时间 (TE)