【通俗易懂】十大排序算法(python代码超详细注释)

本文详细介绍了十大经典排序算法,包括冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、堆排序、计数排序、桶排序和基数排序,分别阐述了它们的算法步骤、图解、代码实现和复杂度分析。这些排序算法在时间和空间复杂度上有所不同,适用于不同的场景和数据特性。
摘要由CSDN通过智能技术生成


冒泡排序


1. 算法步骤

  • 比较每个元素与其相邻元素的大小,把较大元素挪到较小元素的后面
  • 经过一轮比较,该轮中最大的元素将会被置于最后
  • 重复上述过程,直至该序列排序完成,其中每一轮过后比较次数-1

2. 算法图解

冒泡排序

3. 代码实现

nums = [6,5,3,1,8,7,2,4]
for i in range(1,len(nums)): # 遍历轮次
	for j in range(len(nums)-i): # 每一轮比较次数减1
		if nums[j] > nums[j+1]: # 比较
			nums[j],nums[j+1] = nums[j+1],nums[j] # 交换
# 优化:可以用flag记录有无交换,若一轮次无交换则返回该数组(最优时间复杂度O(n))

4. 复杂度分析

遍历n个轮次,每一轮次比较交换n次,故时间复杂度为O(n2),原地排序只使用了常量空间故空间复杂度为O(1)


选择排序


1. 算法步骤

  • 每轮遍历寻找最小元素的位置,将最小元素与起始位置元素交换(每一轮过后起始位置往后挪一个单位)
  • 重复上述操作,直至该序列有序

2. 算法图解

选择排序

3. 代码实现

nums = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]
for i in range(len(nums)-1): # 遍历轮次
    minIndex = i # 每一轮起始位置往后挪一个单位
    for j in range(i+1,len(nums)): # 寻找最小值位置
	    if nums[j]<nums[minIndex]:
		    minIndex = j
    nums[i],nums[minIndex] = nums[minIndex],nums[i] # 交换

4. 复杂度分析

遍历n个轮次,遍历n次寻找最小位置,故时间复杂度为O(n2),原地排序只使用了常量空间故空间复杂度为O(1)


插入排序


1. 算法步骤

  • 将元素抽出,与前面的元素依次进行比较,若小于前面的元素,则前面元素往后移一个单位
  • 直到该元素前面的元素大于该元素,则将该元素插入到该位置
  • 重复上述过程,直至该序列有序

2. 算法图解

插入排序

3. 代码实现

nums = [6, 5, 3, 1, 8, 7, 2, 4]
for i in range(1,len(nums)):  # 遍历轮次
    temp = nums[i]  # 临时储值,把需要插入的值抽出来
    index = i-1 # 从i-1开始比较
    while index>=0 and nums[index]>temp: # 比较temp是否比前面元素小
        nums[index+1] = nums[index] # 比较大的元素往后移
        index -= 1 # 预插入位置
    nums[index+1] = temp # 跳出循环,前面元素已经比temp大了,则将temp插入到预插入位置中

4. 复杂度分析

遍历n轮,每一轮次比较n次,故时间复杂度为O(n2),原地排序只使用了常量空间故空间复杂度为O(1)。该算法相比于冒泡排序少了交换的过程,效率会较高


希尔排序


1. 算法步骤

  • 需要定义一个步长序列(每个步长是上一个步长的一半),进行分组排序
  • 例如步长序列5,2,1
  • 第一次分为5组,每隔5个元素将该元素加入一组直至末尾该组成立,依次对每组元素进行排序。
  • 下一次分为2组,每隔2个元素将该元素加入一组直至末尾该组成立,再进行排序(排序手法为插入排序)
  • 经过几轮分组排序,整个数组将会变得大概有序,最后在进行插入排序会变得很快
  • 最后步长为1,这时相当于插入排序(希尔排序其实就是对于插入排序的一个优化,在数据量比较大时会有较大优势)
  • 关于分组的补充:例如数据[8,9,1,7,4,3,5,2,6,0],增量序列5,2,1,第一次分组为隔5个元素为一组,分为[8,3],[9,5],[1,2],[7,6],[4,0]五组。

2. 算法图解

希尔排序

3. 代码实现

