[算法整理]排序算法
排序算法汇总
总结了各种排序算法,代码由python实现。
冒泡排序
依次比较两个邻居,如果前者比后者大,则交换顺序,如此遍历k次,则最后k个元素处于正确的顺序,因此,遍历n次即可解决问题;
def bubble(ll: List) -> List:
n = len(ll)
for i in range(n):
for j in range(n - i - 1):
if ll[j] > ll[j + 1]:
tem = ll[j]
ll[j] = ll[j + 1]
ll[j + 1] = tem
return ll
遍历次,第次遍历的时候,进行了次对比,时间复杂度是,没有额外的使用空间,因此空间复杂度为,在判断的时候,如果等于就不更改两个数字,所以是稳定排序;
另外,冒泡排序可以用来求一个数组中的逆序对,更换的次数就是逆序对的个数;
插入排序
遍历数据,每次将数据插入到前面有序数组中正确的位置;
def insert(ll: List) -> List:
for i in range(1, len(ll)):
v = ll[i]
j = i - 1
while j >= 0:
if ll[j] > v:
ll[j + 1] = ll[j]
j -= 1
else:
break
ll[j + 1] = v
return ll
个数据,每次插入,移动数据的平均时间复杂度是,所以,总的时间复杂度是,空间复杂度是,每次插入,后插进来的元素放在所有相等元素的后面,便可保证排序的稳定;
选择排序
遍历数组,每次选择一个最小的和当前数字进行交换;
def select(ll: List) -> List:
n = len(ll)
for i in range(n):
m = i
for j in range(i, n):
if ll[j] < ll[m]:
m = j
tem = ll[i]
ll[i] = ll[m]
ll[m] = tem
return ll
个数据,每次寻找最小值平均时间复杂度,总复杂度为,空间复杂度,不稳定,会更改同元素的顺序;
快速排序
主要思路是选择一个元素,然后将该数组分为三个部分:小于的元素,,大于的元素;然后对小于的元素和大于的元素进行排序,元素则处于正确的位置;
主要的难点在于区间分组,算法操作如下:
- 选择随机一个元素作为锚点,然后将该元素放到最后一个位置(下面的代码直接采用最后一个元素作为锚点)
- 设置两个指针,其中一个指针指向的位置为锚点应该处于的位置,另一个指针遍历除了锚点以外的所有元素:
- 如果该元素小于锚点,则应该和所指元素交换位置,同时令指向下一个位置;
- 最终交换锚点和所指位置的元素即可;
# 区间都是前开后闭
def quick_sort(ll: List) -> List:
def quick(nums: List, p, q):
if q - p <= 1:
return
r = quick_partition(nums, p, q)
quick(nums, p, r)
quick(nums, r+1, q)
quick(ll, 0, len(ll))
return ll
def quick_partition(ll: List, p, q):
v = ll[q - 1]
# i,j两个指针,i所指位置之前,为小于锚点的元素
i, j = p, p
while j < q - 1:
if ll[j] < v:
tem = ll[i]
ll[i] = ll[j]
ll[j] = tem
i += 1
j += 1
tem = ll[i]
ll[i] = v
ll[q - 1] = tem
return i
时间复杂度,极端的情况下,如果每次选择的锚点都恰好在端点处,那么每次归位的就一个锚点,所以,最终的时间复杂度为,除了极端情况,其他的时候,复杂度为,空间复杂度为,另外,不是稳定排序;
寻找第K大元素
def find_kth_largest(nums: List[int], k: int) -> int:
t = len(nums) - k
return nums[self.find(nums, 0, len(nums), t)]
def find(nums: List[int], p, q, t):
while q-p>1:
i = self.partition(nums, p, q)
if i==t:
return i
if i<t:
p=i+1
else:
q=i
return p
def partition(nums: List[int], p, q):
v = nums[q - 1]
i, j = p, p
while j < q - 1:
if nums[j] < v:
self.swap(nums, j, i)
i += 1
j += 1
self.swap(nums, i, q - 1)
return i
def swap(nums, i, j):
tem = nums[i]
nums[i] = nums[j]
nums[j] = tem
利用快速排序,每次返回的锚点如果恰好是第K大元素,则直接返回,如果不是,则缩小区间后搜索;
归并排序
主要的思想为分而治之,每次将整个区间均分为两份,然后在分别对两个区间调用排序算法,然后再对两个有序区间进行合并,递归的平凡情况,当区间内仅有一个元素的时候,直接返回;
def merge_sort(ll: List) -> List:
def _merge_sort(nums, p, q):
if q - p <= 1:
return
r = p + ((q - p) >> 1)
_merge_sort(nums, p, r)
_merge_sort(nums, r, q)
res = merge(nums, p, r, r, q)
for index, num in enumerate(res):
nums[index + p] = num
_merge_sort(ll, 0, len(ll))
return ll
def merge(ll: List, p, q, m, n):
temp = []
while p < q and m < n:
if ll[p] <= ll[m]:
temp.append(ll[p])
p += 1
else:
temp.append(ll[m])
m += 1
while p < q:
temp.append(ll[p])
p += 1
while m < n:
temp.append(ll[m])
m += 1
return temp
每次递归调用都是减去了一半的元素,时间复杂度计算公式为:,于是时间复杂度为,由于合并的时候需要用到临时数组,于是空间复杂度为
多路归并中的合并算法
问题的关键是需要在多路归并中快速寻找到最小值,通常的方法,对路归并而言,每次获取最小值的时间复杂度为,也可以通过最小堆的方式获取,建堆的时间复杂度为,但是每次获取最小值的复杂度为,具体的方案在堆排序里会将,还有一种方案就是败者树/胜者树,同样建树的时候复杂度为,获取最小值的复杂度为;
class LoserTree:
def __init__(self):
"""
初始化,数据和数据索引共同构成了这个败者树
"""
# 初始的数据数组(用来构建败者树的)
self.data = []
# 构建起来的败者树,用数组表示,数组内记录的是数据数组对应的索引,其中0号元素代表整个败者树的胜利者的索引
self.inner = []
def build(self, lists):
"""
构建一个败者树,列表内元素需要能够比较大小
:param lists: 要构建的列表内元素
:return:
"""
self.inner = [-1] * len(lists)
self.data = lists
self.inner[0] = self.__build(1)
def __build(self, index):
"""
语义:在这个索引处构建败者树,返回后续构建起来的胜利者的数据的索引
:param index: 在某个索引处
:return: 构建起来的树的胜利者
"""
if index >= len(self.data):
return index - len(self.data)
left = index << 1
right = index << 1 + 1
left_win = self.__build(left)
right_win = self.__build(right)
win, loser = self.play_game(left_win, right_win)
self.inner[index] = loser
return win
def play_game(self, l1, l2):
"""
比较两个元素,以小的那个作为胜利者,返回这次比较的胜者和败者,对于空元素,直接视为败者
:param l1:
:param l2:
:return:
"""
if self.data[l1] is None:
return l2, l1
if self.data[l2] is None:
return l1, l2
if self.data[l1] < self.data[l2]:
return l1, l2
else:
return l2, l1
def replay(self, l1):
"""
将整棵树的胜利者置换为对应的数据节点,从而重新构建败者树:每次仅需要和上次比赛的败者(父节点)进行比赛即可,逐次向上
:param l1:
:return:
"""
win = self.inner[0]
self.data[win] = l1
p = (win + len(self.data)) >> 1
while p > 0:
winner, loser = self.play_game(self.inner[p], win)
self.inner[p] = loser
win = winner
p = p >> 1
self.inner[0] = win
def empty(self):
"""
判空
:return:
"""
return self.get_winner() is None
def get_winner(self):
"""
获取整颗树的胜利者
:return:
"""
if self.inner:
return self.data[self.inner[0]]
首先明确一点,数据和索引数组,两个数组共同构成了一颗满二叉树,同时,索引数组中的第0个元素记录了最终的胜者索引,而第一个元素则是整个树的根,于是左右子树的索引有:
left = index << 1
right = index << 1 + 1
的关系(将两个数组并入为一个数组,数据数据在后,因此所有的数据数组的索引均需要加上len(data)才能对标到树中的数组索引)
构建败者树时的模拟:
对于导出最小值,再插入一个新数据的情况的模拟:
对于胜者树,同样是类似的构建方案,只是,每个节点放置的是胜者的索引,所以,并不需要额外的一个节点来存储最终的胜利者,并且,如果某个节点发生的更改,则需要从这个节点开始,依次重新比赛,向根节点更新胜者索引;
计算逆序对
归并排序过程中可以计算逆序对
class Solution:
def __init__(self):
self.res = 0
def reversePairs(self, nums: List[int]) -> int:
self.merge_sort(nums,0,len(nums))
return self.res
def merge_sort(self,nums:List[int],p,q)->int:
if q-p<=1:
return
r=p+((q-p)>>1)
self.merge_sort(nums,p,r)
self.merge_sort(nums,r,q)
res = self.merge(nums,p,r,r,q)
for index,num in enumerate(res):
nums[index+p]=num
def merge(self,ll:List[int],p,q,m,n):
temp=[]
while p<q and m<n:
if ll[p]<=ll[m]:
temp.append(ll[p])
p+=1
else:
temp.append(ll[m])
# 统计逆序对的个数
self.res+=(q-p)
m+=1
while p<q:
temp.append(ll[p])
p+=1
while m<n:
temp.append(ll[m])
m+=1
return temp
在归并排序的过程中,归并中,后面的数字小于前面的情况,那么逆序对就存在对应对,这个数量等于前面那个数组还剩余的元素个数;
堆排序
最大堆和最小堆,利用最大堆进行排序,每次将最大值放到数组最后,然后对前个值进行重新堆化;
建堆的时间复杂度是,排序的复杂度是,空间复杂度是,非稳定排序;
class MaxHeap:
def __init__(self, ll: List[int]):
self.data = ll
self.size = len(self.data)
self.__init_heap()
def __init_heap(self):
"""
初始化堆
从第一个非叶节点开始,逐个进行堆化,从上到下,时间复杂度为o(n)
:return:
"""
i = (len(self.data) >> 1) - 1
while i >= 0:
p = i
while True:
max_index = p
r = (p << 1) + 1
l = (p << 1) + 2
if r < self.size and self.data[r] > self.data[p]:
max_index = r
if l < self.size and self.data[l] > self.data[max_index]:
max_index = l
if max_index == p:
break
swap(self.data, max_index, p)
p = max_index
i -= 1
def get_max(self):
if self.data:
return self.data[0]
def insert(self, d: int):
"""
插入一个节点,直接插在末尾,从下到上进行优化,时间复杂度o(logn)
"""
self.size += 1
last=self.size-1
if last<len(self.data):
self.data[last]=d
else:
self.data.append(d)
i = last
while i > 0:
p = (i - 1) >> 1
if self.data[p] < self.data[i]:
swap(self.data, i, p)
i = p
def pop_max(self):
"""
删除顶端元素,读取顶端元素后,将末尾元素放到顶端,然后从上到下进行堆化
"""
if self.size > 0:
res = self.data[0]
self.data[0] = self.data[self.size - 1]
self.size -= 1
p = 0
while True:
r = (p << 1) + 1
l = (p << 1) + 2
max_index = p
if r < self.size and self.data[p] < self.data[r]:
max_index = r
if l < self.size and self.data[max_index] < self.data[l]:
max_index = l
if max_index == p:
break
swap(self.data, p, max_index)
p = max_index
return res
def sort(self) -> List[int]:
"""
堆排序,原地排序,排序后整个堆无法再次使用,这里仅做演示
:return:
"""
while self.size > 0:
last = self.size - 1
self.data[last] = self.pop_max()
return self.data
和快速排序相比并不够好,跳跃式的数据访问对CPU不够友好,同时对于同样的数据,数据交换的次数也是明显对于快速排序的;
使用堆寻找中位数
对于一组动态数据,输出它们的中位数,数据随着时间会增加;
思路:
- 对初始数据进行排序
- 数据分为两个部分,前一半构建最大堆,后一半构建最小堆,如果数据个数为奇数,另最大堆的数据多一个
- 获取中位数只要获取最大堆的堆顶即可
- 如果有数据加入,则判断数据小于最大堆的堆顶,则加入到最大堆中,否则加入最小堆中,然后调整两个堆的数据数量,令其满足各一半的条件
桶排序
核心思想就是将数据的划分到天然具备顺序的几个桶里,然后再桶内进行排序(快速排序或者归并排序),排序后再将数据依次放回原数组,要求数据范围有限,同时,如果划分的桶的数量接近数据的总数量,且能令各个桶的数量均匀,则排序的时间复杂度为,一般,例如知道数值在之间,则可以直接列出一个大小为50的数组,数值为其索引,对于相同的值采用链表的方式记录,这样空间复杂度为,同时可以保证相同元素的顺序,所以可以是稳定排序。
def bucket_sort(ll: List) -> List:
# 假设数据均在0-50之间的整数,桶表示的数值为:0-9,10-19,20-29,30-39,40-49
bucket = [[] for _ in range(5)]
for i in ll:
bucket[int(i / 10)].append(i)
# 对每个桶进行排序
res = []
for l in bucket:
quick_sort(l)
res += l
return res
计数排序
桶排序的特殊情况,桶的个数和元素数据范围相同,然后像上述所说使用链表来存储相同的元素,另外还有一种方案,就是通过记录相同元素的数量,然后遍历,将元素放到对应的位置;
def count_sort(ll: List) -> List:
# 假设数据范围是0-6
count = [0] * 7
for i in ll:
count[i] += 1
# 累加
for i in range(1, 7):
count[i] = count[i - 1] + count[i]
res = [None] * len(ll)
i = len(ll) - 1
while i >= 0:
index = count[ll[i]] - 1
res[index] = ll[i]
count[ll[i]] -= 1
i -= 1
ll.clear()
ll.extend(res)
return res
基数排序
主要的思路,对要排序的数据,从最低位开始排序,对每一位都进行一次排序,最终得到整个有序数组,适合那些可以分开位数的数据,例如数字,字符串(字典序)等,对于数据位数不一致的情景,可以在高位补0;
单个位的排序需要是稳定排序,这里采用了计数排序,时间和空间复杂度均为;
def count_sort_radix(ll: List, data_get) -> List:
# 假设数据范围是0-9
count = [0] * 10
for i in ll:
count[data_get(i)] += 1
# 累加
for i in range(1, 10):
count[i] = count[i - 1] + count[i]
res = [None] * len(ll)
i = len(ll) - 1
while i >= 0:
index = count[data_get(ll[i])] - 1
res[index] = ll[i]
count[data_get(ll[i])] -= 1
i -= 1
ll.clear()
ll.extend(res)
return res
def radix_sort(ll: List) -> List:
# 基数排序,从最低位开始进行排序,每次使用计数排序
# 假设输入的是列表的列表,即元素已经被拆成了单独位数的列表了,且各个元素的位数相同
if ll:
n = len(ll[0])
while n > 0:
n -= 1
count_sort_radix(ll, lambda i: i[n])
return ll
if __name__ == '__main__':
print(radix_sort([[1,1,0],[2,5,1],[2,3,0],[0,1,4],[0,3,4],[5,5,1]]))
希尔排序
思路:选择一个增量序列,然后将依次将这个数组视作等价的对应列数的二位数组,例如选择增量序列:1,2,3,5,8,13,将数组视作等价的13列的二维数组,对每一列进行排序(在同一列的元素进行部分排序),然后再讲数组试做8列的二维数组,再对每一列进行部分排序,...直到最终试做1列的二维数组,进行了全排序;
每次排序后都会增加序列的有序度,而如果采用输入敏感的插入排序算法进行部分排序操作,在最终的排序中,由于已经达到了一定的有序度,所以时间复杂度会降低;
采用不同的序列,时间复杂度也不一样,若采用的序列是Papernov-Stasevic序列:,采用插入排序作为迭代的排序算法,可以证明,整个希尔排序的时间复杂度优化为
def insert_shell(ll: List, indexes: List[int]) -> List:
"""
对选定的几个元素进行排序,indexes是一个升序的索引序列,指定了ll中的要排序的索引
:param ll:
:param indexes:
:return:
"""
for i in range(1, len(indexes)):
# 有序的索引
j = i - 1
v = ll[indexes[i]]
while j >= 0:
if ll[indexes[j]] > v:
ll[indexes[j + 1]] = ll[indexes[j]]
j -= 1
else:
break
ll[indexes[j + 1]] = v
return ll
def shell_sort(ll: List):
# 递增序列采用1,2,3,5,8,13,。。。
n = len(ll)
if n < 3:
return insert_shell(ll, [i for i in range(n)])
h = [1, 2]
t = 3
while n > t:
h.append(t)
t = h[-1] + h[-2]
i = len(h)
while i > 0:
i -= 1
w = h[i]
# 将数组试做宽度为w的二维数组,然后对每一列进行排序
for j in range(w):
# 获得每一列的索引
indexes = []
index = j
while index < n:
indexes.append(index)
index += w
# 对第j列进行插入排序
insert_shell(ll, indexes)
return ll
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端