十大排序算法(Python实现)
十种常见排序算法可以分为两大类
非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。执行计算需要消耗的时间长短。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。执行计算需要的内存空间
参考:https://blog.csdn.net/weixin_41571493/article/details/81875088
- 交换排序
1.1 冒泡排序(Bubble Sort)
1.2 快速排序(Quick Sort) - 插入排序
2.1 简单插入排序(Insert Sort)
2.2 希尔排序(Shell Sort)
3.选择排序
3.1 简单选择排序(Select Sort)
3.2 堆排序(Heap Sort) - 归并排序
4.1 二路归并排序(Two-way Merge Sort) - 线性时间非比较类排序
5.1 计数排序(Counting Sort)
5.2 桶排序(Bucket Sort)
5.3 基数排序(Radix Sort)
1. 交换排序
1.1 冒泡排序(Bubble Sort)
基本思想:比较相邻的元素。如果第一个比第二个大,就交换它们两个;小的放前面, 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数。
复杂度:
冒泡排序对n个数据操作n-1轮,每轮找出一个最大(小)值。
操作只对相邻两个数比较与交换,每轮会将一个最值交换到数据列首(尾),像冒泡一样。
每轮操作O(n)次,共O(n)轮,时间复杂度O(n^2)。
额外空间开销出在交换数据时那一个过渡空间,空间复杂度O(1)
Code:
def BubbleSort(lst):
n=len(lst)
if n<=1:
return lst
for i in range (0,n):
for j in range(0,n-i-1):
if lst[j]>lst[j+1]:
(lst[j],lst[j+1])=(lst[j+1],lst[j])
return lst
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in y:
arr.append(int(i))
arr=BubbleSort(arr)
print(arr)
print("数列按序排列如下:")
for i in arr:
print(i,end=' ')
1.2 快速排序(Quick Sort)
基本思想:从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
复杂度:
基准值若能把数据分为平均的两块,划分次数O(logn),每次划分遍历比较一遍O(n),时间复杂度O(nlogn)。
额外空间开销出在暂存基准值,O(logn)次划分需要O(logn)个,空间复杂度O(logn)
Code:
def quick_sort(data):
"""快速排序"""
if len(data) >= 2: # 递归入口及出口
mid = data[0] # 选取基准值,也可以选取第一个或最后一个元素
left, right = [], [] # 定义基准值左右两侧的列表
data.remove(mid) # 从原始数组中移除基准值
for num in data:
if num >= mid:
right.append(num)
else:
left.append(num)
return quick_sort(left) + [mid] + quick_sort(right)
else:
return data
示例:
array = [2,3,5,7,1,4,6,15,5,2,7,9,10,15,9,17,12]
print(quick_sort(array))
输出为[1, 2, 2, 3, 4, 5, 5, 6, 7, 7, 9, 9, 10, 12, 15, 15, 17]
2. 插入排序
2.1 简单插入排序(Insert Sort)
基本思想:• 从第一个元素开始,该元素可以认为已经被排序;
• 取出下一个元素,在已经排序的元素序列中从后向前扫描;
• 如果该元素(已排序)大于新元素,将该元素移到下一位置;
复杂度:简单插入排序操作n-1轮,每轮将一个未排序树插入排好序列。
开始时默认第一个数有序,将剩余n-1个数逐个插入。插入操作具体包括:比较确定插入位置,数据移位腾出合适空位
每轮操作O(n)次,共O(n)轮,时间复杂度O(n^2)。
额外空间开销出在数据移位时那一个过渡空间,空间复杂度O(1)
Code
def insert_sort(data):
l=len(data)
for i in range(1,n):
j=i
while j>0:
if data[j]<data[j-1]:
data[j],data[j-1]=data[j-1],data[j]
j-=1
else:
break
return data
data=[2,4,12,8,34,26,78,1]
p=insert_sort(data)
print(p)
def insertsort(data):
L=len(data)
for i in range(1,L):
for j in range(i):
if data[i]<data[j]:
data.insert(j,data[i])
data.pop(i+1)
break
return data
array=[2,4,12,8,34,26,78,1]
p=insertsort(array)
print(p)
2.2 希尔排序(Shell Sort):https://blog.csdn.net/weixin_42587961/article/details/81072911
基本思想:将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了
复杂度:希尔排序对序列划分O(n)次,每次简单插入排序O(logn),时间复杂度O(nlogn)
额外空间开销出在插入过程数据移动需要的一个暂存,空间复杂度O(1)
Code:
def shell_sort(alist):
n = len(alist)
# 初始步长
gap = n // 2
while gap > 0:
# 按步长进行插入排序
for i in range(gap,n):
j = i
# 原地插入排序,之前的插入排序另辟空间存储的
while j>=gap and alist[j-gap] > alist[j]:
alist[j-gap], alist[j] = alist[j], alist[j-gap]
j -= gap
# 得到新的步长
gap = gap //2
alist = [54,26,93,17,77,31,44,55,20]
shell_sort(alist)
print(alist)
3.选择排序
3.1 简单选择排序(Select Sort)
基本思想 :同样对数据操作n-1轮,每轮找出一个最大(小)值。
复杂度:每轮操作O(n)次,共O(n)轮,时间复杂度O(n^2)。
额外空间开销出在交换数据时那一个过渡空间,空间复杂度O(1)。
Code :https://blog.csdn.net/lerry13579/article/details/82051883
def SelectSort(sorted_list):
l = len(input_list)
if l == 0:
return []
for i in range(l):
# 默认第i个元素是每次的最小值的索引
min_index = i
# 找到后面元素最小的索引
for j in range(i + 1, l):
if sorted_list[min_index] > sorted_list[j]:
min_index = j
# 将找到的最小元素放入前面已经有序序列的末尾
temp = sorted_list[i]
sorted_list[i] = sorted_list[min_index]
sorted_list[min_index] = temp
print("%dth" % (i + 1))
print(sorted_list)
return sorted_list
if __name__ == '__main__':
input_list = [50, 123, 543, 187, 49, 30, 0, 2, 11, 100]
print("input_list:",input_list)
sorted_list = SelectSort(input_list)
print("sorted_list:",input_list)
3.2 堆排序(Heap Sort):https://www.cnblogs.com/shiqi17/p/9694938.html
基本思想 :将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。可称为有序区,然后将剩余n-1个元素重新构造成一个堆,估且称为堆区(未排序)。这样会得到n个元素的次小值。重复执行,有序区从:1—>n,堆区:n–>0,便能得到一个有序序列了
复杂度:对O(n)级别个非叶子节点进行堆调整操作O(logn),时间复杂度O(nlogn);之后每一次堆调整操作确定一个数的次序,时间复杂度O(nlogn)。合起来时间复杂度O(nlogn)
额外空间开销出在调整堆过程,根节点下移交换时一个暂存空间,空间复杂度O(1)
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),是不稳定排序
Code
#调整父节点 与孩子大小, 制作大顶堆
def adjust_heap(data, par_node, high):
new_par_node = par_node
j = 2*par_node +1 #取根节点的左孩子, 如果只有一个孩子 high就是左孩子,如果有两个孩子 high 就是右孩子
while j <= high: #如果 j = high 说明没有右孩子,high就是左孩子
if j < high and data[j] < data[j+1]: #如果这儿不判断 j < high 可能超出索引
# 一个根节点下,如果有两个孩子,将 j 指向值大的那个孩子
j += 1
if data[j] > data[new_par_node]: #如果子节点值大于父节点,就互相交换
data[new_par_node], data[j] = data[j], data[new_par_node]
new_par_node = j #将当前节点,作为父节点,查找他的子树
j = j * 2 + 1
else:
# 因为调整是从上到下,所以下面的所有子树肯定是排序好了的,
#如果调整的父节点依然比下面最大的子节点大,就直接打断循环,堆已经调整好了的
break
#索引计算: 0 -->1 --->....
#父节点 i 左子节点:偶数:2i +1 右子节点:基数:2i +2 注意:当用长度表示最后一个叶子节点时 记得 -1
#从第一个非叶子节点(即最后一个父节点)开始,即 list_.length//2 -1(len(list_)//2 - 1)
#开始循环到 root 索引为:0 的第一个根节点, 将所有的根-叶子 调整好,成为一个 大顶堆
def heap_sort(lst):
"""
根据列表长度,找到最后一个非叶子节点,开始循化到 root 根节点,制作 大顶堆
:param lst: 将列表传入
:return:
"""
length = len(lst)
last = length -1 #最后一个元素的 索引
last_par_node = length//2 -1
while last_par_node >= 0:
adjust_heap(lst, last_par_node, length-1)
last_par_node -= 1 #每调整好一个节点,从后往前移动一个节点
while last > 0:
lst[0], lst[last] = lst[last],lst[0]
# 调整堆少让 adjust 处理最后已经排好序的数,就不处理了
adjust_heap(lst, 0, last-1)
last -= 1
return lst #将列表返回
list_ = [4, 7, 0, 9, 1, 5, 3, 3, 2, 6]
heap_sort(list_)
print(list_)
4. 归并排序
4.1 二路归并排序(Two-way Merge Sort)
基本思想 :把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列。
复杂度:归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
Code :
def MERGE(left, right):
i = 0
j = 0
res = []
while (i < len(left) and j < len(right)):
if left[i] < right[j]:
res.append(left[i])
i += 1
else:
res.append(right[j])
j += 1
res += left[i:] + right[j:]
return res
def MERGE_SORT(A):
print('list:',A)
if (len(A) <= 1):
return A
mid = len(A) // 2
print('mid:',mid)
left = MERGE_SORT(A[:mid])
right = MERGE_SORT(A[mid:])
return MERGE(left, right)
if __name__ == '__main__':
A = [5, 4, 6, 9, 12, 35, 34, 32, 11, 23, 21, 14, 3, 8]
print(MERGE_SORT(A))
5. 线性时间非比较类排序
5.1 计数排序(Counting Sort)
基本思想 :1找出待排序的数组中最大和最小的元素;
2统计数组中每个值为i的元素出现的次数,存入数组C的第i项,计数累加;
3填充目标数组:将每个元素i放在新数组的第arr(i)项。
复杂度:计数数组的大小取决于待排数据取值范围,所以对数据有一定要求,否则空间开销无法承受。
计数排序只需遍历一次数据,在计数数组中记录,输出计数数组中有记录的下标,时间复杂度为O(n+k)。
额外空间开销即指计数数组,实际上按数据值分为k类(大小取决于数据取值),空间复杂度O(k)。
Code :
def CountSort(lst):
n = len(lst)
num = max(lst)
count = [0] * (num + 1)
for i in range(0, n):
count[lst[i]] += 1
arr = []
for i in range(0, num + 1):
for j in range(0, count[i]):
arr.append(i)
return arr
t=CountSort([1,5,2,1,9,4,6,5])
print(t)
5.2 桶排序(Bucket Sort)
基本思想 :
• 设置一个定量的数组当作空桶;
• 遍历输入数据,并且把数据一个一个放到对应的桶里去;
• 对每个不是空的桶进行排序;
• 从不是空的桶里把排好序的数据拼接起来。
复杂度:假设n个数据,划分为k个桶,桶内采用快速排序,时间复杂度为O(n)+O(k * n/klog(n/k))=O(n)+O(n(log(n)-log(k))),
显然,k越大,时间复杂度越接近O(n),当然空间复杂度O(n+k)会越大,这是空间与时间的平衡。
Code
5.3 基数排序(Radix Sort)
基本思想 :将整数按位数切割成不同的数字,然后按每个位数分别比较。
步骤:
1-将整数按位数切割成不同的数字,然后按每个位数分别比较。
从低位开始将待排序的数按照这一位的值放到相应的编号为0~9的桶中。
2-等到低位排完得到一个子序列,再将这个序列按照次低位的大小进入相应的桶中,一直排到最高位为止,数组排序完成。
1获取列表中最大数值的位数d=len(str(max(list)))
2外循环产生d个空列表,每个列表有10个空列表值,
3-依次进行从个位数、十位数、百位数等的操作,按照位数将列表中的数值放入相应的空列表中,内循环过后,依次拿出空列表的值赋给原先的列表
复杂度:基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
Code :
def RadixSort(Lst):
d = len(str(max(Lst))) #列表中的最大元素的位数
for i in range(d): #0-9一共10个桶
BucketLst = [[] for k in range(10)]
print(BucketLst)
for j in range(len(Lst)):
BucketLst[Lst[j]//(10**i)%10].append(Lst[j]) #关键1-取位数 10**i
Lst = [number for B in BucketLst for number in B] #关键2-依次拿出桶中的元素,给原列表Lst,
# 而不是给缓存 temp = [number for B in BucketLst for number in B]
return Lst
if __name__ == "__main__":
Lst1 = [1,44,5,222,1,34,89,6,2,7]
print(Lst1)
print(RadixSort(Lst1))