nums = [20,63,51,8,15,39,82,3,44,13,24,55,1,62,92,12]
inc_sequence = [5,3,1] # 定义步长序列
for inc in inc_sequence: # 遍历步长
	for i in range(inc,len(nums)): # 对每组进行插入排序
		temp = nums[i] # 临时储值,把需要插入的值抽出来
		index = i - inc # 从i-inc开始比较
		while index>=0 and nums[index]>temp: # 比较temp是否比前面元素小
			nums[index+inc] = nums[index] # 比较大的元素往后移
			index -= inc # 预插入位置
		nums[index+inc] = temp # 跳出循环,前面元素已经比temp大了,则将temp插入到预插入位置中

4. 复杂度分析

该排序算法的时间复杂度在于步长的选取,时间复杂度比较多变,若初始增量选取的较差,最坏时间复杂度可以达到O(n2),选取的较好可以达到O(n4/3),由于推导过于复杂,此处不再赘述。空间复杂度为O(1)
此处给出目前最优的步长序列:1, 5, 19, 41, 109,…,详细推导看下行链接
感兴趣可以去wiki查看步长序列的选取,链接:https://en.wikipedia.org/wiki/Shellsort


快速排序


1. 算法步骤

  • 选定一个中间元素pivot(pivot值随意取,一开始一般取最左边元素),将大于pivot的元素放到a的右边,把小于pivot的元素放到pivot的左边
  • 对pivot左右的元素重复上述操作直至序列有序

2. 算法图解

快速排序

3. 代码实现

# 使用头尾双指针进行比较交换,其中每一轮完成后会分成左右两个数组(两个子问题),再对左右两个数组进行操作
# 使用分治法,递归求解
def quicksort(nums,start,end): 
	if start>=end: # 递归结束条件
		return
	pivot_index = partition(nums,start,end) # 分区,并获取中心元素索引
	quicksort(nums,start,pivot_index-1) # 对中心元素左边元素进行排序
	quicksort(nums,pivot_index+1,end) # 对中心元素右边元素进行排序
	return nums
def partition(nums,start,end): # 分区
    pivot = nums[start] # 储存中心元素(取最左边元素),下面进行比较
    lo,hi= start+1,end # 定义头尾指针
    while lo<hi:# 头尾指针相遇则跳出循环
        while lo<hi and nums[lo]<=pivot:# 如果左边的元素已经小于pivot,头指针往后移
            lo += 1
        while lo<hi and nums[hi]>=pivot:# 如果右边的元素已经小于pivot,尾指针往前移
            hi -= 1
        swap(nums,lo,hi) # 两个循环均已退出,交换头尾指针的元素,使得pivot两边满足左边小右边大的条件
    if nums[lo]>pivot: # 分区完毕,将pivot挪到中间,该判断防止头指针位于较大元素点使得交换后不满足条件(可以优化一下省去该判断,但是我懒ToT,懒得想了,大佬可自行优化)
	    swap(nums,start,lo-1)
	   	return lo-1
	else:
		swap(nums,start,lo)
	    return lo
def swap(nums,lo,hi): # 交换
	nums[lo],nums[hi] = nums[hi],nums[lo]
if __name__ == '__main__':
    nums = [35,33,42,10,14,19,27,44,26,31]
    print(quicksort(nums,0,len(nums)-1))

4. 复杂度分析

最差情况每次都取到最小/最大的元素,此时和冒泡排序没什么区别,时间复杂度为O(n2),最优情况就是每次pivot两边有分区,此时时间复杂度为O(nlogn)。由于递归的存在,空间复杂度会比之前的算法要高,最优情况进行logn次递归调用,空间复杂度为O(logn),最坏情况即为冒泡排序的情况,进行n次递归调用,空间复杂度为O(n)


归并排序


1. 算法步骤

  • 分治思想,分:将一个序列重中间拆分,每一次拆分成两个子序列,直至拆分至每个子序列仅有一个元素;治:按照原来的拆分线路进行比较—>合并
  • 拆分使用递归,生成一个递归树,左节点储存父节点序列的左半部分,右节点储存父节点序列的右半部分
  • 合并使用队列思想,先进先出,拿左右序列的头元素进行比较,比较小的就弹出,并添加到合并序列中

2. 算法图解

归并排序

3. 代码实现

