当前位置: 首页 > news >正文

Python 算法基础篇之排序算法(一):冒泡、选择、插入

1. 前言:为什么从排序算法开始?

排序(Sorting)是计算机科学中最基础、最核心的算法问题之一。无论是数据库索引、搜索引擎结果展示,还是日常开发中的数据展示,都离不开排序。

经典问题:给定一个长度为 n 的整数数组,如何将其按升序排列?

本文将深入讲解三种最基础的排序算法:冒泡排序、选择排序、插入排序。它们虽然时间复杂度都是 O(n^2),但在特定场景下仍有应用价值,更重要的是——它们是理解更高级排序算法(快速排序、归并排序、堆排序)的必经之路。

本文学习目标:

  • ✅ 理解三种排序算法的核心思想和执行流程
  • ✅ 能够手写完整代码并分析时间/空间复杂度
  • ✅ 通过可视化图示直观理解排序过程
  • ✅ 掌握算法稳定性、原地排序等核心概念

排序算法分类总览:


2. 冒泡排序(Bubble Sort)

2.1 算法思想

冒泡排序的名字非常形象:就像水中的气泡一样,较大的元素会逐渐浮到数组的末尾

核心操作:重复地走访要排序的序列,一次比较两个相邻的元素,如果它们的顺序错误就把它们交换过来。

2.2 算法步骤详解

初始数组:[3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]

第1趟(i=0):从左到右比较相邻元素,将最大元素冒泡到最后
3<->44 44<->38 38<->5 5<->47 47<->15 47<->36 47<->26 47<->27 47<->2 47<->46 47<->4 47<->19 47<->50 50<->48
结果:[3, 38, 5, 44, 15, 36, 26, 27, 2, 46, 4, 19, 47, 48, 50] <- 50已就位

第2趟(i=1):在[0, n-2]范围内重复,将次大元素放到倒数第2位

结果:[3, 5, 38, 15, 36, 26, 27, 2, 44, 4, 19, 46, 47, 48, 50] <- 48已就位

第n-1趟:只剩一个元素,自然有序

2.3 可视化图示

冒泡排序过程动态演示:

上图展示了冒泡排序的核心过程:

  • 绿色方块:当前正在比较/交换的元素对
  • 蓝色方块:尚未排序的元素
  • 橙色方块:已经排序到位的最大元素
  • 每一轮将当前未排序部分的最大值冒泡到正确位置

2.4 完整代码实现

defbubble_sort(arr:list)->list:n=len(arr)# 外层循环控制排序趟数,共需 n-1 趟foriinrange(n-1):# 优化标志:如果本趟没有交换,说明已经有序swapped=False# 内层循环控制每趟比较的次数# 第 i 趟只需比较到 n-i-1(后面的 i 个元素已排好)forjinrange(n-i-1):# 比较相邻元素ifarr[j]>arr[j+1]:# 交换位置arr[j],arr[j+1]=arr[j+1],arr[j]swapped=True# 优化:如果本趟没有发生交换,提前结束ifnotswapped:breakreturnarr# ========== 测试与验证 ==========if__name__=="__main__":# 测试数据test_data=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]print(f"原始数组:{test_data}")sorted_data=bubble_sort(test_data.copy())print(f"排序结果:{sorted_data}")# 验证正确性assertsorted_data==sorted(test_data)print("验证通过!")

2.5 复杂度分析

指标复杂度说明
最好时间O(n)数组已有序,只需1趟比较
最坏时间O(n^2)数组逆序,需 n-1 趟
平均时间O(n^2)随机数组
空间复杂度O(1)原地排序
稳定性稳定相等元素不交换

2.6 算法优化:鸡尾酒排序(双向冒泡)

defcocktail_sort(arr:list)->list:n=len(arr)left,right=0,n-1whileleft<right:# 正向冒泡:将最大值移到 right 位置new_right=leftforiinrange(left,right):ifarr[i]>arr[i+1]:arr[i],arr[i+1]=arr[i+1],arr[i]new_right=i right=new_right# 反向冒泡:将最小值移到 left 位置new_left=rightforiinrange(right,left,-1):ifarr[i]<arr[i-1]:arr[i],arr[i-1]=arr[i-1],arr[i]new_left=i left=new_leftreturnarr

3. 选择排序(Selection Sort)

3.1 算法思想

选择排序的核心思想非常直观:每一趟从剩余未排序元素中选择最小(或最大)的元素,放到已排序序列的末尾

就像打牌时,每次从牌堆中选出最小的一张,依次放到手中。

3.2 算法步骤详解

初始数组:[3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]

第0趟:在[0, 14]中找最小值 2,与 a[0]=3 交换
-> [2, 44, 38, 5, 47, 15, 36, 26, 27, 3, 46, 4, 19, 50, 48]

