python之常见算法系列

算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。这里介绍几种常见的算法,后续跟进。

python之斐波那契数列算法

python之二分法排序附送第三种最快排序法

python之冒泡排序

排序的基本概念和分类

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。

排序的稳定性:
经过某种排序后,如果两个记录序号同等,且两者在原无序记录中的先后秩序依然保持不变,则称所使用的排序方法是稳定的,反之是不稳定的。

内排序和外排序
内排序:排序过程中,待排序的所有记录全部放在内存中
外排序:排序过程中,使用到了外部存储。
通常讨论的都是内排序。

影响内排序算法性能的三个因素

  • 时间复杂度:即时间性能,高效率的排序算法应该是具有尽可能少的关键字比较次数和记录的移动次数
  • 空间复杂度:主要是执行算法所需要的辅助空间,越少越好。
  • 算法复杂性。主要是指代码的复杂性。

根据排序过程中借助的主要操作,可把内排序分为:

  • 插入排序
  • 交换排序
  • 选择排序
  • 归并排序

按照算法复杂度可分为两类:

  • 简单算法:包括冒泡排序、简单选择排序和直接插入排序
  • 改进算法:包括希尔排序、堆排序、归并排序和快速排序

以下的七种排序算法只是所有排序算法中最经典的几种,不代表全部。

冒泡排序

介绍:

  冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排序)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

步骤:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

img

img

img

函数写法:

1
2
3
4
5
6
7
8
9
l = [6,3,1,9,2,5,8,7,4]
def sort_bubble(l):
for i in range(len(l)-1):
for j in range(i+1, len(l)):
if l[i]>l[j]:
l[i], l[j]=l[j],l[i]
return l

print(sort_bubble(l))

类写法:

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
class SortList:
def __init__(self, lis=None):
self.r = lis

def swap(self, i, j):
"""定义一个交换元素的方法,方便后面调用。"""
temp = self.r[i]
self.r[i] = self.r[j]
self.r[j] = temp

def bubble_sort_simple(self):
"""
最简单的交换排序,时间复杂度O(n^2)
"""
lis = self.r
length = len(self.r)
for i in range(length):
for j in range(i+1, length):
if lis[i] > lis[j]:
self.swap(i, j)

def bubble_sort(self):
"""
冒泡排序,时间复杂度O(n^2)
"""
lis = self.r
length = len(self.r)
for i in range(length):
j = length-2
while j >= i:
if lis[j] > lis[j+1]:
self.swap(j, j+1)
j -= 1

def bubble_sort_advance(self):
"""
冒泡排序改进算法,时间复杂度O(n^2)
设置flag,当一轮比较中未发生交换动作,则说明后面的元素其实已经有序排列了。
对于比较规整的元素集合,可提高一定的排序效率。
"""
lis = self.r
length = len(self.r)
flag = True
i = 0
while i < length and flag:
flag = False
j = length - 2
while j >= i:
if lis[j] > lis[j + 1]:
self.swap(j, j + 1)
flag = True
j -= 1
i += 1

def __str__(self):
ret = ""
for i in self.r:
ret += " %s" % i
return ret

if __name__ == '__main__':
sqlist = SortList([4,1,7,3,8,5,9,2,6])
# sqlist.bubble_sort_simple()
# sqlist.bubble_sort()
sqlist.bubble_sort_advance()
print(sqlist)

简单选择排序

简单选择排序(simple selection sort):时间复杂度O(n^2)
通过n-i次关键字之间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录进行交换。

