排序
1.冒泡排序
(1)冒泡排序(Bubble Sort)
两两比较,交换位置,像水泡一样向上冒,也叫交换排序。(升序和降序)
(2)具体排序方法
升序:使用索引两两比较,若前者(索引0的值)比后者(索引1的值)大交换位置,反之则不交换,将大值放在有序区(右侧),每一轮无序区进行交换比较,有序区不参与比较,直到无序区没有相邻的元素需要交换。降序相反
(3)实现
# 简单冒泡的实现
lst = [
[1, 9 ,3, 2, 6, 8, 7, 4, 5],
[1, 2, 3, 4, 5, 6, 7, 8, 9]
]
nums = lst[0]
length = len(nums)
count_swap = 0
count = 0
for i in range(length):
for j in range(length - i - 1):
count += 1
if nums[j] > nums[j + 1]:
tmp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = tmp
count_swap += 1
print(nums, count_swap, count)
#结果:
[1, 2, 3, 4, 5, 6, 7, 8, 9] 15 36
# 优化
nums = lst[0]
length = len(nums)
count_swap = 0
count = 0
flag = False
for i in range(length):
for j in range(length - i - 1):
count += 1
if nums[j] > nums[j + 1]:
tmp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = tmp
flag = True
count_swap += 1
if not flag:
break
print(nums, count_swap, count)
#结果:
[1, 2, 3, 4, 5, 6, 7, 8, 9] 15 36
# 上面的有问题,flag没有重置过,每一趟都需要重置
nums = lst[0]
length = len(nums)
count_swap = 0
count = 0
for i in range(length):
flag = False # flag开关表示没有交换过 进行下一趟比较
for j in range(length - i - 1): # range(9) [0, 7]
count += 1
if nums[j] > nums[j + 1]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = True
count_swap += 1
# 每一趟看有没有重置过,如果没有交换说明已经排好序了,结束
if not flag:
break
print(nums, count_swap, count)
#结果:
[1, 2, 3, 4, 5, 6, 7, 8, 9] 15 30
(4)总结
- 冒泡法需要数据一轮一轮的比较,可以设定一个标记判断是否有数据交换发生,如果没有发生交换,可以结束排序,因为此时已经排序完毕了,如果发生了交换,那么就需要继续下一轮排序了
- 最差的排序情况是,初始顺序与目标顺序完全相反,遍历次数1,…n-1之和n(n-1)/2
- 最好的排序情况是,初始顺序与目标顺序完全相同,遍历次数n-1
- 时间复杂度为O(n**2)
2.选择排序
(1)选择排序(Selection sort)
每一次选择一个极值放在有序区,从无序区在找极值放在有序区,直到选完为止。
(2) 工作原理
首先在未排序序列中找到最小(大)元素,存放到排序序列的有序区(起始位置(升序或者降序)),然后,再从无序区(剩余未排序元素)中继续寻找最小(大)元素,然后放到有序区(已排序序列)的末尾。以此类推,直到所有元素均排序完毕;可以左、右同时选,左边选最大值(最小值),右边选最小值(最大值)
(3)实现
nums = [1, 9, 8, 5, 6, 7, 4, 3, 2]
length = len(nums)
print(length, ' | ', nums)
count_iter = 0
count_swap = 0
for i in range(length - 1):
maxindex = i
for j in range(i+1, length):
count_iter += 1
if nums[maxindex] < nums[j]:
maxindex = j
if i != maxindex:
nums[i], nums[maxindex] = nums[maxindex], nums[i]
count_swap += 1
print(nums)
print(count_iter, count_swap)
#结果:
9 | [1, 9, 8, 5, 6, 7, 4, 3, 2]
[9, 8, 7, 6, 5, 4, 3, 2, 1]
36 7
#想一下:如果最小值所在的位置刚好是最大值要交换的位置时,如果直接交换最大值,那么最小值的索引就会变为原最大值的索引。针对这种情况在交换最大值时判断一下最大值要交换的位置是否等于最小值所在的位置,如果是,那么需要在最大值修改完毕后,将最小值的索引进行重置(重置为最大值所在的索引)
#思考点1:既然我们一趟可以确定两个元素(1个最大值,1个最小值),那我们还需要length-1次循环吗? 如果元素为7个,只需要判断3次即可,如果元素是8个,只需要判断4次即可,根据以上条件得知,只需要循环length//2次就可以完成排序了。
#思考点2:在一次循环的结果中最大值或最小值和要排的位置的值是相同时,就不需要进行修改了。
#思考点3:如果在一次循环中最大值索引和最小值索引对应的元素相同,比如[4,2,1,1,1,1,3] 这种情况下,[1,1,1,1] 就不需要排序了。
#思考点4:若[1,1,1,1,1,2],索引为0的1和2进行交换,1--->2的位置,这时已经排好序了,索引为-1的1和索引为-2的1没必要交换
# 优化
nums = [1, 9, 8, 5, 6, 7, 4, 3, 2]
nums = [1,1,1,1,1,2]
length = len(nums)
print(length, ' | ', nums)
count_iter = 0
count_swap = 0
# 1 9 2
for i in range(length // 2): # length - 1
maxindex = i # 假设
minindex = -i - 1 #-1 - i#length - 1 - i
for j in range(i + 1, length - i):
count_iter += 1
if nums[maxindex] < nums[j]:
maxindex = j
if nums[minindex] > nums[-j - 1]:
minindex = -j - 1 #j
#print("~~~~~", maxindex, minindex)
if nums[maxindex] == nums[minindex]: # 1112 中间相等不需要交换
break
if i != maxindex: # 5 最大值和索引一样,不需要交换,位置直接固定
nums[i], nums[maxindex] = nums[maxindex], nums[i]
count_swap += 1 # 交换过,i对应数应该是最大数
if i - length == minindex: # 最小值对应的索引等于未交换最大值对应的索引时,最小值对应的索引指向最大值对应的索引 #1 9 2
minindex = maxindex - length # 最大值交换时影响最小值
if minindex != -i - 1 and nums[minindex] != nums[-i - 1]: # 负索引和值相等 #and minindex != length - 1 - i:
nums[-i - 1], nums[minindex] = nums[minindex], nums[-i - 1]
count_swap += 1
print(nums)
print(count_iter, count_swap)
#结果:
6 | [1, 1, 1, 1, 1, 2]
~~~~~ 5 -2
~~~~~ 1 -2
[2, 1, 1, 1, 1, 1]
8 1
(4)总结
- 选择排序需要数据一轮轮比较,在每一轮中找极值
- 没有办法知道当前是否达到排序要求,但是可以知道极值是否在目标索引的位置上
- 遍历次数1,…n-1之和n(n-1)/2
- 时间复杂度为O(n**2)
- 减少了交换次数,提高了效率,性能略好于冒泡法
3.插入排序
(1)直接插入排序(Direct insertion sort)
先两个数比较,若前者(索引0的值)比后者(索引1的值),后者插入到前者的位置,后面的数依次与前面的两个数比较,插入合适位置。
(2) 工作原理
- 在未排序序列中,构建一个子排序序列,直至全部数据排列完成。
- 将待排序的数,插入到已经排序的序列中合适的位置。
- 增加一个哨兵(中间变量),放入待比较的值,让它和后面已经排好序的序列比较,找到合适的插入点。
(3)实现
nums = [9, 7, 8, 5, 6]
nums = [0] + nums
count_iter = 0
count_swap = 0
length = len(nums)
for i in range(2, length):
# 假定第一个在有序区,把第二个放在哨兵位
nums[0] = nums[i]
j = i - 1
count_iter += 1
while nums[j] > nums[0]:
# 右移(覆盖)
nums[j + 1] = nums[j]
# 插入
nums[j] = nums[0] # 必须先移动在插入,每比一次就要插入一次,效率很低,可以一次性比完在插入
# 可以写成如下
# nums[j + 1], nums[j] = nums[j], nums[0]
j -= 1
count_swap += 1
print(nums[1:])
print(count_iter, count_swap)
# 优化
nums = [1, 9, 8, 5, 6]
# 哨兵位,放待比较数字
nums = [0] + nums
count_iter = 0
count_swap = 0
length = len(nums)
for i in range(2, length): # 从2开始
nums[0] = nums[i] # 放置哨兵
j = i - 1
count_iter += 1
if nums[j] > nums[0]:
while nums[j] > nums[0]: # 大数右移,找到插入位置
nums[j + 1] = nums[j] # 依次右移
j -= 1
count_swap += 1
nums[j + 1] = nums[0] # 将哨兵插入,注意插入在右侧要+1
print(nums[1:])
print(count_iter, count_swap)
#因为在列表头部添加了一个元素用于记录待交换元素,所以应该从索引为2的元素,开始,拿来和已经排序好的序列进行比较(认为6已经在排序空间了)
#由于无法判断已排序区到底排了几次,所以只能使用while循环,直到排序区的某个元素比待排序元素小时,表示在上一次插入过后,排序区已经排序完毕,这时就可以退出循环了
附上使用单个变量的方法:
lst = [6, 5, 3, 1, 8, 7, 2, 4]
length = len(lst)
temp = 0
for i in range(1, length): # 7
temp = lst[i]
j = i - 1 # 6
if lst[j] > temp:
while lst[j] > temp:
lst[j+1], lst[j] = lst[j], temp
j -= 1
if j < 0: # 不限制j的索引时,j会取到-1..-n,这样就乱了,因为索引为-1的元素,还没有被排序
break
print(lst)
(4)总结
- 最好情况,正好是升序排列,比较迭代次n-1次
- 最差情况,正好是降序排列,比较迭代1,2,… n - 1 次即n(n-1)/2,数据移动会非常多
- 使用两层嵌套循环,时间复杂度为O(n^2)
- 属于稳定的排序算法
- 使用在小规模数据比较时如果比较操作耗时大的话,可以采用二分查找来提高效率,即二分查找插入排序(由于每次比较还是要进行插入,所以优化效率不是那么高)
4.堆排序
(1)堆排序(Heap Sort)
使用完全二叉树和树的性质排序。
(2) 工作原理
先构造一个完全二叉树,在调整其中元素,也就是调整二叉树中的一棵树的节点,使节点的值大于孩子节点的(假设有(右)孩子节点),然后构建大顶堆,接着接续调整二叉树中的每一棵树的节点,使其符合大顶堆的要求,最后排序,需要注意的是,如果最后剩余2个元素的时候,如果后一个结点比堆顶大,就不用调整了。
(3)实现
1)构造一个完全二叉树
思路: 第一行取1个打印,第二行取2个,第三行取3个,以此类推。如何对齐且不重叠?
代码实现:
# 居中对齐方案
import math
origin = [30, 20, 80, 40, 50, 10, 60, 70, 90]
def print_tree(array: list, unit_width=2):
length = len(array)
depth = math.ceil(math.log2(length))
width = 2 ** depth - 1 # 满二叉树最多元素的投影 15个
index = 0
for i in range(depth): # 层 0 1 2 3
for j in range(2 ** i): # 一个个
# 居中打印,后面追加一个空格,因为7//2=3,3+3=6,少了
print('{:^{}}'.format(array[index], width * unit_width), end=' ' * unit_width)
# 索引不能超界
index += 1
if index >= length:
return
# 居中打印宽度减半
width //= 2
# 控制换行
print()
print_tree(origin)
# print_tree([i + 1 for i in range(31)], 2)
# 投影栅格实现
origin = [0, 30, 20, 80, 40, 50, 10, 60, 70, 90]
def print_tree(array: list, unit_width=2): # unit_width 只考虑两位的数
'''
索引 序号 前空格 间隔空格
0 3 7=2**3-1 0=2**pre+1 # 1
1 2 3=2**2-1 7=2**pre+1 # 2 3
2 1 1 3=2**pre+1
3 0 0 1
'''
length = len(array) # 前面多个数据 n 待比较元素个数
# n = length - 1
# 因为使用时前面补0了,不然应该是math.ceil(math.log2(len(array)+1))
depth = math.ceil(math.log2(length)) # 4
# index = 0
space = ' ' * unit_width
# 索引从1开始,切片
start = 1
for i in range(depth - 1, -1, -1): # 层
pre = 2 ** i - 1
print(pre * space, end='') # 前置空格
offset = 2 ** (depth - i - 1)
line = array[start:start + offset] # 去数字
interval = (2 * pre + 1) * space # 间隔的空格
print(interval.join(map(str, line)))
start += offset
print_tree(origin)
结果:
30
20 80
40 50 10 60
70 90
2)堆调整
堆排序的核心算法就是堆结点的调整:
- 度数为2的结点A,如果它的左右孩子结点的最大值比它大的,将这个最大值和该结点交换
- 度数为1的结点A,如果它的左孩子的值大于它,则交换
- 如果结点A被交换到新的位置,还需要和其孩子结点重复上面的过程
代码实现:
# 为了和编码对应,增加一个无用的0在首位
# origin = [0, 50, 10, 90, 30, 70, 40, 80, 60, 20]
origin = [0, 30, 20, 80, 40, 50, 10, 60, 70, 90]
total = len(origin) - 1 # 初始待排序元素个数,即n
#print(origin)
print_tree(origin)
def heap_adjust(n, i, array: list):
'''
调整当前结点(核心算法)
调整的结点的起点在n//2,保证所有调整的结点都有孩子结点
:param n: 待比较数个数
:param i: 当前结点的下标
:param array: 待排序数据
:return: None
'''
while 2 * i <= n:
# 孩子结点判断 2i为左孩子,2i+1为右孩子
lchild_index = 2 * i
# 先假定左孩子大,如果存在右孩子且大则最大孩子索引就是右孩子
max_child_index = lchild_index # n=2i
if n > lchild_index and array[lchild_index + 1] > array[lchild_index]: # n>2i说明还有右
孩子
max_child_index = lchild_index + 1 # n=2i+1
# 和子树的根结点比较
if array[max_child_index] > array[i]:
array[i], array[max_child_index] = array[max_child_index], array[i]
i = max_child_index # 被交换后,需要判断是否还需要调整
else: # 否则,说明目前子树根节点就是最大的,当前节点不用调整,直接结束
break
# print_tree(array)
heap_adjust(total, total // 2, origin)
#print(origin)
print_tree(origin)
结果:
30
20 80
40 50 10 60
70 90
30
20 80
90 50 10 60
70 40
3)构建大顶堆
起点的选择: 从最下层最右边叶子结点的父结点开始 由于构造了一个前置的0,所以编号和列表的索引正好重合 但是,元素个数等于长度减1
下一个结点: 按照二叉树性质5编号的结点,从起点开始找编号逐个递减的结点,直到编号1
# 构建大顶堆、大根堆
def max_heap(total,array:list):
for i in range(total//2, 0, -1):
heap_adjust(total,i,array)
return array
print_tree(max_heap(total,origin))
结果:
30
20 80
40 50 10 60
70 90
30
20 80
90 50 10 60
70 40
90
70 80
40 50 10 60
20 30
4)排序
思路:
- 每次都要让堆顶的元素和最后一个结点交换,然后排除最后一个元素,形成一个新的被破坏的堆。
- 让它重新调整,调整后,堆顶一定是最大的元素。
- 再次重复第1、2步直至剩余一个元素
def sort(total, array:list):
while total > 1:
array[1], array[total] = array[total], array[1] # 堆顶和最后一个结点交换
total -= 1
heap_adjust(total,1,array)
return array
print_tree(sort(total,origin))
结果:
10
20 30
40 50 60 70
80 90
5)改进
如果最后剩余2个元素的时候,如果后一个结点比堆顶大,就不用调整了。
def sort(total, array: list):
while total > 1:
array[1], array[total] = array[total], array[1] # 堆顶和最后一个结点交换
total -= 1
if total == 2 and array[total] >= array[total - 1]:
break
heap_adjust(total, 1, array)
return array
print_tree(sort(total, origin))
6)完整代码
origin = [0, 30, 20, 80, 40, 50, 10, 60, 70, 90]
def print_tree(array: list, unit_width=2): # unit_width 只考虑两位的数
'''
索引 序号 前空格 间隔空格
0 3 7=2**3-1 0 =2*pre+1 # 1
1 2 3=2**2-1 7=2*pre+1 # 2 3
2 1 1 3=2*pre+1
3 0 0 1
'''
length = len(array) # 前面多个数据 n 待比较元素个数
# n = length - 1
# 因为使用时前面补0了,不然应该是math.ceil(math.log2(len(array)+1))
depth = math.ceil(math.log2(length)) # 4
# index = 0
space = ' ' * unit_width
# 索引从1开始,切片
start = 1
for i in range(depth - 1, -1, -1): # 层
pre = 2 ** i - 1
print(pre * space, end='') # 前置空格
offset = 2 ** (depth - i - 1)
line = array[start:start + offset] # 去数字
interval = (2 * pre + 1) * space # 间隔的空格
print(interval.join(map(str, line)))
start += offset
print_tree(origin)
def heap_adjust(n, i, array: list):
'''
调整当前结点(核心算法)
调整的结点的起点在n//2,保证所有调整的结点都有孩子结点
:param n: 待比较数个数
:param i: 当前结点的下标
:param array: 待排序数据
:return: None
'''
while 2 * i <= n:
# 孩子结点判断 2i为左孩子,2i+1为右孩子
lchile_index = 2 * i
# 先假定左孩子大,如果存在右孩子且大则最大孩子索引就是右孩子
max_child_index = lchile_index # n=2i
if n > lchile_index and array[lchile_index + 1] > array[lchile_index]: # n>2i说明还有右
max_child_index = lchile_index + 1 # n=2i+1
# 和子树的根结点比较
if array[max_child_index] > array[i]:
array[i], array[max_child_index] = array[max_child_index], array[i]
i = max_child_index # 被交换后,需要判断是否还需要调整
else: # 否则,说明目前子树根节点就是最大的,当前节点不用调整,直接结束
break
# print_tree(array)
heap_adjust(total, total // 2, origin)
# print(origin)
print_tree(origin)
# 构建大顶堆、大根堆
def max_heap(total,array:list):
for i in range(total//2, 0, -1):
heap_adjust(total,i,array)
return array
print_tree(max_heap(total,origin))
def sort(total, array: list):
while total > 1:
array[1], array[total] = array[total], array[1] # 堆顶和最后一个结点交换
total -= 1
if total == 2 and array[total] >= array[total - 1]:
break
heap_adjust(total, 1, array)
return array
print_tree(sort(total, origin))
结果:
30
20 80
40 50 10 60
70 90
30
20 80
90 50 10 60
70 40
90
70 80
40 50 10 60
20 30
10
20 30
40 50 60 70
80 90
(4)总结
堆排序是利用堆性质的一种选择排序,在堆顶选出最大值或者最小值
时间复杂度:
- 堆排序的时间复杂度为O(nlogn)
- 由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为 O(nlogn
空间复杂度:
- 只是使用了一个交换用的空间,空间复杂度就是O(1)
稳定性:
- 不稳定的排序算法