Python算法之旅插入排序的故事

插入排序的故事

话说计算机世界有一个诚实国,那里的人们不但诚实,而且尊老,每次排队都让年纪大的人排前面。

有一次小胖到诚实国去旅游,肚子饿了想吃东西,发现一个烧饼店门前有人排着队,他就跟在队伍后面一起排队。没过多久,又来了一个人,站在小胖后面,并问他:“小伙子,你今年多大?”

“26,怎么啦?”

“26?那你得排在我后面,我今年38啦。”

“为什么?明明是我先来的,先来后到你不懂吗?”

“哈哈,先来后到?小伙子你是外地来旅游的吧,还不知道我们这的规矩。我们诚实国人不仅诚实,而且尊老,排队都让年纪大的人排前面。我比你大,所以要排在你前面。”

“原来是这样!对了,在我前面是一个小孩,那我也可以先插到他前面去咯?”

“是的,你先往前面插队,等你弄好了,我再来。”

“还有这种神操作!谢谢你提醒我啊!我要向前去了。”

此时小胖所站位置如图示:(地面下方的序号表示每个人所在的位置,用0-5表示;人体身上的数字表示其年龄。红脸的是小胖,他26岁,站在5号位置)

小胖拍了拍前面少年的肩膀,问他:“小朋友,今年几岁了?”

“叔叔,我今年12岁。”

“那请你让一让,叔叔我今年26了。”

“好的。”

于是二人换了位置。此时站在小胖前面的是一个瘦瘦高高的年轻人,戴着一副眼镜,小胖也吃不准他有多大年纪,就小心翼翼地问他:“兄弟我今年26,请问你多大了?”

“25”,瘦子一边回答一边默默地站到小胖后面去了。

此时站在小胖前面的是一个跟他长得差不多的胖墩,小胖热情地和他打招呼:“嗨,兄弟,哥哥我今年26,你呢?”

“26啊?那跟在我后面吧,哥哥我今年也正好26岁。”

此时小胖所站位置如图示:

如果我们用Python语言描述小胖插队的过程,应该是这样:

lst = [60,40,26,25,12,25]#小胖插队前的站队情况

age = 26 #小胖的年龄

pos = 5  #小胖的初始位置序号

while pos > 0: #不断询问前面的人,直到到达队伍前端

    if lst[pos-1] < age: #若前面的人比小胖小,则后退一位(相当于和小胖交换了位置)

        lst[pos] = lst[pos-1]

        pos -= 1 #继续向前询问下一个人

    else:  #否则说明小胖找到了正确的插队位置,退出循环

        break

lst[pos] = age #最终小胖站在正确的位置上

print(lst) #输出小胖插队后的站队情况

几个月后,小胖带着女朋友花花一起去诚实国旅游,来到同一家店排队买烧饼。正逢黄金周旅游旺季,排队的人可真多啊!花花看看前面长长的队伍,又看了看空空的矿泉水瓶,对小胖撒娇说:“亲爱的,伦家的水喝光了,口干舌燥,不想费口舌和前面的人比谁大谁小。反正你也知道我只比你大3岁,要不你帮我去问,找到正确的位置后,站在那人旁边,给我发个信号,然后我叫前面的人给我挪出位置,就可以直接走过去了。用Python语言表示就是这样。”

#将值为age的元素插入到非递增列表lst中,并保持列表的有序性

def insert(lst,age):

    pos = insert_pos(lst,0,len(lst)-1,age) #小胖帮花花查找插队的位置

    lst.append(age) #花花先站在队伍的后面,使得列表的长度加1

    for i in range(len(lst)-1,pos,-1):#人们逐个后移,为花花腾出插入位置

        lst[i] = lst[i-1]

    lst[pos] = age #最终花花站在正确的位置上

“知道了,不就是帮你找到正确的插入位置吗。没问题,看我的。”

小胖去了,他很卖力气,逐个询问排队人的年龄。半个小时过去了,小胖还在问,花花等得有些不耐烦了。又过了十几分钟,小胖终于在远处朝花花挥手了,花花知道他已经找到正确位置了。于是花花依次跟排队的人说:“不好意思,我男朋友帮我找到了正确的插入位置,是在你前面,麻烦你往后退一步,帮我腾个位置出来。”

大家都照着做了,没过多久花花就站在了自己的位置上,她对小胖表示感谢,但是又有些不满意地说:“你是怎么搞的,怎么花了这么长时间?”

