文章目录
1,插入排序(Insertion-Sort)
直接插入排序
视频素材 - bilibili
工作原理
代码实现
def Insertion_Sort(a):
for i in range(len(a)):
key = a[i]
j = i - 1
while i > 0 and a[i - 1] > key:
a[i] = a[i - 1]
i -= 1
a[i] = key
return a
a = [2, 3, 5, 2, 1, 9, 7, 5, 2]
b = Insertion_Sort(a)
print(b)
>>>[1, 2, 2, 2, 3, 5, 5, 7, 9]
算法分析
时间复杂度为
最佳的情况:已经由小到大排好序
第5行 则只会被执行 n-1 次
第6,7行 则不会被执行
最差情况:列表为逆序
第5行 对任何一个 a[i] 进行排序时 都会被执行 i 次(共有n个a[i])
第6,7行 随同 行5,每一次排序会被执行 i-1 次(进行n次排序)
时间复杂度【最好情况–最坏情况–平均情况】–空间复杂度–稳定性
折半插入排序
视频素材 - bilibili
工作原理
代码实现
def BinaryInsertSort(list):
for i in range(2, len(list)):
list[0] = list[i]
low = 1
high = i - 1
while low <= high:
m = int((low + high) / 2) # 折半
if list[0] < list[m]: # 插入点在低半区
high = m - 1
else: # 插入点在高半区
low = m + 1
j = i - 1 # 记录后移
while j >= high + 1:
list[j + 1] = list[j]
j -= 1
list[high + 1] = list[0]
**其中[0]=-1这一位置是暂存单元,不会参与排序 **
a = [-1, 2, 3, 5, 2, 1, 9, 7, 5, 2]
BinaryInsertSort(a)
print(a)
>>>[2, 1, 2, 2, 2, 3, 5, 5, 7, 9]
算法分析
与直接插入排序法相比:
折半插入减少了比较次数,但没有减少移动次数(平均性能更优)
当数据量 n 较大时,且数据越乱,越适合用折半排序
而在最佳情况下时,直接排序(只用比较一次)反而优于折半排序
希尔排序
视频素材 - bilibili
基本思想
希尔排序的算法特点:
- 缩小增量
- 多遍插入排序
- 一次移动,移动位置较大,跳跃式的接近排序后的最终位置
- 最后一次只需要少量移动
- 增量序列必须是递减的,最后一个必须是 “1”
- 增量序列应该是互质的(互质是公约数只有1的两个整数,叫做互质整数。)
排序思路:
增量 5 > 3 > 1
代码实现
def shell_Sort(alist):
sublistcount = len(alist) // 2 # 除2后取不超过结果的最大整数
while sublistcount > 0:
for startposition in range(sublistcount):
InsertionSort(alist, startposition, sublistcount)
print("增量:", sublistcount,
"此时列表:", alist)
sublistcount = sublistcount // 2 # 增量每次减小一半
def InsertionSort(alist, start, gap):
"""
:param alist: 需要排序的列的表
:param start: 开始位置
:param gap: 增量
:return: 排序后列表
"""
for i in range(start + gap, len(alist), gap):
currentvalue = alist[i]
position = i
while position >= gap and alist[position - gap] > currentvalue:
alist[position] = alist[position - gap]
position = position - gap
alist[position] = currentvalue
alist = [23, 45, 11, 67, 44, 98, 69, 57, 26, 58]
shell_Sort(alist)
print(alist)
>>>增量: 5 此时列表: [23, 45, 11, 26, 44, 98, 69, 57, 67, 58]
增量: 2 此时列表: [11, 26, 23, 45, 44, 57, 67, 58, 69, 98]
增量: 1 此时列表: [11, 23, 26, 44, 45, 57, 58, 67, 69, 98]
最终结果:[11, 23, 26, 44, 45, 57, 58, 67, 69, 98]
代码理解
算法分析
希尔排序算法效率与增量序列的取值有关
时间效率 ↓
希尔排序是不稳定的排序算法
总结
2,交换排序
基本思想:
两两比较,如果发生逆序则交换,直到所有记录都排好序为止。
常见的交换排序:
- 快速排序 O(n**2)
- 快速排序 O( nlog2^n )
冒泡排序
基于简单交换思想:
每趟不断将记录两两比较,并按照“前小后大”规则排序
举个例子:
初始:[ 21, 25, 49, 25*, 16, 08 ] n = 6
比较 5 次
第1趟,结束后:[ 21, 25, 25*, 16, 08, 49 ]
比较 4 次
第2趟,结束后:[ 21, 25, 16, 08, 25*, 49 ]
之后以此类推…
第3趟,结束后:[ 21, 16, 08, 25, 25*, 49 ] 比较3次
第4趟,结束后:[ 16, 08, 21, 25, 25*, 49 ] 比较2次
第5趟,结束后:[ 08, 16, 21, 25, 25*, 49 ] 比较1次
总结:
- n 个记录,总共需要 n-1 趟
- 第 m 趟需要比较 n-m 次
代码实现
传送门:菜鸟教程 - 冒泡排序
(非最优版本)
def bubbleSort(arr):
n = len(arr)
# 遍历所有数组元素
for i in range(n):
# Last i elements are already in place
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
优点:每趟结束时,不仅能挤出一个最大值到最后位置,还能同时部分理顺其他元素
那么还有没有优化空间呢?
有的,若果再一趟中,完全没有发生交换,就可以说是已经排列好,所以可以加一个 变量change 判断是否发生过交换,若没有则 break
(优化版本,增加了change判断变量)
def bubbleSort(arr):
n = len(arr)
# 遍历所有数组元素
for i in range(n):
change = False
# Last i elements are already in place
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
change = True # 发生交换
# 如果未发生交换则跳出回圈
if not change:
break
算法分析
时间复杂度
- 最优情况(正序)
比较次数:n-1
移动次数:0 - 最坏情况(逆序)
比较次数:Σ(n-i) = (1/2)(n^2 - n) 其中 i ←[ 1, n-1 ]
移动次数:3Σ(n-i) = (3/2)(n^2 - n) 其中 i ←[ 1, n ]
快速排序(改进的交换排序)
视频素材 - bilibili
基本思想(使用递归):
-
任取一个元素(如:第一个)为中心(pivot,枢轴)
-
所有比他小的元素一律前放,比他大的元素一律后放,形成左右两个子表
-(【小】(pivot)【大】) -
对各子表中心选择中心元素并依此规则调整
-
直到每个子表的元素只剩一个
快速排序演示
每个子表的形成都是采用从两头向中间交替式逼近法;
由于每趟中对各个子表的操作都相似,可采用递归算法。
代码实现
传送门:菜鸟教程 - 快速排序
def partition(arr, low, high):
i = (low - 1) # 最小元素索引
pivot = arr[high]
for j in range(low, high):
# 当前元素小于或等于 pivot
if arr[j] <= pivot:
i = i + 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return (i + 1)
# arr[] --> 排序数组
# low --> 起始索引
# high --> 结束索引
# 快速排序函数
def quickSort(arr, low, high):
if low < high:
pi = partition(arr, low, high)
quickSort(arr, low, pi - 1)
quickSort(arr, pi + 1, high)
这边要注意一下 n 的取值
n = len(list) - 1
quickSort(list, 0, n)
print(list)
>>>[2, 5, 66, 99, 77, 52, 33, 2, 1, 0, 44, 5, 10]
算法分析
时间复杂度
空间复杂度
稳定性
总结
3,选择排序(Selection Sort)
–(演算法 2,Sorting.pdf )
简单选择排序
视频素材 - bilibili
算法原理
精髓在于:找最小值(要注意每一次找最小值的范围)
代码实现
def Normal_Select_Sort(a):
for i in range(len(a)):
# 找最小值的位置(index)
key = i
for j in range(i, len(a)):
if a[key] > a[j]:
key = j
# 将最小值与a[i]进行交换
if key != i:
key_value = a[key]
a[key] = a[i]
a[i] = key_value
Normal_Select_Sort(list)
print(list)
>>>[0, 1, 2, 2, 5, 5, 10, 33, 44, 52, 66, 77, 99]
算法分析
时间复杂度【最好情况–最坏情况–平均情况】–空间复杂度–稳定性
什么是堆排序
堆的定义
视频素材 - bilibili
(堆实质是满足如下性质的完全二叉树:二叉树中任意非叶子节点均小于(大于)它的孩子节点)
光看定义肯定会比较晕,那么来看一下下边的例子吧~
再来看两个错误的例子
堆排序的原理
实现堆排序需要解决的两个问题:
1,如何由一个无序序列建成一个堆。
2,如何在输出堆顶元素后,调整剩余元素为新的堆。
堆的调整
视频素材 - bilibili
下边不理解的话,上边视频从3:50开始看一下
(大根堆 交换大者)
堆的建立
视频素材 - bilibili
通过反复的筛选,将一个无序的序列建成一个堆
具体的操作:
如果还是一脸懵逼就来看一下这个例子吧~
堆排序的代码实现
视频素材 - bilibili
还没学二叉树,所以下边的代码我自己看也是云里雾里,补完二叉树会回来进行更改和补充的(希望早日给这两行加上删除线)
def heapify(arr, n, i):
largest = i
l = 2 * i + 1 # left = 2*i + 1
r = 2 * i + 2 # right = 2*i + 2
if l < n and arr[i] < arr[l]:
largest = l
if r < n and arr[largest] < arr[r]:
largest = r
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i] # 交换
heapify(arr, n, largest)
def heapSort(arr):
n = len(arr)
# 建立一个最大堆
for i in range(n, -1, -1):
heapify(arr, n, i)
# 一个个交换元素
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i] # 交换
heapify(arr, i, 0)
heapSort(list)
print(list)
>>>[0, 1, 2, 2, 5, 5, 10, 33, 44, 52, 66, 77, 99]
算法分析
堆排序的时间主要耗费在建初始堆和调整建新堆时的反复及筛选上。
对排序的最大优点即是:最坏和最好的情况下,时间复杂度均为↓
无论待排序列中的记录是正序还是逆序,都不会使得堆排序处于“最好”或“最坏”的状态。
另外,堆排序仅需一个记录大小供交换用的辅助存储空间。
然而,堆排序是一种不稳定的排序方法,不适用于排序记录个数 n 较小的情况,但对于 n 较大的文件还是很有效的。
4,归并排序(Merge Sort)
视频素材 - bilibili
工作原理
将两个有序或两个以上有序的子序列,归并为一个有序序列
通常采用 2-路归并排序
即:将两个相邻的有序子序列 L[ 1 … n1+1 ] & R[ 1 … n2+1 ],归并为一个序列
代码实现
参考文献 — 讲的很详细
def merge(arr, l, m, r):
n1 = m - l + 1
n2 = r - m
# 创建临时数组
L = [0] * (n1)
R = [0] * (n2)
# 拷贝数据到临时数组 arrays L[] 和 R[]
for i in range(0, n1):
L[i] = arr[l + i]
for j in range(0, n2):
R[j] = arr[m + 1 + j]
# 归并临时数组到 arr[l..r]
i = 0 # 初始化第一个子数组的索引
j = 0 # 初始化第二个子数组的索引
k = l # 初始归并子数组的索引
while i < n1 and j < n2:
if L[i] <= R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
# 拷贝 L[] 的保留元素
while i < n1:
arr[k] = L[i]
i += 1
k += 1
# 拷贝 R[] 的保留元素
while j < n2:
arr[k] = R[j]
j += 1
k += 1
# 进行递归
def mergeSort(arr, start_index, end_index):
if start_index < end_index:
mid = int((start_index + (end_index - 1)) / 2)
mergeSort(arr, start_index, mid)
mergeSort(arr, mid + 1, end_index)
merge(arr, start_index, mid, end_index)
arr = [12, 11, 13, 5, 6, 7]
n = len(arr)
print("给定的数组", arr)
mergeSort(arr, 0, n - 1)
print("排序后的数组", arr)
>>>给定的数组 [12, 11, 13, 5, 6, 7]
排序后的数组 [5, 6, 7, 11, 12, 13]
算法分析
缺点:需要一个与原序列同样大小的辅助序列(L,R)
详细分析
分治算法(Divide-and-Conquer Algorithm)
也可以写作
5,基数排序
视频素材 - bilibili基本思想:分配 + 收集
也叫桶排序或箱排序:设置若干个箱子,将关键字为 k 的记录放入第 k 个箱子,然后再按序号将非空的链接。
**基数排序:**数字是有范围的,均由0-9这十个数字组成,则只需要设置十个箱子,相继按照个、十、百…进行排序。
举个例子说明一下:
第一趟的收集结果就已经将个位进行了排序,再在此结果的基础下进行第二趟。
(排序完成!)
算法分析
时间效率:O(k*(n+m)) - 线性阶
k:关键字个数(有几类桶)
m:关键字取值范围为m个值(桶的个数)
(进行k趟,每趟收集m次)
再举个例子:10000人按照生日排序
年(89个桶),月(12个桶),日(31个桶)
6,各种排序方法比较
一,时间性能
1,按平均的时间性能来分,有三类排序方法:
时间复杂度为 O(nlogn) 的方法有:
- 快速排序(最佳),堆排序和归并排序
时间复杂度为 O(n^2) 的有:
- 直接插入排序(最佳),冒泡排序和简单选择排序
时间复杂度为 O(n) 的排序方法只有:
- 基数排序
2,当待排记录序列按关键字顺序有序时,直接插入法和冒泡排序法能达到 O(n) 的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间复杂度退化为 O(n^2),因此是应该尽量避免的情况。
3,简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。
二,空间性能
指的是排序过程中所需的辅助空间大小。
- 所有的简单排序方法(包括:直接插入、冒泡和简单选择)和堆排序的空间复杂度为 O(1)。
- 快速排序为 O(logn),为栈所需的辅助空间。
- 归并排序所需辅助空间最多,其空间复杂度为 O(n)。
- 链式基数排序需要附设队列首尾指针,则空间复杂度为 O(rd)。
三,排序方法的稳定性能
- 稳定的排序方法指的是,对于两个关键字相等的记录,他们在序列中的相对位置,在排序的前后不会发生改变。
- 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。
- 对于不稳定的排序方法,只要能举出一个实例说明即可。
- 快速排序和堆排序是不稳定的排序方法。
四,关于“排序方法的时间复杂度下线”
- 本章讨论的各种排序方法,除基数排序,其他方法都是基于“比较关键字”进行排序的排序方法,可以证明,这类排序方法可能达到的最快的时间为 O(nlogn)。
(基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制) - 可以用一颗判断树来描述这类基于“比较关键字”进行排序的排序方法。