通俗的说就是,对尚未完成排序的所有元素,从头到尾比一遍,记录下最小的那个元素的下标,也就是该元素的位置。再把该元素交换到当前遍历的最前面。其效率之处在于,每一轮中比较了很多次,但只交换一次。因此虽然它的时间复杂度也是O(n^2),但比冒泡算法还是要好一点。

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法(比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。(注:选自百度百科)

假如,有一个无须序列A={6,3,1,9,2,5,8,7,4},选择排序的过程应该如下:
第一趟:选择最小的元素,然后将其放置在数组的第一个位置A[0],将A[0]=6和A[2]=1进行交换,此时A={1,3,6,9,2,5,8,7,4};
第二趟:由于A[0]位置上已经是最小的元素了,所以这次从A[1]开始,在剩下的序列里再选择一个最小的元素将其与A[1]进行交换。即这趟选择过程找到了最小元素A[4]=2,然后与A[1]=3进行交换,此时A={1,2,6,9,3,5,8,7,4};
第三趟:由于A[0]、A[1]已经有序,所以在A[2]~A[8]里再选择一个最小元素与A[2]进行交换,然后将这个过程一直循环下去直到A里所有的元素都排好序为止。这就是选择排序的精髓。因此,我们很容易写出选择排序的核心代码部分,即选择的过程,就是不断的比较、交换的过程。整个选择的过程如下图所示:

img

img

函数写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
l = [6,3,1,9,2,5,8,7,4]
# 选择排序
def sort_quick(l):
# 获取每个元素
for i in range(len(l)-1):
min = i
# 依次拿每个元素与其后的每一个元素作比较,小则交换,当前一轮可以将其后最小的元素交换到当前位置
for j in range(i+1, len(l)):
if l[min]>l[j]:
min = j

if min !=i:
l[min], l[i] = l[i], l[min]
return l

print(sort_quick(l))

类写法:

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
class SQList:
def __init__(self, lis=None):
self.r = lis

def swap(self, i, j):
"""定义一个交换元素的方法,方便后面调用。"""
temp = self.r[i]
self.r[i] = self.r[j]
self.r[j] = temp

def select_sort(self):
"""
简单选择排序,时间复杂度O(n^2)
"""
lis = self.r
length = len(self.r)
for i in range(length):
minimum = i
for j in range(i+1, length):
if lis[minimum] > lis[j]:
minimum = j
if i != minimum:
self.swap(i, minimum)

def __str__(self):
ret = ""
for i in self.r:
ret += " %s" % i
return ret

if __name__ == '__main__':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0])
sqlist.select_sort()
print(sqlist)

直接插入排序

直接插入排序(Straight Insertion Sort):时间复杂度O(n^2)
基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。

有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法,插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。插入算法把要排序的数组分成两部分:第一部分包含了这个数组的所有元素,但将最后一个元素除外(让数组多一个空间才有插入的位置),而第二部分就只包含这一个元素(即待插入元素)。在第一部分排序完成后,再将这个最后元素插入到已排好序的第一部分中。  

img

img

函数写法:

1
2
3
4
5
6
7
8
9
10
11
12
l = [6,3,1,9,2,5,8,7,4]
def sort_insert(l:list[int]):
for i in range(1, len(l)):
j=i
# 当前索引对应的数字与前面的数字依次比较,如果较大继续向前比较,直到比较到第一位,如果较小直接停止,将当前索引对象的数字插到该位置,然后删除原来位置上的数字
while j>0 and l[j-1]>l[i]:
j-=1
l.insert(j, l[i])
l.pop(i+1)

return l
print('结果:',sort_insert(l))

类写法:

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
class SQList:
def __init__(self, lis=None):
self.r = lis

def insert_sort(self):
lis = self.r
length = len(self.r)
# 下标从1开始
for i in range(1, length):
if lis[i] < lis[i-1]:
temp = lis[i]
j = i-1
while lis[j] > temp and j >= 0:
lis[j+1] = lis[j]
j -= 1
lis[j+1] = temp

def __str__(self):
ret = ""
for i in self.r:
ret += " %s" % i
return ret

if __name__ == '__main__':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0])
sqlist.insert_sort()
print(sqlist)

该算法需要一个记录的辅助空间。最好情况下,当原始数据就是有序的时候,只需要一轮对比,不需要移动记录,此时时间复杂度为O(n)。然而,这基本是幻想。
image_1b2pgl1061h8s6mdg4416vm6f19.png-119.9kB

希尔排序

希尔排序(Shell Sort)是插入排序的改进版本,其核心思想是将原数据集合分割成若干个子序列,然后再对子序列分别进行直接插入排序,使子序列基本有序,最后再对全体记录进行一次直接插入排序。

