news 2026/10/14 4:07:19

编程入门必学:哈希表、双指针与滑动窗口三大算法思维

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
编程入门必学:哈希表、双指针与滑动窗口三大算法思维

如果你已经跟着刷完了前面几讲,现在应该处于一个“能写但写不快”的阶段:循环能套,数组会遍历,字符串也能处理,但一遇上稍微绕一点的题,第一反应还是暴力解法,甚至暴力解都写得磕磕绊绊。这一期“力扣【新】动计划·编程入门4”要解决的问题,就是把你从“会写代码”推向“会设计代码”的拐点,重点补上三种最常见的解题思维:哈希表、双指针、滑动窗口。

这一阶段非常适合两类人:一类是已经把入门三件套(变量、循环、数组/字符串)过完、想系统接触算法思维的零基础学习者;另一类是准备实习或校招、需要在短期内把简单题刷出面试感的新手。这一讲的题目难度仍然控制在简单到中等偏易,但每道题都代表一类非常典型的套路,学会之后可以直接迁移到很多实际业务场景里,比如统计去重、区间合并、敏感词匹配、日志窗口截取等等。

1. 第4阶段到底在训练什么:从“做出来”到“做得聪明”

1.1 为什么到了第4讲才开始讲算法思维

前3期的内容,本质上是编程语言的“肌肉记忆”:变量类型、分支、循环、数组、字符串、函数。这些是任何语言都绕不开的地基。但刷力扣的人通常会在一个时刻突然卡住:题目看懂了,暴力解也写出来了,提交后却显示“超出时间限制”,或者更尴尬的是——写出来之后自己都觉得代码又臭又长,但不知道怎么优化。

这就是第4讲存在的意义。你需要的不是再刷几十道简单题,而是开始理解“数据结构”和“算法设计”这两个东西到底在解决什么问题。数据结构说白了就是数据怎么组织更方便;算法设计就是一套通用的思考框架。哈希表解决“查找”的问题,双指针解决“扫描”的问题,滑动窗口解决“连续子区间”的问题。这三板斧覆盖了力扣简单到中等题里至少一半的常用场景。

很多同学在这一阶段最容易犯的错误是死记代码模板。今天看到两数之和用哈希表,就把哈希表背下来;明天看到无重复子串用滑动窗口,又把滑动窗口背下来。结果换一道题题就不会了。我建议大家换个心态:第4阶段的重点不是记住某个题的解法,而是理解“为什么这个场景适合用这个结构”。

1.2 本期核心考点地图:三道题带你覆盖三套思维

按照“新动计划”入门第4部分的典型题单,我挑了三条最具有代表性的主线:

核心考点代表题型核心思维复杂度改善
哈希表两数之和用空间换时间,把查找从O(n)降到O(1)暴力O(n²)降到O(n)
双指针有序数组去重 / 移除元素一个指针负责遍历,一个指针负责写结果原地操作,省掉额外数组
滑动窗口无重复字符的最长子串用可变窗口维护连续区间,避免重复扫描暴力O(n²)降到O(n)

这三个考点表面上不是同一类题,但底层逻辑是相通的:都是想办法让程序少做无用功。暴力解的问题不在于它做错了,而在于它反复做了一样的事情。哈希表把“找历史数据”变成“查记录”;双指针让“两边同时比较”替代“两层循环嵌套”;滑动窗口用一个变化的区间替代“每次从头重新算”。等你把这三道题彻底吃透,再看那些“简单偏中”的题,多半都能找到熟悉的感觉。

2. 核心考点的原理拆解:为什么这样做更快

2.1 哈希表:空间换时间的经典策略

先讲两数之和,力扣第1题。这道题几乎所有刷题的人都做过,但真正理解哈希表好处的其实不多。

