算法导论(五):线性时间排序

麻省理工学院公开课:算法导论。B站地址,网易公开课也有对应的资源。
https://www.bilibili.com/video/av1149902/?p=5
这节课的主要内容是用决策树模型证明比较排序算法的时间复杂度不会快于nlgn。以及另外两个非比较类型的排序算法及其分析:计数排序、基数排序。

前面讲过的算法有:
1、快速排序 Θ(nlgn) 或者 Θ(n2)
2、归并排序 Θ(nlgn)
3、插入排序 Θ(n2)
4、堆排序【这个课程上没有讲过,可能是在练习课上讲过】 Θ(nlgn)

提问:排序算法最快可以有多快?
回答:看情况,取决于你使用的计算模型里,哪些操作是被允许的。我们需要关心元素的排列顺序,以及我们可以如何对元素操作?

前面的算法最快是nlgn,如何能使它比nlgn更好呢?
这些排序都属于比较排序的算法模型,这是解决排序问题的一个模型。也就是只能使用比较(如大于、小于、等于、大于等于、小于等于)来决定元素的相对顺序。

事实上,比较排序的算法运行速度不会快于nlgn。下面用决策树模型来证明

决策树模型

决策树模型:这是算法中另一个规定你可以进行哪些操作的模型,比比较模型适用范围更广。

图中数字表示数组中元素的索引

上面是对数组[a1, a2, a3]进行排序的决策树模型。每一个节点表示一个比较操作,如果a1 < a2则往左,a1 > a2则往右继续。

决策树模型的定义如下:
有数组(a1, a2, ... , an),每一个非叶节点都会有一个下标i:j,i,j∈{1,2, ... , n},表示比较ai和aj的大小。
每个非叶节点下会分出2棵子树,左边的子树表示ai≤aj的情况下,算法的后续操作。右边的子树表示ai>aj的情况下,算法的后续操作。
每个叶节点表示一个排序结果,如[π(1), π(2), ... , π(n)]表示:aπ(1)≤aπ(2)≤...≤aπ(n)

决策树模型是算法的一种图形表示,比较排序的算法都可以用决策树模型表示,为什么不用决策树模型来表示排序算法呢?因为决策树模型根据输入n的数值而有不同的大小。


把比较排序的一个算法,转换成决策树形式的算法表达:
1、为每一个n值绘制一棵决策树
2、进行比较时,把树分成两个分支,左子树和右子树。
3、决策树就是把算法中这些比较的所有可能的结果分别列出来。即指出了所有可能的路线。

根据输入n的数值,画出来的决策树会非常庞大。
决策树中叶节点的个数至少是n的阶乘,即n!(列出了所有可能的排序结果),而决策树的高度为lg(n!)。根据斯特林公式,n!的近似值为(n/e)n,即高度为lg(n/e)n = nlg(n/e) = n(lgn-lge),其中lge是常数,所以决策树的高度为Ω(nlgn)。
即所有比较排序算法对应的决策树的深度至少是nlgn。

线性时间的非比较类型的排序算法

计数排序 counting sort

输入为:数组A[1, ... , n],其中Ai∈{1, 2, ..., k}
输出为:数组B[1, ... , n],即已排序的A
其中,需要辅助存储的数组C

这里的数组索引是从1到n,不是平时代码写的0到n-1

计数排序算法的伪代码如下

for i <- 1 to k
    do C[i] <- 0
for j <- 1 to n
    do C[A[j]] <- C[A[j]]+1
    // C[i] = |{key = i}|
for i <- 2 to k
    do C[i] <- C[i] + C[i-1]
    // C[i] = |{key <= i}|
for j <- n downto 1
    do B[C[A[j]]] <- A[j]
    C[A[j]] <- C[A[j]] - 1

偷偷放一下js版本

for(var i=1;i<=k;i++){
    C[i] = 0;
}
// 遍历A,得到数组C,即A中各元素的个数
for(var j=1;j<=n;j++){
    C[A[j]]++;
}
for(i=2;i<=k;i++){
    C[i] = C[i] + C[i-1];
}
// 分配
for(j=n;j>=1;j--){
    B[C[A[j]]] = A[j];
    C[A[j]] = C[A[j]] - 1;
}

看伪代码一共有4个for循环,举个例子来看下代码是如何运行的:
A=[4, 1, 3, 4, 3]

  1. 第一个for循环对数组C进行初始化,其中k=4(数组A中的最大值),得到C=[0, 0, 0, 0]
  2. 第二个for循环遍历A中的元素,对A中的元素进行计数,并把每个元素的数量按顺序存储在C中,得到C=[1, 0, 2, 2]
  3. 第三个for循环遍历C中的元素,进行累加。得到C=[1, 1, 3, 5]
  4. 第四个for循环对A中的元素进行分配,放到数组B中对应的位置,可得到B=[1, 3, 3, 4, 4]

