目录
排序和查找作为典型计算机科学问题,也是算法学习中的基础.今天,我们一起来看看10种常见的排序算法的实现.
排序:排序是指将集合中的元素按照某种顺序(升序/降序)排列的过程
排序算法分类:内排序和外排序
这里有一个误区,这里的内外排序,要和原地排序/占用额外空间相区分
内外:是指排序仅使用内存还是要使用外存的数据(如硬盘),外排序主要用于带排序数据量很大,无法仅使用内存就完成排序,研究的问题时排序过程中内存和外存数据交换的问题,本文的排序算法都属于内排序
原地排序/占用额外空间:排序过程中是否使用除给定序列占用空间以外的内存空间
思维导图
一.3种基本排序算法
01冒泡排序
动图演示
代码示例
'''冒泡排序
思想:扫描n-1轮
第一趟两两比较相邻记录,反序则交换,可以将最大的元素,交换到最后的位置
第二趟....可以将第二大的元素,交换到最后的位置
...
算法分析:
1\时间复杂度:
最好:O(n)
最坏:O(n**2)
平均:O(n**2)
2\空间复杂度:
O(1)
3\ 稳定性:
稳定排序'''
# 优化思路:当一趟没有交换元素时,表示排序完成,打断循环
def bubbleSort(lis):
for i in range(len(lis)-1): # 趟数
state = True # 用于判断有没有发生交换
for j in range(len(lis)-1-i): # 每趟交换的次数
if lis[j] > lis[j+1]:
lis[j],lis[j+1] = lis[j+1],lis[j]
state = False
if state:
break
lis = [1,6,3,9,2,6,]
bubbleSort(lis)
print(lis)
02直接插入排序
动图演示
代码示例
'''直接插入排序
思想:扫描n-1轮
假设第一个元素已排好队,从第二个元素开始扫描,按正确的位置将元素依次插入到有序序列
算法分析:
1\时间复杂度:
最好:O(n)
最坏:O(n**2)
平均:O(n**2)
2\空间复杂度:
O(1)
3\ 稳定性:
稳定排序'''
def insertion_sort(alist):
for i in range(1,len(alist)): # 扫描的轮数 从第二个元素开始扫描
j = i -1
tmp = alist[i] # 待插入的记录
while j >= 0 and tmp < alist[j]: # 需要移动的次数
alist[j+1] = alist[j]
j -= 1
alist[j+1] = tmp
print('第{}轮排序的结果为:{}'.format(i, alist))
lis = [2,5,3,7,9]
insertion_sort(lis)
print(lis)
03简单选择排序
动图演示
代码示例
'''选择排序
思想:扫描n-1轮
每一趟选出最小的元素,放在已排好序的序列的后面.
算法分析:
1\时间复杂度:
最好:O(n**2)
最坏:O(n**2)
平均:O(n**2)
2\空间复杂度:
O(1)
3\ 稳定性:
不稳定排序'''
def selection_sort(alist):
for i in range(len(alist)-1): # 扫描轮数
min_index = i
for j in range(i+1,len(alist)):
if alist[j] < alist[min_index]:
alist[j],alist[min_index] = alist[min_index],alist[j]
print('第{}轮排序的结果为:{}'.format(i+1,alist))
lis = [2,5,9,3,7]
selection_sort(lis)
print(lis)
二.基本排序算法的改进
04快速排序
动图演示
代码示例
'''快速排序
思想:
1.快速排序属于交换类排序
2.该算法运用分治的思想
1>分而治之 把大问题化为子问题
2>子问题和大问题的解决方案类似
3>存在最小子问题
4>合并子问题的答案得到大问题的答案
3.步骤
1>找到带排序序列的基准值
一般选取第一个元素
也可通过三数取中法确定(首中尾)
2>将原序列分割为比基准值小的子区间和比基准值大的子区间
3>分别对子区间递归调用快速排序
4>递归边界:
子区间只有一个元素或子区间为空
5>固定基准值的方法:
1)选取第一个元素为 pivot = alist[start]
2)左右指针分别指向第二个元素和最后一个元素
left = start + 1 /
right = last
3)只要左右指针不交叉
~right持续-1,遇到比基准值小的元素,停止
~left持续+1,遇到比基准值大的元素,停止
~交换left和right
4)交换基准值和right位置的元素
算法分析:
1\时间复杂度:
平均:O(nlogn)
最好:O(nlogn)
最坏:O(n**2)
2\空间复杂度:
最好: O(logn) 递归栈占用的空间
最坏: O(n) tmp占用的空间
3\ 稳定性:
不稳定排序'''
import random
def quickSort(alist, start, end):
if start < end:
pos = partition(alist, start, end) # 返回基准值位置
quickSort(alist, start, pos - 1) # 递归
quickSort(alist, pos + 1, end)
def partition(alist, start, end):
index = start # 基准值索引
pivot = alist[start] # 基准值
left = start + 1
right = end
find = False
while not find:
while left <= right and alist[right] >= pivot: # 先移动右指针
right -= 1
while left <= right and alist[left] <= pivot:
left += 1
if left < right:
alist[left], alist[right] = alist[right], alist[left] # 交换左右元素
else:
find = True
alist[index], alist[right] = alist[right], alist[index] # 交换基准值和right
return right
if __name__ == '__main__':
lis = list(range(10))
random.shuffle(lis)
print(lis)
quickSort(lis, 0, len(lis) - 1)
print(lis)
05希尔排序
图片演示
代码示例
'''希尔排序
思想:
1.希尔排序是对直接插入排序的改进,改进的思想是直接插入排序在数据元素较少\元素基本有序的情况下复杂度较低
2.希尔排序通过对元素分组,然后对每一组元素应用插入排序.分组的依据是按增量分组
3.增量选取的原则:
1>最后一个增量必为1
2>避免增量互为倍数
4.希尔排序效率依赖增量的选取
len // 2 (一般选取方法)
算法分析:
1\时间复杂度:
最好:O(n)
最坏:O(n**2)
平均:O(n**1.5)
2\空间复杂度:
O(1)
3\ 稳定性:
不稳定排序'''
def shell_sort(alist):
lengh = len(alist)
gap = lengh // 2 # 选取增量
while gap >= 1:
for i in range(gap,lengh):
'''插入排序'''
j = i - gap
tmp = alist[i]
while j >= 0 and tmp < alist[j]:
alist[j+ gap] = alist[j]
j -= gap
alist[j+gap] = tmp
gap //= 2
lis = [2,5,3,7,9,-4,4,10,20,53,130]
shell_sort(lis)
print(lis)
06堆排序
动图演示
代码示例
'''堆排序
思想:
1.堆排序属于选择类排序 是直接选择排序的升级版
2.元素之间只比较一次
3.步骤
1>建堆(升序建最大堆)
根据待排序的序列使用build函数建堆
2>堆顶元素(首)和尾交换,则当前堆中最大的元素放在了正确的位置上
3>调整堆,做logn次下沉
4>重复2.3两步,直到只有1个元素,将它放到正确的位置,排序结束
算法分析:
1\时间复杂度:
平均:O(nlogn)
最好:O(nlogn)
最坏:O(nlogn)
2\空间复杂度:
O(1)
3\ 稳定性:
不稳定排序'''
def shiftDown(heapList,index,lengh):
'''下沉元素,比较根节点和左右儿子节点,并和较大的交换
步骤:
1.如果左儿子存在 即左儿子的索引在列表的索引范围内,则循环执行一下操作
2.如果左儿子大于根,同时大于右儿子,则交换根和左儿子
3.如果右儿子存在,并大于左儿子,则交换根和右儿子
'''
while 2 * index + 1 < lengh:
left = 2 * index + 1
right = 2 * index + 2
if (right > lengh-1 or (right <= lengh-1 and heapList[left] > heapList[right])) \
and heapList[left] > heapList[index]:
heapList[left], heapList[index] = heapList[index], heapList[left]
index = left
elif right <= lengh-1 and heapList[right] > heapList[left] and heapList[right] > \
heapList[index]:
heapList[right], heapList[index] = heapList[index], heapList[right]
index = right
else:
break
def buildHeap(Lis):
'''根据已有列表新建一个堆
直接在列表上进行下沉操作,以满足堆的特点'''
mid = len(lis) // 2 - 1
while mid >= 0:
shiftDown(lis,mid,len(lis))
mid -= 1
def heap_sort(alist):
buildHeap(alist)
heap_sort_helper(alist,len(alist))
def heap_sort_helper(alist,lengh):
while lengh > 1:
alist[0],alist[lengh-1] = alist[lengh-1],alist[0]
lengh -= 1
shiftDown(alist,0,lengh)
if __name__ == '__main__':
lis = [2,5,7,8,9,3,4,6]
heap_sort(lis)
print(lis)
三.归并类
07归并排序
图片演示
代码示例
'''归并排序
思想:
1.归并排序属于归并类排序算法
2.该算法运用分治的思想
1>分而治之 把大问题化为子问题
2>子问题和大问题的解决方案类似
3>存在最小子问题
4>合并子问题的答案得到大问题的答案
3.将带排序的数据分成两个子区间
1>左子区间 [left,mid]
2>右子区间 [mid+1,right]
3>分别对左右子区间运用归并排序
4>当left == right时,即子区间中只有一个元素时,作为最小子问题(一个元素已经排好序)
5>合并子区间:
1)额外的存储空间 tmp =[]
2)比较两个子区间的头元素,将小的放入tmp
3)如果其中一个子区间的元素取完, 则将另一个子区间剩下的元素依次追加进tmp
算法分析:
1\时间复杂度:
平均:O(nlogn)
2\空间复杂度:
O(n) tmp占用的空间
3\ 稳定性:
稳定排序
4\ 精简排序:
两个元素最多比较一次,没有重复比较'''
import random
def mergeSort(alist,left,right):
if left == right:
return
else:
mid = (left + right) //2
mergeSort(alist,left,mid)
mergeSort(alist,mid+1,right)
merge(alist,left,mid,right) # 和并
def merge(alist,left,mid,right):
i = left
j = mid + 1
tmp = []
while i <= mid and j <= right:
if alist[i] <= alist[j]:
tmp.append(alist[i])
i += 1
else:
tmp.append(alist[j])
j += 1
while i <= mid:
tmp.append(alist[i])
i += 1
while j <= right:
tmp.append(alist[j])
j += 1
alist[left:right+1] = tmp # 将归并的结果替换原alist中对应的元素
if __name__ == '__main__':
lis = list(range(1,50,5))
random.shuffle(lis)
# lis = [46, 31, 6, 26, 41, 21, 11, 16, 1, 36]
print(lis)
mergeSort(lis,0,len(lis)-1)
print(lis)
四.不比较元素大小的排序
08计数排序
图片演示
代码示例
'''计数排序
思想:
1.不比较元素大小
2.空间换时间
3.适合数据量大,数据范围小的数据排序.
如 年龄\成绩等
4.步骤
1>找带排序序列的最大值k
2>新建一个容量为k+1的计数列表count_list并将所有元素赋初值0
3>遍历待排序序列,将遍历到的元素在count_list中对应的索引位置的值+1
遍历完以后count_list中索引i的值j 表示i的个数为j
4>新建结果列表
5>遍历count_list,依次添加j个i.排序完成
算法分析:
1\时间复杂度:
O(n+k) k为元素取值的范围
2\空间复杂度:
O(n+k)
3\ 稳定性:
稳定排序'''
def count_sort(alist):
if len(alist) < 2:
return alist
res = []
max_value = max(alist)
count_list = [0] * (max_value + 1)
for ele in alist:
count_list[ele] += 1
for i,j in enumerate(count_list):
res += [i] * j
return res
def count_sort2(alist): # 优化计数排序
'''确定元素的范围,减少空间浪费'''
if len(alist) < 2:
return alist
res = []
max_value = max(alist)
min_value = min(alist)
count_list = [0] * (max_value - min_value + 1)
for ele in alist:
count_list[ele - min_value] += 1
for i,j in enumerate(count_list):
res += [i+min_value] * j
return res
if __name__ == '__main__':
# lis = [2,4,3,1,5,6,7,4,5,3,2,1,2,4,6,8,9,10]
# lis = [1]
lis = ['a','e','z','o','f']
print(count_sort(lis))
print(count_sort2(lis))
09桶排序
图片演示
代码示例
'''桶排序
思想:
1.将带排序的数据按照一定的规律,分配到不同的桶中
2.对每一个桶内的元素执行其他的排序算法
3.将各子桶中有序的数据合并成一个大的有序序列,完成排序
4.桶的确定:
1>尽可能地均匀分配
2>空间足够的情况下,尽可能多分桶
3>一种分桶方案>>>(max-min) //length + 1
4.步骤
1>确定桶的数量 bucket_nums = (max_ele - min_ele)//length + 1
2>创建桶二维列表 buckets = [[] for _ in range(bucket_nums)]
3>分配元素进桶
4>桶内排序
5>合并各桶数据
算法分析:
1\时间复杂度:
最好:O(n)
最坏:O(n**2)
平均:O(n+k) k为桶的个数
2\空间复杂度:
O(n+k)
3\ 稳定性:
稳定排序'''
def bucket_sort(alist):
length = len(alist)
if length < 2:
return
else:
bucket_nums = (max(alist) - min(alist))//length+1 # 确定桶的数量
buckets = [[] for _ in range(bucket_nums)] # 创建空桶
# 分配元素
for ele in alist:
buckets[(ele - min(alist))//length].append(ele)
# print(buckets)
# 桶内排序
for bucket in buckets:
bucket.sort()
# print(buckets)
# 合并
index = 0
for bucket in buckets:
for value in bucket:
alist[index] = value
index += 1
if __name__ == '__main__':
lis = [12,4,3,31,5,66,7,4,75,3,2,1111,2,4,6,8,9,210]
bucket_sort(lis)
print(lis)
10基数排序
图片演示
代码示例
'''基数排序
思想:
1.和计数\桶排序都属于分配收集排序的一种
2.不比较元素大小,用空间换时间
3.基数排序桶按排序位的可能取值来确定,如对数字的'个位'\'十位'等则选取10个桶
4.最大数字有几位,则进行几趟分配与收集,每趟趟分配与收集对某一位的数字排好序
算法分析:
1\时间复杂度:
O(n*k) k为桶的个数
2\空间复杂度:
O(n+k)
3\ 稳定性:
稳定排序'''
def radix_sort(alist):
length = len(alist)
if length < 2:
return
else:
nums = len(str(max(alist)))
for i in range(nums):
buckets = [[] for _ in range(10)] # 创建空桶
for ele in alist: # 分配
buckets[ele//10**i % 10].append(ele)
# print(buckets)
index = 0
for bucket in buckets: # 收集
for value in bucket:
alist[index] = value
index += 1
if __name__ == '__main__':
lis = [20,101,39,431,58,600,8,4,999,634,157,199,208,138,389,691,400,932,856,843,401,923]
radix_sort(lis)
print(lis)
五.总结
平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
关于稳定性
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
稳定性记忆:不稳定的只有快些堆!(快速\希尔\堆),另外堆是选择排序的特例,所以再加一个选择排序
参考:
1.1.0 十大经典排序算法 | 菜鸟教程 (runoob.com)
2.dingdangcode算法学习
3.图片来源于网络
35岁学Python,也不知道为了啥?