news 2026/10/2 3:27:18

更弱智的算法学习:暴力枚举、剪枝与排序算法复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
更弱智的算法学习:暴力枚举、剪枝与排序算法复盘

“更弱智的算法学习”这个名字听起来像是在自嘲,但今天是我坚持算法学习的第32天,我反而觉得这个“弱智”标签挺真实、也挺有用。朋友圈打卡时候随手起的标题,没想到成了我三十多天里最好的心理建设工具:不强求一次看懂所有高深理论,不指望十几天就刷穿大厂面试题,就是把每个算法当成一个“笨办法”来学,先想清楚它到底在解决什么问题,再动手写、动手调。今天的主题是暴力枚举和剪枝,顺便把排序算法整个过了一遍,趁着热乎劲儿把这段心得整理出来,希望能给同在学算法、尤其是零基础和弱基础的你一点参考。

1. 第32天为什么选“暴力枚举”当主角

1.1 别小看这个“最笨”的算法

很多初学算法的人一上来就冲KMP、冲击力最强的动态规划、刷满脑子“高级算法”,但真正到写代码的时候就蒙了:套模板都套不对,更别提分析为什么对。我前三十天的教训非常直白——地基没打好,后面的东西全是空中楼堡。

暴力枚举(Brute Force)就是那个地基。它没有任何花哨的技巧,核心就一句:把所有可能的答案一个一个试一遍,选那个符合条件或者最优的。听起来像废话,但实际做起来,大部分人连“把所有可能”这五个字都做不到。你可能会问:这有什么技术含量?我先说一个最真实的场景:LeetCode和蓝桥杯的题目,很多都不要求你一次写出最优解,只要你能在限定时间内跑出正确结果,至少能拿到基础分。很多时候暴力解就是把思路理清楚的最短路径,先写出来跑通,再谈优化。我的习惯是:拿到一道题,先问自己“如果不用任何高级技巧,能不能把答案找出来”,用这种思路把状态空间列清楚,然后再去考虑优化。这一步训练的是对问题空间的理解力,不是代码手速。

1.2 暴力枚举的三个实操层次

把暴力枚举做好,其实有三个层层递进的要求:

第一,枚举什么。也就是要定义清楚状态空间。一个数组里找两个数,让它们之和等于target,暴力的状态空间就是所有二元组下标(i, j);从一个集合里找满足条件的子集,状态空间就是所有子集。很多同学卡在这里,不是因为不会写for循环,而是没想清楚“把所有可能”到底包含了哪几种可能。这步建议画出来,不要直接在脑子里想。

第二,怎么枚举才不重不漏。这是最容易翻车的地方。两个for循环的时候,内层循环是从0开始还是从i+1开始,直接决定了你会不会重复枚举、会不会漏掉组合。养成一个标准姿势:组合类问题里,外层循环i从0到n-1,内层j从i+1到n-1,这样枚举的是所有下标对且不重复;排列类问题里,需要用visited数组记录哪些元素已经用过。这个习惯要形成肌肉记忆。

第三,能不能早点停下来。这就是剪枝,也是暴力枚举进阶到高效算法的起点。

1.3 剪枝不是玄学,是把“明显不对”的路提前堵死

剪枝这个术语听起来很吓人,实际它干的事情就是:在枚举过程中,提前判断某条分支不可能产生答案,就跳过它,不在它身上浪费时间。我学的时候用了生活化的类比来理解:你要在一个小区里找一个人,暴力枚举就是每栋楼每层每户都敲门问一遍;而剪枝就是——如果物业告诉你这人不住在带电梯的楼里,那你就不用去电梯楼挨家挨户问,这就是一个剪枝。

剪枝常见的有三类,我整理成一张表方便对照:

剪枝类型判断依据典型场景
可行性剪枝当前这条路已经不可能满足题目条件已经超过目标值、已经用过非法元素
最优性剪枝当前这条路就算走到头,也比已有最优解差已经花费超过当前最优答案
顺序剪枝先搜更可能成功的分支,失败就早点回头分支数量不同时,从分支少的开始搜

学习的时候关键不是记住这三个名词,而是掌握一个思维习惯:每写一层循环或递归分支,都问一句“有没有什么条件能让我不用继续试了”。比如经典的全排列+求和问题,如果当前累加和已经超过target,后面加什么都超,直接return。就这么一行代码,可能把运行时间从几百毫秒降到个位数。

