Python算法之旅冒泡排序的故事

冒泡排序的故事

        年关将近,丐帮各分舵派代表前往总舵向帮主进贡。只见帮主门前立了一列梅花桩,各分舵代表立于桩上,等待帮主接见。(梅花桩上的数字代表梅花桩的编号,代表身上的数字代表其布袋数量,布袋数量越多辈分越高)

        此时各代表所在位置如图示:

        总管传话,请辈分最高的代表站到最前面(右边)来,其他人依次按辈分大小排队,排队期间禁止喧哗,并不得掉下梅花桩。

        看官您也许会说,辈分最高的代表站到最右边来,这还不简单?让4号桩的6袋弟子和5号桩的4袋弟子换个位置不就行了吗。你这是站在“上帝视角”观看,当然全局在胸,一眼就能看出代表们的辈分高低。可是这些代表们来自不同分舵,彼此并不熟悉,只有站在相邻梅花桩上的代表才能互相交流比较辈分大小,虽然他们都有武功,但为了不从梅花桩上掉下来,也只能和相邻的代表交换位置。

        现在队伍中谁的辈分最高大家并不清楚,需要扫描一遍整个队伍,才能找到他,并让他依次把位置换到最右边来。那么,扫描的顺序如何?是从左向右,还是从右向左呢?相信聪明的你心中已经有了答案。

        因为辈分最高的人最终要站在最右边的梅花桩上,所以我们从最左边的代表开始扫描。第一步是让0号梅花桩和1号梅花桩上的代表交流,由于5袋弟子的辈分更高,所以他和3袋弟子的交换位置;第二步是5袋与1袋弟子交换位置;第三步是5袋与2袋弟子交换位置,此时5袋弟子已经站在了3号梅花桩上;第四步,5袋与6袋弟子交流,发现他们无需交换位置,于是原地不动;第五步是6袋与4袋弟子交换位置。就这样,一趟扫描下来,我们总共比较了5次,交换了4次位置,使得辈分最高的6袋弟子站在了最右边的梅花桩上。此时各代表所在位置如图示:

        如果用python语言来描述这个过程,代码如下:

a = [5,3,1,2,4,6]

#各代表的初始位置

right = len(a) - 1 #最右边的梅花桩编号

for i in range(right):#从左向右扫描

    if a[i] > a[i+1]: #如果辈分高的代表在左边,则交换位置

        a[i],a[i+1] = a[i+1],a[i]

print(a) #输出此时各代表的位置

        现在6袋弟子已经站到正确的梅花桩上了,那剩下的5名代表又该如何操作才能依次排序呢?相信聪明的你心中已经有了答案。

        没错,就是按照同样的方法从左到右扫描一遍,把辈分最高的5袋弟子排到4号梅花桩上;然后依次把4,3,2袋弟子排好,最后1袋弟子就不用再扫描了,因为只剩下他一个人了,只能原地不动。

        因此,我们要对6个代表排序,就总共需要扫描5轮,而且每轮扫描的结果都是把当前辈分最高的弟子排到最右边。这种只比较和交换相邻元素的算法,就像水中气泡浮起,人称冒泡排序。如果用python语言来描述这个过程,代码如下:

a =[5,3,1,2,4,6] #各代表的初始位置

for right in range(len(a)-1,0,-1): #控制待排序队伍的右边界

    for iin range(right): #从左向右扫描

        if a[i] > a[i+1]: #如果辈分高的代表在左边,则交换位置

            a[i],a[i+1] = a[i+1],a[i]

    print(a) #输出此时各代表的位置

        程序输出如下:

        [3, 1, 2, 4, 5,6]

        [1, 2, 3, 4, 5,6]

        [1, 2, 3, 4, 5,6]

        [1, 2, 3, 4, 5,6]

        [1, 2, 3, 4, 5,6]

        仔细观察每轮扫描后的结果,发现实际上第2轮扫描后,整个序列就已经是有序的了,之所以还要进行后3轮扫描,完全是各代表“不知庐山真面目,只缘身在此山中”。我们注意到,如果排序未完成,则在扫描过程中一定会出现交换操作;反过来说,如果某轮扫描过程中未出现交换操作,则表明此时队伍已经是有序的了,可以提前跳出循环。

        为了知道某轮扫描中是否存在交换操作,我们可以让每位代表都准备一面旗子,若相邻的两个代表需要交换位置,则由辈分小的代表举旗示意。一轮扫描下来,我们观察一下是否有人举旗,如果有,说明排序仍未完成,需要继续扫描;否则表示排序结束。

        改进后的python代码如下:

a =[5,3,1,2,4,6] #各代表的初始位置

