AI毛毛的blog

基本排序算法之冒泡排序的Python实现

最近因为业务需要,开始学习Python,作为C/C++程序员,一开始写Python总是忍不住在每行后面加个‘;’。当然除了语法,还是要把基本的数据结构和算法都简单的复习一遍的,下面开始从几类常见的排序开始实现,虽然python自带的sorted方法很好用,但是可以用自己的方法与它做一下比较。以下的代码均在Python 3.6环境下验证通过,排序对象为整形列表,按照由小到大的方式排序。

冒泡排序的原理非常简单,就是个双层循环的过程,列表元素像水里的一串气泡,第一趟冒泡从第一个气泡开始,与它相邻的气泡做比较,大的气泡交换到上面去,这个冒泡的过程就是内循环,需要n-1次。每经过一趟冒泡的过程,可以使一个最大的元素冒出来,需要排序的元素数目就会减1,如果列表里面有n个元素,则冒n-1次就可以完成全部过程,这就是外循环。

基于这种原理,用Python3的方法实现如下:

1
2
3
4
5
6
7
8
9
10
def bubble_sort1(lists):
"""冒泡排序"""
if list_check(lists) is False:
return lists

for i in range(len(lists) - 1):
for j in range(len(lists) - i - 1):
if lists[j] > lists[j + 1]:
lists[j], lists[j + 1] = lists[j + 1], lists[j]
return lists

可以看出这个二层循环的过程,在最坏的情况下,时间复杂度为O(n²),但是换一种思路,如果有一趟冒泡并没有发生交换,我们就可以确定此时列表已经是有序的了,不需要再去执行下一趟的冒泡过程。于是可以设置一个flag用来记录交换是否发生,则可以将算法优化如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def bubble_sort2(lists):
"""冒泡排序添加flag版"""

if list_check(lists) is False:
return lists

for i in range(len(lists) - 1):
flag = True
for j in range(len(lists) - i - 1):
if lists[j] > lists[j + 1]:
lists[j], lists[j + 1] = lists[j + 1], lists[j]
flag = False
if flag:
return lists
return lists

为了比较这两种算法,以及与Python自带的sorted方法的效率,下面用一个装饰器来计算函数执行时间。代码如下:

1
2
3
4
5
6
7
8
9
10
def time_usage(func):
"""时间计算函数"""
def wrapper(*args, **kwargs):
"""用装饰器和timeit提高计算精度"""
start_time = timeit.default_timer()
ret = func(*args, **kwargs)
use_time = timeit.default_timer() - start_time
print('{} elapsed time: {} seconds!'.format(func.__name__, use_time))
return ret
return wrapper

然后从0~20000之间取10000个随机整数组成列表进行排序并统计时间,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# -*- coding:utf-8 -*-
"""常见排序算法的Python实现"""
import timeit
import numpy.random as nprnd

RAND_LIST = nprnd.randint(20000, size=10000).tolist()

def list_check(lists):
"""list类型入参检查函数"""
if not isinstance(lists, list):
print('Only support list!')
return False

if not lists:
print('The list is empty!')
return False


def time_usage(func):
"""时间计算函数"""
def wrapper(*args, **kwargs):
"""用装饰器和timeit提高计算精度"""
start_time = timeit.default_timer()
ret = func(*args, **kwargs)
use_time = timeit.default_timer() - start_time
print('{} elapsed time: {} seconds!'.format(func.__name__, use_time))
return ret
return wrapper


@time_usage
def bubble_sort1(lists):
"""冒泡排序"""
if list_check(lists) is False:
return lists

for i in range(len(lists) - 1):
for j in range(len(lists) - i - 1):
if lists[j] > lists[j + 1]:
lists[j], lists[j + 1] = lists[j + 1], lists[j]
return lists


@time_usage
def bubble_sort2(lists):
"""冒泡排序添加flag版"""

if list_check(lists) is False:
return lists

for i in range(len(lists) - 1):
flag = True
for j in range(len(lists) - i - 1):
if lists[j] > lists[j + 1]:
lists[j], lists[j + 1] = lists[j + 1], lists[j]
flag = False
if flag:
return lists
return lists

if __name__ == '__main__':
time_usage(sorted)(RAND_LIST)

SORT_LIST = copy.deepcopy(RAND_LIST)
bubble_sort1(SORT_LIST)

SORT_LIST = copy.deepcopy(RAND_LIST)
bubble_sort2(SORT_LIST)

执行一下比较它们的效率:

bubble5.PNG

可以看出事与愿违,第二种反而比第一种慢了一点点,多次排序比较后依然如此,作为对比我又用C++去实现了一遍,结果是一样的,如下图所示,第二种比第一种慢一点点:

bubble4.PNG

其原因可能是因为flag只有在大部分数都排序好的情况下才用得上,对于这种大规模随机数,每次循环的过程中都要对flag的赋值与判断,占用的时间累积起来却是得不偿失。

但是不管是哪种冒泡排序,同内置的sorted函数的0.003s执行时间相比,效率仍然低了几个数量级。

冒泡排序的实现简单,适合教科书中详解循环的用法,但是它效率低下,在数据规模很小的场景下使用比较方便,对于大规模数据的处理,还是需要考虑使用别的排序方法。