题目要求很简单:给定一个整数数组和一个目标值,返回两个数的下标,使得这两个数之和等于目标值。暴力解法谁都会:两层循环,外层定一个数,内层找另一个数,找到就返回。这个方案直观,但提交之后在数据量大一点的例子上会跑得很慢,因为时间复杂度是O(n²)——每增加一个元素,要比较的次数按平方增长。

优化思路的核心是回答一个问题:内层循环到底在做什么?它做的是在数组里“查找”一个目标值。如果查找这个动作本身是O(1)的,总复杂度不就成了O(n)?哈希表(在Python里就是字典)的查找平均复杂度刚好是O(1)。于是我们就有了新的做法:一次遍历数组,边遍历边把“数值 -> 下标”存进字典,同时对当前元素检查“目标值 - 当前值”是否已经在字典里出现了。如果出现,说明之前已经遇到过你需要的另一半。

这里值得多讲一句:为什么不是先建好整个哈希表再遍历?因为会遇到“同一个元素被用了两次”的情况。比如数组[3, 3],目标值是6,如果先全部存进去,遍历第一个3的时候会发现另一个3也在表里,但它自己也在表里,看起来好像是自己加自己,实际上是复用了同一个元素。边遍历边存的好处就在于,每个元素只跟它之前的元素配对,天然规避了这个坑。

哈希表这种思路可以迁移到很多其他题目,比如判断两字符串是否为异位词、统计出现次数、找第一个重复出现的元素。说白了,这些题的共同点是:都需要在遍历过程中快速回顾“之前我见过什么”,哈希表就是最佳拍档。

2.2 双指针:把两件事合并到一趟扫描

第二个考点是双指针。力扣的第26题“删除有序数组中的重复项”是这一类的经典入门题。

题目要求在一个排好序的数组里原地去重,不能额外建一个新数组,最后返回去重后的长度。排序数组有一个天然性质:重复元素是连续排列的,所以去重这件事本质上就是“把每个第一次出现的元素往前挪”。

暴力做法可能是这样:每遇到一个新元素,就把它跟前面所有已经保留下来的元素对比一遍,看有没有重复。这样做的复杂度又是O(n²),而且在原地操作时还会涉及大量的元素搬移。双指针的做法则完全换了个思路。

准备两个指针:一个指针负责往前扫描,挨个看每一个元素,叫它“读指针”;另一个指针负责记录下一个不重复元素应该放到哪个位置,叫它“写指针”。因为数组有序,读指针遇到的“新元素”只需要跟“写指针前一个位置的元素”比较一次——如果不一样,说明这是个新出现的数,把它写到写指针位置,两个指针一起往前走;如果一样,说明是重复项,只让读指针往前走,写指针原地等待。

这个思路的妙处在于,一趟扫描就完成了遍历和写入两件事,时间复杂度O(n),空间复杂度O(1)。很多同学第一次看到这个解法时会觉得有点抽象,但实际手动模拟几个例子之后就明白了。双指针的应用面非常广:两数之和(数组有序版本)、移除元素、合并两个有序数组、链表找中间节点,全都是一家人。

2.3 滑动窗口:让区间像一个窗口一样移动

滑动窗口大概是这一阶段最需要“悟”的一个技巧。以“无重复字符的最长子串”为例,这道题要求你从一个字符串里找出最长的一个连续子串,这个子串里所有字符不能重复。

想都不用想,暴力解法就是穷举所有子串,每个子串再检查有没有重复字符,复杂度直接O(n³)甚至更高。滑动窗口的思路则特别符合人的直觉:想象一个可以伸缩的窗口在字符串上滑动,窗口始终满足“没有重复字符”。窗口右边每扩展一个字符就试着让窗口变大;一旦新字符跟窗口里的字符重复了,左边就收缩到“把那个重复字符请出去”为止。

这里面最关键的问题:怎么快速判断“字符是否在窗口里出现过”?这又要借用哈希表了。拿Python举例,你可以用一个字典记录每个字符最后出现的下标,遇到一个字符时,先看它之前有没有出现过、以及出现的位置是否还在当前窗口内。如果在窗口内,就把窗口左边界移动到那个位置加一;不管怎么样,更新当前字符的最新位置,然后计算窗口长度,记录最大值。