1.4 暴力枚举阶段最容易踩的坑

我把自己踩过和看别人踩过的坑集中写在下面,每条都是用时间换来的:

  • 枚举范围看错。题目给的数范围是0到n,你写了for(i=0; i<n; i++),漏掉了最后一个位置;或者数组长度是n,你for到n+1直接越界。聪明办法是统一用“左闭右开”的区间写法,也就是for循环里写i=0; i<len; i++,不要一会儿<一会儿<=。
  • 剪枝剪过头。剪枝的前提是“这一条分支绝不可能产生正确答案”,如果你只是“感觉可能不对”就剪掉,很容易把正确的答案也剪掉。判断标准是:必须能严格证明这条分支不考虑答案也一定错误或一定不优,否则别剪。
  • 递归枚举忘记恢复状态。比如在全排列里,你用过某个元素后要标记visited,回溯时必须撤销这个标记,否则后续分支会漏掉这个元素。这个操作有个专门的名字叫回溯,可以说是递归枚举的灵魂。
  • 认为暴力一定超时。实际情况是,题目给的数据范围很小的时候,暴力就是最优解。判断依据很简单:估算枚举次数。如果一次操作规模在10的7次方量级,多数语言在1秒到2秒内都能跑完;到10的8次方就得考虑优化了。学会估算枚举次数,比盲目优化重要得多。

2. 排序算法全复习:从O(n²)到O(n log n)的分水岭

2.1 为什么学习进度里必须插一天专门搞排序

排序是所有算法里出场率最高的一类基础操作,不管你是做数据分析还是写业务代码,不管你是刷题还是搞工程,几乎天天和排序打交道。而且排序算法是个非常典型的学习载体:同一个问题,有十几种解法,每种解法的思路、时间空间复杂度、稳定性都不一样。对我来说,学习排序算法最大的意义不在于“会用sort函数”,而在于理解“同一个目标可以有不同的算法路径”,这是算法思维的启蒙课。

我在day32安排了一个两小时专项,把最常见的一类排序算法挨个手写了一遍,包括冒泡排序、插入排序、选择排序、归并排序、快速排序、堆排序。这个过程看着多,实际只要理清一条主线就好:每个算法都要回答两个问题——它每轮在做什么?它每轮能确定什么?

2.2 O(n²)三兄弟:冒泡、插入、选择

这三个排序常被放在一起对比,因为它们都是两层循环,时间复杂度都是O(n²)。但细节差别很大,我用一个实战视角来解释:

冒泡排序每轮从前往后扫描,相邻元素比较,如果前大后小就交换。这样一轮下来,最大的元素会被“冒泡”到最后面,所以第i轮结束后,倒数第i个位置就确定了。它的优点是代码简单直观,缺点是交换次数多。冒泡有个特性:对于已经有序的数组,一轮扫描如果没有发生任何交换,就可以提前结束,这是它天然的优化点。

插入排序更像是在打扑克牌摸牌:把新摸到的牌从右往左和手里已有的牌比较,找到位置插进去。实际写的时候,是把当前元素记下来,逐个往后挪动比它大的元素,最后放入正确位置。插入排序在数据局部有序的时候特别猛,最优情况复杂度能降到O(n),所以很多高级排序在小规模子问题上会用它收尾。

选择排序每轮扫描剩余未排序部分,找到最小值和当前位置交换。它的特点是交换次数少,但比较次数固定,无论如何都是O(n²)。这里要特别提醒一个大家都会犯的错:找到最小值下标后,如果这个最小值就在当前位置,不需要交换,直接跳过,否则会白白增加一次赋值的运行开销。

2.3 归并排序:分治思想的第一次正面接触

归并排序的核心操作只有两个字:合并。把数组切成两半,各自排好序,再准备一个临时数组,把两边按大小依次合并回去。这个“切到不能再切,再逐层合并”的过程,就是分治思想最标准的范本。它最稳定的地方在于:不管数据原本是什么顺序,时间复杂度都是O(n log n),不会退化。代价是需要额外O(n)的临时数组空间。

我第一次写归并排序的时候,被合并那一步各种下标越界问题折磨,后来总结出一个不出错的写法:合并时用三个游标,一个是左半区游标i,一个是右半区游标j,一个是临时数组游标k。每次比较两个半区的当前元素,谁小放谁,放完对应游标加一。某个半区放完了,就把另一个半区剩余元素全部依次放进去。这里最关键的问题在于终止条件:必须是i到左半区末尾和j到右半区末尾都处理完,而不是简单的i<n。

