尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

2016年360内推笔试编程题解析:字符串与哈希表经典模型

2016年360内推笔试编程题解析:字符串与哈希表经典模型 清理旧网盘的时候翻到一份2016年整理的题单名字就叫“360公司2016研发工程师内推笔试编程题”旁边还贴着一堆手写笔记和当时的提交记录。说实话看到那些代码的第一反应是“这写得也太粗糙了”但认真重做一遍之后又觉得这类老笔试编程题的价值比很多新题都高。题面确实有些年头考点却一点都不过时——字符串、数组、哈希表、模拟全是研发工程师笔试里反复出现的基础模型。无论你准备的是内推、校招还是社招笔试把这几个模型吃透比盲目刷几十道冷门题管用得多。1. 为什么一道2016年的笔试编程题现在还值得刷1.1 内推笔试的本质是“基础能力抽查”很多人一听到“内推笔试”下意识觉得题目会非常难甚至会出现冷门算法、复杂数学推导。以我这些年看到的笔试题来看恰恰相反。内推笔试承担的是一个筛选功能出题人要在有限时间内判断候选人能不能写干净、正确的代码所以题目通常集中在三类字符串处理、数组操作、基础算法思想。2016年前后这一批笔试题尤其典型。当时C和Java是绝对主流题目描述很短输入输出格式描述也很直接不会像现在有些平台题面绕来绕去。它考的就是你能不能快速读懂题、选择合适的数据结构、写出没毛病的循环和边界处理。说白了这是一次“基础能力抽查”不是ACM竞赛选拔。也正因为如此这类老题对现在的求职者依然有参考价值。你可以不做那些“偏难怪”但必须保证基础模型稳。我见过不少候选人简历写得很好一上笔试就栽在“字符串循环移位”这类基础题上不是不会是平时刷题太少现场手生。1.2 老题比新题的参考价值更稳定新题的问题在于很多平台为了“创新”会把简单模型包装得花里胡哨比如加上复杂的业务背景、多阶段交互、大规模并发之类的设定。这种题不是不好但不太适合用来练基本功。而2016年这批题目的风格非常朴实你会明显感觉到出题人是在认真考察“这个人能不能写代码”而不是“这个人是不是正好见过这道题”。我整理了一下当年的高频考点分布大致是字符串相关占三成左右数组和哈希占三成模拟和数学逻辑占两成其他算法如动态规划、递归占两成。这个比例在现在的笔试题里依然说得通。所以把老题做熟等于把考点的“抽样框”摸清楚了。2. 先别急着敲代码拆解一道笔试题的五步法很多人在笔试时一看到题目就立刻开始写代码这个习惯我不太推荐。除非是已经做过无数次的题目否则最好花两到三分钟把题目拆一遍。拆题不是浪费时间恰恰是节省时间。2.1 第一步把输入输出的边界抠清楚笔试编程题和LeetCode这类平台最大的区别是输入输出格式需要自己处理。2016年很多笔试平台还要求选手处理多组输入如果没写 while 循环第一组数据能过第二组就报错。拿到题目先问自己几个问题输入是一行还是多行是否有多个测试用例需要循环读到文件结尾字符串里有没有空格能不能用 cin 读输出行尾能不能有空格最后一行要不要换行这些细节看起来琐碎但真到笔试环境它们决定了你的代码能不能通过全部测试点。举个例子判断“两个字符串是否互为循环移位”这类题输入描述可能是“每行两个字符串以空格分隔”也可能是“先给一个 n再给 n 组数据”。前者用 while (cin a b) 就行后者要先读 n 再循环。现场再改输入逻辑会非常浪费时间。2.2 第二步用数据范围反推算法复杂度这是拆题里最核心的一步。题目一般会给数据范围比如字符串长度不超过 1000数组大小不超过 10^5。根据这个范围可以直接判断应该用什么复杂度的算法。数据规模可接受的复杂度对应思路n 100O(n^3)三重循环、暴力枚举n 1000O(n^2)双重循环、动态规划基础版n 10^5O(n log n) 或 O(n)排序、二分、哈希表、滑动窗口n 10^7O(n)线性扫描、线性筛很多选手看到 n 10^5 还在用 O(n^2) 的双重循环结果数据一大就超时。笔试平台会明确给出运行时间限制遇到大范围数据第一反应应该是“这里不能用暴力解”而不是“我试试看能不能跑得动”。2.3 第三步先写伪代码再写正式代码不要小看伪代码。尤其在笔试紧张的环境下直接写正式代码容易漏逻辑。我会先在草稿纸上写出类似这样的结构初始化标记数组为 -1 left 0 for right in 所有字符: 如果当前字符在 [left, right] 中出现过: left 上次出现位置 1 更新当前字符位置 更新答案 max(答案, right - left 1)这个步骤能帮你把关键逻辑理顺正式写代码的时候只需要机械地翻译成语法。最重要的是伪代码阶段发现思路有问题改起来成本极低等到代码写了一半才发现思路不对心态容易崩。2.4 第四步用边界用例“打补丁”笔试时大部分人都会用题目给的样例测试但样例往往很温和根本不会暴露边界问题。真正拉开差距的是这些用例空字符串只有一个字符的字符串所有字符都相同的字符串数组里元素全部相等时找两数之和目标值本身由两个负数组成输入数据达到题目给定的最大值我在检查代码时会在心里模拟一遍这些用例。比如判断字符串循环移位时如果两个字符串长度不同直接返回 false如果不加这个判断AA 里可能找到意外匹配结果错误。2.5 第五步在正确性、复杂度和编码速度之间做取舍笔试时间有限不需要追求“最优中的最优”。如果一道题 O(n log n) 能过就不必为了 O(n) 的解法多花二十分钟去抠常数优化。先把正确、能过的版本写出来再考虑有没有明显可以优化的地方。我见过一些选手明明哈希表能解决两数之和非要手写一个红黑树结构最后代码写了一百多行还出了 bug。笔试不是炫技现场是“用最稳妥的方式在有限时间内写出正确答案”的现场。3. 字符串与滑动窗口最高频的题目模型如果只能练一个模型我建议优先练字符串。2016年内推笔试里字符串相关题目出现频率极高而且往往不是单纯考某个 STL 函数而是考你能否把问题抽象成滑动窗口、双指针或哈希统计。3.1 题目长什么样最典型的题目是“给定一个字符串找出其中不含有重复字符的最长子串的长度。”这题在今天依然是各大平台的高频题但在2016年它已经是笔试常客。题目变化很多比如字符串只包含小写字母还是所有ASCII字符要求返回长度还是返回子串本身如果存在多个最长子串是否要求输出任意一个不同问法会导致代码细节完全不同。笔试时一定要看仔细题目问的是长度就别费劲去记录子串内容。3.2 完整题解最长不重复子串核心思路是维护一个左边界 left 和一个右边界 right让 [left, right] 始终不包含重复字符。每次右边界向右移动一个字符检查该字符是否在窗口内出现过如果出现过就把 left 跳到上一次出现位置的下一个字符保证窗口内仍然无重复。#include iostream #include string using namespace std; int lengthOfLongestSubstring(string s) { int last[256]; for (int i 0; i 256; i) last[i] -1; int left 0; int ans 0; for (int right 0; right (int)s.size(); right) { unsigned char c s[right]; if (last[c] left) { left last[c] 1; } last[c] right; int len right - left 1; if (len ans) ans len; } return ans; } int main() { string s; while (cin s) { cout lengthOfLongestSubstring(s) endl; } return 0; }这段代码的时间复杂度是 O(n)因为 left 和 right 各自最多扫描一遍字符串空间复杂度 O(1)因为 last 数组大小固定。几个注意点last 数组用 256 而不是 26因为题目没保证只含小写字母。访问 last 时用 unsigned char 做下标避免 char 为负数时越界。last[c] left 这个判断非常关键表示该字符上一次出现的位置还在当前窗口内。3.3 变式延伸K个不同字符的最长子串把“无重复”改成“最多包含 K 个不同字符”之后解法就变成带计数的滑动窗口。我们需要维护窗口内每个字符的出现次数同时维护当前不同字符的数量。#include iostream #include string #include cstring using namespace std; int longestSubstringWithKDistinct(string s, int k) { int cnt[256]; memset(cnt, 0, sizeof(cnt)); int left 0, distinct 0, ans 0; for (int right 0; right (int)s.size(); right) { unsigned char c s[right]; if (cnt[c] 0) distinct; cnt[c]; while (distinct k) { unsigned char lc s[left]; cnt[lc]--; if (cnt[lc] 0) distinct--; left; } int len right - left 1; if (len ans) ans len; } return ans; }这个变式是我非常推荐练的因为它强迫你理解“窗口收缩”的本质不是简单地把左边界加一而是要让窗口重新满足题目条件。掌握了这个思路连续子数组相关的问题也会顺手很多。4. 数组与哈希表看似简单实则充满陷阱数组题看起来是最简单的但它有两个隐藏考点一个是时间复杂度另一个是重复元素的处理。哈希表是解决这类问题的首选数据结构但用不好会踩坑。4.1 题目长什么样经典模型是“给定一个整数数组和一个目标值在数组中找出和为目标值的两个数返回下标。”这题在2016年笔试里几乎人手一道现在也还是面试高频。有些题目会强调“假设每种输入只对应一个答案且不能使用同一个元素两次”有些则不会。如果题目没说一组还是多组输出格式也要留意。4.2 完整题解两数之和暴力做法是双重循环复杂度O(n^2)如果 n 到达 10^5 就会超时。正确做法是用哈希表记录每个数已经出现的位置遍历到当前数时只查一下 target - 当前数 是否已经在哈希表里。#include iostream #include vector #include unordered_map using namespace std; vectorint twoSum(vectorint nums, int target) { unordered_mapint, int pos; for (int i 0; i (int)nums.size(); i) { int need target - nums[i]; if (pos.find(need) ! pos.end()) { return {pos[need], i}; } pos[nums[i]] i; } return {-1, -1}; } int main() { int n, target; while (cin n) { vectorint nums(n); for (int i 0; i n; i) cin nums[i]; cin target; vectorint ans twoSum(nums, target); if (ans[0] ! -1) cout ans[0] ans[1] endl; else cout not found endl; } return 0; }这里有个细节必须先查 need 再插入当前元素否则会出现重复使用同一个元素的问题。比如 nums [3, 3]target 6如果先把第一个 3 插进哈希表再查第二个 3 时哈希表里已经有一个 3答案就是 [0, 1]这没问题。但如果是 [3]target 6先插入后查询会查到自身返回 [0, 0]这就不符合题意了。所以顺序必须是“先查再存”。如果要处理负数哈希表依然没问题如果要找所有组合而不是一个下标可以先把数组排序再用双指针但这时候下标信息会丢失需要额外处理。4.3 变式延伸三数之和与无序去重三数之和是两数之和的常见扩展给定数组找出所有和为0的三元组不能重复。这题就不太适合哈希表了因为要去重。更稳的做法是排序 双指针固定一个数然后在剩余区间里找两数之和。#include iostream #include vector #include algorithm using namespace std; vectorvectorint threeSum(vectorint nums, int target) { vectorvectorint res; sort(nums.begin(), nums.end()); int n nums.size(); for (int i 0; i n; i) { if (i 0 nums[i] nums[i - 1]) continue; int left i 1, right n - 1; while (left right) { int sum nums[i] nums[left] nums[right]; if (sum target) { res.push_back({nums[i], nums[left], nums[right]}); while (left right nums[left] nums[left 1]) left; while (left right nums[right] nums[right - 1]) right--; left; right--; } else if (sum target) { left; } else { right--; } } } return res; }“去重”是这类题最容易出错的地方。如果排序之后不跳过重复元素结果里会出现大量重复三元组。笔试判题时重复输出很可能被判错误。我在实际刷题中会专门去练“在有序数组里用双指针跳过重复值”这个手感因为它在很多题目里都会出现。5. 字符串循环移位一个数学小结论解决一类题字符串循环移位是2016年笔试里很有代表性的一类题。说它难吧思路很简单说它简单吧现场推导容易卡住。这个模型值得单独拿出来讲是因为它涉及一个特别漂亮的数学结论。5.1 题目长什么样题目通常是“给定两个字符串 s1 和 s2判断 s2 是否由 s1 循环移位得到。”所谓循环移位就是把字符串的一部分从头部挪到尾部或者从尾部挪到头部比如 “abcde” 循环移位可以得到 “bcdea”、“cdeab” 等。有些题还会写成“判断 s2 是否是 s1 循环移位后字符串的子串。”这两个问法有区别前者要求长度相等且位置对齐后者只要求包含即可。笔试时看清楚题目到底问的是哪一种。5.2 核心小结论AA 包含所有循环移位这是一个简单但很反直觉的结论如果 s1 的长度为 n那么 s1 s1 组成的字符串中包含了 s1 的所有 n 种循环移位结果。为什么以 s1 “abcde” 为例“abcdeabcde” 这个拼接串里从位置 0 开始取 5 个字符是 “abcde”从位置 1 开始取 5 个字符是 “bcdea”从位置 2 开始取 5 个字符是 “cdeab”依此类推。每个位置开头的长度为 n 的子串恰好对应一种循环移位。所以在判断 s2 是否是 s1 的循环移位时只需要做两件事检查长度是否相同检查 s2 是否在 s1 s1 中出现。如果题目允许 s2 只是 s1 循环移位串的子串那么长度不需要相等直接在后一个拼接串里查找即可。5.3 完整题解与复杂度#include iostream #include string using namespace std; bool isRotation(string s1, string s2) { if (s1.length() ! s2.length()) return false; string combined s1 s1; return combined.find(s2) ! string::npos; } int main() { string s1, s2; while (cin s1 s2) { if (isRotation(s1, s2)) cout true endl; else cout false endl; } return 0; }时间复杂度取决于字符串查找算法。C 标准库的 find 在大多数实现里性能已经足够好笔试场景下基本不会超时。如果字符串长度很大可以手写 KMP 算法把查找降到 O(n)但这属于锦上添花不是必须。5.4 变式延伸字典序最小的循环移位这个变式在思维上更进一层“给定字符串求它的所有循环移位中字典序最小的一个。”最直接的做法是枚举所有循环移位然后取最小复杂度 O(n^2)。当 n 不大时可以这么写。如果要追求线性复杂度就要用到“最小表示法”。核心思路是维护两个指针 i 和 j逐个字符比较失配时把较小的指针跳过去。这部分笔试不常考但如果面试聊到字符串算法把这个结论拿出来会有加分效果。建议先把 AA 的结论吃透再去碰最小表示法。6. 笔试现场最容易翻车的五个细节算法思路对代码却通过不了大概率是栽在细节上。这里列一下我当年和这几年帮人复盘笔试时最常看到的五个翻车点。6.1 翻车点总览翻车点常见表现应对策略多组输入只处理了一组数据用 while (cin ...) 或 while (scanf(...) ! EOF)数组越界字符串下标访问到负值使用 unsigned char 做下标分配足够空间整数溢出target 或求和结果超过 int 范围用 long long 存中间结果状态未重置多组测试用例共用全局数组上次的数据污染下次每组数据开始前初始化输出格式错误行尾多空格、少换行严格按题目描述输出测试样例时注意空白字符6.2 多组输入数据的处理2016年很多笔试平台的多组输入是“读到文件结尾”模式。以C为例如果输入是每行两个整数正确写法是int a, b; while (cin a b) { // 处理每一组 }如果题目先给一个测试用例数 T再给 T 组数据那就不能这么写而是int T; cin T; while (T--) { // 处理每组 }这两种模式混在一起非常容易踩坑。我当年第一次参加这类笔试时把第二种模式写成了 while (cin T)结果平台读不到结束标志直接超时。后来学乖了读题时先把“输入格式”四个字圈出来先判断是哪种模式再动手。6.3 数组长度与越界字符串题目里char 类型在部分编译器下默认是 signed char取值是 -128 到 127。如果用字符串中的字符直接做数组下标比如 last[256]遇到 ASCII 码大于 127 的字符时char 会先转换成负数导致数组越界。解决办法是强制转换成 unsigned char。另外有些题要求字符串长度最大为 10^5如果开数组只开了 1000后面的测试点全部数组越界表现不是报错就是答案错误。笔试时看到数据范围的上限数组大小就直接按上限加一点余量来开。6.4 整数溢出两数之和的变式里如果数组元素范围很大比如每个数在 [-10^9, 10^9]两个数相加可能超过 int 范围。C里 int 一般是 32 位最大值约 21亿两个十亿级别的数相加就溢出了。出现负数时问题更隐蔽溢出后结果变成一个奇怪的负数程序不会报错但答案完全不对。这类题目最好的习惯是涉及加和、乘积、长度统计的场景统统用 long long 保存中间结果。笔试环境不会因为你多写一个 long long 扣分但会因为溢出判错。6.5 输出格式与调试很多题目要求“每个测试用例输出一行行尾没有多余空格”。如果输出多个数常见写法是for (int i 0; i n; i) { if (i) cout ; cout arr[i]; } cout endl;这样既能保证数字之间只有一个空格又不会在行尾多输出空格。另一个很实用的经验是不要只依赖题目样例。样例可能只有一组数据无法覆盖“多组输入”和“空数据”这些情况。我会在本地额外构造几个边界数据比如空字符串、全相同字符、超大数相加跑通之后再提交。这个过程看起来耗时实际能避免一两次提交失败带来的心理波动。7. 吃透这几道题之后下一步练什么基础模型想清楚了最重要的是把它们转化成自己的解题反射。只看解析不亲手写代码等于白看。7.1 给每道题找一个“变式练习”我复习时的做法是每道真题做完之后立刻找两到三个变式题继续做直到能在不参考解析的情况下独立写出来。比如最长不重复子串练完之后练“至多包含K个不同字符的最长子串”。两数之和练完之后练三数之和、四数之和。字符串循环移位练完之后练“旋转字符串”系列题目。这个过程能极大地提高你对模型本质的理解。只做原题容易形成“背题”的错觉变式题会逼着你理解算法为什么有效。7.2 模拟题怎么练才有效2016年笔试里还有一类模拟题比如按规则报数、洗牌、队列进出等。这类题不需要高深算法难点在“把规则完整地翻译成代码”以及“不要漏掉循环边界”。我建议模拟题用“先手推、再编码”的方式练。拿到题后先在纸上用一个小例子把整个过程走一遍标出每个变量的变化再写代码。比如判断约瑟夫环问题n5, m2 时淘汰顺序是什么现场不推演直接写大概率会栽在边界上。7.3 笔试前一周的安排建议如果离笔试还有一周不建议再大量刷难题。我会把时间分成三块前两天重做高频基础题主要练手速和输入输出。中间三天挑一些中等难度的变式题每道题控制在20-30分钟。最后两天严格按照笔试时间做整套模拟题训练时间分配和心态。整套模拟非常重要。笔试不是“会做就行”而是“在规定时间内做出来”。很多人在单独刷题时能写出正确代码但整套模拟时因为紧张和时间压力连基础题都会卡壳。提前适应这种节奏比多刷一百道题更有价值。7.4 最后分享一个我自己的小习惯我每次笔试前都会在草稿纸上写一行字“先看输入格式再定数据结构。”这句话帮我避开过无数次低级错误。即使到了现在我写工程代码或参加线上编程评估时也保留着这个习惯。工具会变、平台会变、题目描述会变但拆题的方法和基础算法模型不会变。把这几道经典题吃透你就有了应对大多数笔试编程题的底气。
返回列表