def mergesort(nums):
    if len(nums) == 1:  # 递归退出条件,拆分至每个子序列只有一个元素时退出
        return nums
    mid = len(nums)//2
    left, right = mergesort(nums[:mid]), mergesort(nums[mid:])  # 拆分左序列和右序列
    return merge(left, right)  # 进行归并
def merge(left, right):  # 比较->合并两个子序列
    merge_nums = []  # 归并后的序列
    while left and right:  # left和right都不为空时进行比较合并
        if left[0] <= right[0]:  # 比较left和right的第一个元素,哪个小哪个就弹出,并添加进merge_nums中进行合并
            merge_nums.append(left.pop(0))
        else:
            merge_nums.append(right.pop(0))
    merge_nums += left + right  # 若left或right还剩余元素,则把他们加入进去
    return merge_nums
if __name__ == '__main__':
    nums = [6,5,3,1,8,7,2,4]
    print(mergesort(nums))

其中python内置heapq模块提供了归并函数heapq.merge(),只需要将分割好的数组作为参数作用到函数中去即可

from heapq import merge
def mergesort(nums):
    if len(nums) == 1:  # 递归退出条件,拆分至每个子序列只有一个元素时退出
        return nums
    mid = len(nums)//2
    left, right = mergesort(nums[:mid]), mergesort(nums[mid:])  # 拆分左序列和右序列
    return merge(left,right) # 进行归并,返回一个可迭代对象
if __name__ == '__main__':
    nums = [6,5,3,1,8,7,2,4]
    print(list(mergesort(nums)))

4. 复杂度分析

递归树深度log(n),每一层需要n次操作进行合并,故时间复杂度为O(nlogn)。由于需要新开辟空间进行合并,有n个元素就要开辟n个空间,故空间复杂度为O(n)


堆排序


1. 算法步骤

  • 用数组模拟大顶堆(类似于二叉树,父节点元素大于子节点),如果子节点的元素大于父节点的元素则将其交换(该部为堆的维护)
  • 每次当满足大顶堆的性质时,将堆顶的元素和堆尾进行交换,并使堆的尺寸缩小一个单位,然后继续维护堆,使其满足大顶堆的性质
  • 重复上述步骤,直至堆的尺寸缩短为1,则排序完成
  • 关于用数组模拟堆,关于父节点和子节点的索引计算:
    • 索引为i的节点的父节点:(i-1)/2
    • 索引为i的节点的左节点:i*2 + 1
    • 索引为i的节点的右节点:i*2 + 2

2. 算法图解

堆排序

3. 代码实现

