vlambda博客
学习文章列表

【排序算法 一】选择排序

【练习】http://111.229.45.229/

P1000 冒泡排序


        排序问题是编程中最经典的也是最重要的问题,至今围绕该问题已产生数十种算法。于是今天再开一个大坑,面向纯新手,以排序为主题介绍各类排序的思想与应用,从冒泡与选择开始,主讲几大排序算法:主要包括冒泡排序与选择排序、归并排序、快速排序、插入排序与希尔排序、计数排序与索引、桶排序、堆排序、基数排序以及排序思想在各类算法中的应用(果然是挖坑一时爽...)。当然,在真正应用时我们只需一两种排序已经可以解决几乎所有的问题。对于排序算法的学习,更多的意义在于逻辑思维的强化与对算法理解的深入,还可以装B,毕竟排序用到的思维多与数据结构或者分治有关,几种思想相辅相成,还是那句话,算法学透了,也就没有那么多算法了。


【算法介绍】

        在操场上,教官让你们按照矮的在左、高的在右的顺序站成一排(动作慢的要罚站军姿),于是你们慌慌张张地看着左右两边的同学的身高(两边的同学也在打量着你),互相交换着位置,好在你们可以一瞬间判断出两个人的身高,所以很快就排好了顺序。

        在一旁打球的计算机老师(计算机老师居然会跑出来打球)看到这美妙的一幕,受到了启发,他想着,如果将这些人的身高数据放到电脑中,电脑会如何进行排序呢?

        回顾这一过程,关键信息在于“每个人能够主动地判断自己是否需要交换位置”,但是很遗憾,计算机虽然强大,但是其运行原理决定了计算机中的数据无法做到这种“多人同时进行的并行操作”,虽然很多人在玩游戏时可能会发现每个NPC(电脑角色)都会同时进行活动,但事实上那只是电脑以性能为代价模拟出来的(电脑的计算速度实在太快了,快到可以一个人做数几百个人的事情,而且还让人们看起来好像真的有数几百个人)。那么计算机中本质的操作是如何进行的呢?

        回到操场上,教官认为刚才的排队是同学们自己排出来的,这实在是太没有成就感了,他重新打乱了顺序,这回他要通过自己的命令让同学们排好队,他做着如下的步骤:

  • 他先扫了一眼队列,很快,他找到了最高的同学

  • 他命令那位同学和最后一个位置的同学交换

  • 接下来他忽略了最后一个位置,扫了一眼剩下队列中的人,又找到了最高的

  • 他命令那位同学和倒数第二位交换

  • 接下来他忽略最后两个位置,继续扫队列,在剩下的人中找到最高的

        ……

        ……

        似乎快轮到你了,但你已经神游到外太空去了,不过幸运的是,教官并没有理你而是直接跳过并命令下一位同学(为什么?)

        ……

        终于,队列有序了。

每次在未操作队列中选择出最高的,和末尾交换

        教官欣慰地看着自己排好的队列,他不知道自己无意识地在模拟计算机程序的运行,更不知道自己已经领悟了几乎是世界上第一个出现的排序算法——选择排序。


        选择排序是一种非常直观的排序算法,其最大的优点是简单易懂...

算法步骤:

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始(末尾)位置

再从剩余未排序元素中继续寻找最小(大)元素,然后放到未排序序列的末尾。

重复第二步,直到所有元素均排序完毕。

【代码实现】

        我们在程序中可以利用循环进行操作,找到最大(小)值。假设我们存在5位同学,其身高分别为165、175、180、170、179

        我们用列表存储:a = [ 165, 175, 180, 170, 179 ](注意,下标从0开始,随后一个元素的下标为n-1)

maxx = 0#maxx变量用于记录最大值所在的位置,许多程序中max与系统函数冲突,所以用maxxfor i in range(0, 5): if a[i] > a[maxx]:    maxx = i

一趟下来之后,我们便可以找到这个最大值所在的位置

在样例中显然是5

于是我们可以与末尾交换位置

tmp = a[maxx]a[maxx] = a[4]a[4] = tmp

这样一来,列表就变成了:a = [ 165, 175, 179, 170, 180 ]

接下来,就无需考虑最后一个位置,循环以上操作便可

可以注意到每次操作的队列在缩小,所以在排序时,操作队列的范围应该用一个循环变量来表示。

        以下是完整代码。

a = [ 165, 175, 170, 170, 179 ]n = 5for i in range(n-10-1): #外层的循环只需n-1遍,因为剩下的最后一个数据是不用排序的    maxx = 0 for j in range(0, i+1): if a[j] > a[maxx]:            maxx = j tmp = a[i] a[i] = a[maxx] a[maxx] = tmpprint(a)

        程序中,只需注意外侧循环的i变量表示当前操作队列的末尾,所以i需要每次减1,而内循环中循环的范围也需要调用i变量。

【效率分析】

        我们一般以循环次数来衡量程序运行的效率,那么n个数据的选择排序,其循环次数是多少呢?

        我们考虑第1次选择最大值需要n-1次的循环

        第2次需要n-2次

        一直到第n-1次(第n次没必要做了),只需一次比较便可

        总次数 = n-1+n-2+……1 = n(n-1)/2 = n^2-n/2

        不同的写法会出现一些小的偏差,不过不影响总体的效率。

        我们一般将最高阶项保留去除低阶项,得到n^2,于是用O(n^2)表示程序的运行效率,至于为什么?

       我们看到这张图,红色线条表示n^2的函数,当n较大时(比如10000),那么n/2这样的低阶项的影响是微乎其微的,因此,在衡量时间效率时,我们不是单纯地看某一个数据的效率,而是看程序随数据量增长后自身效率变化的趋势。

        总体看来,选择排序的运行效率是不高的,我们的家用计算机一般情况下运行速度在每秒1亿条语句上下,那么当n达到10000时,可能就需要1到10s的时间等待,而之后如果n再增大,可能就等不住了(甚至可能数年数十年都无法运行结束),这种时候就需要更为高效的算法解决。



【总结】

        选择排序的效率不高,少有使用,但是作为经典算法非常适合新手学习。

        关于选择排序有一种优化--双向选择排序。即每一次在找最大值的同时,也找到最小值,一次分别交换到头尾。

        不过优化效果不明显(可以推算出效率曲线仍旧是以n^2的趋势上升的),也少有使用,可以自行实现。

        文中出现的代码只作参考,事实上写法五花八门,比如可以从前往后走,也可以从后往前走等,但原理相同。