2.4 快速排序和堆排序:工程级的选手

快速排序的平均时间复杂度也是O(n log n),它的核心是选取一个基准值(pivot),把小于它的放左边,大于它的放右边,然后对左右两边递归排序。它的性能高度依赖基准的选择:如果每次选到的都是中位数,那么递归树是平衡的;如果每次选到的是最大或最小值,那快排就退化成了O(n²)。这也是为什么工程实现里不会简单取第一个元素当pivot,而是采用三数取中或者随机选法。C++标准库的std::sort底层用的是Introsort,它结合了快排、堆排序和插入排序的优点,在递归深度过深时会切换成堆排序,避免退化。

堆排序则是利用堆这种数据结构来排序:先把整个数组构建成一个大顶堆,堆顶就是最大值,把堆顶和末尾交换,缩小堆范围,再向下调整堆。这个过程有意思的地方在于,它不需要额外空间,属于原地排序。但它的成绩和快排相比在日常测试中通常偏慢一点,因为堆的访问模式对CPU缓存不友好。不过作为学习堆结构的重要载体,它必须亲手实现一遍。

2.5 排序算法对比速查表

算法最好情况平均情况最坏情况空间稳定性
冒泡排序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)稳定
快速排序O(n log n)O(n log n)O(n²)O(log n)不稳定
堆排序O(n log n)O(n log n)O(n log n)O(1)不稳定

注意:稳定性指相等元素的相对顺序是否保持原样。业务系统里如果有“先按时间排序,再按优先级排序”的需求,稳定排序是必须的,否则第二次排序会打乱第一次的结果。这也是为什么归并排序在很多场景里没法被快排完全替代。

3. 30天算法学习路线复盘与调整

3.1 阶段一:头十天,千万不要一上来就刷题

很多入门者最大的问题就是过早进入题海。我第一天也想学“大神”直接刷困难题,结果被教做人。后来把前十天的任务定成:学基础语法和复杂度分析。语言的循环、数组、字典、函数得写到不用查文档的程度;算法复杂度的概念得理解到“能估算代码运行次数”的程度,不需要会严格的数学证明。这是后面一切的基础。我建议这个阶段可以结合教材,像《图解算法》这种有大量图示和简单例子的书,比较适合建立直观认知。

3.2 阶段二:第11到20天,把数据结构摸熟

学算法绕不开数据结构。我在这个阶段按照“数组、链表、栈、队列、哈希表、树、堆”的顺序逐个击破。这个顺序很有讲究:数组和链表是基础存储结构,栈和队列是受限制的操作接口,哈希表是解决快速查找问题的利器,树和堆开始引入层级结构和优先级概念。每个结构都要动手实现一遍基本操作,而不是只在题目里用现成的库。比如手写一个链表反转、手写一个栈用来判断括号匹配、手写一个小顶堆用来解决TopK问题,这样才能真正体会到每个结构适合解决什么问题。

3.3 阶段三:第21到31天,开始接触搜索和一点动态规划

搜索算法(DFS深度优先搜索、BFS广度优先搜索)是暴力枚举的升级版,也是图论的基础。DFS用递归或者栈实现,BFS用队列实现。刷题的话,可以找几道经典的迷宫问题、全排列问题、岛屿数量问题来练手。动态规划在这个阶段我不建议扎太深,但至少要理解最基础的思路:把一个大问题拆成重叠的子问题,用数组把子问题的答案存起来避免重复计算。一个简单的斐波那契数列,从递归到记忆化搜索再到递推数组,就能串起动态规划的核心思想。贪心算法在这个阶段也只挑最经典的几个题看,比如区间调度、跳跃游戏。

3.4 第32天这个节点:我做了三件事

一是把前三十天学的核心专题过了一遍思维导图,跟着脑海里复述每个算法的核心思路,不看笔记,说不上来就回看;二是专门用半天时间把排序算法和暴力枚举+剪枝这两个专题打透;三是找了三道以前觉得困难的综合题,限时完成,检验自己的掌握程度。这三道题的检验结果让我明确了下一阶段的方向:字符串匹配里的KMP算法、二分图匹配的匈牙利算法、还有图论里的Tarjan算法,这些热搜里频繁出现、也是很多面试题库常客的内容,要提上日程了。但我不打算一口气全学,还是沿用“更弱智”的策略,一天只死磕一个点。

