☰
Day 2刷题实战:哈希表与双指针,复杂度分析入门指南
2026/10/20 21:17:20 网站建设 项目流程

从第一天追到第二天,编程题练习的习惯算是初步立住了。昨天还在纠结输入输出格式,今天已经开始享受“把思路变成代码”的快感了。Day 2我用的是“热身题+一道稍难的数组题+一道字符串题”的组合,整体节奏保持在一小时左右。今天练完的最大感受是:做题顺序真的很关键,先易后难能快速进入状态,而坚持把每一步的复杂度分析写下来,比“AC了就完事”有用太多。

今天这组题型主要围绕数组、哈希表和双指针展开,都是面试里非常高频的基础类型。如果你也正在坚持刷题,欢迎对照着我今天的节奏来一遍,也许能帮你把“只会看题解”变成“自己推出来”。

1. Day 2练习的内容定位与目标拆解

1.1 为什么第二天要开始侧重“思路复盘”而不是盲目刷量

很多人刷题第一天热情拉满,一口气吞下八九道题,第二天发现前一天的解法全忘光了。这不是记忆力差,而是大脑在收到过量信息时往往会优先丢弃“没有形成框架”的内容。所以Day 2我刻意把题目数量压到了三到四道,但把每道题的思考时间拉长,特别是强迫自己先写伪代码,再写真实代码。

比如我今天处理“查找数组中第一个不重复的字符”这种基础题时,没有直接上手写哈希表,而是先在草稿纸上把“字符映射次数”的过程画了一遍,标注出最坏情况下需要扫描两趟数组,时间复杂度是O(n)。这个看起来很笨的习惯,在第2道双指针题里帮我省了至少二十分钟——因为我已经习惯了先估算复杂度再动手,避免了写出一个能跑但性能糟糕的版本。

Day 2的另一个目标是建立“解题流程感”。第一天的重点往往是熟悉语法和常用容器,第二天则应该把注意力转移到“读题->分析约束->选数据结构->设计算法->写代码->走查用例”这条链路上。如果这一步养成习惯了,后面练动态规划、递归回溯时会轻松很多。

1.2 这类练习适合谁

如果你的情况符合下面任何一条,Day 2的内容建议认真跟练一遍:

  • 刚开始刷题,觉得“看得懂题解但自己写不出来”的人。
  • 需要在一个月内完成笔试或者在线测评,时间紧但不想盲目刷海量题目的人。
  • 已经工作但想找回算法手感,平时只能抽出碎片时间的人。

今天这组题难度适中,不需要高深的数学基础,也不需要提前掌握复杂的算法模板。只要熟悉一门语言的循环、数组和字典/哈希表基本用法,就能完整跟下来。我在文章后半部分还会给出完整代码和踩坑记录,你可以直接对照运行。

2. Day 2核心知识体系:哈希表、双指针、复杂度估算

2.1 为什么哈希表是基础题里的“大杀器”

今天的练习里有一道题是“判断数组中是否存在两个数之和等于目标值”,这是最经典的入门题之一。最自然的想法是双层循环遍历所有两两组合,代码确实简短,但时间复杂度是O(n²)。当数组长度跑到一万以上时,运行时间会从毫秒级变成秒级,在真实笔试环境里很容易直接超时。

优化的核心思路就是用空间换时间。具体到这道题,我们可以借助哈希表(在Python里是dict,JavaScript里是Map或普通对象)把“要找的数”记下来。每遍历到一个数,就去哈希表里查找“目标值减去当前数”是否已经出现过。这样一来,查找操作的平均时间复杂度降到O(1),整个算法的复杂度就是O(n),只需要一趟遍历。

这里有一个容易困惑的细节:哈希表到底应该先存数再查,还是边查边存?正确的做法是边查边存,因为我们要找的是“已有的配对”,如果先一次性把所有数都存进去,再重新遍历查找,就有可能把“同一个数本身”误当成一对,比如目标值6,数组里有一个3,先存后查时会错误地认为3+3=6成立,但数组中只有一个3,并不能组成有效配对。边查边存则天然规避了这个陷阱。