滑动窗口这个名字听着唬人,拆开看就三个动作:右指针往前走、左指针按条件收缩、窗口长度更新。整个流程走下来是O(n),因为每个字符最多被左右指针各碰一次。滑动窗口能解的题目也很多:长度最小的子数组、字符串排列、替换后的最长重复字符,套路几乎一模一样。

3. 实操过程与代码实现:从零写出一份能过的解

3.1 环境准备与刷题节奏建议

不需要用什么重型的开发环境,力扣自带的网页编辑器就够用了。我个人的建议是:用你平时最熟悉的语言刷这一阶段的题,JavaScript、Python、Java、C++都可以,但最好固定一门,不要今天用Python、明天换Java,否则精力会浪费在语言切换上。

刷题节奏上,我建议每道题给自己20到30分钟的思考时间。如果到了时间还是没有思路,直接看题解,看懂之后关掉题解自己默写一遍,第二天再独立重做一遍。这个“重做”的环节一定不能省,它是把短期记忆变成长期能力的唯一有效途径。

下面三份代码都以Python为例,兼顾可读性和规范性,你甚至可以稍微改改直接背下来作为模板。

3.2 两数之和的完整实现

def two_sum(nums, target): # 记录每个数字最后出现的位置 seen = {} for i, num in enumerate(nums): complement = target - num # 另一半已经出现过,直接返回 if complement in seen: return [seen[complement], i] # 把当前数字放进字典,方便后续配对 seen[num] = i return [] # 题目保证必有解,这里兜底

需要注意,这个写法利用的是“边遍历边存储”,每个元素只和它之前的元素匹配。如果你想不依赖“题目保证有解”这个条件,可以在开头加一条判断,或者在循环结束后抛异常。实际笔试中我一般会保留return [],确保函数在边界情况下的行为是可预测的。

3.3 有序数组去重的原地写法

def remove_duplicates(nums): if not nums: return 0 # 慢指针指向下一个不重复元素的写入位置 slow = 1 for fast in range(1, len(nums)): # 发现新元素,写入到slow位置 if nums[fast] != nums[fast - 1]: nums[slow] = nums[fast] slow += 1 return slow

fast从1开始,每次跟它前一个位置的元素比较。因为数组是有序的,只要有变化就意味着出现了一个新值。slow记录的不只是新元素的数量,同时也是下一个可以写入的位置。整个数组在处理完毕后,前slow个元素就是不重复的序列,顺序也不会乱。

这里有个容易忽略的小细节:为什么fast不是从0开始?因为nums[0]一定是第一个不重复的元素,它天然站好了位置。slow从1开始,正好留出坑位给后面可能出现的新元素。边界条件“空数组”也必须先处理掉,否则nums[fast-1]会越界。

3.4 无重复字符最长子串的滑动窗口

def length_of_longest_substring(s): last_pos = {} # 字符 -> 最后一次出现的下标+1 left = 0 max_len = 0 for right, ch in enumerate(s): if ch in last_pos and last_pos[ch] >= left: # 窗口内已经出现过这个字符,收缩左边界 left = last_pos[ch] + 1 # 更新这个字符的最新位置 last_pos[ch] = right # 当前窗口长度是 right - left + 1 max_len = max(max_len, right - left + 1) return max_len

这里字典存的是“字符 -> 该字符当前出现下标的下一个位置”,为什么不是直接存下标?因为当窗口收缩时,我们关心的是“如果字符重复,左边界至少应该移动到重复位置的后一格去”。存下一个位置,赋值和比较会更直观。这也是很多题解里left更新写成last_pos[ch] + 1的原因——这个+1初学者最容易漏,漏掉之后会发现结果总是比正确答案大一点。

4. 常见问题与排查技巧实录

4.1 高频踩坑点速查