第1趟:在[1, 14]中找最小值 3,与 a[1]=44 交换
-> [2, 3, 38, 5, 47, 15, 36, 26, 27, 44, 46, 4, 19, 50, 48]

第2趟:在[2, 14]中找最小值 4,与 a[2]=38 交换
-> [2, 3, 4, 5, 47, 15, 36, 26, 27, 44, 46, 38, 19, 50, 48]

第13趟:在[13, 14]中找最小值 48,与 a[13]=50 交换
-> [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

3.3 可视化图示

选择排序过程演示:

上图展示了选择排序的核心特征:

  • 每轮从未排序区间中找到最小元素
  • 将其与未排序区间的第一个元素交换
  • 已排序区间逐渐扩大,未排序区间逐渐缩小

3.4 完整代码实现

defselection_sort(arr:list)->list:n=len(arr)# 外层循环:控制已排序部分的边界# i 表示当前要放置第 i 小元素的位置foriinrange(n-1):# 假设当前位置 i 的元素就是最小值min_idx=i min_value=arr[i]# 内层循环:在[i, n-1]范围内寻找真正的最小值forjinrange(i+1,n):ifarr[j]<min_value:min_value=arr[j]min_idx=j# 将找到的最小值与位置 i 的元素交换# 注意:如果 min_idx == i,说明 i 位置已经是最小值,无需交换ifmin_idx!=i:arr[i],arr[min_idx]=arr[min_idx],arr[i]returnarr# ========== 测试与验证 ==========if__name__=="__main__":test_data=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]print(f"原始数组:{test_data}")sorted_data=selection_sort(test_data.copy())print(f"排序结果:{sorted_data}")assertsorted_data==sorted(test_data)print("验证通过!")

3.5 复杂度分析

指标复杂度说明
最好时间O(n^2)比较次数固定
最坏时间O(n^2)比较次数固定
平均时间O(n^2)比较次数固定
空间复杂度O(1)原地排序
稳定性不稳定跨距离交换破坏稳定性

稳定性破坏示例:[5, 8, 5, 2],第一次选择最小值 2 与第一个 5 交换,两个 5 的相对顺序改变。

3.6 双向选择排序(每次找最大和最小)

defbidirectional_selection_sort(arr:list)->list:left,right=0,len(arr)-1whileleft<right:min_idx,max_idx=left,right# 在[left, right]范围内找最小和最大foriinrange(left,right+1):ifarr[i]<arr[min_idx]:min_idx=iifarr[i]>arr[max_idx]:max_idx=i# 最小值放左边arr[left],arr[min_idx]=arr[min_idx],arr[left]# 如果最大值恰好在 left 位置,上面交换后它到了 min_idxifmax_idx==left:max_idx=min_idx# 最大值放右边arr[right],arr[max_idx]=arr[max_idx],arr[right]left+=1right-=1returnarr

4. 插入排序(Insertion Sort)

4.1 算法思想

插入排序的灵感来自我们整理扑克牌的方式:将未排序的元素逐个插入到已排序序列的合适位置

就像摸牌一样,每次摸到一张新牌,从后往前找到合适的位置插入。

4.2 算法步骤详解

初始数组:[3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]

第1张牌 a[0]=3:手中只有一张牌,自然有序
手中:[3] | 牌堆:[44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]

第2张牌 a[1]=44:比3大,放后面
手中:[3, 44] | 牌堆:[38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]

第3张牌 a[2]=38:从后往前比较,44>38,44后移;3<38,停止
手中:[3, 38, 44] | 牌堆:[5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]

第4张牌 a[3]=5:44>5后移,38>5后移,3<5停止
手中:[3, 5, 38, 44] | 牌堆:[47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]

第15张牌 a[14]=48:插入到合适位置
手中:[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50] | 牌堆:[]

4.3 可视化图示

插入排序过程演示:

上图展示了插入排序的核心过程:

  • 已排序区间(灰色背景):已经排好序的部分
  • 当前元素(蓝色高亮):正在插入的元素
  • 后移操作:将大于当前元素的已排序元素依次后移一位
  • 插入位置:找到正确位置后插入当前元素

4.4 完整代码实现

definsertion_sort(arr:list)->list:n=len(arr)# 从第2个元素开始(下标1),逐个插入已排序部分foriinrange(1,n):# 当前要插入的元素(摸到的牌)current=arr[i]# 在已排序部分[0, i-1]中从后往前找插入位置j=i-1# 将大于 current 的元素依次后移whilej>=0andarr[j]>current:arr[j+1]=arr[j]# 后移j-=1# 找到插入位置,放入 currentarr[j+1]=currentreturnarr# ========== 更直观的实现(带注释版)==========definsertion_sort_verbose(arr:list)->list:n=len(arr)foriinrange(1,n):# 第 i 张牌(当前要处理的元素)value=arr[i]insert_idx=0# 默认插入到最前面# 从后往前扫描已排序部分 [0, i-1]forjinrange(i-1,-1,-1):ifarr[j]>value:# 当前元素比 value 大,需要后移arr[j+1]=arr[j]else:# 找到插入位置:arr[j] <= value,插在 j+1 处insert_idx=j+1break# 插入元素arr[insert_idx]=valuereturnarr# ========== 测试与验证 ==========if__name__=="__main__":test_data=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]print(f"原始数组:{test_data}")sorted_data=insertion_sort(test_data.copy())print(f"排序结果:{sorted_data}")assertsorted_data==sorted(test_data)print("验证通过!")

4.5 复杂度分析

指标复杂度说明
最好时间O(n)数组已有序
最坏时间O(n^2)数组逆序
平均时间O(n^2)随机数组
空间复杂度O(1)原地排序
稳定性稳定相等元素不移动

4.6 插入排序的优势场景

deftest_partial_sorted():importrandomimporttime# 生成几乎有序的数组(90%元素已有序)n=10000arr=list(range(n))# 随机交换少量元素for_inrange(n//10):i,j=random.randint(0,n-1),random.randint(0,n-1)arr[i],arr[j]=arr[j],arr[i]# 测试插入排序start=time.time()insertion_sort(arr.copy())insert_time=time.time()-start# 测试快速排序start=time.time()sorted(arr.copy())quick_time=time.time()-startprint(f"几乎有序数组 (n={n}):")print(f" 插入排序耗时:{insert_time:.4f}s")print(f" 快速排序耗时:{quick_time:.4f}s")# 插入排序通常更快!# 实际应用:Python 的 Timsort(sorted() 底层)# 当子数组长度较小时(<< 64),会退化为插入排序

5. 三大基础排序算法对比总结

5.1 核心特性对比表

特性冒泡排序选择排序插入排序
核心思想相邻比较,大元素后移选最小元素放前面逐个插入已排序部分
最好时间O(n)O(n^2)O(n)
最坏时间O(n^2)O(n^2)O(n^2)
平均时间O(n^2)O(n^2)O(n^2)
空间复杂度O(1)O(1)O(1)
稳定性稳定不稳定稳定
比较次数O(n^2)固定 n(n-1)/2O(n^2)
交换次数O(n^2)O(n)O(n^2)
适用场景教学/小规模交换代价高时部分有序数据

十大经典排序算法复杂度总览:

5.2 算法选择决策树

数据规模小(n < 100)? ├── 是 -> 数据基本有序? │ ├── 是 -> 插入排序(O(n)) │ └── 否 -> 交换代价高(如链表)? │ ├── 是 -> 选择排序(O(n)次交换) │ └── 否 -> 插入排序(常数小) └── 否 -> 使用高级排序(快排/归并/堆排)

5.3 稳定性为什么重要?

稳定性:排序后,相等元素的相对顺序与排序前一致。

实际应用示例

# 学生成绩表,先按班级排序,再按成绩排序students=[("A班",90),("B班",85),("A班",85),("C班",90),("B班",90),("A班",95)]# 使用稳定排序先按班级排# 再使用稳定排序按成绩排# 结果:同成绩的学生,班级顺序保持

三大算法稳定性分析

  • 冒泡排序:稳定。arr[j] > arr[j+1] 才交换,相等不交换。
  • 选择排序:不稳定。跨距离交换可能破坏相对顺序。
  • 插入排序:稳定。arr[j] > current 才后移,相等不移动。

6. 实战练习:蓝桥云课 LQ3225 宝藏排序

6.1 题目描述

在一个神秘的岛屿上,有一支探险队发现了一批宝藏,这批宝藏是以整数数组的形式存在的。每个宝藏上都标有一个数字,代表了其珍贵程度。然而,由于某种神奇的力量,这批宝藏的顺序被打乱了,探险队需要将宝藏按照珍贵程度进行排序,以便更好地研究和保护它们。作为探险队的一员,肖恩需要设计合适的排序算法来将宝藏按照珍贵程度进行从小到大排序。请你帮帮肖恩。

输入格式

  • 第一行:整数 n(宝藏数量)
  • 第二行:n 个整数,表示每个宝藏的珍贵程度

输出格式

  • 一行,排序后的 n 个整数,空格分隔

样例

输入: 5 3 44 38 5 47 输出: 3 5 38 44 47

6.2 三种算法 AC 代码

解法一:冒泡排序
n=int(input())a=list(map(int,input().split()))foriinrange(1,n):forjinrange(n-i):ifa[j]>a[j+1]:a[j],a[j+1]=a[j+1],a[j]print(' '.join(map(str,a)))
解法二:选择排序
n=int(input())a=list(map(int,input().split()))foriinrange(0,n-1):min_value=a[i]min_idx=iforjinrange(i,n):ifa[j]<min_value:min_value=a[j]min_idx=j a[i],a[min_idx]=a[min_idx],a[i]print(' '.join(map(str,a)))
解法三:插入排序
n=int(input())a=list(map(int,input().split()))foriinrange(1,n):value=a[i]insert_idx=0forjinrange(i-1,-1,-1):ifa[j]>value:a[j+1]=a[j]else:insert_idx=j+1breaka[insert_idx]=valueprint(' '.join(map(str,a)))

6.3 复杂度分析(针对本题)

算法时间复杂度能否通过说明
冒泡排序O(n^2)n <= 10^3 时通常可过
选择排序O(n^2)同上
插入排序O(n^2)同上
sorted()O(n log n)Python 内置 Timsort,推荐

竞赛建议:实际比赛中,数据范围较大时(n > 10^4)建议使用 sorted() 或手写快速排序。本文练习目的是掌握基础算法思想。


结语

本文详细讲解了三种基础排序算法:

算法核心口诀掌握要点
冒泡排序“大元素往后冒泡”相邻比较、逐趟归位、提前终止优化
选择排序“每趟选最小放前面”减少交换次数、但不稳定
插入排序“像摸牌一样插入”部分有序时最优、稳定且高效

思考题:为什么快速排序最坏情况是 O(n^2),但实际应用中却比归并排序更常用?下篇揭晓答案!

点赞 + 收藏 + 关注,算法学习不迷路!

http://www.jsqmd.com/news/856232/

相关文章:

  • 告别手动核对!用这个ABAP报表一键导出所有物料的库存与需求清单
  • 从Simulink模型到S32K3xx芯片:手把手教你玩转NXP官方MBD工具包(v1.4实战)
  • 告别乱码!手把手教你用FontCvt为STM32的emWin项目定制精简中文字库
  • 别再只会真彩色了!用ENVI玩转波段组合:揭秘植被红、水体蓝背后的遥感密码
  • 实战指南:如何将SPIN的超像素思想,迁移到你的图像修复项目里(附思路)
  • 告别云盘限速!手把手教你用群晖NAS+cpolar搭建Zotero私有同步库(附永久公网地址配置)
  • 2026年4月知名的抛光蜡厂商推荐,模具/麻轮/抛光机/千叶轮/抛光蜡/焊管机,抛光蜡公司推荐分析 - 品牌推荐师
  • 3分钟永久保存B站缓存:m4s-converter让珍贵视频永不消失
  • 仓库盘点、物流交接?用UniApp+PDA扫码提升效率的实战配置与避坑指南
  • 告别HAL_Delay!用STM32CubeMX定时器PWM模式优雅驱动ULN2003步进电机
  • Windows 10 下 GAMMA 遥感软件安装全攻略:从加密狗驱动到 MSYS2 环境配置避坑指南
  • 深入拆解:IGT-DSER网关如何把AB PLC的标签(TAG)映射成Modbus地址?一个案例讲透
  • 手机芯片异构计算:从通用到专用,解析三芯协同如何重塑计算摄影与能效体验
  • 告别轮询!用STM32 RTC内部唤醒实现超低功耗数据采集(附STM32L476+CubeIDE工程)
  • 从信息学奥赛真题到LeetCode:全排列问题的通用解法迁移与避坑指南(以C++为例)
  • 瑞萨RA4M2开发板入门:从零搭建LED闪烁工程与FSP配置详解
  • Mac/Win双平台保姆级教程:从零配置ADB环境到连接真机/模拟器
  • 别再乱搜教程了!用ESP8266-01S和CH340G模块实现稳定AT指令通信的保姆级接线指南
  • 用ESP32和EC11编码器做个无极调光台灯,Arduino代码全解析(附防抖电路)
  • 加肋非矩形板无网格模型应用【附代码】
  • WebAssembly调试优化与Whamm架构实践
  • 告别手动下载!用微软商店和PowerShell脚本自动化搞定winget全家桶
  • 告别重复登录:手把手教你用Requests库模拟校园网认证(Python脚本版)
  • 保姆级教程:在CentOS 7上用Docker搞定Zabbix 5.0 + MySQL 8.0,监控H3C交换机不掉坑
  • 音视频开发避坑:YUV420P图像处理时Stride不对齐,你的内存拷贝为啥总出错?
  • Arm架构扩展详解:从A-profile到性能优化实践
  • 深入STM32WLE5的LoRa核心:对比SX126x裸驱与LoRaWAN协议栈,哪个更适合你的项目?
  • CANN-ops-nn和ops-transformer-昇腾NPU两个算子仓库怎么分工
  • 别再死记硬背PLL原理了!用这个Python小脚本,5分钟直观理解锁相环的捕获与锁定过程
  • 内网环境救星:保姆级教程,用zypper的--download-only参数搞定SUSE离线包全家桶