2.2 双指针的直觉与适用范围

今天第二道题是“原地反转字符串数组”,我选择了双指针解法。所谓双指针,就是维护一左一右两个下标,每次交换左右指向的字符,然后左指针右移、右指针左移,直到两者相遇。由于只遍历了大约一半的元素,时间复杂度是O(n),额外空间复杂度是O(1),完美满足“原地操作”的要求。

这个解法初看很简单,但很多人在写交换逻辑时容易写出如下边界问题:当数组长度是偶数时,左右指针会“错肩而过”,但内部的while条件若是left <= right,理论上不会越界;当长度是奇数时,中间那个字符不需要交换。我第一次写的时候就因为循环条件多写了一个等号,导致中位字符被交换了两次,结果字符串看起来没变,但仔细推敲逻辑其实是错的。这也是双指针题里最常见的测试点,一定要用“长度为奇数和偶数”两组用例分别验证。

2.3 复杂度估算习惯的养成

刷题时很多人会忽略复杂度估算,觉得“能跑通就行”。Day 2我专门给自己列了一个小公式速查:

  • 一层循环遍历数组 -> O(n)
  • 两层嵌套循环 -> O(n²)
  • 每次查找用哈希表 -> 单次O(1),整体可以接受
  • 数组一半元素操作 -> O(n/2),复杂度含义等同O(n)

有了这个清单,我在动手前就会快速判断方案可行性。比如一道题如果数据范围是10^5,那么O(n²)基本没戏,只有往O(n log n)甚至O(n)的方向思考才有活路。这种敏感度只能靠刻意练习获得,千万不要跳步。

3. Day 2经典题型拆解与完整代码实现

3.1 题目一:求两数之和(返回下标)

题目描述很常见:给定一个整数数组和一个目标值,找出数组中和为目标值的两个数的下标,假设每种输入只对应一个答案,且不能使用同一个元素两次。

我给出的Python版本实现如下:

def two_sum(nums, target): seen = {} for i, num in enumerate(nums): need = target - num if need in seen: return [seen[need], i] seen[num] = i return []

这段代码的核心逻辑很简单:遍历到第i个数时,先把需要的另一个数need算出来,去seen里查。如果存在,直接返回之前记录的下标和当前下标;如果不存在,就把当前数和它的下标存进seen。

很多初学者会在这里反问:为什么不先把所有元素都存进哈希表,再二次遍历?前面已经提过“重复元素被误匹配”的问题,这里再补充一个例子:nums = [3, 3],target = 6。先存完再遍历,第一次遇到3时,查找6-3=3在哈希表里是存在的,但那个元素其实就是当前元素本身,下标相同,不满足“不能使用同一个元素两次”。除非额外增加“找到的下标不能等于当前下标”的判断,否则就会出错。边查边存则不需要这种额外判断,逻辑更干净。

写完这段代码后建议立即验证四个用例:

print(two_sum([2, 7, 11, 15], 9)) # 期望 [0, 1] print(two_sum([3, 2, 4], 6)) # 期望 [1, 2] print(two_sum([3, 3], 6)) # 期望 [0, 1] print(two_sum([1, 2, 3], 7)) # 期望 []

第二和第三组用例重点检查“元素重复”和“无解”两种边界场景,很容易暴露实现缺陷。

3.2 题目二:反转字符串(原地修改)

这道题通常要求输入是字符数组,不能额外分配空间,必须在原数组上修改。JavaScript的实现可以这样写:

function reverseString(s) { let left = 0; let right = s.length - 1; while (left < right) { [s[left], s[right]] = [s[right], s[left]]; left++; right--; } return s; }

这里用到了解构赋值来实现交换,代码很简洁,但要注意:解构赋值会创建临时数组,不过这个数组只存在于表达式内部,不影响“原地修改”的整体复杂度判断。如果面试严格考察“不使用额外空间”,可以改成传统的临时变量交换:

let temp = s[left]; s[left] = s[right]; s[right] = temp;

在笔试环境中,解构赋值的写法没有问题,但面试手撕代码时建议用临时变量版本,并主动解释清楚为什么它满足O(1)空间复杂度。

