算法+数据结构总结系列(二)
排序
排序: 将一组“无序”的记录序列调整为“有序”的记录序列。
列表排序:将无序列表变为有序列表。
输入:列表
输出:有序列表
两种基本方式:升序与降序
python内置排序函数:sort()
常见的排序算法:
排序 Low B 三人组: 冒泡排序;选择排序;插入排序
排序 Niu B 三人组: 快速排序;堆排序; 归并排序
其他排序:希尔排序; 计数排序; 基数排序
冒泡排序(Bubble Sort)
步骤:
- 列表每两个相邻的数,如果前面比后面大,则交换这两个数;
- 一趟排序完成后, 则无序区减少一个数,有序区增加一个数;
- 代码关键点:趟数、无序区范围。
栗子:
7, 5, 4, 6, 3, 8, 2, 9, 1
第一趟:
7比5大,交换7和5: 5, 7, 4, 6, 3, 8, 2, 9, 1;
7比4大,交换7和4: 5, 4, 7, 6, 3, 8, 2, 9, 1;
7比6大,交换7和6: 5, 4, 6, 7, 3, 8, 2, 9, 1;
7比3大,交换7和3: 5, 4, 6, 3, 7, 8, 2, 9, 1;
7比8小,不交换: 5, 4, 6, 3, 7, 8, 2, 9, 1;
8比2大,交换8和2: 5, 4, 6, 3, 7, 2, 8, 9, 1;
8比9小,不交换: 5, 4, 6, 3, 7, 2, 8, 9, 1;
9比1大,交换9和1: 5, 4, 6, 3, 7, 2, 8, 1, 9;
那么最大的数 9 “冒泡”到了最前面,这就完成了第一趟。9作为有序区的第一个数,其余继续组成无序区。
5, 4, 6, 3, 7, 2, 8, 1, 9;
在第二趟中,在无序区重复相同的操作。
第二趟:
5比4大,交换5和4: 4, 5, 6, 3, 7, 2, 8, 1, 9;
5比6小,不交换: 4, 5, 6, 3, 7, 2, 8, 1, 9;
6比3大,交换6和3: 4, 5, 3, 6, 7, 2, 8, 1, 9;
6比7小,不交换: 4, 5, 3, 6, 7, 2, 8, 1, 9;
7比2大,交换7和2: 4, 5, 3, 6, 2, 7, 8, 1, 9;
7比8大,不交换: 4, 5, 3, 6, 2, 7, 8, 1, 9;
8比1小,交换8和1: 4, 5, 3, 6, 2, 7, 1, 8, 9;
无序区最大的数8 “冒泡”到了最前面, 这就完成了第二趟。8,9 作为有序区的前两个数, 其余继续组成无序区。
4, 5, 3, 6, 2, 7, 1, 8, 9;
在接下来的几趟中, 在剩余无序区重复相同操作。
栗子的总结:
假设一无序列表长度为n,那么在冒泡排序中:
- 每一趟无序区长度减一,有序区长度加一,最后一趟无序区只有一个元素,无需进行,因此一共进行n-1趟。
- 在第i趟中,无序区元素指标从1到n-i+1遍历 (若是列表指标则为从0到n-i)。
- 在第i趟中, 比较的两个数元素指标j,j+1从1到n-i遍历 (若是列表指标则为从0到n-i-1)。
- 冒泡排序只需原地进行排序,无需开辟新内存。
def bubble_sort(li):
for i in range(len(li) - 1): #第i趟
for j in range(len(li) - i - 1):
if li[j] > li[j+1]:
li[j], li[j+1] = li[j+1], li[j]
时间复杂度:O(n2)
冒泡排序的改进:
原理:
在某一趟过程中,若没有发生交换操作,那么该列表已经排好,无需进行后续的趟数。
栗子:
对于列表[9,8,7,1,2,3,4,5,6](更极端的情况[1,2,3,4,5,6,7,8,9]),在第三趟后就已经不再发生交换,即第三趟后就已经排好, 第三趟后结束即可。
对于大规模已经是有序片段的列表来说,此改进还是很显著的。
策略:
在每一趟设置一个标记位,发生交换时标记位为True,否则为False,根据标记位来判断是否要进行后续的趟数。
def bubble_sort(li):
for i in range(len(li) - 1): #第i趟
exchange = False
for j in range(len(li) - i - 1):
if li[j] > li[j+1]:
li[j], li[j+1] = li[j+1], li[j]
exchange = True
if not exchange:
return
选择排序(Select Sort)
步骤:
- 遍历一遍列表,找到最小的元素;
- 再次遍历剩余列表,找到第二小的元素;
- 依次进行下去…
- 依次记录这些找到的元素,得到有序列表。
问题:对于第4步,如何记录这些元素?
回答:一种策略是每次将最小元素记录在新列表,并在旧列表中删除…
基于如上的策略,代码如下:
def simple_select_sort(li):
li_new = []
for i in range(len(li)):
min_val = min(li)
li_new.append(min_val)
li.remove(min_val)
return li_new
注意:上面代码有几个致命的缺点:
- 生成了两个列表,多占用了内存。对比冒泡排序,不是原地排序,内存多占用一倍。
- 代码中函数min(),remove()都不是O(1)的操作,都是O(n)的操作,从而整个代码块是O(n2)的时间复杂度。
问题:如何克服?
回答:记录最小元素指标,和无序区第一个元素交换,实现原地排序。这与冒泡排序有相似之处:在无序区每一趟找出最小元素的指标。
def select_sort(li):
for i in range(len(li) - 1):
min_index = i
for j in range(i + 1, len(li)):
if li[j] < li[min_index]:
min_index = j
li[i], li[min_index] = li[min_index], li[i]
时间复杂度:O(n2),但占用内存会比较少。
插入排序(Insert Sort)
步骤
- 初始时手里(有序区)只有一张牌;
- 每次(从无序区)摸一张牌,插入到手里已有牌的正确位置。
栗子:
5, 7, 4, 6, 3, 1, 2, 9, 8
有序区为5,摸出7,插入5右边:5, 7, 4, 6, 3, 1, 2, 9, 8;
有序区为5,7,摸出4,插入5左边:4, 5, 7, 6, 3, 1, 2, 9, 8;
有序区为4,5,7,摸出6,插入5右边:4, 5, 6, 7, 3, 1, 2, 9, 8;
有序区为4,5,6,7,摸出3,插入4左边:3,4, 5, 6 ,7, 1, 2, 9, 8;
有序区为3,4,5,6,7,摸出1,插入3左边:1,3,4, 5, 6 ,7, 2, 9, 8;
有序区为1,3,4,5,6,7,摸出2,插入1左边:1,2,3,4, 5, 6 ,7, 9, 8;
有序区为1,2,3,4,5,6,7,摸出9,插入7右边:1,2,3,4, 5, 6 ,7, 9, 8;
有序区为1,2,3,4,5,6,7,9,摸出8,插入7右边:1,2,3,4, 5, 6 ,7, 8,9;
栗子总结:
假设一无序列表长度为n,那么在插入排序中:
- 一共进行n-1次摸牌,从而有n-1趟;
- 在第i趟中,新摸出的牌仅在有序区进行比较插入,范围是元素指标从1到i(若为列表指标则从0到i-1)。
- 插入过程中涉及到有序区列表元素的移动。
def insert_sort(li):
for i in range(1, len(li)): #表示摸到的牌的下标
tag = li[i]
j = i-1 # 表示手里最后一张牌的下标
while j > -1 and li[j] > tag:
li[j+1] = li[j] #向前移动
j -= 1
li[j+1] = tag
时间复杂度:O(n2)
本次内容即为Low B三人组中的三种排序方法,其时间复杂度均为O(n2),相对来说都是比较慢的,下一篇中将带来较快的排序方法—— Niu B 三人组。