前言双向冒泡排序cocktail shaker sort也叫鸡尾酒排序、摇晃排序、双向气泡排序是冒泡排序的一个小改进每一轮先从左往右把最大的元素送到右端再从右往左把最小的元素送到左端扫描区间逐轮向中间收缩。它最典型的应用场景是「大部分元素已有序只有少数元素位于错误的一端」——比如序列2 3 4 5 6 7 8 9 1普通冒泡要把那个 1 一路向左换八次而双向冒泡一轮就能把它带到最左。围绕它的误解主要有两个。第一是把它当成「更快的冒泡排序」实际上两者的最坏复杂度和平均复杂度都是 O(n²)双向扫描只是把常数因子和某些特定输入下的轮数降下来不改变渐进阶。第二是以为它和冒泡排序一样只在「已经有序」时提前退出实际上双向版本还能利用「某一方向本轮没有发生交换」来判断该方向已经有序从而把边界收得更紧。本文从冒泡排序的瓶颈讲起推导双向扫描的边界更新规则给出完整实现并逐轮手工推演最后讨论它的稳定性、复杂度以及什么时候根本不该用它。代码以 C17 为基准。一、冒泡排序的瓶颈普通冒泡排序每一轮从下标 0 扫描到n - 1 - round比较相邻元素逆序就交换。一轮结束后未排序区间里最大的元素一定被推到了区间末尾所以下一轮的右边界可以收缩一位。问题在于「小元素向左移动」的速度。冒泡排序每轮只让一个元素向左移动一位准确说是每轮把一个元素送到它该去的位置如果最小的元素恰好在最右端它需要经过 n-1 轮才能到位。而最大的元素在同样一轮里可以被连续交换一路推到右端两个方向的移动速度并不对称。双向冒泡的思路就是补上这个不对称正向扫描结束后右端已经就位紧接着做一次反向扫描把最小的元素一路推到左端然后左边界收缩。这样每一轮能同时确定两个位置最坏情况下轮数大约减半。二、边界收缩与提前退出设未排序区间为[lo, hi)初始lo 0hi n。正向扫描从lo到hi - 2比较a[i]与a[i1]逆序则交换。扫描结束后a[hi-1]是区间最大值于是--hi。反向扫描从hi - 1到lo 1比较a[i-1]与a[i]逆序则交换。扫描结束后a[lo]是区间最小值于是lo。这里有两个边界细节必须盯住。第一反向扫描的下标是递减的std::size_t。如果写成for (std::size_t i hi - 1; i lo; --i)当i减到 0 之后再执行--i会回绕成SIZE_MAX循环永远不会结束而且会越界访问。正确写法是让循环在i lo时继续循环体内访问a[i-1]与a[i]这样最小的下标是lo 1不会碰到 0 回绕的问题。第二空数组和单元素数组要能被正确处理。hi - lo在hi lo 0时等于 0如果代码里写while (hi - lo 1)是安全的但如果写成while (hi lo 1)而lo是无符号数也没有问题。真正危险的是先做hi - 1再判断hi为 0 时会回绕。因此循环条件应当在使用下标之前判断。提前退出用两个来源一是本轮任何方向都没有发生交换说明整个区间已经有序二是正向扫描没有交换说明右半部分有序但仍需反向扫描确认左侧。实现上用一个swapped标志在每轮开始时清零任一方向发生交换就置位整轮结束后若仍为假则退出。三、完整实现// 适用于 C17#include cstddef#include iostream#include utility#include vectorvoid cocktail_sort(std::vectorint a) {std::size_t lo 0;std::size_t hi a.size();bool swapped true;while (swapped hi - lo 1) {swapped false;// 正向把当前区间最大值送到 hi-1for (std::size_t i lo; i 1 hi; i) {if (a[i] a[i 1]) {std::swap(a[i], a[i 1]);swapped true;}}--hi;// 反向把当前区间最小值送到 lofor (std::size_t i hi; i lo; --i) {if (a[i - 1] a[i]) {std::swap(a[i - 1], a[i]);swapped true;}}lo;}}void print(const std::vectorint a) {for (int v : a) {std::cout v ;}std::cout \n;}int main() {std::vectorint data{5, 1, 4, 2, 8, 0, 2};cocktail_sort(data);print(data);return 0;}for (std::size_t i lo; i 1 hi; i)这个条件比i hi - 1更稳妥当hi为 0 时hi - 1会回绕成巨大值而i 1 hi在hi 0时直接为假循环一次都不执行。这类「用加法代替减法避免无符号下溢」的技巧在边界处理中很常见值得养成习惯。用输入5 1 4 2 8 0 2手工推演一轮正向扫描比较5,1交换得1 5 4 2 8 0 25,4交换得1 4 5 2 8 0 25,2交换得1 4 2 5 8 0 25,8不换8,0交换得1 4 2 5 0 8 28,2交换得1 4 2 5 0 2 8。此时hi从 7 减到 6。反向扫描从i 6开始比较a[5]2与a[6]8不换i 5a[4]0与a[5]2不换i 4a[3]5与a[4]0交换得1 4 2 0 5 2 8i 3a[2]2与a[3]0交换得1 4 0 2 5 2 8i 2a[1]4与a[2]0交换得1 0 4 2 5 2 8i 1a[0]1与a[1]0交换得0 1 4 2 5 2 8。循环条件i lo在i减到 0 之前就结束了lo变为 1。第二轮继续最终得到0 1 2 2 4 5 8。手工推演的价值在于验证边界如果反向循环的终止条件写成i lo最后一步会访问a[SIZE_MAX]如果正向循环用i hi而不检查i 1最后一次比较会越界。这两处是这类算法最常见的崩溃点。四、复杂度、稳定性与替代方案比较次数方面双向冒泡每轮做两次扫描总轮数在最坏情况下约为 n/2比较次数与普通冒泡同阶仍是 O(n²)交换次数的期望也更低因为每一轮把两个元素送到位。空间复杂度 O(1)只用了几个下标和标志。稳定性方面算法只在严格逆序a[i] a[i1]时交换相等元素不会互换位置因此是稳定排序。这一点在需要保持相等元素原始次序时很重要比如先按姓名排序、再按年龄排序第二次排序必须稳定才能保留第一次的结果。std::sort不保证稳定需要稳定时用std::stable_sort。算法最好平均最坏空间稳定冒泡排序O(n)O(n²)O(n²)O(1)是双向冒泡O(n)O(n²)O(n²)O(1)是插入排序O(n)O(n²)O(n²)O(1)是归并排序O(n log n)O(n log n)O(n log n)O(n)是std::sort内省排序O(n log n)O(n log n)O(n log n)O(log n)否实际工程里几乎不会用双向冒泡排序给真实数据排序std::sort在任何规模下都更合适。它的价值主要在教学用最小的代码量展示「边界收缩」「提前退出」「无符号下标回绕」这三个通用技巧。如果题目明确要求「冒泡排序的变体」或者考察对循环边界的把控它才是合适的选择。常见坑点坑点一反向循环的无符号下标回绕。❌for (std::size_t i hi - 1; i lo; --i)i为 0 时再减一会变成SIZE_MAX循环失控并越界。✅for (std::size_t i hi; i lo; --i)循环体内访问a[i-1]与a[i]最小下标是lo 1。坑点二空数组时先算hi - 1。❌std::size_t last a.size() - 1;空数组时回绕成SIZE_MAX。✅ 先判断a.size() 2直接返回或用i 1 hi这样的上界写法。坑点三正向扫描的上界写成i hi。❌ 循环体访问a[i1]最后一次迭代越界std::vector::operator[]越界是未定义行为。✅ 条件写i 1 hi保证i1始终合法。坑点四忘记在每轮开始时重置 swapped。❌ 把swapped false;写在循环外第一轮之后它永远是 true提前退出失效。✅ 每轮进入循环体第一件事就是清零两个方向共用同一个标志。坑点五以为双向冒泡是 O(n log n)。❌ 在需要处理十万级数据的场景选它理由是「双向所以快一倍」。✅ 它的渐进复杂度仍是 O(n²)大数据用std::sort或std::stable_sort。坑点六自定义比较时用。❌if (!(a[i] a[i1])) swap(...)相等元素也会交换稳定性被破坏。✅ 用严格大于判断逆序if (a[i] a[i1])相等时保持原序。坑点七反向扫描的起点写成hi - 1而不是hi。❌ 先--hi再让i hi - 1等于跳过了刚就位的那个元素的前一个位置边界错位。✅ 明确不变式hi是开区间上界反向扫描从i hi开始比较a[i-1]与a[i]。坑点八在排序过程中修改容器大小。❌ 循环里对a调用push_back或erasesize()变化导致边界失效迭代器/下标全部作废。✅ 排序前确定容器大小排序期间只做元素交换。总结要点结论核心思想正向送最大值到右端反向送最小值到左端边界表示未排序区间为[lo, hi)正向条件i 1 hi避免越界与下溢反向条件i lo循环体访问i-1提前退出整轮无交换即有序时间复杂度最好 O(n)平均与最坏 O(n²)空间复杂度O(1)稳定性稳定严格大于才交换双向冒泡排序不是一个「更优的算法」而是一个「更对称的冒泡」。它的全部技巧都集中在循环边界上区间怎么表示、下标怎么递减、什么时候可以提前收手。把这三件事写对代码只有二十行写错任何一处得到的不是慢一点的结果而是越界访问这种未定义行为。
企业数字化 ERP 产品动态
相关推荐
人脸表情识别全链路可控方案:从数据清洗到边缘部署 简介:本资源为基于深度学习的人脸面部表情识别项目配套模型文件包,面向计算机视觉初学者、深度学习实践者及PyTorch开发者,聚焦真实场景下的表情分类任务落地。压缩包共5个文件,含3个训练完成的模型文件(model_cnn.pkl… · 2026/11/2 15:31:02
C++实现LeetCode(205.同构字符串) 前言
LeetCode 205 要求判断两个字符串是否同构(isomorphic)。定义是:字符串 s 中的每个字符都能被替换成 t 中对应位置的字符,替换必须保持一致,且不同字符不能映射到同一个字符;反过来,t 中的不同字符也不能由 s 中的同一个字符映射而来。换句话说,两个字符串的字符之间要存在… · 2026/11/2 15:30:01
射频自动化测试序列配置框架:从扫频实战到二次开发避坑 简介:面向高通芯片测试、射频验证与产线调试工程师的技术文档,围绕QSEQ测试序列系统梳理序列设计、环境与参数配置、执行记录及结果分析等核心环节,帮助读者理解如何组织复杂测试流程、定位执行异常并衔接QRCT相关操作。压缩包内共1个PDF文件… · 2026/11/3 10:53:23
卷积码与Viterbi译码MATLAB仿真及误码率排查 简介:这份文档围绕卷积码编译码算法研究与实现展开,面向学习数字通信、信道编码及维特比译码的本科生与工程入门者,可用于课程设计、毕业设计选题参考和MATLAB仿真复现。包内仅含1个doc文件,压缩包约991KB,主体为排版完… · 2026/11/3 10:53:23
电磁波吸收材料市场:82%集中度下,先进材料2%渗透率如何撬动7.56%增长 简介:这份文档面向电子、通信、军工及材料领域的研究人员与市场分析从业者,系统梳理全球电磁波吸收材料行业的市场格局与技术演进路径,帮助读者理解传统吸波材料与先进吸波材料之间的替代趋势及竞争态势。压缩包内为1个docx文件,约… · 2026/11/3 10:53:23
医疗AI战略计划拆解:价值链、证据链与工程接口 简介:这份PDF是美国卫生与公众服务部(HHS)于2025年1月发布的《医疗卫生人工智能战略计划》官方文本,面向医疗AI研究者、政策制定者、医疗产品研发人员及公共卫生管理者,系统回答AI如何在医疗研究、产品开发与公共卫生服… · 2026/11/3 10:53:23
HackRF One软件无线电:驱动、SDRSharp与GNURadio实践 简介:HackRF One 软件无线电平台用户手册面向刚接触 SDR 的爱好者、嵌入式开发者与无线电实验人员,帮助其从硬件组成、Windows/Linux 开发环境搭建到 GNURadio 使用与发射接收实例建立完整认识。资源包仅含 1 个 PDF 文件,大小约 2.49MB&… · 2026/11/3 10:53:23
化工在线分析仪器配置:从取样预处理到DCS联锁应用 简介:《在线分析仪器配置和应用技术2》面向化工、仪器仪表与过程控制领域的工程技术人员及学习者,围绕合成氨、甲醇装置中在线分析仪器的配置选型与应用展开,帮助读者建立从工艺环节到仪器选型的整体认识。内容以水煤浆加压气化、干煤粉加压气… · 2026/11/3 10:52:22
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46