O(n^2)系列
一般人第一次接触到的排序算法应该都是 O(n^2) 等级的吧。常见的有 冒泡排序,选择排序,插入排序···
¶冒泡排序 Bubble sort
冒泡排序是利用两层循环 不断地将数据中比较大的数字交换到最上面来,看上去就像水中的气泡冒出来一样。
想法很简单,实现起来也是如此。循环中,看到循序不一样的就交换一次。
1 | def bubble_sort(data): |
是不是很直接,当然效率也是很感人··· 在我这个小奔腾上面进行, 进行10000个数据的排序就要10秒左右(9.5665s)。
在第13行上,range(i + 1, length)
循环从i + 1
开始而不是从0
或者i
开始,是为了提高效率,因为我们知道第一层循环是从第一个元素开始的,开头的i
个元素是有序的,就不需要进行大小比较了,而第range(x, y)
的循环范围是[x, y),第i
个不需要重复比较,因为就是它本身。可以看到Python本身神奇的 **tuple assignment **功能用在了交换数组数据上面,简单清晰。
总共需要进行近n^2,这么多次的排序操作,二次函数增长可是很快的。
我们可以在每次循环结束的时候添加一个打印操作,显示当前进行的状态,比如这是10个元素[3, 5, 4, 8, 2, 7, 6, 0, 9, 1]
的排序过程,每次交换输出一行:
1 | 调用函数: bubble_sort |
这是从大到小排序的,我们还可以添加一个reverse
参数来让函数返回一个从小到大排序的列表。
比如将循环体改成这样
1 | for i in range(length): |
巧妙地利用了if/else
来控制了判断情况。但是效率不佳,因为每次循环都需要进行if
的判断,消耗了时间。还不如在结束的时候return data[::-1] if reverse else data
,这样效率还来得高。
¶鸡尾酒排序 Cocktail shaker sort
这其实是一个冒泡排序的改进版本,通过从左到右再从右到左,
1 | def cocktail_shaker_sort(data): |
和上面相同的一组数据[3, 5, 4, 8, 2, 7, 6, 0, 9, 1]
,可以看到对列表的交换次数没有变化,但循环次数减少了一点,这是因为数据已经提前有序了,程序提前结束了。然而效率却更低了,10000个数据花了13s(12.8131s),
大的数字一次次地向左走,小的数字一次次地向右走…
1 | 调用函数: cocktail_shaker_sort |
可以改进一下 让它只是标记最大(最小)的数据的序号而不是直接进行交换 来提升效率。
¶选择排序 Selection sort
选择排序也是一种简单直接的排序方式,每一次循环从乱序部分中寻找出最大的那个数字,交换到有序部分的最后去,直到列表都有序。
1 | def selection_sort(data): |
在第13行使用了一个index
变量来记录当前的位置,用于在第19行与i
进行比较 避免多余的交换操作。
总共的运行次数和冒泡排序一样,n(n-1)/2次,但是由于交换次数少得多了,速度也就更快了。 10000个数据大概需要6秒(6.2190s) 很明显的提升。
1 | 调用函数: selection_sort |
当然对数组的交换次数小于等于数组个数减一。
¶插入排序 Insertion sort
插入排序也是一种很直接的排序方式,同时也是很低效的一种。
它的思想就是 将每一个无序部分的元素插入到有序部分中。在数组元素比较少的时候,用几行就能实现的这个算法看上去很简洁,但一旦数量上去了,每一轮插入需要比较的次数越来越多,拖慢了速度。
插入算法的实现有递归法和迭代法。然而由于递归的方式每一次插入都是一层递归,很快就会触及Python的最大递归层数限制,我们就使用迭代循环的方式。
1 | def insertion_sort(data): |
在第20行,利用Python的切片功能将元素插入。 不过list
自身有list.insert()
的方法,也可以实现,两种方法的效率有待对比。
1 | 调用函数: insertion_sort |
对于10000个元素,随手测试一下,插入排序的速度甚至比选择排序还要快一些(5.7829s)。
接下来我们可以给这几种常见的 O(n^2) 算法排个序了。 通过修改一下之前的count_time
我们能够生成一个统计表:
1 | def count_time(func): |
调用函数 | bubble_sort | cocktail_shaker_sort | selection_sort | insertion_sort |
---|---|---|---|---|
第1次 | 9.51917391 | 12.35336638 | 6.022212266 | 5.317802566 |
第2次 | 9.344330736 | 12.40192877 | 6.04458919 | 5.28499465 |
第3次 | 9.410286147 | 12.36908828 | 6.040824377 | 5.280491777 |
第4次 | 9.420827358 | 12.39769274 | 6.04954766 | 5.267566825 |
第5次 | 9.375585338 | 12.40759763 | 6.072944658 | 5.289787123 |
执行时间 | 47.07020349 | 61.9296738 | 30.23011815 | 26.44064294 |
平均时间 | 9.414040698 | 12.38593476 | 6.04602363 | 5.288128588 |
最好 | 9.344330736 | 12.35336638 | 6.022212266 | 5.267566825 |
最差 | 9.51917391 | 12.40759763 | 6.072944658 | 5.317802566 |
可以看出 插入 > 选择 > 冒泡 > 鸡尾酒
接下来就是强势的 O(nlog n) 组别了