小胖诉苦说:“花花你是不知道,排队的人实在太多了,而且来自世界各地,说什么话的人都有,我是使尽了浑身解数才找到这个正确位置的啊!”

“是吗?那你告诉我你是怎么做的?”

“还能怎么做?一个个问过来呗,我还是用Python语言写个函数给你看吧。”

#顺序查找age的插入位置,left和right是非递增列表lst的左右边界

def insert_pos(lst,left,right,age):

    pos = right+1 #花花的初始位置序号

    #不断询问前面人的年龄,直到到达队伍前端或遇到不小于花花的人

    while pos > left and lst[pos-1] < age:

        pos -= 1

    return pos #返回正确的插入位置

“原来你是用顺序查询的办法啊,怪不得这么慢。前面的队伍都已经是有序的了,难道你就不能动动脑子吗?”

“动动脑子?哦,我想起来了,前面队伍有序,我可以用折半查找法寻找插入位置,这样速度可以快上很多!”

“现在才想到,刚才干什么吃的去了!真是个猪脑袋!罚你用Python语言把折半查找插入位置的函数给我写出来。”

“夫人,遵命!”

#折半查找age的插入位置,left和right是非递增列表lst的左右边界

def binary_insert_pos(lst,left,right,age):

    while left <= right:

        mid = (left + right) // 2

        if age > lst[mid]:  #中间值小于age,则插入位置在左半边

            right = mid - 1

        else: #否则插入位置在右半边

            left = mid + 1

    return left #返回正确的插入位置

小胖从诚实国回来,刚好赶上公司给大家发奖金,大家在财会室门口挤作一团,领导看不下去了,让小胖帮忙组织大伙把队伍排好。小胖想起来自己在诚实国排队买烧饼的经历,就依葫芦画瓢,很快把队伍排好了。

领导看了非常满意,问小胖怎么做到的。

小胖很谦虚地回答:“其实也很简单,我这是借鉴了诚实国人排队的方法。先随便找一个人排在最前面,然后第二个人跟在他后面,如果第二个人比第一个人大,就插队到前面,否则不动。这样前两个人是有序的。接下来第三个人用同样的方法插队,找到正确的位置,这样前三个人是有序的。采用同样的方法,把后面的人一个个插入,直到队伍全部有序。”

“嗯嗯,不错,你用Python语言把程序写出来吧,我要把这个好办法推广到全公司。”

“领导,遵命!”

#直接插入排序算法

def insert_sort(lst):

    for i in range(1,len(lst)): #从第二个元素开始插入排序

        t = lst[i] 

        j = i 

        while j > 0 and lst[j-1] < t: #一边比较一边向后退腾出位置

            lst[j] = lst[j-1] 

            j -= 1 

        lst[j] = t

#折半查找插入排序算法

def binary_insert_sort(lst):

    for i in range(1,len(lst)): #从第二个元素开始插入排序

        t = lst[i]

        pos = binary_insert_pos(lst,0,i-1,lst[i]) #调用前面的折半查找插入位置函数

        for i in range(i,pos,-1): #元素后移,为t腾出插入位置

            lst[i] = lst[i-1]

        lst[pos] = t 

几十年过去了,此时的小胖已是排序界的高手,插入排序算法运用得炉火纯青。几十年的排序经验让他认识到插入排序在对“基本有序”的数据操作时,效率非常高,都快赶上线性排序的效率了(所谓“基本有序”是指待排序的数组逆序数比较少)。但进行插入操作时,每次只能将数据移动一位,难免出现大量重复移动。例如在诚实国排队的时候,小孩子就特别受累,每次后面来一个人,他们就要后退一步,所以有些小孩索性直接一次性多退几步,让后来的人直接站到他前面去。

小胖因此得到启发,如果将元素尽可能快的移动到它“该去”的地方,肯定会减少重复移动的次数,提高效率。小胖的想法是把全部元素分组排序,将所有距离为步长gap的元素放在同一个组中,通过“跳跃式移动”的方法,能让元素每次移动一大步,即步长gap>1,大大提高了移动的效率。一趟排序下来,虽然同组的元素没有挨在一起,各组元素相互隔开,但是由于每一组都已经各自排好序了,所以整个序列的“有序性”还是变好了。

我们来看一个例子:

图1是原始序列,序列长度len=8,我们先取步长gap=len/2=4,把序列分成4组(容易看出来,分组数量和步长相等)。地面下方的序号表示每个人所在的位置,用0-7表示;人体身上的数字表示其年龄,人体头部的颜色相同表示他们在同一组,此时分组情况为(0,4),(1,5),(2,6),(3,7)。