4. 算法学习里那些踩过的坑和排查技巧

4.1 时间复杂度的致命误判

我给自己的算法题卡时间的时候,经常出现“明明没几行代码,为什么超时”的尴尬。后来排查发现,大部分超时问题的根源不是某个for循环本身,而是循环里调用了高代价操作。最典型的是在循环里使用字符串拼接,比如在Java里用String的加号循环拼接大量字符串,实际每次拼接都会创建一个新的字符串对象,代价从表面上看起来的O(1)变成了O(n),整体复杂度直接升一档。我总结了一条排查方法论:先把每行代码写成“时间复杂度乘以执行次数”,列成一张清单,再找最重的那一项。这一步做完,基本上90%的复杂度问题都能发现。

4.2 边界条件就是用来出错的

算法题最折磨人的不是算法本身,而是边界条件。数组长度为0的情况、数组长度为1的情况、目标值出现在数组第一位、目标值出现在数组最后一位、递归到只剩一个元素、左右指针相遇的瞬间……这些场景我每个都出过错。后来养成了一个习惯:写完代码先不看示例数据,而是自己构造三组边界用例,空的、最小的、最大的,跑完再手动模拟一遍过程。说句真心话,这个方法帮我避免了很多次提交后的一片红。

4.3 调试要先打印,再谈抽象

我这里分享一个对新手特别有用的调试方法:当代码结果不对的时候,不要急着去猜哪里错了,而是在关键节点打印中间状态。比如排序算法每轮结束后打印整个数组,暴力枚举里每个分支进入时打印当前选中的元素。打印出来的数据能非常直观地告诉你:是哪一轮开始错的,是枚举少了还是比较逻辑错了。这一步定位准确之后,再回头审视代码逻辑,往往一眼就能看到问题。等调试熟练了,再学习使用断点和单步执行来排查,效率更高。

4.4 常见问题速查表

现象可能原因排查思路
程序运行超时循环里套了高代价操作,或枚举状态空间过大估算执行次数,列出每行的复杂度
输出结果总是少一种情况循环范围少一个,或递归忘记回溯状态检查for边界,检查visited标记是否恢复
结果全都不对比较运算符写反,或剪枝条件过于激进打印中间结果,单独测试剪枝条件
数组下标越界区间写法不统一统一使用左闭右开写法
排序结果不稳定用于稳定排序的场景用了不稳定算法确认业务对稳定性要求,选择归并

5. 这段时间亲测好用的工具和下一步方向

5.1 可视化工具和刷题平台

我学算法用到的高频工具,值得单独列一下。可视化方面,强烈推荐在浏览器里打开VisuAlgo这个网站,里面用动画演示了各种排序、搜索、图算法,直观程度远远超过看代码。第一次看到快速排序的动画时,我瞬间理解了基准值是如何把数组层层划分的,这是看文字描述完全没有的效果;同时力扣平台仍然是刷题首选,可以按专题分类刷题,并且能看到别人的解题思路和复杂度分析。我自己给每个专题设置一个最低刷题数量,比如排序三题、DFS五题、BFS五题、DP三题,量不大,但每道都要吃透。

5.2 算法笔记怎么记才有用

踩了一个月的坑之后,我发现最有效的笔记方式不是抄代码,而是记录三件事:这道题的核心思路是什么,拿到这题时怎么从题干联想到这个思路的,以及我的代码在哪里最容易写错。这种“思路-联想-易错点”方式比“题目-答案”式记录对复习更有用。我还会隔三天翻一次旧笔记,专门检验自己不看答案能不能重新做出来。很多当时觉得“看懂了”的题,隔三天再做发现完全不会,这才是真实的学习状态。重复到第三次能独立完成,这个知识点才算真正属于我。

5.3 下一步:KMP、图论和更广阔的世界