for right in range(len(a)-1,0,-1): #控制待排序队伍的右边界

    flag = False #每轮扫描开始前均无人举旗

    for i in range(right): #从左向右扫描

        if a[i] > a[i+1]: #如果辈分高的代表在左边,则交换位置,并举旗示意

            a[i],a[i+1] = a[i+1],a[i]

            flag = True

    if not flag: #若本轮扫描结束后无人举旗,则排序结束

        break

    print(a) #输出此时各代表的位置

        程序输出如下:

        [3, 1, 2, 4, 5,6]

        [1, 2, 3, 4, 5,6]

        通过举旗示意的方法,我们可以尽早发现排序是否完成,从而避免不必要的扫描,应该说算法有了很大的改进。那有没有更好的方法呢?答案是有。

        按照前面的算法,我们每轮扫描的结果都是把当前辈分最高的弟子排到最右边,然后让右边界左移1位,使得下一轮扫描时的范围缩小一格。每轮扫描过后,扫描范围只缩小了一格,效率是不是有点低?快速缩小扫描范围,这就是我们思考的方向。

        为了便于大家理解,我们先对各代表的初始位置做一些调整:

        我们注意到第一轮排序后,举旗的代表是2袋和1袋弟子。虽然我们第一轮排序的目标是确定6袋弟子的位置,但由于4,5,6袋弟子本身就处在正确的位置上,他们无需进行交换操作,最后一次交换事件发生在1号和2号梅花桩上,由3袋弟子和1袋弟子交换位置,而2号梅花桩右侧的代表都是原地不动的,相当于这一轮扫描不仅确定了6袋弟子的位置,实际上连3袋弟子及其前辈们的位置都一并确定了。那么下一轮扫描的时候,右边界就不是4号梅花桩,而是1号梅花桩了。因此我们只要记住本轮扫描中最后一个举旗代表的位置,就可以把它作为下一轮扫描的右边界,从而快速缩小扫描的范围。

        更进一步思考,前面的算法我们只缩小了右边界,左边界一直都是0,这显然是不够的。如果我们能够左右开弓,让左边界右移,这样扫描范围不就缩小得更快了吗?那如何改变左边界呢?答案就是从右向左扫描,把辈分最小的弟子转移到最左边。综合起来就是先向右扫描一轮,选出最大值,并确定下一轮扫描的右边界;然后向左扫描一轮,选出最小值,并确定新的左边界。这样左右交错冒泡,可以大大提高排序的效率。

        交错冒泡排序算法(又称鸡尾酒排序)python代码如下:

a = [3,2,1,4,5,6] #各代表的初始位置

left, right = 0, len(a)-1

while left < right:

    swap_pos = left #先假设最近一次发生交换操作的位置为left

    for i in range(left,right): #顺序扫描a[left,right]

        if a[i] > a[i+1]: #如果辈分高的代表在左边,则交换位置,并记录交换位置

            a[i], a[i+1] = a[i+1], a[i]

            swap_pos = i

    right = swap_pos #设置新的右边界为最近一次发生交换操作的位置

    for i in range(right,left,-1): #逆序扫描a[left,right]

        if a[i] < a[i-1]: #如果辈分低的代表在右边,则交换位置,并记录交换位置

            a[i], a[i-1] = a[i-1], a[i]

            swap_pos = i

    left = swap_pos #设置新的左边界为最近一次发生交换操作的位置

    print(a) #输出此时各代表的位置

        小结:冒泡排序算法的特点是只能比较和交换相邻的元素。

        基础冒泡排序算法有两重循环,外层循环负责控制待排序数组的右边界,每次向左移动一位,直到只剩下一个元素为止;内层循环负责从左向右扫描,依次比较相邻元素,若有需要则进行交换操作,每轮扫描下来可将最大元素移动到右边界位置。

        改进冒泡算法通过设置交换标记,可以知道数组是否已经完成排序,提前跳出外层循环,从而避免无效的扫描。

        交错冒泡排序算法通过快速调整左右边界,大幅度减少扫描的范围和次数,效率最高。先从左向右扫描,确保将最大值放到最右边,并记录最近一次发生交换操作的位置,作为下一轮扫描的右边界;再从右向左扫描,,确保将最小值放到最左边,并记录最近一次发生交换操作的位置,作为下一轮扫描的左边界。如此交错冒泡,直至左右边界重合,完成排序。

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

推荐阅读更多精彩内容

  • 总结一下常见的排序算法。 排序分内排序和外排序。内排序:指在排序期间数据对象全部存放在内存的排序。外排序:指在排序...
    jiangliang阅读 1,323评论 0 1
  • 简单来说,时间复杂度指的是语句执行次数,空间复杂度指的是算法所占的存储空间 时间复杂度计算时间复杂度的方法: 用常...
    Teci阅读 1,080评论 0 1
  • 7种常用的排序算法总结 2016.04.30PoetryAlgorithm 排序算法:一种能将一串数据依照特定的排...
    raining_804f阅读 783评论 0 0
  • 排序的基本概念 在计算机程序开发过程中,经常需要一组数据元素(或记录)按某个关键字进行排序,排序完成的序列可用于快...
    Jack921阅读 1,416评论 1 4
  • 今天我在仁寿花了1300元买了一个新手机。这是我用的最贵的一个手机。现在正在体验中。 我在手机里装上了微信,QQ,...
    俊妈李利阅读 172评论 0 1