1、冒泡排序
核心算法
- 排序算法,一般都实现为就地排序,输出为升序
- 扩大有序区,减小无序区。
- 每一趟比较中,将无序区中所有元素一次进行两两比较,升序排序将大的数调整至两数中的右侧
- 每一趟比较完成,都会把这一趟的最大数推到当前无序区的最右侧
代码实现:
nums = [9,8,1,2,3,4,5,6,7]
print(nums)
print('-----------------')
length = len(nums) #读取列表的长度
#i 排序的趟数
count = 0
swap_count = 0 #两个count只是为了进行计数
for i in range(length-1): #实际是9个数,排序的趟数是8,所以-1;再因为range后不包,所以这里实际上-2
for j in range(length-1 -i): #无序区中排序的次数,正好是i,8次,8-8正好可得j索引为0,然后依次进行排序
count += 1
if nums[j] > nums[j+1]: #j len-2;j+1 len-1 ;如果nums索引的值 > nums索引位置+1的值,则执行以下操作
temp = nums[j] #引入了一个中间变量temp来存储nums当前索引的值
nums[j] = nums[j+1]
nums[j+1] = temp #如果nums索引的值>nums索引位置+1的值,那么到这一步,大小数值,交换完成。
swap_count += 1
print(nums) #在j循环之外,打印每一趟完成的当前排序情况
print('------------------')
print(nums) #最终排序完成的情况
print(count,swap_count)
打印的结果是:
[1, 2, 3, 4, 5, 6, 7, 8, 9] #完成了排序
36 15 #排序的次数是36次,交换的次数是15次,但是发现没有,其实只要在9和8完成排序之后,nums列表的顺序已经是期望的升序排序,但是该程序依旧一次次进行不必要的排序,效率低下;因此,可以对以上代码进行优化,如下:
nums = [9,8,1,2,3,4,5,6,7]
print(nums)
print('-----------------')
length = len(nums)
count = 0
swap_count = 0
for i in range(length-1):
flag = False ###打标记,假设这一趟没有交换过
for j in range(length-1 - i):
count += 1
if nums[j] > nums[j+1]:
temp = nums[j]
nums[j] = nums[j+1]
nums[j+1] = temp
swap_count += 1
flag = True ###当执行到这一步,flag置为True
print(nums)
if not flag:
break ###当flag置为True,而不是False时,表明已经没有交换,则break
print('--------------++++-')
print(nums)
print(count,swap_count)
总结:
冒泡法需要数据一趟趟比较
可以设定一个标记判断此轮是否有数据交换发生,若没有,可结束排序,如果发生交换,继续下一轮排序
最差的排序情况是,初始顺序与目标顺序完全相反,遍历次数为n(n-1)/2
最好的排序情况是,初始顺序与目标顺序完全相同,遍历次数为n-1
时间复杂度是O(n^2)
2、简单选择排序
核心算法:
结果可为升序或降序排列,默认升序排列
扩大有序区,减小无序区。
以降序为例;相邻元素依次两两比较,获得每一次比较后的最大值,并记住此值的索引
每一趟都从无序区中选择出最大值,然后交换到当前无序区的最左端
m_list=[
[1,9,8,5,6,7,4,3,2],
[1,2,3,4,5,6,7,8,9],
[9,8,7,6,5,4,3,2,1]
]
nums = m_list[0]
length = len(nums)
print(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 maxindex != i:
nums[maxindex],nums[i]=nums[i],nums[maxindex]
count_swap += 1
print(nums)
print(count_iter,count_swap)
--->
[1, 9, 8, 5, 6, 7, 4, 3, 2]
[9, 8, 7, 6, 5, 4, 3, 2, 1]
36 7
当然还可以使用二元选择排序的方法:
同时选择出每一趟的最大值和最小值,并分别固定到两端的有序区
可以减少迭代的趟数
总结:
简单选择排序需要数据一趟趟比较,并在每一趟中发现极值
没有办法知道当前这一趟是否已经达到排序要求,但是可以知道极值是否在目标索引位置上
遍历次数1...n-1之和n(n-1)/2
时间复杂度O(n^2)
减少了交换次数,提高了效率,性能略好于冒泡法
3、简单插入排序
核心算法:
结果可为升序或降序排列,默认升序排列;以升序为例
扩大有序区、减小无序区;图中绿色部分就是增大的有序区,黑色就是减小的无序区
增加一个哨兵位,图中最左端红色数字,其中放置每一趟待比较数值
将哨兵位数值与有序区数值从右往左依次比较,找到哨兵位数值合适的插入位置
算法实现:
增加哨兵位
为了方便,采用列表头部索引0位置插入哨兵位
每一次从有序区最右端的下一个数,即无序区最左端的数放到哨兵位
比较与挪动
从有序区最右端开始,从右至左依次与哨兵位比较
比较数比哨兵大,则右移一位,换下一个左边的比较数
直到找不到大于哨兵的比较数,这时把哨兵插入到这个数右侧的空位即可
m_list=[1,9,8,5,6]
nums=[0] + m_list
print(nums[1:])
length=len(nums)
count_move=0
for i in range(2,length): #测试的值从nums的索引2开始向后直到最后一个元素
nums[0] = nums[i] #索引0位哨兵、索引1位假设的有序区,都跳过
j = i - 1 #i左边的那个数就是有序区末尾
if nums[j] > nums[0]: #如果最右侧数大于哨兵才需要挪动和插入
while nums[j] > nums[0]:
nums[j+1] = nums[j] #右移,不是交换
j -= 1 #继续向左
count_move += 1
nums[j+1] = nums[0] #循环中多减了一次
print(nums[1:])
print(count_move)
--->
[1, 9, 8, 5, 6]
[1, 5, 6, 8, 9]
5
总结:
最好情况,正好是升序排列,比较迭代n-1次
最差情况,正好是降序排列,比较迭代n(n-1)/2次,数据移动非常多
使用两层嵌套循环,时间复杂度O(n^2)
是稳定排序算法:
如果待排序序列R中两元素相等,即Ri等于Rj,且i<j,那么排序后这个先后顺序不变,这种排序算法就称为稳定排序。
一般使用在小数据规模比较
优化:如果比较操作耗时大,可以使用二分查找提高效率,即二分查找插入排序
4、堆排序
import math
origin = [None,30,20,80,40,50,10,60,70,90] #None为补位,设计列表索引从1开始使用数据
total = len(origin) - 1 #初始待排序个数,即n
# print_tree函数可以不实现,只是为了打印观察方便
def print_tree(array, unit_width=2):
length = len(array)
depth = math.ceil(math.log2(length))
start = 1
spaces = ' ' * unit_width
for i in range(depth-1, -1 ,-1):
pre = 2 ** i -1
print(pre * spaces, end='')
end = start + start
line = array[start:end]
interval = (2 * pre + 1) * spaces
print(interval.join(map(lambda x:'{:{}}'.format(x, unit_width),line)))
start = end
def heap_adjust(i,n,array:list):
'''
调整当前结点(核心算法);调整的结点的起点在n//2,保证所有调整的结点都有孩子结点
:param i: 当前结点的下标
:param n: 待比较个数
:param array: 待排序数据
'''
while 2 * i <= n: #一定有左孩子结点
lchild_index = 2 * i #2i为左孩子,2i+1为右孩子
max_child_index = lchild_index #假设孩子结点中左孩子目前最大
if lchild_index + 1 <= n and array[lchild_index + 1] > array[lchild_index]: #一定有右孩子结点,并且大于左孩子结点
max_child_index = lchild_index + 1 #则右孩子结点最大
if array[max_child_index] > array[i]: #和自己子树的根结点比较
array[max_child_index],array[i] = array[i],array[max_child_index] #最大子结点和它的根结点进行数据交换
i = max_child_index #子树的根结点就换成最大子结点
else: #否则,说明目前子树根结点是最大的,不用调整,直接结束
break
#这一步作完后,可以打印出如下
# 30
# 20 80
# 90 50 10 60
# 70 40
def max_heap(n, array:list): #构建大顶堆
for i in range(n//2, 0, -1): #range右不包,所有到0
heap_adjust(i, n, array)
return array
#max_heap(total,origin),已经构成了大顶堆
# 90
# 70 80
# 40 50 10 60
# 20 30
def heap_sort(n, array):
while n > 1: #n的个数要大于1,保证至少有两个待排序数
array[1], array[n] = array[n], array[1] #堆顶和最后一个结点交换
n -= 1 #排序的n的个数每次减少一个
if n == 2 and array[2] >= array[1]: #优化;当n只有2个了并且子结点大于它的根结点则结束
break
heap_adjust(1, n, array) #调整之后,获得新的大顶堆
return array
max_heap(total,origin)
print_tree(heap_sort(total,origin))
print(origin[1:])
#最后执行如下,完成排序
# 10
# 20 30
# 40 50 60 70
# 80 90
# [10, 20, 30, 40, 50, 60, 70, 80, 90]
--------
总结:
是利用堆性质的一种选择排序,在堆顶选出最大值或最小值
时间复杂度:
堆排序的时间复杂度为O(nlogn)
由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)
空间复杂度:
只是使用了一个交换的空间,空间复杂度就是O(1)
是不稳定的排序算法