运行时间分析
四个for循环的运行时间一次为k+n+k+n,即k+n。
如果k=O(n),那么运行时间为O(n),那这个算法是合适的。如果k=2n之类的值,那么运行时间就不太好了。。
而且这里需要保证数组中的元素均为正整数,范围够小。

基数排序算法

基数排序是用来在线性时间内处理大范围数据的。这个算法有一个非常重要的性质,即稳定性。算法的整体思想为:按位排序

这里一段小插曲,这个算法是一个非常老的算法,最早起源于1890年,是MIT的一位讲师发明的。这位讲师因此赚了很多钱【笑

下面用一个例子来说明一下基数排序。
有下列数字[329, 457, 657, 839, 436, 720, 355],进行排序:

329      720      720      329
457      355      329      355
657      436      436      436
839      457      839      457
436      657      355      657
720      329      457      720
355      839      657      839

排序过程:

  1. 第一列数字为数组内元素的初始排序,即[329, 457, 657, 839, 436, 720, 355]
  2. 针对这些元素的最后一位进行排序,得到第二列数字[720, 355, 436, 457, 657, 329, 839]
  3. 针对倒数第二位元素进行排序,得到第三列数字[720, 329, 436, 839, 355, 457, 657]
  4. 针对第一位元素进行排序,得到最后一列数字[329, 355, 436, 457, 657, 720,
    839],即最终的结果。

在每一轮排序中,相同大小的元素保持原有顺序不变。

分析
1、 如果每一轮排序都用计数排序,其时间复杂度为O(k+n)。
2、 用二进制来表示数组中的元素。一共有n个整数,每一个整数均为b比特的长度,那么数组中元素的取值范围为(0, 2b-1)。
一共需要进行b轮排序,每一轮排序花费的时间为n,那么总时间为b·n。
假如b很小,为lgn,那么数字的取值范围为(0, n-1)。前面的计数排序已经可以做到在线性时间内对(0, n-1)范围内的数进行排序,这里却需要nlgn的时间,显然不太好。。。
可以把连续的比特看作是一位数来进行比较,看看用计数排序可以一次处理的最多比特为多少。


算法:把每个整数拆分成b/r位数字,每个数字都是r比特的长度。也就是将数组中的元素用2r进制来表示。
这里一共需要进行b/r轮运算,其中每一轮比较中,最大的数字为2r,即每位数字的取值范围为(0, 2r),也就是k=2r,所以每一轮使用计数排序需要(2r+n)的时间。
所以算法的总时长为(b/r)·(2r+n),即O((b/r)·(2r+n))。

这里要选择合适的r,使运行时间最小。
(b/r)·(2r+n),可以拆分为 (b/r)·n + (b/r)·2r 两项
对于(b/r)·n,r越大,其值越小。
对于(b/r)·2r,r越小,其值越小。

当n≥2r,r = lgn
如果r=lgn,总时长为(b/r)·(2r+n) = (b/lgn)·(n+n),即为O(bn/(lgn))。
待排序数列是某个范围内的整数,即(0, 2b-1),b是数的比特位数,对应于数的取值范围。
将取值范围写成(0, nd-1),d = b/r = b/lgn,d是一个常数。
对于n∈{0, ..., nd-1},总时长为O(dn)

计数排序 vs 基数排序

计数排序可以用线性时间处理0到某个常数乘以d范围内的数。
基数排序可以在线性时间内处理0到n的某个常数次方范围内的数。

假设处理32比特的数,即取值范围为(0, 232),使用基数排序分四轮进行计算,每轮处理8比特的数,需要28=256的存储空间进行计数排序,时间复杂度为4*(8+n),时间复杂度不高。但在实际情况中,计数排序因为需要比较多的存储空间,所以并没有理想中那么快。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 226,780评论 6 526
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 97,642评论 3 411
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 174,427评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,220评论 1 306
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,026评论 6 405
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,544评论 1 319
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,643评论 3 433
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,796评论 0 283
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,300评论 1 329
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,281评论 3 352
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,426评论 1 364
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,988评论 5 354
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,684评论 3 342
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,090评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,311评论 1 280
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,002评论 3 385
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,409评论 2 370

推荐阅读更多精彩内容

  • -分析基于比较的排序能够达到的最快效率-介绍几种非比较的线性时间排序算法 排序最快能够达到多快的速度?取决于你使用...
    Alex90阅读 781评论 0 1
  • 一. 写在前面 要学习算法,“排序”是一个回避不了的重要话题,在分析完并查集算法和常用数据结构之后,今天我们终于可...
    Leesper阅读 2,540评论 0 40
  • 1 序 2016年6月25日夜,帝都,天下着大雨,拖着行李箱和同学在校门口照了最后一张合照,搬离寝室打车去了提前租...
    RichardJieChen阅读 5,113评论 0 12
  • 总结一下常见的排序算法。 排序分内排序和外排序。内排序:指在排序期间数据对象全部存放在内存的排序。外排序:指在排序...
    jiangliang阅读 1,357评论 0 1
  • 李茂书法
    洗墨人阅读 172评论 0 0