第32天之后,我给自己排了三个方向,还是保持一天一个专题的节奏:字符串方向的KMP算法,核心是利用部分匹配表避免重复匹配;图论方向的匈牙利算法和Tarjan算法,一个解决二分图最大匹配问题,一个解决强连通分量问题;树状数组和线段树这种更高级的数据结构,排在更靠后的位置。另外,热搜词里还有大量强化学习、深度强化学习、粒子群算法这类机器学习方向的名称,虽然短期之内我不会深入学数学原理,但至少需要了解这些算法的基本思想框架和适用场景。这种“广泛了解、定向深入”的方式,适合用来建立自己的算法知识地图,避免被网络上铺天盖地的热搜词带偏。

5.4 一个建议:给自己的学习留一点“弱智”的空间

我越来越觉得,“更弱智地学习算法”不是自嘲,而是一种刻意的方法。每学一个新算法,先不急着看结论、背模板,而是问一句:如果我不学这个算法,用最笨的办法能不能解决这个问题?想清楚笨办法的痛点,再看高级算法聪明在哪里。这个从“笨”到“聪明”的过程,才是算法学习最核心的乐趣。比如不先自己写一遍O(n²)的两数之和暴力解,就很难真正理解哈希表把查找从O(n)降到O(1)是多么惊艳的设计。

第32天是这段旅程的中间点,前面的一个月证明了我这种“地毯式、逐步推进”的方法在我身上有效,后面的日子里,我依然会保持每天只啃一小块硬骨头的节奏,认真暴力,认真剪枝,认真把每一个“不弱智”的算法,都用“弱智”的方式学明白。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 3:27:14

Arch Linux 移动硬盘安装指南:UEFI 启动与 GRUB 配置实战

1. 为什么要把 Arch Linux 装进移动硬盘——不是“能不能”&#xff0c;而是“值不值得”Arch Linux 装进移动硬盘&#xff0c;这事听起来像极了老司机在茶水间随口一提的骚操作&#xff1a;用一块几百块的 USB 3.2 Gen2 移动固态硬盘&#xff08;比如三星 T7 Shield、闪迪 E61…

作者头像 李华
网站建设 2026/10/2 3:27:09

自然连接与等值连接的区别:从原理到SQL实操

说实话&#xff0c;只要写SQL&#xff0c;就躲不开连接。自然连接和等值连接这两个词我第一次听的时候&#xff0c;还以为是一个东西有两个名字。后来在业务里写JOIN踩了坑&#xff0c;回去翻《数据库系统概论》&#xff0c;才发现教科书里的一句话&#xff0c;放到真实数据上能…

作者头像 李华
网站建设 2026/10/2 3:26:52

基于Hadoop的宠物用品推荐系统设计与实现详解

很多同学在选大数据毕业设计题目的时候&#xff0c;第一反应都是“推荐系统”&#xff0c;原因很简单——这个方向既有算法内容可以写&#xff0c;又有可视化可以展示&#xff0c;还能和“大数据”这个关键词牢牢挂钩。但真正动手做的时候&#xff0c;才发现坑比想象中多得多。…

作者头像 李华
网站建设 2026/10/2 3:26:52

HarmonyOS 6 AVSession Kit API 20新特性:一次接入全系统媒体控制

大家在做音乐类App的时候&#xff0c;应该都遇到过类似的困境&#xff1a;播放页面自己实现了&#xff0c;通知栏也想搞个自定义控制器&#xff0c;耳机线控要单独接按键事件&#xff0c;锁屏封面又是一套逻辑&#xff0c;恨不得每个系统入口都写一遍对接代码。等真把这一堆全做…

作者头像 李华
网站建设 2026/10/2 3:26:40

RustDesk 自建远程桌面:3分钟部署私有中继,摆脱商业工具限制

看到“62.3k Star”和“远程桌面”这两个词搁在一起&#xff0c;老玩家应该都能笑出来&#xff1a;说的就是RustDesk。这个开源项目这几年在GitHub上几乎成了远程桌面自托管代名词&#xff0c;六万多个Star不是凭空涨出来的&#xff0c;而是被商业远程软件一轮接一轮涨价、被各…

作者头像 李华
网站建设 2026/10/2 3:26:22

智能产品如何“说人话”?表达设计的三大层次与落地方法

1. 内容整体设计与思路拆解1.1 “表达”在人本智能六大原则里的特殊位置把《人本智能产品设计6原则》读到“04表达&#xff08;上&#xff09;”&#xff0c;我明显感觉到前三条原则和第四条之间的“坡度”不一样了。前三条如果按常见的框架来对应&#xff0c;大致是“感知—理…

作者头像 李华