问题表现可能原因解决办法
两数之和返回了两个相同的下标预先建表导致元素和自己匹配改成边遍历边存入字典
去重后返回的长度对,但数组不对slow指针的语义没理清手动走一遍[1,1,2]流程
滑动窗口结果偏大left更新漏了+1,或没检查索引是否在窗口内在python里打印left/right逐个核对
字符串为空时代码报错没有处理空输入边界第一步就判断if not s: return 0
超时,但本地测试正常暴力解法在极端用例下复杂度爆炸重新检查是不是两层或三层循环嵌套

这些坑我基本都踩过,尤其是滑动窗口的left更新,当时调试了快一个小时,打印每个字符的最后位置才反应过来:当一个字符在窗口外也出现过时,不能把left移过去,只有位置>=left才算在窗口内。

4.2 实战排错思路:三连问

如果你提交后测试没过,别急着东改一行西改一行。按下面的顺序排查,绝大多数问题都能快速定位。

第一问:输入为空、长度为1时程序是否正常?这是最便宜、最快的边界检查。第二问:我的“窗口/指针/哈希表”里维护的状态变量是否正确?比如双指针中slow到底代表写入位置还是有效长度,这俩在这种题里经常混。第三问:手动在纸上跑一遍中小规模的样例,把每个变量在每个循环开始和结束时的值写出来,跟预期对比,通常一眼就能看出问题在哪。

调试时我喜欢在里面加上简单打印,比如:

print(f"left={left}, right={right}, ch={ch}, max_len={max_len}")

不要嫌烦,入门阶段花在调试上的时间都是赚的,它在帮你建立“程序是如何一步步执行”的直觉。

4.3 关于面试表达:代码之外的注意事项

如果刷题的目标是面试,那这阶段就要开始刻意练习“讲思路”。我见过很多人题能解出来,但讲的时候只会说“嗯这题用哈希表就行了”,面试官追问一句“为什么用哈希表?复杂度怎么分析?”就卡住了。建议每道题都按这个模板练习:先描述暴力解,再说暴力解的瓶颈在哪,接着说优化思路,最后分析时间空间复杂度。哪怕只有自己一个人,也出声讲一遍,会比闷头写效果好非常多。

另外,力扣的编辑器默认没有代码补全,建议平时练习时也关掉自动提示,尽量手写常用函数,避免面试时依赖IDE导致节奏全乱。

5. 这一阶段的题目还能怎么扩展:举一反三的思路

5.1 把同一套路套到新题目上

学会三道题不算本事,能把套路迁移到五道、八道题才算真正理解。两数之和的哈希表思路可以改成“三数之和”(配合排序和双指针),也可以改成“两个字符串是否互为字母异位词”(用字符计数哈希表)。有序数组去重的双指针思路可以改成“移除给定值的元素”,做法基本一致,只是把“相邻重复”的判断条件换成“元素是否等于目标值”。

滑动窗口的扩展就更常见了:长度最小的子数组、字符串中的所有字母异位词、最大连续1的个数III,核心都是维护窗口的“左右指针”和“有效状态”。做扩展题的时候,不用一上来就硬做,先回想一下母题是怎么解的,哪里变了、哪里没变,比盲目刷十道同类题更有用。

5.2 从刷题到工程:这些思维能用在哪儿

有人可能觉得力扣题跟实际开发关系不大,其实这三套思维在工作里随处都是。哈希表自不必说,几乎所有统计类需求都会用到,缓存设计、去重计数、字典映射,本质都是一回事。双指针思想在做有序数组/链表的合并、日志区间合并、两个有序列表求交集时很常见,代码写出来比嵌套循环干净很多。滑动窗口则特别适合那些“连续区间统计”的需求,比如限流统计最近一分钟的请求数、监控最近N条日志里的异常比例、分析一段时间窗口内的用户活跃度。

