十大经典排序算法详解(Python)
排序算法是计算机科学中最基础且重要的算法之一,本文详细介绍了十种常见的排序算法,包括原理、实现代码、优缺点、适用场景以及复杂度分析。
一、冒泡排序 (Bubble Sort)
算法原理
冒泡排序通过重复遍历数组,比较相邻元素并交换位置,使较大的元素逐渐"浮"到数组末端。每一轮遍历都会将当前未排序部分的最大值移动到正确位置。
第一轮排序:分别确定两个相邻数值,进行比较后确定是否交换,最后可确定排序最后一位。
剩余轮次排序和第一轮排序一样,但是范围不包括第n - i 位及之后的数值(之前的排序轮次已经确定这些数值的位置)。
示例演示:
初始数组: [5, 3, 8, 4, 2]
第1轮: [3, 5, 4, 2, 8] // 8沉到底部
第2轮: [3, 4, 2, 5, 8] // 5沉到倒数第二
第3轮: [3, 2, 4, 5, 8] // 4沉到中间
第4轮: [2, 3, 4, 5, 8] // 排序完成初始数组: [5, 3, 8, 4, 2]
第1轮: [3, 5, 4, 2, 8] // 8沉到底部
第2轮: [3, 4, 2, 5, 8] // 5沉到倒数第二
第3轮: [3, 2, 4, 5, 8] // 4沉到中间
第4轮: [2, 3, 4, 5, 8] // 排序完成
代码实现
def bubble_sort(arr):
n = len(arr)
for i in range(n): #表示排序的轮次,最多进行n轮排序,最少进行1轮
is_swap = False #表示是否在该轮次有交换操作
for j in range(1,n-i):
if arr[j] < arr[j-1]:
arr[j], arr[j-1] = arr[j-1],arr[j]
is_swap = True
if not is_swap: #如果没有交换,表示排序结束
return arr
特点分析
优点:实现简单,代码易于理解;对于基本有序的数组效率较高;稳定排序(相等元素相对位置不变);原地排序(空间复杂度O(1))
缺点:效率较低,时间复杂度为O(n²);不适用于大规模数据排序
时间复杂度:最优:O(n) - 数组已经有序时; 平均:O(n²); 最差:O(n²)
空间复杂度:O(1)
适用范围:小规模数据、教学演示、基本有序的数据
二、插入排序 (Insertion Sort)
算法原理
插入排序将数组分为已排序和未排序两部分,初始时首个元素分到已排序部分,每次从未排序部分取出一个元素,插入到已排序部分的正确位置。
示例演示:
初始数组: [5, 3, 8, 4, 2]
步骤1: [5| 3, 8, 4, 2] // 5已排序
步骤2: [3, 5| 8, 4, 2] // 3插入到5前面
步骤3: [3, 5, 8| 4, 2] // 8插入到末尾
步骤4: [3, 4, 5, 8| 2] // 4插入到5前面
步骤5: [2, 3, 4, 5, 8] // 2插入到最前面
代码实现
def insertion_sort(arr):
n = len(arr)
for i in range(1,n):
tmp = arr[i] #确定遍及到的该元素,防止后续因数组的改变而改变该元素
j = i - 1
while arr[j] > tmp and j >= 0:
#从后向前遍历已排序元素,若大于tmp ,后移一位,否则将tmp插入到该元素后面
arr[j+1] = arr[j]
j -= 1
arr[j+1] = tmp
return arr
特点分析
优点:实现简单,对小规模数据效率高;对基本有序的数组效率很高;稳定排序;原地排序
缺点:大规模数据效率低;每次只能移动一个位置
时间复杂度:最优:O(n) - 数组已经有序时; 平均:O(n²); 最差:O(n²)
空间复杂度:O(1)
适用范围:小规模数据、基本有序的数据、作为其他排序算法的子过程
三、归并排序 (Merge Sort)
算法原理
归并排序采用分治策略,将数组递归地分成两半,直到每组只剩一个元素,分别排序后再合并。这是一个典型的"分而治之"算法。
示例演示:
原始数组: [38, 27, 43, 3, 9, 82, 10]
分解过程:
[38, 27, 43, 3] [9, 82, 10]
[38, 27] [43, 3] [9, 82] [10]
[38] [27] [43] [3] [9] [82] [10]
合并过程:
[27, 38] [3, 43] [9, 82] [10]
[3, 27, 38, 43] [9, 10, 82]
[3, 9, 10, 27, 38, 43, 82]
代码实现
def merge_sort(arr):
n = len(arr)
if n <= 1: return arr
#递归地对半分,直到子数组长度小于等于1
mid = n // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
#回溯时,两两合并有序数组
res = []
i = j = 0
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.extend(left[i:]) #剩余结果直接加入list
res.extend(right[j:])
return res
特点分析
优点:时间复杂度稳定为O(nlogn);稳定排序;适合处理大规模数据;适合外部排序(数据在磁盘中)
缺点:需要额外空间;对于小规模数据可能不如插入排序
时间复杂度:O(nlogn)
空间复杂度:O(n)
适用范围:大规模数据、需要稳定排序的场景、链表排序
四、快速排序 (Quick Sort)
算法原理
快速排序选择一个基准元素,将数组分成两部分:小于基准的和大于基准的,然后递归地对这两部分进行排序,直到子数组只剩一个元素的位置。
示例演示:
原始数组: [5, 3, 8, 4, 2]
选择基准5:
小于5: [3, 4, 2] 大于5: [8]
递归排序[3, 4, 2]:
选择基准3: 小于3: [2] 大于3: [4]
合并: [2, 3, 4, 5, 8]
代码实现(三种方法)
#1 递归+双指针1
def quick_sort1(arr,low,high):
##1、递归终止条件(子数组长度小于等于1)
if high - low <= 0: return #该函数是原地修改,无需返回值
##2、选定基准值,将小/大于基准值的数值交换到基准值的前/后面
pivot = arr[low]
left, right = low, high
## 用双指针left、right分别从数组两端向中心遍历寻找大于/小于基准值的元素,
## 找到后交换彼此位置,然后继续遍历,直到双指针相遇,将基准值放在相遇点
while left < right:
while left < right and arr[right] >= pivot:
right -= 1
arr[left] = arr[right]
while left < right and arr[left] < pivot:
left += 1
arr[right] = arr[left]
arr[left] = pivot
##3、递归地对基准值两侧的子数组执行上述操作
quick_sort1(arr,low,left-1)
quick_sort1(arr,left+1,high)
#2 递归+双指针2
def quick_sort2(arr,low,high):
## 1、递归终止条件(子数组长度小于等于1)
if high - low <= 0: return
##2、选定基准值,将小/大于基准值的元素挪到基准值的左/右侧
## 注意:为了便于后续操作,如果选择其他值作为基准值,需要将其与arr[high]交换位置,
pivot = arr[high]
## 使用双指针i、j将小于基准值的元素挪到位置 i 及之前,大于等于基准值的元素在i之后,
## 遍历结束后,将基准值放在 i+1 处,原i+1处的值放在high处
i = low - 1
for j in range(low,high):
if arr[j] < pivot:
i += 1
arr[i],arr[j] = arr[j], arr[i]
arr[i+1],arr[high] = arr[high],arr[i+1]
##3、递归地对基准值两侧的子数组执行上述操作,直到递归终止
quick_sort2(arr,low,i)
quick_sort2(arr,i+2,high)
#3 栈+双指针
def quick_sort3(arr):
satck = [(0,len(arr) - 1)]
while satck:
##1、获取子数组端点索引
low, high = satck.pop()
##2、子数组长度小于等于1,无需改动,跳过
if high - low <= 0:
continue
##3、选定基准值,将小/大于基准值的元素分别挪到基准值的左/右侧
pivot = arr[high]
## 将小于基准值的元素挪到位置 i 及其之前,大于等于基准值的元素在i之后,
## 遍历结束后,将基准值放在 i+1 处,将原i+1处的值放在high处
i = low - 1
for j in range(low,high):
if arr[j] < pivot:
i += 1
arr[i],arr[j] = arr[j],arr[i]
arr[i+1],arr[high] = arr[high],arr[i+1]
##4、将基准值两侧的子数组端点索引加入栈中
stack.append((low,i))
satck.append((i+2,high))
特点分析
优点:平均情况下效率很高;原地排序;在实践中通常是最快的排序算法
缺点:最坏情况下性能较差;不稳定排序;递归深度可能很大
时间复杂度:最优:O(nlogn); 平均:O(nlogn); 最差:O(n²) - 数组已经有序且选择最值作为基准时
空间复杂度:O(logn) - 递归栈空间
适用范围:通用排序、大规模数据、对性能要求高的场景
五、选择排序 (Selection Sort)
算法原理
选择排序每次从未排序部分选择最小(或最大)元素,放到已排序部分的末尾,直到未排序数组为空。
示例演示:
初始数组: [64, 25, 12, 22, 11]
第1轮: [11, 25, 12, 22, 64] // 11最小
第2轮: [11, 12, 25, 22, 64] // 12最小
第3轮: [11, 12, 22, 25, 64] // 22最小
第4轮: [11, 12, 22, 25, 64] // 已完成
代码实现
def select_sort(arr):
## 遍历数组,每轮将未排序部分的最小值交换到其首位
n = len(arr)
for i in range(n-1): # i指向未排序部分的首位
min_idx = i # 用于记录未排序部分最小值的索引
for j in range(i+1,n):
if arr[min_idx] > arr[j]:
min_idx = j
arr[i],arr[min_idx] = arr[min_idx],arr[i] # 将最小值移到未排序部分首位
return arr
特点分析
优点:实现简单;不占用额外内存;交换次数少,最多n-1次交换
缺点:时间复杂度高;不稳定排序;无论输入数据如何都需要O(n²)次比较
时间复杂度:O(n²)
空间复杂度:O(1)
适用范围:小规模数据、交换成本较高的场景
六、堆排序 (Heap Sort)
算法原理
堆排序利用堆这种数据结构,首先构建大顶堆,然后反复将堆顶元素(最大值)与末尾元素交换,并重新调整堆,重复上述步骤,直到堆中只剩一个元素。
大顶堆:每个节点值大于等于其子节点值,用于堆排序中的升序:
key[i]>=key[2i+1];key[i]>=key[2i+2];
小顶堆:每个节点值小于等于其子节点值,用于堆排序中的降序:
key[i]<=key[2i+1],key[i]<=key[2i+2];
示例演示:
初始数组: [4, 10, 3, 5, 1]
构建大顶堆: [10, 5, 3, 4, 1]
交换堆顶和末尾: [1, 5, 3, 4, 10]
调整堆: [5, 4, 3, 1, 10]
继续交换和调整...
最终结果: [1, 3, 4, 5, 10]
代码实现
def heapify(arr,i,end): # 用于维护大顶堆
## 从第i个元素所在节点开始,向下比较,保证每个节点值大于其子节点值
dad = i
son = 2 * dad + 1
while son < end:
if son+1 < end and arr[son+1] > arr[son]:
son += 1
if arr[son] <= arr[dad]: # 如果父节点本身就大于等于子节点值,无需再向下操作
break
arr[dad],arr[son] = arr[son],arr[dad] # 否则交换二者的值
dad = son # 继续向下操作
son = 2 * dad + 1
def heap_sort(arr):
n = len(arr)
## 初始化大顶堆(从最后一个非叶子节点开始构建,保证每个节点值大于等于其子节点值)
for i in range(n//2,-1,-1):
heapify(arr,i,n)
## 每轮取出堆顶元素,将其与数组未排序部分的末尾元素交换位置
## 维护新的大顶堆
for i in range(n-1,0,-1):
arr[0],arr[i] = arr[i],arr[0]
heapify(arr,0,i) # i位置及其之后是排好序的,所以end=i
特点分析
优点:时间复杂度稳定;原地排序;适合获取前k个最大/最小元素
缺点:不稳定排序;缓存不友好;常数因子较大
时间复杂度:O(nlogn)
空间复杂度:O(1)
适用范围:需要稳定时间复杂度的场景、实时系统、获取前k个元素
七、希尔排序 (Shell Sort)
算法原理
希尔排序是插入排序的改进版,通过比较相距一定间隔的元素来进行排序,逐步缩小间隔直到为1。
示例演示:
初始数组: [8, 3, 6, 1, 7, 2, 5, 4]
间隔为4: [7, 2, 5, 1, 8, 3, 6, 4]
间隔为2: [5, 1, 6, 2, 7, 3, 8, 4]
间隔为1: [1, 2, 3, 4, 5, 6, 7, 8]
代码实现
def shell_sort(arr):
n = len(arr)
gap = n // 2
while gap > 0:
for i in range(gap,n):
tmp = arr[i]
j = i - gap
while j >= 0 and arr[j] > tmp:
arr[j+gap] = arr[j]
j -= gap
arr[j+gap] = tmp
gap = gap // 2
return arr
特点分析
优点:比简单插入排序快很多;原地排序;是插入排序的高效改进版
缺点:不稳定排序;时间复杂度分析复杂;性能依赖于间隔序列的选择
时间复杂度:最优:O(nlogn); 平均:O(n^1.3); 最差:O(n²)
空间复杂度:O(1)
适用范围:中等规模数据、需要比插入排序更高效的场景
八、计数排序 (Counting Sort)
算法原理
计数排序不是基于比较的排序,它通过统计每个元素出现的次数,然后计算每个元素在输出数组中的位置。
示例演示:
输入数组: [4, 2, 2, 8, 3, 3, 1]
计数数组: [0, 1, 2, 2, 1, 0, 0, 0, 1]
累加数组: [0, 1, 3, 5, 6, 6, 6, 6, 7]
输出数组: [1, 2, 2, 3, 3, 4, 8]
代码实现
#1、非稳定
def counting_sort1(arr):
n = len(arr)
max_vel, min_vel = max(arr), min(arr) # 找出数组的最大值和最小值
counter = [0] * (max_vel - min_vel + 1) # 定义计数器数组,长度为数组元素的范围
for i in arr: # 用counter数组统计每个元素出现的次数,索引为元素值-最小值
counter[i-min_vel] += 1
# 遍历counter数组,根据每个元素出现的次数,将其依次添加到原数组中
k = 0
for i in range(max_vel-min_vel+1):
for j in range(counter[i]):
arr[k] = i + min_vel
k += 1
return arr
a = [23,34,15,22,16]
print('八、1',counting_sort1(a))
#2、稳定
def counting_sort2(arr):
# 只计算数组最大值,最小值默认为0,建立计数数组
m = max(arr)
counter = [0] * (m+1)
for a in arr:
counter[a] += 1
# 计算累加和,此时counter[i-1]代表的是i元素在排序后的数组存放的最后一个位置(同一元素可能存在多个)
for i in range(1, m+1):
counter[i] += counter[i-1]
# 建立一个与原数组同size的数组res,倒序遍历原数组,将每个元素根据上述所说放入适当的位置
n = len(arr)
res = [0] * n
for i in range(n-1, -1, -1):
a = arr[i]
res[counter[a]-1] = a
# counter[i-1]代表的是i元素在排序后的数组存放的最后一个位置(同一元素可能存在多个)
counter[a] -= 1
# 当放了一个元素后,该元素对应的counter值要减一,对应该元素下次出现时的位置
# 将排序后的数组放回原数组
for i in range(n):
arr[i] = res[i]
return arr
特点分析
优点:线性时间复杂度;当k较小时效率很高
缺点:需要额外空间;只适用于整数排序;当数据范围很大时效率低
时间复杂度:O(n+k) - k是数据范围
空间复杂度:O(k)
适用范围:数据范围较小的整数排序、作为基数排序的子过程
九、桶排序 (Bucket Sort)
算法原理
桶排序将数据分到有限数量的桶中,每个桶再分别排序,最后合并所有桶。
示例演示:
输入数组: [0.42, 0.32, 0.33, 0.52, 0.37, 0.47, 0.51]
分桶:
桶1[0.3-0.4]: [0.32, 0.33, 0.37]
桶2[0.4-0.5]: [0.42, 0.47]
桶3[0.5-0.6]: [0.52, 0.51]
各桶排序后合并: [0.32, 0.33, 0.37, 0.42, 0.47, 0.51, 0.52]
代码实现
def bucket_sort(arr):
n = len(arr)
min_val,max_val = min(arr), max(arr) # 找出最大值、最小值
bucketsize = 2 #定义桶的大小
bucketnum = (max_val - min_val) // bucketsize + 1 #计算桶的数量
buckets = [[] for _ in range(bucketnum)] #定义桶的数组
for i in arr: #将元素放入对应桶中
b_idx = (i - min_val) // bucketsize #计算元素
buckets[b_idx].append(i)
for i in range(bucketnum):
buckets[i].sort()
k = 0
for tmp in buckets:
for j in tmp:
arr[k] = j
k += 1
return arr
特点分析
优点:在数据分布均匀时效率很高;稳定排序(如果桶内排序使用稳定算法);适合外部排序
缺点:需要额外空间;性能依赖于数据分布;需要知道数据范围
时间复杂度:最优:O(n+k); 平均:O(n+k); 最差:O(n²) - 所有数据在一个桶中
空间复杂度:O(n+k)
适用范围:数据分布均匀的场景、外部排序、浮点数排序
十、基数排序 (Radix Sort)
算法原理
基数排序按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
示例演示:
输入数组: [170, 45, 75, 90, 2, 802, 24, 66]
按个位排序: [170, 90, 2, 802, 24, 45, 75, 66]
按十位排序: [2, 802, 24, 45, 66, 170, 75, 90]
按百位排序: [2, 24, 45, 66, 75, 90, 170, 802]
代码实现
def counting_sort_digit(arr,exp,n):
counter = [0] * 10 #定义计数器数组,长度为10
output = [0] * 10 #定义输出数组,长度为n
for i in arr: #用counter数组统计每个元素的第exp位数出现的次数
idx = (i // exp) % 10 #计算元素的第exp位数
counter[idx] += 1
for i in range(1,10):
#计算累加和,此时counter[i-1]代表的是i元素在排序后的数组存放的最后一个位置
counter[i] += counter[i-1]
i = n - 1
while i >=0: #倒序遍历原数组,将每个元素根据上述所说放入适当的位置
idx = (arr[i] // exp) % 10
output[counter[idx] - 1] = arr[i]
counter[idx] -= 1
i -= 1
for i in range(n):
arr[i] = output[i]
def radix_sort(arr):
n = len(arr)
max_val = max(arr)
exp = 1 #定义基数
while max_val // exp > 0: #基数排序的次数
counting_sort_digit(arr,exp,n)
exp *= 10 #基数翻倍
return arr
特点分析
优点:线性时间复杂度;稳定排序;适合整数和字符串排序
缺点:需要额外空间;只适用于有限长度的数据;对于浮点数需要特殊处理
时间复杂度:O(d×(n+k)) - d是最大位数,k是基数
空间复杂度:O(n+k)
适用范围:整数排序、字符串排序、数据位数较少的场景
总结
选择建议:
-
小规模数据:插入排序、冒泡排序
-
通用排序:快速排序
-
需要稳定排序:归并排序
-
大规模数据:归并排序、快速排序
-
整数排序:计数排序、基数排序
-
内存受限:堆排序、希尔排序
每种排序算法都有其特定的适用场景,在实际应用中应根据数据特点、性能要求和资源限制来选择合适的算法。
本文代码均实现升序排序,降序排序请自行根据原理进行修改。
更多推荐


所有评论(0)