Java冒泡排序从入门到精通:原理、代码实现与面试考点
做Java开发数组和集合打交道多了排序是绕不开的操作。Java里Arrays.sort()一行就能解决问题但“会用API”和“懂排序原理”是两码事。这篇博文聊聊排序算法里的入门第一课——Java冒泡排序面向零基础读者从原理讲到代码从调试讲到面试尽量做到“收藏这篇就够了”。为什么从冒泡开始因为它的思路足够直观、代码量足够短却把排序算法最核心的概念全带出来了比较、交换、轮次、复杂度分析。学透它再去看插入排序、快速排序会轻松很多。我先说个结论冒泡排序在实际工程里基本不会直接用来排大数据量但它在教学上的价值是其他算法替代不了的。它是一块最合适的敲门砖。这篇文章会用生活化的比喻讲清楚原理给出可以直接复制运行的Java代码再带着你把代码从粗糙逐步优化最后整理面试里常见的坑。不管你是刚入门的Java新手还是想复习算法基础的开发者跟着走一遍都会有收获。1. 先从生活场景理解冒泡排序的核心思路1.1 为什么排序算法要从冒泡讲起排序算法这个家族很大常见的有十几种。对零基础的人来说如果一上来就啃快速排序的“分治法”、堆排序的“完全二叉树”很容易被绕晕。冒泡排序的好处在于是所有排序算法里最符合直觉的——它的名字自带画面感就像汽水里的气泡轻的往上漂重的往下沉。而且从学习路径来看冒泡排序涉及到的几个基础概念恰好是理解所有排序算法必备的怎么比较两个元素怎么交换位置总共需要循环几轮每一轮里又需要比较多少次这些思考一旦形成后面学任何排序算法都是同样的套路。我在带新人写代码的时候通常也让他们先手写一版冒泡排序不是为了考核而是为了确认“交换变量”“嵌套循环”“边界条件”这些基本功是否过关。还有一个现实原因冒泡排序的Java代码最短也就十几行零基础的人完全可以先照着写一遍再慢慢理解背后的逻辑不会产生“看都看不懂”的挫败感。这篇既然敢说“零基础到精通”那我就从源头开始讲一步步带你写出来比直接扔给你一段写好的代码要靠谱得多。1.2 冒泡到底是怎么个冒法用一个真实的小数组来演示最直观。假设数组是{5, 1, 4, 2, 8}目标是从小到大排序。第一轮的操作是这样的第1步拿下标0的5和下标1的1比较5 1交换数组变成{1, 5, 4, 2, 8}。第2步继续比较下标1的5和下标2的45 4交换数组变成{1, 4, 5, 2, 8}。第3步比较下标2的5和下标3的25 2交换数组变成{1, 4, 2, 5, 8}。第4步比较下标3的5和下标4的85 8不交换数组保持{1, 4, 2, 5, 8}。这一轮结束最大的数字8已经稳稳地落到最后一位。你看它就像一块大石头在第一轮冒泡中直接沉到了底而比较小的1则不断往前“漂”。仔细看中间过程5这个数字是逐步往后移的每比较一次就一直往右挪一位直到遇到比它更大的8才停下来。这就是“冒泡”名字的由来。第一轮结束后的数组是{1, 4, 2, 5, 8}。前4个元素里最大值是5所以第二轮只需要比较前4个元素5会像上一轮的8一样沉到倒数第二个位置。第二轮的过程比较1和4不交换比较4和24 2交换数组变成{1, 2, 4, 5, 8}比较4和5不交换。第二轮结束其实整个数组已经有序了。第三轮如果继续跑下去比较1和2、2和4都不会发生交换。所以后续轮次本质上只是在重复确认“没有逆序对”。这一点很重要后续优化代码的时候就是利用“一整轮都没有交换”来判断排序可以提前结束。1.3 从过程反推代码基本框架刚才手动跑的过程翻译成代码就是两个循环。外层循环管“一共要几轮”内层循环管“每一轮比较到哪个位置”。关键点在于每一轮都会把当前范围内的最大值送到最后所以下一轮的比较范围就可以往左收缩一位。伪代码是这样的外层轮数i从 0 到n - 2因为最后一个元素不需要再跟别人比较。内层比较j从 0 到n - 1 - i因为最后i个元素已经在上一轮确定好位置了。判断条件如果arr[j] arr[j 1]就交换两个元素的位置。为什么内层是n - 1 - i而不是n - 1因为每完成一轮数组末尾就多一个已经排好序的元素这些元素不需要再参与比较。用的例子{5, 1, 4, 2, 8}里第一轮结束8就位第二轮再比较时j最远到下标2也就是比较4和2完全不需要碰8。边界条件在排序算法里非常容易出错很多人面试写冒泡翻车就翻在内层循环写成arr.length - 1功能虽然没大错但白白多做了很多次无效比较。这就是冒泡排序最核心的骨架。接下来把它变成真正的Java代码再看看怎么优化。2. Java代码实现从基础版到优化版的演进2.1 初始实现先跑通再谈优化写算法代码我一向的建议是先实现一个逻辑正确的版本再考虑性能优化不要一上来就写花活。下面是最容易理解的初始版本public class BubbleSortDemo { public static void main(String[] args) { int[] arr {5, 1, 4, 2, 8}; for (int i 0; i arr.length - 1; i) { for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } for (int num : arr) { System.out.print(num ); } } }这段代码有四个要点值得专门说第一交换逻辑要背熟。int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp;这种三行交换是所有排序算法的基础操作相当于“把两个杯子里的水互换需要第三个空杯子”。很多新手会写成arr[j] arr[j 1]两行搞定那样不是交换是覆盖直接丢数据。第二比较符号决定升降序。上面代码用是从小到大排升序。如果你想从大到小排降序把判断条件改成就行只要当前元素比后一个元素小就往后换小的自然沉底大的慢慢冒到最前。第三运行结果验证要靠自己。我建议你把这5个数字换成{9, 8, 7, 6, 5}这种完全倒序的数组这是最坏情况每一轮都要交换最能验证排序是否正确再换成{1, 2, 3, 4, 5}这种已经有序的数组观察程序会做多少次无效比较。第四这个版本还有性能浪费。现在虽然结果正确但就算是已经有序的{1, 2, 3, 4, 5}它依然会按照arr.length - 1轮跑完白白做了很多无意义的比较和循环。下面两节就把这些浪费砍掉。2.2 优化方向一内层循环的边界收缩先看第一个优化点其实初始版本的代码里已经用到了arr.length - 1 - i这个“减 i”就是边界收缩。为什么要减还是那句话每排完一轮数组末尾就多一个已经归位的最大元素它不会再移动了你下一轮还去跟它比纯属浪费。拿{5, 1, 4, 2, 8}举例第0轮j从0到3比较4次最大值8归位。第1轮j从0到2比较3次最大值5归位。第2轮j从0到1比较2次最大值4归位。第3轮j从0到0比较1次最大值2归位。4轮一共比较了10次。如果不做边界收缩每一轮都从头比较到尾那就是4 × 4 16次多了60%的操作。数据量小的时候感觉不出来数据量一上来差别非常明显。所以“内层循环的边界随着外层轮数动态收缩”这属于必写的优化项不是可选操作。这个优化写起来很简单你只要保证内层循环j arr.length - 1 - i就行。初始版本其实已经做到了所以这一步更多是理解“为什么这么写”。真正需要重点讲的是下一个优化。2.3 优化方向二提前跳出已经有序的情况如果数组本身已经有序或者排序到中途就已经完全有序后面的轮次其实是在空转。判断方法很简单如果在某一轮内层循环里一次交换都没有发生那就说明数组已经有序可以立即终止整个排序。改进后的代码public class BubbleSortOptimized { public static void main(String[] args) { int[] arr {5, 1, 4, 2, 8}; int n arr.length; boolean swapped; // 记录每轮是否发生交换 for (int i 0; i n - 1; i) { swapped false; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped true; } } // 如果这一轮没有任何交换说明已经有序直接退出 if (!swapped) { break; } } for (int num : arr) { System.out.print(num ); } } }增加的只有两处外层循环开头设置swapped false内层循环每次发生交换就置为true每轮结束后检查如果还是false说明整轮下来没有任何元素需要交换数组已经有序直接break跳出循环。这个优化在最好情况下效果极好。对于已经有序的数组第一轮跑完发现没有交换立刻终止时间复杂度直接降到 O(n)就扫了一遍。而对完全倒序的最坏情况这个标记不会减少交换次数但也不会带来额外负担无非是多设置一个布尔变量而已。所以这个优化没有任何副作用哪怕你只想记住一个优化方案也应该记这个。2.4 封装成可复用的工具方法写算法练习时代码放main方法里没问题但实际用起来最好封装成一个静态工具方法谁都能调。下面是我常用的封装写法public class SortUtil { /** * 冒泡排序升序 * param arr 待排序数组排序会直接修改原数组 */ public static void bubbleSort(int[] arr) { if (arr null || arr.length 2) { return; // 空数组或单元素数组不需要排序 } boolean swapped; for (int i 0; i arr.length - 1; i) { swapped false; for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped true; } } if (!swapped) { break; } } } }这里加了个前置判断如果arr为null或者长度小于2直接返回。这个防御性写法在工程里很重要因为方法调用方可能传进来空指针你直接在方法开头挡一道就不会在下面的循环里抛空指针异常。实际项目中这种“参数进来先做合法性校验”的习惯比算法本身更能帮你少背锅。使用时直接写SortUtil.bubbleSort(arr);就行。注意一点这个方法是原地排序会直接修改传进来的数组不会返回新数组。如果你不想改原数组那就先Arrays.copyOf(arr, arr.length)复制一份再对副本排序。这点之前我也吃过亏面试时有人会追问“原数组被改了怎么办”这就是考察你对“引用传递”的理解。3. 复杂度与算法特性内行看门道3.1 时间复杂度最坏、最好、平均分别怎么算先说最坏情况。假设数组完全倒序比如{5, 4, 3, 2, 1}每一轮从头到尾的每一次比较都需要交换。总比较次数是(n-1) (n-2) ... 1 n(n-1)/2交换次数也接近这个数。在算法分析里常数系数不算只看最高阶所以最坏时间复杂度是 O(n²)。最好情况是数组已经有序比如{1, 2, 3, 4, 5}。加了swapped标记之后第一轮跑完发现没有任何交换直接退出循环总共只比较了n-1次也就是 O(n)。如果不加这个优化就算数组有序程序也会傻乎乎跑完n-1轮复杂度还是 O(n²)。这就是为什么上面那个优化不是可有可无的。平均情况就复杂一些。随机打乱的数据每一轮大概会有一定比例的比较需要交换总交换次数大约是最坏情况的一半量级。在计算机界默认的平均复杂度分析里冒泡排序仍然是 O(n²)因为 n² 级别的增长趋势没有改变前面那点常数系数差异在大数据量下无足轻重。这里我多说一句很多人背结论时说“冒泡排序时间复杂度是 O(n²)”其实不够严谨。正确的是分情况最好 O(n)、最坏 O(n²)、平均 O(n²)。面试里能答出这个区别比单纯背一个结论要加分得多。3.2 空间复杂度与稳定性空间复杂度很简单。整个排序过程只用了一个临时变量temp来交换元素外加一个布尔变量额外空间不随数据规模增长而增长所以空间复杂度是 O(1)。这种“不需要额外内存就能完成排序”的算法统称为“原地排序”算法。稳定性是个容易被忽略但很重要的特性。先解释什么叫稳定如果数组里有两个值相等的元素比如{3a, 1, 3b, 2}其中3a和3b值相同但原本顺序是 a 在前 b 在后。排序后如果3a仍然在3b前面就说这个排序算法是稳定的。冒泡排序是稳定的证据就在比较条件里只有当arr[j] arr[j 1]时才交换。注意这里是严格大于而不是大于等于。当两个相邻元素相等时条件不成立它们不会交换位置所以相对顺序始终能保持。稳定性在实际业务里有什么用最经典的例子是多重排序。比如你先按年级排学生再按分数排如果第二个排序算法是稳定的那么分数相同的学生会按照年级的相对顺序排列整体结果就是“先按分数分数相同再按年级”。如果算法不稳定第二次排序可能把第一次排好的顺序打乱。这是稳定性最重要的使用场景之后处理对象排序时还会再提到。3.3 用数据说话不同数据规模的表现实测理论说完了用实际数据验证一下更直观。我在普通台式机上用一万个随机整数测试过冒泡排序的耗时结果大致是一万个数据耗时约 0.05 秒十万个数据耗时约 5 秒二十万个数据直接飚到 20 秒以上。这个增长趋势正好吻合 O(n²)数据量翻一倍耗时翻四倍数据量从一万涨到十万扩大了10倍耗时从0.05秒涨到5秒约100倍。你可能觉得5秒还能接受。但请注意排序只是业务里的一个小步骤真实项目中可能还要查库、调接口、渲染页面。5秒足够让用户焦虑了。再想想如果是100万个数据冒泡排序可能要跑400秒以上这在工程里完全不可接受。所以当数据量超过十万这个级别没人会用冒泡排序这才是它“教学价值大于工程价值”的真正原因。不过也别因此小看它。早在五六十年代计算机萌芽时期冒泡排序是真实投入使用的。那时候的数据规模远没有今天这么夸张O(n²)的算法跑几千个数据完全没问题。技术的进步不是淘汰了某一种算法而是把性能瓶颈的边界往后推了。理解冒泡排序等于理解了为什么今天还要花大力气去优化那些“看起来更高端”的排序算法。4. 把排序从数组扩展到真实对象4.1 用Comparable给对象排序工作中要排序的往往不是int数组而是对象列表。比如一个班级成绩表里面有姓名和分数你想按分数从低到高排。这时直接套用int数组的写法就不行了因为对象之间默认没有“谁大谁小”的概念你需要告诉Java怎么比较。Java提供了一个接口Comparable意思是“我可比较”。让Student类实现它重写compareTo方法定义比较规则冒泡排序就能继续用了class Student implements ComparableStudent { private String name; private int score; public Student(String name, int score) { this.name name; this.score score; } public String getName() { return name; } public int getScore() { return score; } Override public int compareTo(Student other) { return this.score - other.score; // 按分数升序 } }compareTo的返回值有讲究返回负数表示当前对象“小于”参数对象返回0表示相等返回正数表示“大于”。所以this.score - other.score表示按分数升序排列。如果你把 this 和 other 反过来减就是降序。有了这个定义排序代码几乎不用改只把比较条件从arr[j] arr[j1]换成arr[j].compareTo(arr[j1]) 0就行for (int j 0; j students.length - 1 - i; j) { if (students[j].compareTo(students[j 1]) 0) { Student temp students[j]; students[j] students[j 1]; students[j 1] temp; } }顺便提醒一下this.score - other.score这种写法在分数差值很大时可能溢出因为 int 有上限。更稳妥的写法是调用Integer.compare(this.score, other.score)它的内部实现再做比较不会溢出。虽然成绩场景一般不会触发但这是代码严谨性的一个体现。4.2 用Comparator指定灵活规则Comparable的问题在于每个类只能定义一个默认排序规则。可有时候同一个类有多种排序需求按分数排、按姓名排、按学号排……总不能每次改compareTo吧。这时候就需要Comparator。Comparator是独立于实体类之外的比较器。比如你想按分数降序排列可以写ComparatorStudent scoreDesc new ComparatorStudent() { Override public int compare(Student a, Student b) { return Integer.compare(b.getScore(), a.getScore()); } };如果用的是Java 8以上版本可以直接用Lambda表达式ComparatorStudent scoreDesc (a, b) - Integer.compare(b.getScore(), a.getScore());冒泡排序也改成接收一个Comparator参数这样同一个排序方法就能适配任意排序规则public static void bubbleSort(Student[] arr, ComparatorStudent comparator) { boolean swapped; for (int i 0; i arr.length - 1; i) { swapped false; for (int j 0; j arr.length - 1 - i; j) { if (comparator.compare(arr[j], arr[j 1]) 0) { Student temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped true; } } if (!swapped) { break; } } }调用时传进入对应的比较器就行bubbleSort(students, (a, b) - Integer.compare(b.getScore(), a.getScore()))。Comparator 和 Comparable 的区别面试常问一句话总结就是Comparable 是类自己跟自己比Comparator 是外部帮你制定比较规则。4.3 泛型工具方法既然Student可以用Comparable或者Comparator排序那别的类型呢如果每个类型都写一个专属的bubbleSort重载方法代码会膨胀得没法维护。方法用Java泛型把排序方法写成一套通用的。public static T extends ComparableT void bubbleSort(T[] arr) { boolean swapped; for (int i 0; i arr.length - 1; i) { swapped false; for (int j 0; j arr.length - 1 - i; j) { if (arr[j].compareTo(arr[j 1]) 0) { T temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped true; } } if (!swapped) { break; } } }注意泛型约束T extends ComparableT它的意思是传入的数组元素类型T必须实现了Comparable接口否则编译器直接报错。这样写完之后String[]、Integer[]、Student[]都能直接用这个方法排序因为它们都实现了Comparable。不过再结合刚才说的Comparator还可以写一个更灵活的通用版本。好多框架源码里排序工具就是这么设计的既能用元素自带的compareTo也能临时传入外部比较器。为了不让篇幅太长我这里就不把两种方案合并展示了。你只要记住排序的底层逻辑跟数据类型无关抽出来做成泛型方法才是工程里常用的姿态。5. 练手与调试把代码真正变成自己的5.1 零基础练习的进阶阶梯算法光看是看不熟的必须先动手敲。我给零基础的人推荐一个练习阶梯照着走大概三遍就能形成肌肉记忆第一遍照抄代码。不要觉得抄代码没用抄的过程中你会注意到很多细节循环变量初始值、边界是arr.length - 1还是arr.length - 2、交换三要素有没有写全。抄完跑一遍看到控制台输出1 2 4 5 8这是建立信心的第一步。第二遍闭卷手写。把代码合上看着题目“用冒泡排序对数组升序排序”自己在IDE里从头写一遍。写错也没关系编译报错、结果不对都正常。我见过不少新手在这一步发现自己写的内层循环边界有问题或者交换写成了赋值。错误在这一遍暴露出来比以后在面试现场暴露要好一百倍。第三遍改造成降序版。把改成运行看结果再想想为什么降序是这么改。然后尝试把int[]换成double[]或者自定义一个类去排序。到这一步说明你不是死记硬背而是真的理解了这个算法的运作方式。5.2 常见运行时问题与排查思路练习过程中最容易遇到这些坑我逐个说排查思路。数组越界异常。报错信息通常是ArrayIndexOutOfBoundsException。原因多数是内层循环的j走到了arr.length - 1然后你访问arr[j 1]就越界了。解决办法是把内层循环的终止条件改成j arr.length - 1 - i。想验证的话可以在循环里打印j和j 1的最大值日志一打出来边界问题一目了然。排序结果不对部分元素没排好。最常见原因是外层循环轮数不够。如果外层循环只写了i arr.length多一轮没关系如果写成i arr.length - 1也没关系因为最后一个元素不需要再参与比较。真正容易错的是写少了一轮比如i n - 2会导致倒数第二个元素没归位。正常外层是n - 1轮这一点可以自己用{9, 8, 7, 6, 5}这种逆序数组验证排完如果还是乱的多半就是轮数少了。交换后相同元素丢失。比如数组从{3, 3, 1}排成了{3, 1, 3}看起来数值没少但你可能觉得顺序不对。回头看稳定性分析冒泡排序只有严格大于才交换所以相同元素不会互换。如果你写成交换稳定性就没了而且也可能让原本相等元素位置交换。刻意用{3a, 3b, 1}这种带标记的数据运行一遍观察3a和3b的顺序是否保持。空指针异常。如果排序前没检查数组是否为null调用方传空进来循环里一访问arr.length就会抛NullPointerException。解决办法就是前面说的方法开头加一道if (arr null)的防御判断。排序后原数组变了。这不是bug这是原地排序的预期行为。如果你希望保留原数组先int[] copy Arrays.copyOf(original, original.length);再排序。有些初学者会在这里困惑“为什么调用完原数组变了”其实这正是引用传递的特点需要专门记一下。5.3 打印排序过程的小技巧光看最终结果很难直观感受冒泡“一步一步把最大值推到末尾”的过程。调试阶段可以在内层循环里加打印System.out.println( i i , j j , 比较 arr[ j ] arr[j] 和 arr[ (j 1) ] arr[j 1] Arrays.toString(arr) );这样每比较一次控制台就会输出当前比较的元素和此时的数组状态。你会看到最大值像一个气泡一样在一轮中一步步往数组末尾移动。看几遍输出比背十遍原理都管用。等你看懂了过程记得把这些调试打印删掉或者在方法外面加个参数控制是否打印不然输出太吵了。6. 面试与工程中的高频考点6.1 面试中关于冒泡排序的几个经典问题整理一下我见过的冒泡排序相关面试题目覆盖面还算广逐一过一遍。第一手写冒泡排序并做优化。这是出现频率最高的基本属于“送分题”。能写出swapped标记的版本说明你考虑过最好情况能把 Comparator 版本写出来说明你有对象排序的经验。建议背熟前面 2.4 节的工具方法再背一个泛型带 Comparator 的版本基本够用。第二复杂度分析。面试官一般会追问“时间复杂度是多少”“复杂度能不能降低”。你最好能答出最坏 O(n²)、平均 O(n²)、优化后最好 O(n)。然后顺带说一句空间复杂度 O(1)、稳定。这几个答案串在一起说显得思路清晰。第三什么情况下冒泡排序性能最好答数据接近有序的时候。尤其是已经排好序的数据优化版只需要 O(n) 次比较。这一点很多面试官喜欢接着问“那你能想到哪个算法更适合这种场景吗”引出插入排序的话题你如果学过插入排序就可以顺势接上。第四冒泡排序是稳定排序吗为什么答稳定因为只在严格大于时交换相等的元素不会交换位置。再举学生排名的例子说明稳定排序在实际业务中的用途。这题不难但很多人只记得结论说不出原因原因才是面试官想听的。第五100万条数据你会直接用冒泡排序吗答不会O(n²) 太慢了。大数据量应该用更高效的排序算法比如快速排序、归并排序Java 内置的Arrays.sort()底层就是 DualPivotQuicksort某些情况下还会用 TimSort。对基础元素使用快速排序对引用类型使用归并排序因为归并排序稳定。能展开到这层面试官会觉得你确实有工程经验。6.2 冒泡排序的真正价值在哪里这一节算是我个人多年的体会。冒泡排序在现代工程里的“直接可用价值”确实很低Java内置排序方法的性能远远优于手写的冒泡排序正常项目里你根本不需要自己实现。但它的价值体现在三个地方第一教学价值。它是理解其他高级排序算法的地基。你掌握了“比较是什么”“交换是什么”“循环边界怎么控制”后面学插入排序、快速排序时就会发现自己不会卡在最基础的地方。我辅导过不少新人凡是冒泡排序能闭眼写出来的人学后面的排序算法都很快凡是这一关抄都抄不明白的人后面大概率要在数组边界问题上一错再错。第二面试价值。现在很多公司面试喜欢从简单算法入手考察候选人思路是否清晰、代码是否整洁。冒泡排序虽然简单但能看出一个人有没有提前优化的意识、懂不懂稳定性、能不能处理边界条件。我见过有人写得飞快但没加swapped优化也有人加了优化但不能解释为什么最好情况是 O(n)说到底还是“会写代码但未必理解算法”。第三迁移价值。冒泡排序的优化思路——“如何通过一个标记变量避免无效操作”“如何收缩边界”——这种思维方式是可以迁移的。很多实际业务里的循环嵌套代码都存在类似的冗余你能用同样的思路去优化。算法学习从来不只是为了面试是在训练一种把问题抽象成逻辑流程的能力。如果你把这篇文章看完了我建议你做的第一件事不是继续学下一个排序算法而是把这几个版本的代码全部手敲一遍跑通再自己改成降序、改成对象排序。代码这东西手过一遍和眼过一遍效果差得不是一点半点。等你真正掌握了冒泡排序再去看选择排序、插入排序你会发现它们像是同一个家族里的兄弟上手难度直线下降。最后分享一个小技巧在IDE里写排序算法时记得随手用Arrays.toString(arr)打印每一轮的结果然后拿随手写的小数组去验证。我踩过几次坑之后现在每次写新排序算法都是先打印过程再分析结论比自己干瞪眼猜哪里写错了快得多。

相关新闻

如何快速恢复大会话:pi_agent_rust 的 SQLite 索引与 v2 Sidecar 存储架构完全指南

如何快速恢复大会话:pi_agent_rust 的 SQLite 索引与 v2 Sidecar 存储架构完全指南

人工智能大模型AI Agent代码智能体CLI开发工具工具调用MCP Clients 【免费下载链接】pi_agent_rust High-performance AI coding agent CLI written in Rust with zero unsafe code 项目地址: https://gitcode.com/gh_mirrors/pi/pi_agent_rust 点击查看 免费下载 …

2026/10/11 17:31:19 阅读更多 →
FAST-LIO 源码解读:从李群代数到迭代卡尔曼滤波的 LiDAR-Inertial SLAM 实践

FAST-LIO 源码解读:从李群代数到迭代卡尔曼滤波的 LiDAR-Inertial SLAM 实践

简介:面向SLAM学习者的FAST-LIO论文与代码理解笔记,适合已有LiDAR、IMU基础或正在研读源码的读者。资源共包含1个docx文档,压缩包大小约为5.09MB,结构完整,便于按章研读。目前已有3100余人学习下载,关注度较…

2026/10/11 17:31:19 阅读更多 →
Shardeum NFT市场搭建指南:如何在低手续费EVM链上构建数字藏品交易平台

Shardeum NFT市场搭建指南:如何在低手续费EVM链上构建数字藏品交易平台

Shardeum NFT市场搭建指南:如何在低手续费EVM链上构建数字藏品交易平台 【免费下载链接】shardeum Shardeum is an EVM based autoscaling blockchain 项目地址: https://gitcode.com/GitHub_Trending/sh/shardeum Shardeum NFT市场:低手续费的EV…

2026/10/11 17:31:19 阅读更多 →

最新新闻

全国水系矢量数据:可计算的地理底图骨架与空间分析实战指南

全国水系矢量数据:可计算的地理底图骨架与空间分析实战指南

简介:本资源为全国水系矢量数据集,面向GIS初学者、地理信息专业学生、城乡规划与水利相关从业者,解决基础空间分析中缺乏权威、分级清晰的中国水系底图问题。压缩包共27个文件,含6组shp(几何)、dbf&#xf…

2026/10/11 18:18:47 阅读更多 →
SAM+UNet息肉分割实战:边界先验注入与调参避坑指南

SAM+UNet息肉分割实战:边界先验注入与调参避坑指南

简介:本资源面向医学图像分割方向的研究者与开发者,提供一套在Unet基础上引入SAM提示框机制的息肉肿瘤语义分割完整方案,适合具备一定深度学习基础、希望复现或改进医学分割模型的中高级学习者。压缩包共2000个文件,约263.61MB&am…

2026/10/11 18:18:47 阅读更多 →
YOLOv8+PyQt5课堂行为检测系统:从数据集训练到界面部署全攻略

YOLOv8+PyQt5课堂行为检测系统:从数据集训练到界面部署全攻略

简介:基于YOLOv8与PyQt5的课堂检测系统完整实现,面向教育技术研究者、计算机视觉初学者及有课堂管理需求的学校/机构,解决传统人工课堂监控效率低、无法实时量化学生行为的问题。系统利用YOLOv8目标检测算法进行高精度实时推理,通…

2026/10/11 18:18:47 阅读更多 →
一条命令完成模型后训练:DataArc SynData Toolkit的SFT与GRPO训练完全指南

一条命令完成模型后训练:DataArc SynData Toolkit的SFT与GRPO训练完全指南

【免费下载链接】DataArc-SynData-Toolkit Synthetic Data Generation Platform By DataArcTech 项目地址: https://gitcode.com/gh_mirrors/da/DataArc-SynData-Toolkit 点击查看 免费下载 DataArc SynData Toolkit 是一个开源合成数据生成平台,除了能…

2026/10/11 18:18:47 阅读更多 →
C++ OpenCV 虹膜分割实战:从灰度预处理到霍夫圆与微积分算子

C++ OpenCV 虹膜分割实战:从灰度预处理到霍夫圆与微积分算子

简介:这是一份面向计算机视觉初学者与图像处理方向开发者的虹膜分割实现代码,采用C与OpenCV编写,可用于生物特征识别、人眼图像分析等场景的入门实践与课程设计参考。压缩包共39个文件,约3.26MB,包含2个cpp源码文件、1…

2026/10/11 18:18:47 阅读更多 →
基于SpringBoot的网约巴士定制出行管理平台【源码+文档】

基于SpringBoot的网约巴士定制出行管理平台【源码+文档】

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/10/11 18:17:46 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →