文章目录
0 前言
在算法面试时,一般都会被面试官三连问,"这算法的时间复杂度是怎样的?空间复杂度呢?它的稳定性如何?” ,为了能够清晰的作答该类问题,我梳理了基础的排序算法。掌握好这些经典方法,就可以做到在排序类的coding中游刃有余,加深对该类问题的理解。
1 算法部分
在学习前,先对排序的分类有个大致的了解,可以分为比较类排序和非比较类排序
比较类排序有:
- 交换类排序:冒泡排序、快速排序
- 插入类排序:简单插入排序、希尔排序
- 选择类排序:简单选择排序、堆排序
- 归并排序
非比较类排序有:
- 计数排序
- 桶排序
- 基数排序
理解算法的稳定性
定义:存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
case:序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
理解原地排序概念
指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序,空间复杂度为O(1);需要借助多余空间的排序为非原地排序,其空间复杂度通常大于O(1),归并排序、计数排序、桶排序、基数排序都属于非原地排序。
1.1 冒泡排序
冒泡排序的算法思想是,重复地遍历数据,依次比较相邻元素的大小,如果它们的顺序错误就进行交换,直到所有数都不再发生交换的时候退出遍历,此时排序完成。
def sort_buble(lists):
for i in range(len(lists)-1): # n个数,比较只需n-1次
flag = 0 # 标记是否进入内循环
for j in range(len(lists)-1-i):
if lists[j]>lists[j+1]:
lists[j], lists[j+1] = lists[j+1], lists[j]
flag = 1
if flag == 0: # 如果一次也没有进入内循环,则表示数据已经排序完成,提前退出
break
return lists
根据代码可以得知冒泡排序的时间复杂度为O(n2),空间复杂度为O(1),该算法是稳定的。
1.2 快速排序
快速排序是对冒泡排序的一种改进,算法思想是,先在数据中找到一个基准数(通常取第一个元素),并按此基准数将数据分为两部分,其中小于基准数的数据放在左边,大于基准数的数据放在右边,再以同样的方式分别处理左右两边的数据,直到排完序为止。
def sort_quick(lists,i,j):
if i >= j:
return lists
pivot = lists[i]
low = i
high = j
while i < j:
while i < j and lists[j] >= pivot:
j -= 1
lists[i]=lists[j]
while i < j and lists[i] <=pivot:
i += 1
lists[j]=lists[i]
lists[j] = pivot
quick_sort(lists,low,i-1)
quick_sort(lists,i+1,high)
return lists
根据代码可以得知快速排序的时间复杂度为O(nlogn),最坏的情况下,时间复杂度是O(n2),空间复杂度为O(1),该算法是不稳定的。
1.3 简单插入排序
简单插入排序的算法思想是,摸牌,默认已插入的数据都是排好序的,将待插入的数与已插入的数据逐个比较,如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
def sort_insert(lists):
for i in range(1, len(lists)):
j = i - 1
temp = lists[i]
# 将第i个数据与前面排好序的数据逐个比较
while j >= 0 and temp < lists[j]:
lists[j+ 1] = lists[j]
j -= 1
lists[j + 1] = temp
return lists
根据代码可以得知该排序的时间复杂度为O(n2),空间复杂度为O(1),该算法是稳定的。
1.4 希尔排序
希尔排序是对简单插入排序的一个改进,算法思想是,将数据区分成特定区间的几个小块,用插入排序法排完区块内的数据后,在渐渐减少间隔距离。
def sort_shell(lists):
gap = len(lists)//2
while gap != 0:
# 插入排序
for i in range(gap, len(lists)):
j = i-gap
temp = lists[i]
while j >= 0 and temp < lists[j]:
lists[j+gap] = lists[j]
j = j-gap
lists[j+gap] = temp
# 排序完调整间隔
gap = gap//2
return lists
希尔排序的的时间复杂度跟增量序列的选择有关,时间复杂度为O(n^(1.3~2)),空间复杂度为O(1),该算法是不稳定的。
1.5 简单选择排序
简单选择排序的算法思想是,在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
def sort_selection(lists):
for i in range(0,len(lists)-1):
for j in range(i+1,len(lists)):
if lists[i] <= lists[j]:
continue
else:
lists[i],lists[j] = lists[j],lists[i]
return lists
简单选择排序的的时间复杂度为O(n2),空间复杂度为O(1),该算法是不稳定的。
1.6 堆排序
堆排序的算法思想是,首先将数据建成堆积树,然后循环的将堆积树的树根放到堆的末尾。
# 从最后一棵子树开始向上建立堆积树
def buildMaxHeap(data):
for i in range(len(data) // 2, -1, -1):
heapify(data, i)
# 构建堆积树
def heapify(data, i):
left = 2 * i + 1
right = 2 * i + 2
largest = i
# 判断左子节点是否需要交换
if left < length and data[left] > data[largest]:
largest = left
# 判断右子节点是否需要交换
if right < length and data[right] > data[largest]:
largest = right
if largest != i:
data[i], data[largest] = data[largest], data[i]
# 如果交换了,要继续检查子节点为根的子树是否需要交换
heapify(data, largest)
def sort_heapify(lists):
# 使用堆积树排序
global length
length = len(lists)
buildMaxHeap(lists)
# 将堆积树的树根和最后一个子节点交换,此处用length来控制不考虑最后一个子节点
for i in range(len(lists) - 1, 0, -1):
lists[0], lists[i] = lists[i], lists[0]
length -= 1
# 首尾交换后需要重新构建堆积树
heapify(lists, 0)
return lists
堆排序的时间复杂度为O(nlogn),空间复杂度为O(1),该算法是不稳定的。
1.7 归并排序
归并排序的算法思想是,先使每个子序列有序,再将已有序的子序列合并,得到完全有序的序列,该算法采用了分治思想。
def merge(left, right):
result = []
while left and right:
if left[0] <= right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
if left:
result.extend(left)
if right:
result.extend(right)
return result
def sort_merge(lists):
if len(lists) < 2:
return lists
else:
mid = len(lists)//2
left, right = lists[:mid], lists[mid:]
return merge(sort_merge(left), sort_merge(right))
归并排序的时间复杂度为O(nlogn),由于是非原地排序,空间复杂度为O(n),该算法是稳定的。
1.8 计数排序
计数排序比前面的比较类排序速度都快,其算法思想是,先找到给定数据的最大最小值,创建统计数组并计算统计对应元素个数,然后统计数组变形,最后倒序遍历原始数组,从统计数组中找到正确位置,输出到结果数组。
def sort_count(lists):
# 求最大值最小值
minnum, maxnum = lists[0], lists[0]
for i in range(len(lists)):
if lists[i]<minnum:
minnum = lists[i]
if lists[i]>maxnum:
maxnum = lists[i]
d = maxnum-minnum
# 创建统计数组并计算统计对应元素个数
countArray = [0]*(d+1)
for num in lists:
countArray[num-minnum] += 1
# 统计数组变形
sumnum = 0
for i in range(len(countArray)):
sumnum += countArray[i]
countArray[i] = sumnum
# 倒序遍历原始数组,从统计数组中找到正确位置,输出到结果数组
sortedArray = [0]*len(lists)
for i in range(len(lists)-1, -1, -1):
sortedArray[countArray[lists[i]-minnum]-1] = lists[i]
countArray[lists[i]-minnum] -= 1
return sortedArray
计数排序的时间复杂度为O(n+k),由于是非原地排序,空间复杂度为O(n+k),该算法是稳定的。
1.9 桶排序
桶排序的算法思想是,将数据中处于同一个值域的元素存入同一个桶中,即根据元素值特性将数据拆分为多个桶。对每个桶中元素进行排序,则所有桶中元素构成的集合是已排序的。可看出该算法采用了分治思想。(对桶内排序有可能使用别的排序算法或是以递归方式继续使用桶排序)
桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素。桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效。
def sort_bucket(lists):
min_num, max_num = min(lists), max(lists)
bucketsize = 3
# 桶的大小
bucket_num = (max_num - min_num) // bucketsize + 1
# 桶数组
buckets = [[] for _ in range(int(bucket_num))]
# 向桶数组填数
for num in lists:
buckets[int((num - min_num) // bucketsize)].append(num)
# 桶内部排序直接调用了sorted
new_lists = []
for i in buckets:
for j in sorted(i):
new_lists.append(j)
return new_lists
桶排序的时间复杂度为O(n+k),由于是非原地排序,空间复杂度为O(n+k),该算法是稳定的。
1.10 基数排序
基数排序的算法思想是,先按照低位先排序,然后合并;再按照高位排序,然后再合并;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
def sort_radix(lists):
# 先算出最大数的位数digits,参数maxnum表示最大数字是几位数,如果是三位数将其设置成100
digits = len(str(lists[0]));
for lst in lists:
if len(str(lst)) > digits:
digits =len(str(lst))
maxnum= 10**(digits-1)
# n为基数,从个位数开始排序;
n = 1
while n<=maxnum: #
# 将各个数字按个位(十位/百位)放入表格中
size = len(lists)
temp = [[0]*size for i in range(10)]
for i in range(size):
m = (lists[i]//n)%10
temp[m][i] = lists[i]
# 从表格按照顺序取出数字放入一维数组中
k = 0
for i in range(10):
for j in range(size):
if temp[i][j] != 0:
lists[k] = temp[i][j]
k += 1
n = n *10 #将个位调整为十位/百位
return lists
基数排序的时间复杂度为O(n*k),由于是非原地排序,空间复杂度为O(n+k),该算法是稳定的。
2 总结
综上,对经典的排序逻辑及实现有一个整体的了解,通过分类和比对形成一个清晰的认识,可以通过以下图表来记忆。
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 |
快速排序 | O(n logn) | O(n2) | O(n logn) | O(logn) | 不稳定 |
简单插入排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n log2n) | O(n2) | O(n) | O(1) | 不稳定 |
简单选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
堆排序 | O(n logn) | O(n logn) | O(n logn) | O(1) | 不稳定 |
归并排序 | O(n logn) | O(n logn) | O(n logn) | O(n) | 稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n2) | O(n+k) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
PS:实例代码
#性能测试代码
if __name__=="__main__":
lists=[30,24,5,58,18,36,12,42,39]
print("排序前的序列为:")
for i in lists:
print(i,end =" ")
print("\n排序后的序列为:")
for i in sort_{排序函数的英文}(lists):
print(i,end=" ")
最后的最后
在工作中遇到的问题远比这些算法复杂,算法只是一种工具,只有当你熟练掌握这些工具后,才能在关键的时候想到并应用,使算法成为解决问题的方案之一。文中的实现代码可能不是最优解,欢迎指正、补充。
参考文献
https://blog.csdn.net/weixin_44683255/article/details/111880015
https://www.runoob.com/python3/python3-examples.html
https://zhuanlan.zhihu.com/p/63227573
https://blog.csdn.net/orangerfun/article/details/105297739
https://blog.csdn.net/MobiusStrip/article/details/83785159
https://juejin.cn/post/6844903876550737928