这里最关键的是跳跃和分割的策略,也就是我们要怎么分割数据,间隔多大的问题。通常将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。下面的例子中通过:increment = int(increment/3)+1来确定“增量”的值。

希尔排序的时间复杂度为:O(n^(3/2))

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
class SQList:
def __init__(self, lis=None):
self.r = lis

def shell_sort(self):
"""希尔排序"""
lis = self.r
length = len(lis)
increment = len(lis)
while increment > 1:
increment = int(increment/3)+1
for i in range(increment+1, length):
if lis[i] < lis[i - increment]:
temp = lis[i]
j = i - increment
while j >= 0 and temp < lis[j]:
lis[j+increment] = lis[j]
j -= increment
lis[j+increment] = temp

def __str__(self):
ret = ""
for i in self.r:
ret += " %s" % i
return ret

if __name__ == '__main__':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0,123,22])
sqlist.shell_sort()
print(sqlist)

堆排序

堆是具有下列性质的完全二叉树:
每个分支节点的值都大于或等于其左右孩子的值,称为大顶堆;
每个分支节点的值都小于或等于其做右孩子的值,称为小顶堆;
因此,其根节点一定是所有节点中最大(最小)的值。
image_1b3bbf6pq1f847cq6uf70jpte13.png-142.4kB
如果按照层序遍历的方式(广度优先)给节点从1开始编号,则节点之间满足如下关系:
image_1b3bbe6ml1s5r6cq1dd41hctuaim.png-36.9kB

堆排序(Heap Sort)就是利用大顶堆或小顶堆的性质进行排序的方法。堆排序的总体时间复杂度为O(nlogn)。(下面采用大顶堆的方式)

其核心思想是:将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆的根节点。将它与堆数组的末尾元素交换,然后将剩余的n-1个序列重新构造成一个大顶堆。反复执行前面的操作,最后获得一个有序序列。

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
class SQList:
def __init__(self, lis=None):
self.r = lis

def swap(self, i, j):
"""定义一个交换元素的方法,方便后面调用。"""
temp = self.r[i]
self.r[i] = self.r[j]
self.r[j] = temp

def heap_sort(self):
length = len(self.r)
i = int(length/2)
# 将原始序列构造成一个大顶堆
# 遍历从中间开始,到0结束,其实这些是堆的分支节点。
while i >= 0:
self.heap_adjust(i, length-1)
i -= 1
# 逆序遍历整个序列,不断取出根节点的值,完成实际的排序。
j = length-1
while j > 0:
# 将当前根节点,也就是列表最开头,下标为0的值,交换到最后面j处
self.swap(0, j)
# 将发生变化的序列重新构造成大顶堆
self.heap_adjust(0, j-1)
j -= 1

def heap_adjust(self, s, m):
"""核心的大顶堆构造方法,维持序列的堆结构。"""
lis = self.r
temp = lis[s]
i = 2*s
while i <= m:
if i < m and lis[i] < lis[i+1]:
i += 1
if temp >= lis[i]:
break
lis[s] = lis[i]
s = i
i *= 2
lis[s] = temp

def __str__(self):
ret = ""
for i in self.r:
ret += " %s" % i
return ret

if __name__ == '__main__':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0, 123, 22])
sqlist.heap_sort()
print(sqlist)

堆排序的运行时间主要消耗在初始构建堆和重建堆的反复筛选上。
其初始构建堆时间复杂度为O(n)。
正式排序时,重建堆的时间复杂度为O(nlogn)。
所以堆排序的总体时间复杂度为O(nlogn)。

堆排序对原始记录的排序状态不敏感,因此它无论最好、最坏和平均时间复杂度都是O(nlogn)。在性能上要好于冒泡、简单选择和直接插入算法。

空间复杂度上,只需要一个用于交换的暂存单元。但是由于记录的比较和交换是跳跃式的,因此,堆排序也是一种不稳定的排序方法。

此外,由于初始构建堆的比较次数较多,堆排序不适合序列个数较少的排序工作。

归并排序

归并排序(Merging Sort):建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

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
class SQList:
def __init__(self, lis=None):
self.r = lis

