今天照例打开力扣,准备刷今天的每日一题。长期关注“勤劳的小蜜蜂系列”的朋友应该知道,我这个系列的定位一直很明确:不追难题、不炫技,每天老老实实刷几道力扣简单题,把基础打得结结实实。有人可能会觉得,简单题有什么好刷的?但如果你正处于准备面试的阶段,或者在数据结构和算法上总觉得“会做但说不清”,那这篇文章应该能给你一些不一样的启发。
今天这一篇,我除了记录今天刷的几道题、拆解思路之外,还想聊聊一个最近搜索热度很高的题目方向——“雇员按共同特征分组”这类题。很多人在搜“力扣1875将雇员相同的分组”,说明一旦题目从纯数组操作跳到一个带业务背景的“分组”场景,不少人是会卡住的。这恰恰是我一直强调的:简单题从来不只是简单题,它练的是你脑子里的“算法直觉”,这种直觉能直接迁移到真实的业务开发里。
下面我把今天的刷题过程、思路拆解、以及一些坚持刷题的心得,一次性整理出来。
1. 为什么我坚持刷力扣简单题?这事比看上去有价值得多
1.1 简单题不是“水题”,而是稳定性的地基
有相当一部分人会有一个误区:觉得刷简单题没什么技术含量,要刷就刷中等题、困难题。我早期也有过这个阶段,上来就死磕困难题,结果一道题能卡两三个小时,最后看题解都费劲,挫败感特别强。
后来我调整了策略:每天固定先刷一两道简单题,把状态热起来,再考虑要不要碰难题。坚持一段时间后发现,简单题给我带来的收益被严重低估了。拿“合并两个有序链表”这种经典题来说,看着简单,但你真能在面试的紧张状态下,一次性写出没有边界漏洞的代码吗?递归写法想清楚终止条件了吗?迭代写法记得用虚拟头节点吗?这些细节就是简单题的价值——它们在训练你的“肌肉记忆”,让你在真正写代码的时候不用分心去想基础语法和常见套路。
另一个角度是知识体系的查漏补缺。大部分简单题考察的都不是偏门算法,而是数组、字符串、链表、哈希表、栈、队列这些最核心的数据结构基础操作。这些内容恰恰是复杂题目的“原子操作”。我开始系统刷简单题之后,才发现自己对StringBuilder的适用场景、哈希表遍历时的删除规则、链表反转的指针顺序这些细节,其实并没有完全吃透。
1.2 面试中简单题的真实权重
我自己参与过几次技术面试的旁听,也和其他做面试官的朋友聊过。一个很真实的观察是:面试手撕代码环节,真正出困难题的公司其实很少,大多数面试官更愿意用中等题来考察,而中等题的下限,往往就是简单题的上限。
比如“有效的括号”这道题,算简单题,但它依然是很多公司面试题库里的常客。再比如“买卖股票的最佳时机”,简单到不能再简单,但面试官把它包装成一个业务场景——“给你一个数组,判断哪天买哪天卖收益最大”,照样能筛掉一批人。原因很简单:一道看似简单的题,能考察你对数据结构的选择、边界条件的把握、时间复杂度的优化意识,这些才是工程能力的体现。
所以我一直劝准备面试的朋友:别把简单题当热身,要当主菜来吃。你把简单题刷出条件反射了,中等题里拿到核心思路的概率会大幅提升。
1.3 “小蜜蜂系列”存在的意义:对抗遗忘
说点更贴近这个系列主题的事情。我叫这个系列“勤劳的小蜜蜂”,核心不在于“勤劳”,而在于“每天”。算法能力的衰减速度比你想象中快很多。我有个阶段连续两周没刷题,再回头做一道普通的数组题,手生了不说,连最常用的双指针套路都要想半天。
每天固定刷简单题,本质上是一种非常低成本的“手感维护”。它不需要你腾出大块时间,不需要你死磕到深夜,只要每天花二三十分钟,让大脑保持“数据结构在线”的状态,这个收益长期来看是非常可观的。我自己的切身体会是:坚持这个系列之后,我面对陌生题目时的第一反应不再是“这题我肯定做不出来”,而是“这题的考点应该落在哪个区间里”——这种直觉的提升,靠的就是每天一小步的积累。
2. 今天的刷题清单与核心解题思路拆解
今天的清单我选了四道题,覆盖了四个最常见的考察方向:字符串匹配、哈希计数、链表操作、二分查找。每一道题我都按“我的第一思路 → 优化 → 易错点”这个顺序写出来,方便你参考。
2.1 字符串与栈:括号匹配类题目的“状态机”思维
题目方向:给定一个只包含括号字符的字符串,判断括号是否有效闭合。
这类题的经典解法是用栈,遇到左括号就入栈,遇到右括号就看栈顶是否匹配。我最初写的时候犯过一个很蠢的错误:只判断了左右括号数量是否相等,没考虑顺序问题,比如“)(”这种字符串,数量上是一比一,但显然是无效的。栈这种结构之所以适合这道题,就是因为它天然带着“最近的左括号先被匹配”的语义——这和真实世界的嵌套结构完全一致。
一个小技巧是:入栈的时候不要存左括号本身,而是存对应的右括号。这样遇到右括号时,直接和栈顶元素比较是否相等就行,不用写一堆switch-case。代码看起来会清爽很多:
def isValid(s: str) -> bool: stack = [] mapping = {')': '(', ']': '[', '}': '{'} for ch in s: if ch in mapping: if not stack or stack[-1] != mapping[ch]: return False stack.pop() else: stack.append(ch) return not stack这道题给我最大的提醒是:别把简单题想当然。我至少见过三种错误写法——忘了判空栈、忘了括号顺序、忘了最后栈可能不为空。面试里这些全是扣分点。
2.2 哈希表与计数:“两数之和”到变体题的思路跃迁
题目方向:给定一个整数数组和一个目标值,找出数组中两个数之和等于目标值的下标。
“两数之和”大概是力扣上最出名的一道题了。很多人背答案都能写出来,但未必理解为什么需要用哈希表。本质上,我们是在把“查找”这件事从O(n)降到O(1)。遍历数组时,每看到一个数num,就检查target-num在不在哈希表里——在的话,答案直接出来了;不在的话,把num和它的下标存进哈希表,留给后面的数来匹配。
这类哈希计数的思路,迁移性极强。今天我看一道变体题,要求返回的不是下标而是“是否存在这样两个数”,那更简单,用set就够了。如果题目改成“三个数之和”,先排序再固定一个数,剩下两个数用双指针往中间夹,又是另一个经典套路。
我的经验是:遇到“给你一个数组,问你能不能凑出某个条件”的题,第一反应先想哈希表,第二反应想双指针。这两个招法能覆盖相当大比例的简单和中等问题。
2.3 链表操作:虚拟头节点和双指针的固定套路
题目方向:删除链表中的某个节点,或者返回链表倒数第k个节点(按具体题目变形)。
链表题是很多初学者的噩梦,因为指针操作太容易绕晕了。我自己总结出了两个固定套路,基本能应对大部分简单题:
第一,虚拟头节点。凡是要删除节点、或者在头部插入节点,我都先建一个dummy节点,指向真正的head。这样就不用单独讨论“删除的是头节点”这种边界情况,最后直接返回dummy.next即可。核心逻辑统一了,错误率能降一半。
第二,快慢指针。找倒数第k个节点,让快指针先走k步,然后快慢指针一起走,快指针到结尾时,慢指针正好停在目标位置。这个套路熟练之后,像“环形链表检测”这些题也能一眼看穿本质——一个走两步一个走一步,追上就是有环。
写代码的时候,我习惯在纸上先把指针的每一步画出来,特别是涉及节点互换的场景。不要觉得画图浪费时间,链表题靠脑子硬想,十有八九在指针重连那一步会出错。
2.4 二分查找:不是“在数组里找数字”那么简单
题目方向:在有序数组中查找目标值,或查找第一个满足条件的元素位置。
二分查找的模板我见得多了,基本写法没问题,但有一个小坑很容易踩:循环条件写left < right还是left <= right,取决于你的搜索区间是开还是闭。我自己的习惯是统一用左闭右闭区间,循环条件写left <= right,每次更新left = mid + 1或者right = mid - 1。这样逻辑最容易自洽。
另一个实用技巧是:不用死记模板,只要抓住“每次循环必须把搜索区间缩小一半”这个核心,就能自己推导出各个边界怎么写。还有,计算mid的时候,用mid = left + (right - left) // 2比(left + right) // 2更稳妥,能避免极端情况下的整数溢出问题。虽然力扣的测试用例一般不会让你溢出,但养成这个习惯,在生产代码里是有意义的。
今天刷的这几道题,难度都不高,但每一道都有值得记录的细节。我刷完之后会趁热打铁写一篇简短题解,把思路、代码、易错点存到自己的笔记里,这样下次复习的时候效率会高很多。
3. 从热词“雇员按相同特征分组”看简单题的迁移价值
3.1 分组统计的本质:从“数数”到“建索引”
最近“力扣1875将雇员相同的分组”这个搜索热度不低。光从题名看,这道题的核心应该就是把具有相同特征的雇员分到同一组——本质上是分组统计问题。
这种题在力扣里太常见了。最简单的一档是:“给你一个数组,统计每个元素出现的次数”。解法就是哈希表计数,遍历一遍,count[num] = count.get(num, 0) + 1。稍微变形成“按字符串长度分组”、“按首字母分组”,思路是一样的——选定一个“分组键”,然后把数据塞进以这个键为索引的桶里。
很多人在解这类题的时候,会把注意力放在“怎么把代码写出来”上,但我觉得更重要的,是理解“为什么要用哈希表做分组”。哈希表的key天然就是一个“分组标识”,value就是这一组的聚合结果。所以哈希表 = 分组 + 聚合,这五个字能贯穿从简单题到中等题的一大片题目。
3.2 SQL分组与哈希分桶的思维同构
我特别想多说一句:“雇员按相同特征分组”这个场景,和SQL里的GROUP BY在逻辑上几乎一模一样。GROUP BY department_id,就是把同一部门的雇员凑到一组,然后可以COUNT(*)、AVG(salary)——这是后端开发、数据分析、报表开发里每天都在用的操作。
为什么很多人在LeetCode上看到“雇员分组”会卡壳?我觉得是因为他们把“写代码”和“业务思维”分成两件事了。实际上,数据结构的训练就是在练业务思维的底层能力。比如:
- 哈希表分桶 = 按用户的某个标签做人群圈选
- 双指针 = 在有序数据里高效找配对
- 栈 = 函数调用栈、嵌套结构解析、浏览器的前进后退
- 队列 = 任务排队、消息队列、广度优先搜索
我平时给团队新人做分享时经常说一句话:算法不是面试完就扔的东西,它就是你对数据做操作的思维模型。你今天刷了一道“统计股票价格最大涨幅”的题,明天工作中同事让你“统计每个商品类目的月销量变化”,你会发现这俩是一回事。
3.3 简单题里的工程启示
拿“雇员按共同特征分组”延伸开去,真实业务中类似的需求比比皆是。
举个例子:一个电商后台要给运营同学做一张“用户订单聚合表”,需要按“用户ID + 月份”分组,统计每个组内的订单数和GMV。如果你熟悉哈希分组,你会立刻想到两层结构:外层先按用户ID分桶,内层再按月份分桶。如果数据量大,你会自然会想到“map-reduce”的思想——先分组再聚合,这本质上就是你在力扣简单题里练过一百遍的东西。
再比如,日志分析场景里,要按“接口路径 + 状态码”统计请求量。你脑子里浮现的应该是:遍历日志,拼一个复合key,然后计数。这和“两数之和”里用哈希表存target-num有什么本质区别?没有。都是“用一个可比较的key快速定位到对应的桶”。
所以我强烈建议:当你刷到哈希表相关的简单题时,刻意多想一步——“这个分组逻辑,放到真实场景里会对标什么需求?”想多了,你的迁移能力会比单纯刷题强好几倍。
3.4 这类题的通用解题框架
为了方便你直接套用,我把这类“分组统计”简单题的通用框架整理出来:
第一步,确定分组键。把所有数据按照哪个维度归为一组?可能是元素本身,可能是元素的某个属性,也可能是多个属性的组合。组合键在编程时可以直接用一个元组(Python的tuple)当key,非常方便。
第二步,确定聚合方式。分组之后要做什么?计数、求和、取最大最小、收集列表?这决定了value的类型——计数用int,聚合列表用list,求最值可以用一个初始值不断比较。
第三步,处理边界情况。空数组怎么办?key不存在怎么办?比如collections.defaultdict(int)和defaultdict(list)就是解决“key不存在”这个痛点的最好工具,用上之后可以省掉大量if key not in dict的判断。
第四步,考虑遍历顺序是否需要保留。如果要求输出顺序和第一次出现顺序一致,就用普通的dict(Python 3.7+默认保序);如果要求按key排序,就最后统一sorted一次;如果不要求顺序,普通dict或Counter都可以。
这套框架不仅能解“雇员分组”这类题,对“字符出现次数排序”、“按频率统计单词”这类热门简单题同样适用。把框架内化成自己的,比背十道题的代码有用得多。
4. 把“小蜜蜂”式坚持落地的刷题日常:节奏、记录与防遗忘
4.1 每天怎么安排刷题时间?
说到坚持,很多人第一个问题就是:“我也知道应该坚持,但就是坚持不下来。”
我的答案很简单:把启动成本降到最低。我现在的固定动作是,每天上午打开电脑,第一步不是看邮件,而是打开力扣,刷一道简单题。不需要规划今天要刷几道、要刷哪几道,就是“打开网站,做今天的每日一题”。这个动作的启动成本低到几乎没有,但积累下来非常可观。
如果当天状态好,我就额外再刷一两道同类题,加深今天的主题;如果状态一般,做完一道就收工,绝不硬撑。有人可能觉得这不够“努力”,但长期主义恰恰是一场马拉松,不是百米冲刺。因为我见过太多人,第一天立flag刷十道,第二天刷五道,第三天打开网站看了一眼关掉了,第四天就把这事儿忘了。持续的小步前进,远比间歇性的猛冲有效。
中午吃完饭,我也会花五分钟时间,在手机上看一眼题解区的高赞评论,学别人的简洁写法。晚上睡前,如果当天刷的题里有值得记录的,我会把思路和代码存进笔记。这样一整天的碎片时间都被利用起来了,又不会觉得负担重。
4.2 错题本怎么做才有效?
我见过不少人刷题,刷完一道,AC了,就过了。下次遇到同类型题目,还是要想半天。问题出在哪?缺乏沉淀。
我的习惯是给每道题打个标签:考点、难度、错误次数、易错点。标签可以自己定,比如“链表-虚拟头节点”“哈希-计数”“双指针-有序数组”。这样做的目的是把题目从“一道一道”的散点状态,归纳成“一类一类”的网状结构。等到复习的时候,我不会去重刷所有题,而是按标签挑代表性的题目各做一遍。
举个小例子:我把“两数之和”“三数之和”“四数之和”打上同一个标签“双指针/哈希-数对求和”,复习的时候一起看,立刻能看出这类题的演进路径——两数之和的哈希解法,是怎么变形成三数之和的排序+双指针,又怎么扩展到四数之和。理解了这个关联,比单独背三道题的答案有用得多。
另外,错题本上一定要写“当初我为什么错”。我发现很多错误其实可以归为几类:边界条件没考虑(数组为空、只有一个元素)、循环条件写错(>=还是>)、数据溢出或类型不匹配。把这些错误分类之后,你会发现自己有一个固定的“臭毛病清单”,比如我就特别容易在“数组下标越界”上翻车——知道这一点之后,我每次写循环前都会刻意检查边界,这个问题后来真的很少再犯。
4.3 一个可复制的复习节奏表
最后一个实用建议是复习。很多刷题人最大的痛点就是“刷了忘,忘了刷,刷了再忘”。我的应对方案是:给自己定一个简单的复习节奏。
刷完一道有价值的题之后,当天不复习;第1天回顾一下思路;第3天不看书,自己重新写一遍;第7天做一道同类的新题验证迁移能力;第14天再翻一次错题本,确认没有遗漏。这五个时间点,已经把“短期记忆”转成“长期记忆”的关键节点都覆盖住了。
如果你觉得时间太紧,至少也要保证“第3天重写一遍”和“第7天做同类题”这两步,前面提到的那套“算法直觉”,主要就是靠这两个动作长出来的。我自己回头去看,很多以为自己已经掌握的题,就是在第3天重写的时候暴露出了理解上的漏洞。
复习的时候还有一个原则:尽量不直接看原来的代码,而是先口述思路,写得出思路再动手写代码。这样能逼你把“看懂答案”升级成“独立推导”。如果发现卡住了,就回错题本看错误原因,用红笔(或者在你的笔记App里用高亮)标一遍,然后隔天再来一次。
4.4 把刷题变成一件“不痛苦”的事情
最后聊一点心态层面的东西。
我为什么把这个系列叫“勤劳的小蜜蜂”?因为蜜蜂采蜜的时候,不觉得自己在“坚持”,它就是每天飞出去,一朵花一朵花地采,然后把蜜带回来。刷力扣简单题对我来说也是这么一件事。我不是在完成什么苦难重重的任务,而是在每天和这些基础数据结构打打交道,维护自己的手感,顺便从每道题里抠一点点新东西出来——有时候是一个API的新用法,有时候是一个想通了的边界条件,有时候是一段比我写得更优雅的代码。
这种心态很重要。一旦你觉得刷题是在“受苦”,你就很难真正长期做下去;一旦你把它当成“每天收一点新东西”,它反而会变成一种惯性。我自己坚持这个系列这么久,最大的体会就是:不要高估某一次猛刷几个小时的收益,也不要低估每天认真刷一道题的复利。一年下来,三百多道题,覆盖常见的考点和套路,加上反复复习和错题整理,应付面试和日常开发里的算法问题,是完全够用的。
如果你还没找到自己的节奏,我建议你今天就可以开始:打开力扣,找一道最简单的题,别管时间,认真把它做透,然后把思路写下来。明天再做一道。后天遇到同类型的,试着不看答案独立写一遍。一个月后你再回来看,大概率会感谢那个从今天开始“每天一点点”的自己。