1. 项目拆解为什么排序和查找是绕不开的基本功“day 7 数组排序查找”光看这个标题你可能觉得又是一个编程入门的老生常谈。但说实话真正写过几年代码之后你就会发现几乎所有业务系统里那些看着高大上的搜索、推荐、排行榜功能底层拆到骨头里就是数组的排序和查找问题。第七天把这个话题单独拎出来安排得很讲究——前几天的变量、循环、函数基础刚刚打牢恰好是进入算法思维的最佳时机既不突兀又有足够的挑战性。排序和查找为什么总是放在一起讲因为它们本质上是同一个问题的两面。查找依赖数据的有序性排序又是建立有序性的唯一手段。一个系统如果数据是乱的你只能老老实实从头遍历时间成本线性增长一旦排好序二分查找能把复杂度直接砍到对数级别——数据量从一万涨到一亿查找时间只增加大约四倍这个收益在真实业务里是肉眼可见的。我见过不少刚工作的开发者在内存列表上盲目用循环查找等到数据量起来才回头补排序的课其实第七天的内容早点学透后面很多场景都能少走弯路。这篇文章适合谁看两类人。一类是正在按部就班学编程的初学者刚学完基础语法想系统性搞定数组这个最重要的数据结构另一类是已经写过一段时间代码、但算法这块一直是“黑盒”的开发者——你每天都在调sort()但不知道它背后在干什么也不知道面试时那些排序算法题到底在考什么。我按真实的学习路径把第七天的内容拆开揉碎从设计思路讲到手写实现再讲我看过的那些典型翻车现场照着练基本能一次打通。要特别说明的是这里的示例我统一用 Python 来实现。原因很直接它语法足够简洁能把算法本身的逻辑暴露得最清楚不会让语言细节干扰你对核心思路的理解。你如果是用 Java、C 或其他语言在学逻辑完全通用只是语法换个皮而已。2. 方案设计排序算法那么多到底该从哪种入手2.1 先看数据规模再说没有万能的排序算法很多初学者看到排序算法清单就开始焦虑冒泡、选择、插入、希尔、归并、快排、堆排……每种都得学吗我的建议是第七天先把三种最基础的吃透——冒泡排序、选择排序、插入排序。这三种代表了最核心的排序思维交换、选择、插入。理解了这三种后面任何一个高级排序算法对你来说都只是“改进版”不会再有认知障碍。至于为什么不让新手直接上手快速排序或归并排序因为高级排序背后的分治思想、递归调用、空间换时间这些概念需要更成熟的编程思维做支撑。你直接背快排模板也能跑通但一旦面试官问“partition 为什么要这样写”或者让你在链表上实现排序就全露馅了。基础排序虽然时间复杂度看着不够亮眼但它们能帮你把“排序到底在干什么”这件事彻底想明白。我实战中的经验是选排序方案要看数据规模和类型。数据量在几百个以内插入排序的实际性能往往比快排还好——因为它的常数因子极小而快排的递归开销在这个规模下反而拖后腿。数据量上了万才开始轮到快排和归并表演。另外还得看数据是否近乎有序——日常业务里很多数据其实已经大差不差排好了这时候插入排序的性能能逼近 O(n)这是快排做不到的。这些细节没人告诉你是踩过坑之后才会懂的。2.2 查找的核心逻辑有序数据是二分的前提查找算法比排序简单但陷阱更深。线性查找是个人都会写遍历一遍比对即可时间复杂度 O(n)无需任何前提条件。二分查找才是真正的分水岭——它要求数据必须已经排好序然后每次取中间值跟目标比较把搜索范围砍掉一半。这个过程用到的是归约思维。很多初学二分的人容易钻牛角尖二分查找的法律依据不就是数据有序吗如果数据本身无序怎么办答案很简单先排序再查。这看起来是句废话但在工程中确实就是标准方案。比如你要在用户列表里按 ID 查账号那只要维护列表时保持有序即可查询时二分秒出结果如果你要按年龄查那就得额外建一个按年龄排序的索引结构——你看MySQL 的索引本质上干的就是这个事。所以第七天学习查找的核心不是背二分代码而是理解“排序”是“高效查找”的前提两者互为表里。能把这两点连起来想你对数据结构的理解就已经超过大多数停留在语法层面的初学者了。3. 核心实现细节排序和查找最容易出错的隐藏雷区3.1 冒泡排序的常数优化加一个标志位就够了冒泡排序的教科书写法是两层循环挨个比较相邻元素大的往后冒。时间复杂度稳定在 O(n²)。但很多教材没告诉你标准写法对“数据已经有序”的场景完全浪费——即使一趟跑下来一个交换都没发生程序还是会继续跑完所有轮次。解决方案简单得不像话每轮开始前设一个swapped标志循环内只要发生过交换就把它置 True。整轮结束发现一次交换都没有直接break。这个优化看似不起眼但对本就是有序或接近有序的数据冒泡排序的效率能直接从 O(n²) 降到 O(n)。我在实际编码时几乎总是带上这个优化习惯了之后再看不带标志位的写法总觉得像是少了什么。另一个很容易被忽略的细节是内层循环的边界。第 i 轮结束后数组末尾的 i 个元素已经是排好序的所以内层循环只需要跑到 n-1-i 的位置即可。写错这个边界产生的不是巨大错误而是“总有几个元素排不对位置”这种最让人头疼的 bug——因为结果看起来接近正确很难一眼定位问题。3.2 选择排序为什么“看上去省事实际上依然慢”选择排序的思路非常直观每次从未排序区间里找到最小元素放到已排序区间的末尾。它跟冒泡最大的区别是交换次数极少——每轮只有一次交换而冒泡最坏情况每轮要交换很多次。但别高兴太早。选择排序的时间复杂度仍然是 O(n²)因为每轮找最小值的比较一次都省不掉。换句话说它只是把“交换”的代价换成“比较”的代价并没有从本质上解决问题。选择排序有个非常容易被忽略的特性它是不稳定排序。比如有三个同名的人按年龄排序选择排序可能会把同名者的相对顺序打乱。这在实际业务里很要命——如果你的数据本身携带某种“原始顺序”信息或者你希望多字段排序时次要字段的相对顺序保持稳定那就不能盲目用选择排序。Python 的sorted()底层是稳定排序Java 的对象排序也是稳定归并这些细节不是巧合而是工程考量后的选择。3.3 二分查找的边界控制一个下标的坑能让你 Debug 一整天二分查找的代码翻来覆去就那么几行但前后写法变体很多。最经典的两种左闭右闭left, right 0, n-1和左闭右开left, right 0, n。初学者最容易栽在死循环上——比如用左闭右闭写法时循环条件写成了while left right结果当左右指针指向同一个元素时直接退出循环漏掉最后一个比对机会。死循环的另一个常见原因是mid的计算。很多人写成mid (left right) // 2这在两数相加溢出时会出问题。Python 因为整数可以无限大不太会遇到溢出但 C 和 Java 里这就是经典 bug。更稳妥的写法是mid left (right - left) // 2既防溢出语义上也更清晰。我自己练习时列出过一张对照表帮助记忆两种写法区间定义初始条件循环条件收缩方式适用场景左闭右闭left0, rightn-1left rightleftmid1rightmid-1标准查找左闭右开left0, rightnleft rightleftmid1rightmid查找左边界/右边界查找左边界和右边界的问题在 LeetCode 上很常见也是经典题。很多人背模板背得很熟但一旦换成“找第一个大于等于目标值的位置”就傻眼了。原因就是没有理解区间的开闭和收缩规则只是机械地套代码。建议你把这两种方式都手推一遍二分过程把每一步 left、right、mid 的值写出来比对几次就能形成手感。4. 实操记录从画流程图到手写代码完整过一遍4.1 第七天的练习清单与输出我按一个推荐的学习节奏列一份当天任务表基本三到四小时可以完成手写冒泡排序要求加swapped优化并打印每轮排序后的数组状态。手写选择排序对比它与冒泡排序在交换次数上的差异。手写插入排序理解“挪位”和“交换”的实现差异。手写二分查找分别实现左闭右闭和左闭右开两种版本。用无序数组和有序数组分别测试查找对比结果。所有实现统一用 Python 写。有一点很重要一定不要只截图跑通的结果就完事。我当年是把每轮排序的中间状态都打印出来对着输出逐行核对。这个过程虽然烦但效果极佳——你看见数据是怎么一步一步变成有序的算法的“动感”才会真正刻进脑子里。4.2 参考实现冒泡排序与二分查找的干净版本下面是冒泡排序带优化和轮次打印的 Python 实现代码风格尽量贴近教学中容易理解的样子def bubble_sort(arr): n len(arr) for i in range(n - 1): swapped False for j in range(n - 1 - i): if arr[j] arr[j 1]: arr[j], arr[j 1] arr[j 1], arr[j] swapped True print(f第 {i1} 轮结果: {arr}) if not swapped: break return arr test_arr [5, 1, 4, 2, 8] sorted_arr bubble_sort(test_arr) print(最终排序结果:, sorted_arr)执行后你会看到第 3 轮时数组已经有序第 4 轮因为swapped为 False 直接跳出省掉了最后一轮无意义的空转。这个打印细节是理解优化原理的最好方式。二分查找我建议写一个比较完整、带检查的版本。先要求数组有序然后定位目标索引def binary_search(arr, target): left, right 0, len(arr) - 1 while left right: mid left (right - left) // 2 if arr[mid] target: return mid elif arr[mid] target: left mid 1 else: right mid - 1 return -1 arr [1, 3, 5, 7, 9, 11, 13] print(binary_search(arr, 7)) print(binary_search(arr, 8))一组测试数据包含命中和未命中两种情况确保代码不是“碰巧能在某个用例上跑通”。如果有精力再补一组空数组和单元素数组的测试边界情况才是真正拉开人与人差距的地方。4.3 插入排序的“挪位”实现值得多看一眼插入排序的思路很像打扑克时理牌——新抓一张牌往手里已经有序的牌堆里插到正确位置。很多人的第一版实现用的是交换操作新元素往前一步步“冒”到正确位置。这种写法直观但交换是三次赋值的代价效率偏低。更地道的实现是“挪位”先把待插入元素存到临时变量然后从后往前逐个比较不符合条件的元素整体后移一位最后腾出空位把临时变量放进去。这个技巧在工程里很常见也直接关系到你对“数组搬移”这个底层操作的理解def insertion_sort(arr): for i in range(1, len(arr)): key arr[i] j i - 1 while j 0 and arr[j] key: arr[j 1] arr[j] j - 1 arr[j 1] key return arr arr [12, 11, 13, 5, 6] print(insertion_sort(arr))看到j 1那个赋值了吗它就是“把右边的数挪到左边占据的位置”的核心动作。很多人第一次写完会发现自己把某个值覆盖丢了原因就是忘了先把key存起来——这个坑几乎每个人都踩过踩一次就记住了。5. 第七天最容易踩的坑这些问题几乎每个新手都问过5.1 为什么我“有序”的数组二分查找还是返回 -1这个问题的排查思路很直接——先确认数组是否真的有序。很多人用列表推导式生成数据时忘了排序比如arr [i % 3 for i in range(10)]它输出的是[0, 1, 2, 0, 1, 2, ...]根本不是有序的二分查找自然找不到目标。第二个被忽略的点是数组元素可能包含重复值而二分查找只保证找到一个位置不代表是第一个或最后一个。如果你需要的是“查找第一个等于目标的下标”就得用 3.3 里提到的左闭右开变体。我在实际调试时遇到过不止一次——用户明明有多条记录满足条件结果只返回了一个排查半天才想起来是边界处理的问题。5.2 排序后数据“莫名其妙”丢失了问题出在哪这是初学排序时最常见也最崩溃的错误。问题根源几乎总是混淆了“返回新数组”和“原地修改”两种操作方式。Python 的list.sort()是原地排序直接改原数组并返回None而sorted()是返回新数组原数组不动。如果你写arr arr.sort()那你把None赋给了数组名——之后任何操作都会报错。排查这个问题的快速办法是打印arr的类型和值一看是None就明白了。但更根本的是养成习惯明确你要的是“原地改”还是“重新生成”。业务场景里如果要保留原始数据用于回溯审计必须用sorted()如果只是临时排序用于展示list.sort()就够用。两种思路对应不同的内存和数据安全考量。5.3 快排和归并学不学第七天要不要直接上算法模板我的建议是第七天不要。不是说你学不会而是基础排序的动手经验还不够。快排的核心是 partition 操作而归并的核心是合并两个有序数组这些动作如果直接在高级算法里接触你会觉得“每一步都懂合起来就是不懂”。打个比方基础排序像是练扎马步看着枯燥但快排、堆排那些高难度动作能不能站稳完全取决于这一步。我见过不少人第七天就急着背快排模板后来问他为什么快排最坏情况下是 O(n²)答案支支吾吾——这说明“会背”和“会了”是两回事。扎扎实实把冒泡、选择、插入写到闭着眼都能默写的程度再进快排一点都不迟。不过有个扩展可以现在就做拿一张卡片记录每个排序算法的核心指标——最佳时间、最坏时间、平均时间、空间复杂度、是否稳定。这个习惯保持到学完全部排序算法你就拥有一张自己的速查表。我到现在写代码前偶尔还会扫一眼这张表选择排序算法时完全不慌。5.4 排序查找的边界条件与测试用例设计第六天的数组题可能还会容忍你函数写错了直接肉眼 Debug但排序和查找这种“中间状态非常多”的题目没有系统的测试用例排查起来非常痛苦。建议每次写完排序后立刻跑这组用例空数组、单元素数组、逆序数组、全相同元素数组、随机大数组比如一万个元素、以及一个刚好有序的数组。每类覆盖一种边界情况或设计意图空数组和单元素数组验证循环边界是否稳健。逆序数组验证最坏情况下的排序正确性。全相同元素数组验证相等值处理逻辑排序算法遇到全相等数据时如果不做特殊处理表现会很有意思。随机大数组初步验证性能也能暴露 O(n²) 算法在数据量上来时的力不从心。已有序数组结合优化后的冒泡直观感受加了swapped标志前后的轮数差异。这套测试方法我到现在都在用只是从手写数组换成了脚本自动生成。你把它当成第七天的习惯养成后面学任何树、图、动态规划测试思维都是同一个套路。6. 排序查找的应用延伸这不只是考试题是生产力工具很多人学完排序查找会觉得这东西只在面试和算法竞赛里有用。这是个天大的误解。我给你举几个真实业务里天天遇到排序查找问题的场景。第一个是排行榜。游戏里玩家按积分排名直播平台按在线人数排序电商后台按销量排序——这些功能几乎全是对数组做排序。数据量小时直接内存排序数据量大了以后就要用外部排序或者引入索引结构但核心逻辑还是“排序”。你第七天打下的基础就是理解那些看似厉害的“实时排行榜系统”的第一块积木。第二个是去重与统计。统计一段文本里每个单词出现次数标准做法是排序后相邻比较或者用哈希表。但如果数据需要在有序结构上做范围查询——“找出所有年龄在 18 到 25 岁之间的用户”——那就必须先让数据有序然后二分查边界。这个“边界查找”能力在 3.3 里专门练过属于直接可以迁移的技能。第三个是查找峰值。有序数组的二分是基础无序数组的“局部峰值查找”其实也能用二分思想——你发现数组是乱的但依然可以通过比较中点和邻点的关系决定往哪边走。这是第七天内容的思想延伸也说明“二分”这个减治思维的应用范围远比“有序数组查值”要广。这些延伸不需要在第七天全部掌握但知道它们的存在你的学习目标会从“完成打卡”变成“建立一套可迁移的数据处理思维方式”这是完全不同的境界。7. 最后再多说两句学习上的真心话第七天的内容如果只是照着一篇教程敲一遍代码说实话也就半小时的事。但我的建议是分配至少三到四倍的时间去“折腾”改改循环边界看会发生什么把数组原地排序改成返回新数组把一个左闭右闭的二分改成左闭右开再跑一遍测试。每一次刻意折腾带来的理解都远比顺手敲一遍代码要深。我在实际学习阶段感受最深的一点是手写算法脑子会了但手不会。看视频、看博客、看示例代码每一步都觉得理所当然一旦关掉屏幕自己写立刻卡在循环边界和交换逻辑上。这是非常正常的“手脑不同步”现象唯一的解法就是多写几遍。写第一遍是模仿写第二遍是查漏写第三遍才是你自己的东西。另外一个容易被忽略的习惯是代码风格。排序查找的代码虽然短但恰恰是练习变量命名、函数拆分、注释书写的好机会。比如用left和right做指针名而不是i和j把核心逻辑抽成独立函数而不是在测试脚本里写成一坨。这些职业习惯从第七天开始养后面自然长在代码里比以后专门花时间练要轻松得多。学习计划到第七天正好过了一周按这个节奏推进后面的哈希、递归、树结构都会顺利不少。排序查找这一关过了你就拥有了数据结构的第一个“杠杆点”——从此面对的不是零散的知识点而是一条可以反复套用的思维链路。保持这个手写、测试、总结的习惯第七天远不是终点而是你真正开始用算法思维解决问题的起点。