def swap(self, i, j):
"""定义一个交换元素的方法,方便后面调用。"""
temp = self.r[i]
self.r[i] = self.r[j]
self.r[j] = temp

def merge_sort(self):
self.msort(self.r, self.r, 0, len(self.r)-1)

def msort(self, list_sr, list_tr, s, t):
temp = [None for i in range(0, len(list_sr))]
if s == t:
list_tr[s] = list_sr[s]
else:
m = int((s+t)/2)
self.msort(list_sr, temp, s, m)
self.msort(list_sr, temp, m+1, t)
self.merge(temp, list_tr, s, m, t)

def merge(self, list_sr, list_tr, i, m, n):
j = m+1
k = i
while i <= m and j <= n:
if list_sr[i] < list_sr[j]:
list_tr[k] = list_sr[i]
i += 1
else:
list_tr[k] = list_sr[j]
j += 1

k += 1
if i <= m:
for l in range(0, m-i+1):
list_tr[k+l] = list_sr[i+l]
if j <= n:
for l in range(0, n-j+1):
list_tr[k+l] = list_sr[j+l]

def __str__(self):
ret = ""
for i in self.r:
ret += " %s" % i
return ret

if __name__ == '__main__':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0, 12, 77, 34, 23])
sqlist.merge_sort()
print(sqlist)

另外一个版本:

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
69
70
def merge(lfrom, lto, low, mid, high):
"""
两段需要归并的序列从左往右遍历,逐一比较,小的就放到
lto里去,lfrom下标+1,lto下标+1,然后再取,再比,再放,
最后lfrom里的两段比完了,lto里留下的就是从小到大排好的一段。
:param lfrom: 原来的列表
:param lto: 缓存的列表
:param low: 左边一段的开头下标
:param mid: 左右两段的中间相隔的下标
:param high: 右边一段的最右下标
:return:
"""
i, j, k = low, mid, low
while i < mid and j < high:
if lfrom[i] <= lfrom[j]:
lto[k] = lfrom[i]
i += 1
else:
lto[k] = lfrom[j]
j += 1
k += 1
while i < mid:
lto[k] = lfrom[i]
i += 1
k += 1
while j < high:
lto[k] = lfrom[j]
j += 1
k += 1


def merge_pass(lfrom, lto, llen, slen):
"""
用来处理所有需要合并的段,这需要每段的长度,以及列表的总长。
最后的if语句处理表最后部分不规则的情况。
:param lfrom: 原来的列表
:param lto: 缓存的列表
:param llen: 列表总长
:param slen: 每段的长度
:return:
"""
i = 0
while i+2*slen < llen:
merge(lfrom, lto, i, i+slen, i+2*slen)
i += 2*slen
if i+slen < llen:
merge(lfrom, lto, i, i+slen, llen)
else:
for j in range(i, llen):
lto[j] = lfrom[j]


def merge_sort(lst):
"""
主函数。
先安排一个同样大小的列表,作为辅助空间。
然后在两个列表直接做往复的归并,每归并一次slen的长度增加一倍,
逐渐向llen靠拢,当slen==llen时说明归并结束了。
归并完成后最终结果可能恰好保存在templist里,因此代码里做两次归并,
保证最后的结果体现在原始的lst列表里。
:param lst: 要排序的原始列表
:return:
"""
slen, llen = 1, len(lst)
templist = [None]*llen
while slen < llen:
merge_pass(lst, templist, llen, slen)
slen *= 2
merge_pass(templist, lst, llen, slen)
slen *= 2
  • 归并排序对原始序列元素分布情况不敏感,其时间复杂度为O(nlogn)。
  • 归并排序在计算过程中需要使用一定的辅助空间,用于递归和存放结果,因此其空间复杂度为O(n+logn)。
  • 归并排序中不存在跳跃,只有两两比较,因此是一种稳定排序。

总之,归并排序是一种比较占用内存,但效率高,并且稳定的算法。

快速排序

快速排序(Quick Sort)由图灵奖获得者Tony Hoare发明,被列为20世纪十大算法之一。冒泡排序的升级版,交换排序的一种。快速排序的时间复杂度为O(nlog(n))。

快速排序算法的核心思想:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,然后分别对这两部分继续进行排序,以达到整个记录集合的排序目的。

快速排序(Quicksort)是对冒泡排序的一种改进。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性。

步骤:

  1. 从数列中挑出一个元素,称为 “基准”(pivot),
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

排序效果:

img

函数写法:

1
2
3
4
5
6
7
8
9
10
11
12
# 快速排序(以基准分成大小两组分别递归排序)
def quicksort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)
l = [6,3,1,9,2,5,8,7,4]
print('结果:',quicksort(l))

类写法:

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
class SQList:
def __init__(self, lis=None):
self.r = lis

def swap(self, i, j):
"""定义一个交换元素的方法,方便后面调用。"""
temp = self.r[i]
self.r[i] = self.r[j]
self.r[j] = temp

def quick_sort(self):
"""调用入口"""
self.qsort(0, len(self.r)-1)

def qsort(self, low, high):
"""递归调用"""
if low < high:
pivot = self.partition(low, high)
self.qsort(low, pivot-1)
self.qsort(pivot+1, high)

def partition(self, low, high):
"""
快速排序的核心代码。
其实就是将选取的pivot_key不断交换,将比它小的换到左边,将比它大的换到右边。
它自己也在交换中不断变换自己的位置,直到完成所有的交换为止。
但在函数调用的过程中,pivot_key的值始终不变。
:param low:左边界下标
:param high:右边界下标
:return:分完左右区后pivot_key所在位置的下标
"""
lis = self.r
pivot_key = lis[low]
while low < high:
while low < high and lis[high] >= pivot_key:
high -= 1
self.swap(low, high)
while low < high and lis[low] <= pivot_key:
low += 1
self.swap(low, high)
return low

def __str__(self):
ret = ""
for i in self.r:
ret += " %s" % i
return ret

if __name__ == '__main__':
sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0, 123, 22])
sqlist.quick_sort()
print(sqlist)

另外一个版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def quick_sort(nums):
# 封装一层的目的是方便用户调用
def qsort(lst, begin, end):
if begin >= end:
return
i = begin
key = lst[begin]
for j in range(begin+1, end+1):
if lst[j] < key:
i += 1
lst[i], lst[j] = lst[j], lst[i]
lst[begin], lst[i] = lst[i], lst[begin]
qsort(lst, begin, i-1)
qsort(lst,i+1,end)
qsort(nums, 0, len(nums)-1)
  • 快速排序的时间性能取决于递归的深度。
  • 当pivot_key恰好处于记录关键码的中间值时,大小两区的划分比较均衡,接近一个平衡二叉树,此时的时间复杂度为O(nlog(n))。
  • 当原记录集合是一个正序或逆序的情况下,分区的结果就是一棵斜树,其深度为n-1,每一次执行大小分区,都要使用n-i次比较,其最终时间复杂度为O(n^2)。
  • 在一般情况下,通过数学归纳法可证明,快速排序的时间复杂度为O(nlog(n))。
  • 但是由于关键字的比较和交换是跳跃式的,因此,快速排序是一种不稳定排序。
  • 同时由于采用的递归技术,该算法需要一定的辅助空间,其空间复杂度为O(logn)。

下面是一个实例测试数据:

测试数据量(个)1001000100001000001000000
冒泡0.0010.1111s--
反序冒泡0.0010.1414s--
快速排序0.0010.003~0.0040.040~0.0460.51~0.536.36~6.56s

从数据中可见:

  • 数据过万,冒泡算法基本不可用。测试时间忠实的反映了n平方的时间复杂度,数据扩大10倍,耗时增加100倍
  • 对于Python的列表,反序遍历比正序遍历还是要消耗一定的时间的
  • 快速排序在数据较大时,其威力显现,但不够稳定,总体还是维护了nlog(n)的复杂度。

基本的快速排序还有可以优化的地方:

1. 优化选取的pivot_key