测试用例方面,一定记得同时验证偶数和奇数长度:

console.log(reverseString(['h', 'e', 'l', 'l', 'o'])); // ['o', 'l', 'l', 'e', 'h'] console.log(reverseString(['H', 'a', 'n', 'n', 'a', 'h'])); // ['h', 'a', 'n', 'n', 'a', 'H']

遇到第二个用例时,指针会分别指向两端,一步步向中间走。用“奇数长度多出来的中间元素不动”这个规则去推演一遍,比直接跑测试更能加深印象。

3.3 题目三:找出第一个不重复字符的下标

这道题是哈希表的进阶用法。先遍历一趟字符串统计每个字符出现的次数,再遍历第二趟找到第一个出现次数为1的字符,返回下标;不存在则返回-1。

Python实现:

def first_unique_char(s): from collections import Counter count = Counter(s) for i, ch in enumerate(s): if count[ch] == 1: return i return -1

使用Counter可以一行完成统计,但如果在笔试环境里不允许使用标准库,可以用普通字典手动统计:

count = {} for ch in s: count[ch] = count.get(ch, 0) + 1

两趟遍历的总时间复杂度是O(n),额外空间是O(k),其中k是不同字符的数量。这个解法很容易理解,但容易忽略的一个点是:为什么要两趟而不是一趟?原因是第一趟必须先把所有字符出现次数完整统计出来,第二趟才能判断某个字符是不是“只出现一次”。如果只遍历一趟,在遇到中间的字符时,根本不知道后面的字符会不会再次出现同一个字母。这个逻辑想明白了,对这种“统计+查询”类题目的理解就会更扎实。

4. 实操中绕不开的坑:边界条件与细节错误实录

4.1 两数之和的“误用自身”问题

我在第一次手写两数之和时,就踩了“先存后查”的坑。当时的代码是:

seen = {} for i, num in enumerate(nums): seen[num] = i for i, num in enumerate(nums): need = target - num if need in seen and seen[need] != i: return [i, seen[need]]

这个版本必须加seen[need] != i这个判断来防止匹配到自身,虽然能通过测试,但逻辑比边查边存复杂。更重要的是,这种写法在遇到重复元素时需要额外处理“应该返回哪一个下标”的问题,容易引发歧义。对比之下,边查边存版本天然规避了这类问题,推荐优先掌握。

4.2 反转字符串的循环边界

反转字符串最容易出错的是循环条件。如果写成while (left <= right),在偶数长度数组的时候,最后一轮交换的是中间两个元素,完成后left和right恰恰相邻,下一次进入循环时left > right,实际上最后一次交换是必要的;但如果长度是奇数,left等于right时交换同一个元素相当于没有操作,属于多余操作,不影响正确性,只是逻辑不干净。写成while (left < right)则统一适用于奇偶两种情况,语义上更准确。

另一个细节是数组为空或只有一个字符的极端情况。此时while条件直接不成立,代码返回原数组。这个“空转”行为是安全的,不需要额外判断。

4.3 第一题中的易错点:哈希表更新时机的顺序

边查边存虽然能避免“自身匹配”,但要注意更新的顺序。如果你把seen[num] = i放在查找之前,会破坏后续逻辑吗?事实上,对于两数之和来说,先存后查和先查后存的差异主要体现在“第一个元素会不会立即被自己匹配”。比如target=6,第一个元素是3时,如果先把3存进seen,再查找need=3,就会发现seen里已经有3,直接返回了相同下标。这就是为什么必须“先查再存”。很多文章只给了最终代码,没有解释这个先后顺序的原因,今天专门提一下,希望帮你少踩一次坑。

5. 常见问题排查速查表与调试技巧

5.1 典型报错与排查思路

下面的表格汇总了我练习Day 2时遇到的高频问题、可能原因和解决方向,可以直接对照着检查。

