排序
排序:将一组无序的记录调整为有序记录的过程
列表排序:将无序的列表调整为有序的列表
- 输入:无序列表
- 输出:有序列表(升序或者降序)
python内置的排序函数:sort()
常见的排序算法
Low B排序算法
冒泡排序(Bubble Sort)
-
比较列表每两个相邻的数, 如果前面的比后面的大,则交换两者
-
一趟排序完毕,无序区减少一个数,有序去区增加一个数
-
代码实现关键点:排序趟数,和无序区域范围
# 原版冒泡排序代码
from typing import List
def bubble_sort(values: List[int]) -> List[int]:
"""
冒泡排序算法实现
Args:
values:
Returns:
"""
for i in range(len(values) - 1): # 排序趟数
for j in range(len(values) - 1 - i): # 遍历无序区的数进行交换
if values[j] > values[j + 1]:
values[j], values[j + 1] = values[j + 1], values[j]
print(f"第{i + 1}趟: {values}")
return values
- 冒泡排序代码改进:如果在一趟排序过程中未发生交换,这说明列表已有序,可终止排序返回结果
# 改进版冒泡排序代码
from typing import List
def bubble_sort(values: List[int]) -> List[int]:
"""
冒泡排序算法实现
Args:
values:
Returns:
"""
for i in range(len(values) - 1): # 排序趟数
exchange = False # 是否已排序完毕,如果一趟不发生交换,则表明列表已排序完毕
for j in range(len(values) - 1 - i): # 遍历无序区的数进行交换
if values[j] > values[j + 1]:
values[j], values[j + 1] = values[j + 1], values[j]
exchange = True
if not exchange: # 检查列表是否已经有序
return values
print(f"第{i + 1}趟: {values}")
return values
选择排序
原理:假设列表ls长度为n,遍历列表n - 1次,第i次(i从0到n-1)遍历找到索引i到n-1的最小值的索引min_idx,然后交换ls[i] 与ls[min_idx]的值
def select_sort(values):
for i in range(len(values) - 1): # 第i趟
min_idx = i
for j in range(i + 1, len(values)): # 遍历无序区
if values[j] < values[min_idx]: # 寻找无序区最小值的索引
min_idx = j
values[i], values[min_dix] = values[min_dix], values[i] # 交换无序区的初始索引和最小值索引的值
时间复杂度:O( n 2 n^2 n2)
插入排序
原理:
-
列表values,长度为n
-
将无序区的第一个元素插入到有序区中,保证有序区任然有序。n-1趟之后列表就变得有序
python实现:
-
方式1:
def insert_sort(values: List): for i in range(1, len(values)): # 无序区的起始索引从1到len(values)-1 # 遍历有序区将无序区收个元素插入到有序区中,保证有序区任然有序 for j in range(i): if values[i] < values[j]: # 如果无序区收个元素小于有序区索引为j的元素,则应将该元素插入有序区j处 values.insert(j, values.pop(i)) break
-
方式2
def insert_sort_v2(values: List): for i in range(1, len(values)): # 第i趟,i为无序区收个元素的索引 # 查找在有序区的插入位置 item = values[i] j = i - 1 # 无序区的最后一个元素索引 while item < values[j] and j >= 0: values[j + 1] = values[j] j -= 1 values[j + 1] = item # 将item插入有序区
时间复杂度:O( n 2 n^2 n2)
NB排序算法
快速排序
原理: 在列表中选取一个基准元素,将小于基准的元素放在基准左侧,反之将大于基准的元素放在基准右侧。在基准的左半区间和右半区间进行递归调用。
步骤:
- 列表values, 初始左右边界索引left =0 ,right =len(values) - 1
- 传入参数:列表values和左右索引边界left和right, 将values[left]作为基准, 暂存于mid_value
- 将right从右向左移动(递减),直到找到第一个小于基准的数,left和right索引处元素交换。将left从左向右移动,直到找到第一个大于基准的数,交换left和right索引处元素。
- 重复执行步骤3, 直到不满足left < right为止。
- 将所用为left处元素赋值为mid_value, 返回left或者right
- 在基准的左半区间values[left: mid-1]和右半区间[mid + 1, right]递归执行2-5步(递归参数分别为 values, left ,mid-1 和values, mid + 1, right),直到满足递归停止条件。递归停止条件 left >= right(待排序子列表元素少用两个)
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
python实现:
def partition(values: List[int], left: int, right: int) -> int:
mid_value = values[left]
while left < right:
# 从右向左寻找第一个小于基准的数,放在left所指位置
while values[right] >= mid_value and left < right:
right -= 1
values[left] = values[right]
# 从左向右寻找第一个大于基准的数,放在right所指位置
while values[left] <= mid_value and left < right:
left += 1
values[right] = values[left]
values[left] = mid_value
return left
def quick_sort(values: List[int], left: int, right: int):
if left < right: # 判断当前列表元素是否大于1
# 选择第一个元素作为基准,将大于基准的元素放在右侧,小于基准的元素放在左侧,返回基准的索引
mid = partition(values, left, right)
# 递归调用
quick_sort(values, left, mid - 1) # 对左版区间
quick_sort(values, mid + 1, right) # 对右半区间
快速排序算法的问题:
- 快速排序设计递归调用,如果使用python实现,注意不能超过python的最大递归深度(可以修改)
- 快速排序的最坏情况(列表降序排列),时间复杂度为 O ( n 2 ) O(n^2) O(n2)
堆排序
树的相关概念:
- 树的定义:一个集合有n个节点, 如果n=0,则是一棵空树,如果n>0, 则树有一个根节点,剩余的n-1个节点组成m个集合,每个集合也是一棵树
- 根节点,子节点
- 输的高度(深度):树的最大层数, 上图的树的深度为4
- 树的度:树的节点中包含最多子节点的节点的子节点数量, 上图的树的度为6
- 孩子节点、父亲节点: 上图中B是A的子节点,A是B的父节点
- 子树:上图中EIJPQ是一个子树
二叉树
什么是二叉树?
数的度小于等于2的树,称为二叉树
满二叉树:二叉树的每一层的节点都达到最大值称为满二叉树
完全二叉树:叶子节点只能出现在最下层和次下层,且最下层的叶子节点从右到左排列,不会出现空缺
二叉树的存储:
-
链式存储
-
顺序存储
- 父节点和孩子节点的索引关系:
- 父亲节点索引为 i i i, 左孩子节点索引为 2 i + 1 2i+1 2i+1, 右孩子节点索引为 2 i + 2 2i+2 2i+2
- 孩子节点为 i i i, 父亲节点索引为 ( i − 1 ) / / 2 (i-1)//2 (i−1)//2
堆
什么是堆?:
-
堆是一种特殊的完全二叉树结构
-
大根堆:任一节点都比孩子节点大
-
小根堆:任一节点都比孩子节点小
-
堆的向下调整属性:
- 节点的左右子树都是堆,单自身不是堆
-
通过向下调整将上面的树调整为一个大根堆
-
步骤:
-
取出根节点2,空出根节点, 比较下层的节点9和7的大小,将大的节点9放到根节点位置,空出原来9的节点
-
由于2小于8或者5,所以将8(8大于5)移动到空节点,原来节点8的位置变为空
-
由于2小于6或者4, 所以将6(6大于4)移动到空节点,原来节点6的位置变为空
-
由于空节点为叶子节点,所以将2放在空节点处
-
-
使用堆的向下调整完成堆排序
堆排序步骤:
- 建立大根堆(降序)
- 获得堆顶元素(最大值)
- 去掉堆顶,将堆最后一个元素放在堆顶,然后通过一次向下调整使堆重新有序
- 堆顶元素为第二大元素
- 重复步骤3,直到堆为空
如何构造堆?
步骤:
- 先让最后一个非叶子节点作为根节点子树变得有序
- 再让倒数第二个非叶子节点作为根节点的子树变得有序
- 以此类推,从后向前使得整个树变得有序
堆排序python实现:
def sift(values, low, high):
"""
堆的向下调整函数
Args:
values: 表示堆的数组
low: 堆的根节点索引
high: 堆的最后一个元素的索引
Returns:
"""
temp = values[low] # 暂存堆顶元素
i = low
j = 2 * i + 1 # 左孩子节点索引
while j <= high: # 存在左孩子节点
if j + 1 <= high and values[j] < values[j + 1]: # 存在右孩子节点且右孩子节点大于左孩子节点
j += 1 # 将j指向右孩子节点
if temp < values[j]: # 子孩子节点大于父亲节点
values[i] = values[j] # 将子孩子节点放在父节点位置
# 更新i和j, 下移一层
i = j
j = 2 * i + 1
else: # 子孩子节点小于父亲节点,则找到合适位置,插入元素,退出循环
values[i] = temp
break
else: # 如果顺利退出循环,需要将temp放入叶子节点
values[i] = temp
def heap_sort(values):
"""
堆排序实现
Args:
values: 列表
Returns:
"""
# 建立堆(大根堆)
high = len(values) - 1 # 最后一个叶子节点索引
for i in range((high - 1) // 2, -1, -1): # 从后向前遍历非叶子节点
sift(values, i, high) # 可以将high作为向下调整的最后一个叶子节点的索引
# 挨个取出数字
for i in range(high, -1, -1):
values[0], values[i] = values[i], values[0] # 堆顶和堆最后一个叶子节点交换
sift(values, 0, i - 1) # 堆向下调整
堆排序时间复杂度: n l o g ( n ) nlog(n) nlog(n)
堆排序 解决topk问题
topk问题:取出一个集合(n个元素)中前k大的数,k<n
解决思路:
- 先排序后切片, 时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
- 冒泡排序,排序k趟, 时间复杂度 O ( k n ) O(kn) O(kn)
- 堆排序思路,时间复杂度
O
(
n
l
o
g
k
)
O(nlogk)
O(nlogk)
- 取出列表中前k个元素建立一个小根堆
- 遍历列表中剩余的n-k个元素,如果当前元素比堆顶元素大,则将堆顶替换为该元素,然后进行一次向下调整,否则丢弃该元素
def sift_down(values: List, low: int, high: int):
"""
堆向下调整实现
Args:
values:
Returns:
"""
temp = values[low]
i = low
j = 2 * i + 1
while j <= high:
# 若i节点存在右孩子节点且小于左孩子节点
if j + 1 <= high and values[j] > values[j + 1]:
j += 1 # j指向有孩子节点
if temp > values[j]:
values[i] = values[j]
i = j
j = 2 * i + 1
else:
values[i] = temp
break
else:
values[i] = temp
def topk_heap(values: List, k: int) -> List:
k_vlaues = values[:k]
res_values = values[k:]
# 取出列表的前k个元素构建小根堆
end = (k - 1 - 1) // 2 # 小根堆最后一个非叶子节点索引
# 取出列表的前k个元素构建小根堆
for i in range(end, 0, -1):
sift_down(k_vlaues, i, k - 1)
# 依次取出列表中剩余元素与堆堆顶元素比较,若大于堆顶元素,则替换堆顶元素,否则跳过
for item in res_values:
if item > k_vlaues[0]:
k_vlaues[0] = item
sift_down(k_vlaues, 0, k - 1)
# 从小根堆末尾一次弹出所有元素
for i in range(k - 1, -1, -1):
k_vlaues[0], k_vlaues[i] = k_vlaues[i], k_vlaues[0] # 交换堆顶和末尾元素
sift_down(k_vlaues, 0, i - 1) # 向下调整
return k_vlaues
归并排序
归并
若果一个列表示分段有序的([1, 3, 5, 7, 2, 4, 6, 8]),则可以通过一次归并, 使得整个列表变得有序
归并排序
分解:将列表进行分解,直到每个子列表只有一个元素
终止条件: 子列表之后一个元素,一个元素是有序的
合并:将两个有序列表合并,直到合并索引的子列表
python实现归并排序:
import random
from typing import List
def merge(values: List, low: int, high: int, mid: int):
"""
一次归并实现将两段有序列表变得有序
Args:
values: 两段有序列表
mid: 第一段有序列表的最后一个元素的索引
Returns:
"""
i = low # 第一段有序列表的起始索引
j = mid + 1 # 第二段有序列表的起始索引
ordered_values = []
while i <= mid and j <= high:
if values[i] < values[j]:
ordered_values.append(values[i])
i += 1
else:
ordered_values.append(values[j])
j += 1
else: # 将两段剩余的元素加入结果列表
while i <= mid: # 将第一段有序列表的剩余元素加入结果列表
ordered_values.append(values[i])
i += 1
while j <= high: # 将第二段有序列表剩余元素加入结果列表
ordered_values.append(values[j])
j += 1
# 另一种实现
# ordered_values.extend(values[i:mid + 1])
# ordered_values.extend(values[j:high + 1])
values[low:high + 1] = ordered_values
def merge_sort(values: List, low: int, high):
"""
归并排序实现:
1. 将左半段列表进行归并排序,变得有序
2. 将you半段列表进行递归排序, 变得有序
3. 合并左右半段数组
4. 递归调用1-3步
递归终止条件: 列表少于两个元素
Args:
values: 列表
low: 起始索引
high: 结束索引
Returns:
"""
if low < high: # 列表至少有两个元素
mid = (low + high) // 2
merge_sort(values, low, mid)
merge_sort(values, mid + 1, high)
merge(values, low, high, mid)
print(values[low:high + 1])
if __name__ == "__main__":
values = list(range(8))
random.shuffle(values)
print(values)
merge_sort(values, 0, len(values) - 1)
print(values)
递归排序时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
python内置的排序算法sort()是基于归并排序实现的
快排、堆排序和归并排序比较
- 三种排排序算法的时间复杂度都是 O ( n l o g ) O(nlog) O(nlog)
- 一般情况下,就运行速度而言:快速排序 < 归并排序 < 堆排序
- 三种排序算法的缺点:
- 快速排序在极端情况下效率低
- 归并排序需要额外的内存消耗
- 堆排序在相对快的排序算法中较慢
其他排序算法
-
希尔排序
-
计数排序
-
基数排序