我们对同组的人进行简单插入排序,结果如图2所示。一趟排序下来,虽然同组的元素没有挨在一起,各组元素相互隔开,但是由于每一组都已经各自排好序了,所以整个序列看上去还是比以前要“有序”些,即逆序数要少一些。

接下来我们取更小的步长gap=2,把序列分成2组,此时分组情况为(0,2,4,6),(1,3,5,7)。各元素位置和分组情况如图3所示:

我们对同组的人进行简单插入排序,结果如图4所示。很明显,现在逆序数又少了很多,整个序列变得更加“有序”了。

同样的,我们继续减少步长,取gap=1,把序列分成1组,此时各元素位置和分组情况如图5所示:

现在步长gap=1,就是普通的插入排序。现在整个序列已经是“基本有序”了,直接插入排序也能高效完成。排序结果如图6所示:

通过上面的例子我们可以看到,将相隔一定步长的元素组成一个子序列,分别对他们进行插入排序,可以实现跳跃式的移动,使得排序的效率提高。经过一轮分组排序后,虽然整个序列还是无序的,但每个相互隔开的子序列已经变得有序,总的逆序数减少,整个序列变得更加“有序”了。每完成一轮分组排序后,我们就将步长减半,继续分组排序,直至步长step=1,就变成普通的插入排序了。由于此时整个序列已经“基本有序”了,直接插入排序也能高效完成。

这种另类的插入排序算法就是传说中的“希尔排序”。用Python语言实现代码如下:

#希尔排序

def shell_sort(lst):

    gap = len(lst) // 2

    while gap > 0:

        for i in range(gap,len(lst)): #将相隔为step的元素组成一个子序列,分别对各组进行插入排序

            t = lst[i]

            j = i

            while j >= gap and lst[j-gap] > t: #跳跃插入,跳跃距离为gap

                lst[j] = lst[j-gap]

                j -= gap

            lst[j] = t

        gap //= 2 #步长gap每次减半

希尔排序的关键在于不能将元素随便分组后各自排序,而是将相隔一定步长的元素组成一个子序列,实现跳跃式的移动,使得排序的效率提高。

步长的选择是希尔排序的重要部分。前面的算法中,我们让步长step每次减半,这是最简单的方法,但不是唯一的方法。事实上只要满足让步长逐渐减小,最终步长为1的规则,无论采用何种递减规律都可以。算法最开始以某个步长进行排序,然后会继续以更小的步长排序,最终算法以步长为1排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。

Donald Shell 最初建议选择步长step=n/2,然后让步长step每次减半,直到步长step=1。虽然这样取可以比O(n^2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。比如,如果一个数列以步长5进行了排序然后再以步长3进行排序,那么该数列不仅是以步长3有序,而且是以步长5有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。

已知的最好步长序列是由Sedgewick提出的 (1, 5, 19, 41, 109,...),该序列的项来自 9 * 4^i - 9 * 2^i + 1 和 4^i - 3 * 2^i + 1 这两个算式。这项研究也表明在希尔排序中比较是最主要的操作,而不是交换。用这种步长序列的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。

另一个在大数组中表现优异的步长序列是(斐波那契数列除去0和1将剩余的数以黄金分区比的两倍的幂进行运算得到的数列):(1, 9, 34,182, 836, 4025, 19001, 90358,428481, 2034035, 9651787, 45806244, 217378076,1031612713, …)。

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

推荐阅读更多精彩内容

  • 总结一下常见的排序算法。 排序分内排序和外排序。内排序:指在排序期间数据对象全部存放在内存的排序。外排序:指在排序...
    jiangliang阅读 1,313评论 0 1
  • Ba la la la ~ 读者朋友们,你们好啊,又到了冷锋时间,话不多说,发车! 1.冒泡排序(Bub...
    王饱饱阅读 1,781评论 0 7
  • 简单来说,时间复杂度指的是语句执行次数,空间复杂度指的是算法所占的存储空间 时间复杂度计算时间复杂度的方法: 用常...
    Teci阅读 1,073评论 0 1
  • 排序算法说明 (1)排序的定义:对一序列对象根据某个关键字进行排序; 输入:n个数:a1,a2,a3,…,an 输...
    code武阅读 645评论 0 0
  • 7种常用的排序算法总结 2016.04.30PoetryAlgorithm 排序算法:一种能将一串数据依照特定的排...
    raining_804f阅读 782评论 0 0