问题现象可能原因排查方向
运行结果与期望值差一个下标边查边存的顺序写反了检查是否“先查再存”,避免匹配自身
字符串反转后原数组没变化解构赋值写在了函数内部但返回值未接收原地修改后直接返回数组本身即可
空数组或单元素数组报错没有处理边界条件,while循环越界确认循环条件是left < right而非left <= right
结果超时,数据量大时卡住使用了O(n²)暴力解法改造成哈希表或双指针降低复杂度
统计字符次数不准确字典的get默认值使用错误确认count.get(ch, 0)第2个参数是默认值0

5.2 定位问题的三个实用手段

代码不通过时,不要急着看题解。我这几年的习惯是按顺序做三类检查。

第一步,检查边界输入。把数组长度为0、长度为1、所有元素都相同、目标值与现有元素完全相同这几种极端情况跑一遍,往往能快速暴露逻辑漏洞。

第二步,在关键位置打印中间状态。比如两数之和里在if内打印need和seen内容,可以清晰看到哈希表增长顺序;反转字符串里打印每一轮的left和right,能直观感受指针移动是否符合预期。打印几行代码消耗的时间很少,但比默读代码强十倍。

第三步,重置思路,回到“人肉模拟”。找一张纸,把数组写在上面,手动模拟一遍算法过程,尤其是在出错的那一步前后画清楚当前数据和状态。这个动作看起来原始,但能在十分钟内解决靠读代码半小时也解决不了的逻辑问题。

5.3 从“做对题”到“会讲题”

Day 2我还额外做了一个练习:每做完一道题,用三句话解释自己的解法。第一句话说明用了什么数据结构,第二句话说明核心循环是怎么工作的,第三句话说明时间和空间复杂度。这个习惯的灵感来自模拟面试经验:很多人代码能写对,但被问到“为什么这样做”就卡壳。如果解释不清楚,说明理解还不够透彻,下次换个皮又不会了。

6. 今日练习总结与扩展延伸建议

6.1 今天练完后一定要掌握的三件事

  • 第一件:手写两数之和的“边查边存”写法,能解释为什么先存后查会产生误匹配。
  • 第二件:双指针反转字符串,能说清奇偶长度时循环条件的差异。
  • 第三件:用“遍历三件套”即边界、单步、全量来自查代码,形成习惯后正确率明显提升。

掌握了这三件事,Day 2就算真正过关了。今天这三道题看起来基础,但它们背后的哈希表思想、双指针思想、复杂度分析方法几乎出现在后续每一个进阶题目里。把地基打牢,后面学到哈希映射的变体、滑动窗口、快慢指针时,才不会发懵。

6.2 后续练习可以怎么延伸

如果今天的题做完还有余力,建议尝试以下几个变体,难度逐步递增,非常有助于巩固今日所学:

  • 两数之和的变体:输入数组已排序,尝试用双指针从两端向中间逼近,把空间复杂度从O(n)降到O(1)。
  • 反转字符串的变体:不反转整个字符串,而是反转字符串中的每个单词,单词内部顺序不变,但单词间顺序反转。
  • 找不重复字符的变体:不再找第一个,而是找出所有只出现一次的字符,并按原顺序输出。

这些变体不会完全超出今天的能力圈,但每一个都强迫你重新思考“哈希表存什么”“指针怎么走”这两个核心问题。我个人经验是,把一个基础题吃透五个变体,比闷头刷二十道新题效果更好,因为面试时真正考察的不是“做过没有”,而是“能不能灵活变形”。

6.3 关于坚持练习的一个小建议

做编程题最怕的不是不会做,而是今天做了明天忘。我自己用的复习节奏是“三天一轮”:Day 2做完的题,Day 4不看书重写一遍,Day 6再改写成另一种语言或者换一种解法。第三次重写时,很多解题步骤已经内化成直觉,面对面试官提问也能更快组织语言。

今天这篇文章里所有代码我都实际运行过,确认能直接使用。如果你的运行结果和文章不一致,大概率是环境版本差异导致的输出格式问题,可以尝试打印中间变量检查具体差异。编程刷题这条路,前期最快乐的部分就是每天都能看到自己的进步,坚持到Day 10再回头看Day 2的代码,你会明显感觉到思维层面的变化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询