前面我们每次选取pivot_key的都是子序列的第一个元素,也就是lis[low],这就比较看运气。运气好时,该值处于整个序列的靠近中间值,则构造的树比较平衡,运气比较差,处于最大或最小位置附近则构造的树接近斜树。
为了保证pivot_key选取的尽可能适中,采取选取序列左中右三个特殊位置的值中,处于中间值的那个数为pivot_key,通常会比直接用lis[low]要好一点。在代码中,在原来的pivot_key = lis[low]这一行前面增加下面的代码:

1
2
3
4
5
6
7
m = low + int((high-low)/2)
if lis[low] > lis[high]:
self.swap(low, high)
if lis[m] > lis[high]:
self.swap(high, m)
if lis[m] > lis[low]:
self.swap(m, low)

如果觉得这样还不够好,还可以将整个序列先划分为3部分,每一部分求出个pivot_key,再对3个pivot_key再做一次上面的比较得出最终的pivot_key。这时的pivot_key应该很大概率是一个比较靠谱的值。

2. 减少不必要的交换

原来的代码中pivot_key这个记录总是再不断的交换中,其实这是没必要的,完全可以将它暂存在某个临时变量中,如下所示:

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
def partition(self, low, high):

lis = self.r

m = low + int((high-low)/2)
if lis[low] > lis[high]:
self.swap(low, high)
if lis[m] > lis[high]:
self.swap(high, m)
if lis[m] > lis[low]:
self.swap(m, low)

pivot_key = lis[low]
# temp暂存pivot_key的值
temp = pivot_key
while low < high:
while low < high and lis[high] >= pivot_key:
high -= 1
# 直接替换,而不交换了
lis[low] = lis[high]
while low < high and lis[low] <= pivot_key:
low += 1
lis[high] = lis[low]
lis[low] = temp
return low

3. 优化小数组时的排序

快速排序算法的递归操作在进行大量数据排序时,其开销能被接受,速度较快。但进行小数组排序时则不如直接插入排序来得快,也就是杀鸡用牛刀,未必就比菜刀来得快。
因此,一种很朴素的做法就是根据数据的多少,做个使用哪种算法的选择而已,如下改写qsort方法:

1
2
3
4
5
6
7
8
9
10
11
12
def qsort(self, low, high):
"""根据序列长短,选择使用快速排序还是简单插入排序"""
# 7是一个经验值,可根据实际情况自行决定该数值。
MAX_LENGTH = 7
if high-low < MAX_LENGTH:
if low < high:
pivot = self.partition(low, high)
self.qsort(low, pivot - 1)
self.qsort(pivot + 1, high)
else:
# insert_sort方法是我们前面写过的简单插入排序算法
self.insert_sort()

4. 优化递归操作

可以采用尾递归的方式对整个算法的递归操作进行优化,改写qsort方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def qsort(self, low, high):
"""根据序列长短,选择使用快速排序还是简单插入排序"""
# 7是一个经验值,可根据实际情况自行决定该数值。
MAX_LENGTH = 7
if high-low < MAX_LENGTH:
# 改用while循环
while low < high:
pivot = self.partition(low, high)
self.qsort(low, pivot - 1)
# 采用了尾递归的方式
low = pivot + 1
else:
# insert_sort方法是我们前面写过的简单插入排序算法
self.insert_sort()

排序算法总结

排序算法的分类:
image_1b3cphm9hdq25sl2a1ccn1mn01t.png-373.9kB

没有十全十美的算法,有有点就会有缺点,即使是快速排序算法,也只是整体性能上的优越,也存在排序不稳定,需要大量辅助空间,不适于少量数据排序等缺点。

七种排序算法性能对比

image_1b3cpmfb7dnhciglnfirbi1s9.png-277.3kB

  • 如果待排序列基本有序,请直接使用简单的算法,不要使用复杂的改进算法。
  • 归并排序和快速排序虽然性能高,但是需要更多的辅助空间。其实就是用空间换时间。
  • 待排序列的元素个数越少,就越适合用简单的排序方法;元素个数越多就越适合用改进的排序算法。
  • 简单选择排序虽然在时间性能上不好,但它在空间利用上性能很高。特别适合,那些数据量不大,每条数据的信息量又比较多的一类元素的排序。