def heapsort(nums,n):
    # 创建大顶堆
    for i in range(n//2-1,-1,-1): # 遍历每一个父节点
        heapify(nums,n,i)
    # 排序
    for i in range(n-1,0,-1): # 堆的尺寸i缩短至1,排序完成
        swap(nums,i,0) # 堆顶元素与末尾元素进行交换
        heapify(nums,i,0) # 由于交换,堆顶元素变小,需要重新对堆顶元素进行维护(其中i为目前堆的尺寸,每次减一)
    return nums
def heapify(heap_arr,n,i): # n为堆的尺寸,i为需要维护的节点
    father = i # 父节点索引
    lson = i*2 + 1 # 左节点索引
    rson = i*2 + 2 #右节点索引
    if lson<n and heap_arr[father]<heap_arr[lson]: # 预交换,如果左子节点较大,则让父节点索引变成左子节点索引
        father = lson
    if rson<n and heap_arr[father]<heap_arr[rson]: # 预交换,如果右子节点较大,则让父节点索引变成右子节点索引
        father = rson
    if father!=i: # 如果父节点索引改变则交换
        swap(heap_arr,father,i) # 交换
        heapify(heap_arr,n,father) # 交换后再维护一下堆的性质,保持大顶堆结构
def swap(nums,i,j):
	nums[i],nums[j] = nums[j],nums[i]
if __name__ == '__main__':
    nums = [6,5,3,1,8,7,2,4]
    print(heapsort(nums,len(nums)))

4. 复杂度分析

循环n-1次,堆一共有logn层,从根节点往下遍历需要logn次,故时间复杂度为O(nlogn),堆排序属于原地排序,故空间复杂度为O(n)


计数排序


1. 算法步骤

  • 需要知道待排序数组的最大数值maxValue,建立一个大小为maxValue的计数数组
  • 遍历待排序数组,使用元素值当作maxValue的索引以来统计每个元素的出现次数
  • 将计数数组转化为前缀和数组,映射每个元素的最终位置(保证了计数排序的稳定性)
  • 倒叙遍历(保证了计数排序的稳定性)待排序数组,根据元素值寻找前缀和数组中的值,用该值-1既是该元素所在的位置

2. 算法图解

计数排序

3. 代码实现

def countingsort(nums,maxValue):
    count_arr = [0]*(maxValue+1)
    for i in nums: # 计数
        count_arr[i] += 1
    for i in range(1,maxValue+1): # 将计数数组转化为映射数组(前缀和数组)
        count_arr[i] += count_arr[i-1]
    result= [0]*len(nums)
    for i in range(len(nums)-1,-1,-1): # 倒序遍历,将元素映射到结果数组
        count_arr[nums[i]] -= 1 # 映射后该元素的索引值
        result[count_arr[nums[i]]] = nums[i] # 将该元素添加到结果数组中
    return result
if __name__ == '__main__':
    nums = [4,0,0,1,0,2,4,5,1]
    print(countingsort(nums,5))
  • 从代码可以看出,计数排序有明显的缺点:
    • 当maxValue非常大时,内存不足以存得下这么大的数据
    • 只适用于对数据范围比较集中的数据进行排序
    • 只能排序非负整数

4. 复杂度分析

最多只需要遍历n次原数组和k次计数数组,故时间复杂度为O(n+k),额外创建了大小为k的计数数组,额外空间复杂度为O(k)


桶排序


1. 算法步骤

  • 创建数个桶(桶的个数按照数据规模自定),每个桶储存一定区间内的数
  • 将待排序数组中的元素映射到桶中, 映射公式:bucketIndex = (nums[i]-minValue)/bucketNum
  • 对每个桶内的元素进行排序,再依次将桶内元素拿出来
  • 其中每个桶的大小为:(maxValue-minValue)/bucketNum

2. 算法图解

桶排序

3. 代码实现

def bucketsort(nums,bucketNum,maxValue,minValue):
	bucket = [[] for i in range(bucketNum)] # 用二维数组模拟桶
	for i in range(len(nums)): # 将nums中的元素映射到桶中
		bucketIndex = (nums[i]-minValue)//bucketNum
		bucket[bucketIndex].append(nums[i])
	for i in range(bucketNum): # 对桶中的元素进行排序,排序算法可自选,此处图方便使用内置排序函数
		bucket[i].sort()
	result = []
	for i in range(bucketNum): # 依次将桶内元素拿出来
		for j in range(len(bucket[i])):
			result.append(bucket[i][j])
	return result
if __name__ == '__main__':
    nums = [11,9,21,8,17,19,13,1,24,12]
    print(bucketsort(nums,5,24,1))

4. 复杂度分析

最多遍历n个元素,k个桶,故时间复杂度为O(n+k),需要额外创建k个桶桶内共塞n个元素,故额外空间复杂度为O(n+k)


基数排序


1. 算法步骤

  • 先根据元素的个位进行排序,再根据元素的百位进行排序,再根据元素的千位进行排序…序列中元素最高有多少位就要排序多少次
  • 其中每个位的排序需要创建10个队列来存储各个位的大小关系,每遍历一个元素将他压入对应位的队列中去,排序完成将该队列的元素依次弹出,并放回原序列中

2. 算法图解

基数排序

3. 代码实现

def radixsort(nums,maxDigit):
    quene = [[] for i in range(10)] # 创建队列
    for i in range(maxDigit): # 有多少位就排序多少次
        for num in nums:
            quene[num//(10**i)%10].append(num) # 找到该元素的第i位数,并将该元素添加到对应位的队列中
        nums = [] # 将数组初始化为空,为下一步做准备
        for i in range(len(quene)): # 将队列中元素放回原序列做准备
            for j in range(len(quene[i])):
                nums.append(quene[i].pop(0))
    return nums
if __name__ == '__main__':
    nums = [882,3,5,345,254,606,588,808,535,784,715,710]
    print(radixsort(nums,3))

4. 复杂度分析

最多遍历n个数,需要排序k次(k为最大位数),故时间复杂度为O(kn),需要创建k个队列队列里面塞n个元素,故空间复杂度为O(n+k)


如文章有错误,望大家指正,共同学习,感谢!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值