理解这一点之后,再刷题的感觉会很不一样。你不是在为面试刷题,而是在练习结构化拆解问题的能力,只不过练习场景恰好是算法题罢了。

5.3 下一步怎么规划

“新动计划”第4讲如果刷完了,下一步的路线图通常有两个方向:一个是继续深入哈希表和双指针的中等难度题,比如三数之和、乘积小于K的子数组、最长重复字符替换;另一个方向是开始接触二叉树和递归,这是很多人的下一个分水岭,因为递归的思考方式和前面所有循环思维完全不一样。

我个人更建议先把哈希表、双指针、滑动窗口这三类简单题刷到“看到题目条件就能想到大概该用什么方法”的程度,再进入树和递归,地基打牢了,上楼才不会晃。走得太快的结果往往是前面看着都懂,一到中等题就卡壳,再回头看基础才发现原来根本没吃透。

最后分享一个自己的体会:我在刷这些入门题的时候,一个特别大的转折点是接受了一个事实——解不出来很正常,看题解也不丢人。真正拉开差距的不是谁一次性写对,而是谁能在看会之后真正脱离答案自己写出来。所以错题重刷、讲题给别人听、隔几天再做一遍,这三件事的价值可能比刷十个新题都大。编程入门阶段,慢就是快。

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

COMSOL激光打孔热应力仿真文献复现:从温度场到应力场关键技巧

激光打孔的热应力分析,听起来是个小而美的仿真题目,真做起来却能把人磨得没脾气。我最近花了几周时间复现一篇关于COMSOL模拟激光打孔热应力分析的文献,从温度场一路做到应力场,把文献里一笔带过的细节重新踩了一遍。这篇文章就聊…

作者头像 李华
网站建设 2026/10/14 4:06:44

字节级保存docx/xlsx/pptx:从ZIP结构到最小增量实现

接手AI办公套件的时候,我接到的第一个硬骨头就是“字节级保存”。当时产品经理的需求描述很简单:AI模型改完一个段落,落盘的文件不能像用普通编辑器另存过一样面目全非,docx/xlsx/pptx 这三类Office格式必须做到最小增量、可对比、…

作者头像 李华
网站建设 2026/10/14 4:06:39

TensorFlow计算图与tf.data管道:工业级模型训练的底层原理

1. 这不是“学个框架”那么简单:TensorFlow到底在解决什么问题?TensorFlow这个词,最近几年在技术社区里出现的频率高得有点反常——它既不是某个新出的网红App,也不是某家公司的营销口号,而是一个实实在在、每天被成千…

作者头像 李华
网站建设 2026/10/14 4:06:33

单卡显存不足如何跑大模型?airllm分层加载推理实战指南

1. 单卡跑大模型这件事,到底卡在哪先把结论摆在前面:airllm这个项目解决的核心问题,是让显存不够的普通显卡也能推理参数量远超显存容量的大语言模型。它做的事情不是把模型压缩、不是量化到面目全非,而是用一种“分层加载、逐层计…

作者头像 李华
网站建设 2026/10/14 4:06:24

FBMC-OQAM与SC-FDMA混合调制:Matlab仿真与PAPR性能分析

1. 当FBMC遇见SC-FDMA:这个混血方案到底想解决什么做物理层算法仿真这几年,我一直觉得多载波调制是个“按了葫芦起了瓢”的领域。OFDM把频谱利用率做上去了,但峰均功率比(PAPR)高得让射频前端头疼;FBMC-OQAM把旁瓣压下去了、还省掉…

作者头像 李华
网站建设 2026/10/14 4:06:18

Flutter三方库castscreen在OpenHarmony上的跨屏镜像适配实践

最近在一台 OpenHarmony 的开发板上折腾 Flutter 应用时,接了一个挺有意思的适配任务:把三方库 castscreen 在鸿蒙环境里跑起来,目标是实现跨屏镜像投影,让手机、平板、电视这些设备之间可以实时共享屏幕内容。castscreen 在 Andr…

作者头像 李华