
今天是杭电网安复试编程训练的第20天。说实话比起前面19天里任何一天我都觉得今天才是真正开始“入题”的一天。不是今天刷的题量有多大而是我终于想明白了一个问题杭电网安的复试编程到底在考什么如果只是把数据结构题刷成机械记忆大概率要翻车。这篇文章既是我的Day20复盘也是给正在准备网安类复试、尤其是目标在杭电的同学们一份“编程训练到底该怎么练”的参考。里面所有题目都是我真实写过的踩的坑也是真实的希望能帮你们少走这几天弯路。1. 为什么第20天是复习的分水岭1.1 前19天我在练什么练错了什么先说点实话。前19天我一直在刷各种各样的题目刷得很猛但方向是偏的。前十天我很老实地把C语言的基础语法、指针、结构体、链表这些重新过了一遍那时候觉得自己稳得很。等到第十一天我开始焦虑了因为看到网上有人说杭电复试会考算法题有人说什么图论、动态规划、字符串匹配我就开始慌然后把自己扔进了难题堆里。结果就是第十五天到第十八天我刷的题越来越难但我越来越没底。具体表现是每道题我都“看得懂答案”“敲得出来”但一旦把答案遮住让我独立写一遍就会卡壳。卡壳的点还特别集中——不是算法思路不懂而是边界条件想不全、C语言细节处理不干净。比如字符串数组忘了加结束符、指针操作时对空指针没有判断、malloc完之后忘了free。这些问题在刷难题的时候被华丽丽地掩盖了因为难题考的是思路简单基础题才真正暴露代码功底。到了第十九天晚上我拿了一套杭电往年网安方向复试的回忆版题目做模拟结果三题只完整做出一道半。那一刻我才意识到复试编程不是竞赛它考的是在你压力很大的情况下能不能写出正确、干净、可读的代码。而这些东西恰恰是我前19天最忽略的。1.2 复试编程的真实定位不是竞赛题是安全素养的敲门砖网安方向的复试编程和普通计算机专业的机试看着像实际侧重点有差别。普通机试可能考一个综合应用题比如实现一个带复杂逻辑的模拟器但网安方向的题目往往带着“安全的壳子”。我整理了一下近几年考生回忆的题目类型出现频率很高的是这几类输入数据的合法性校验比如IP地址、MAC地址、端口号、URL格式字符串和字节流的处理比如进制转换、编码解码、十六进制转储数据结构的经典应用比如日志频率统计哈希表、文件目录遍历递归/栈、网络数据包队列队列/链表基础加密或校验算法的实现思路比如MD5的替换思路、CRC校验的模拟、异或加解密树的遍历和图的最短路径这类基础算法题。这背后的逻辑很简单。导师招你进来不是指望你马上能挖漏洞、写EXP而是希望你已经具备基本的编程素养能在读代码、分析日志、写小工具这些事情上直接上手。复试编程题就是用一个带安全背景的题目考察你三件事代码能不能跑对、边界能不能想到、代码风格像不像一个正经搞安全的人。想通这一点之后我就把刷题策略全改了。今天这一天的训练就是从“竞赛思维”回到“工程安全思维”的转折点。2. 今天的核心训练三道题完整拆解今天我只精做了三道题。题量不大但我把每道题的思考过程、不同的解法、可能被追问的点全部复盘了一遍。这三道题非常能代表杭电网安复试的“口味”。2.1 第一题IP地址合法性校验——字符串操作与边界直觉题目原型写一个函数int isValidIP(const char *ip)判断一个点分十进制表示的IPv4地址是否合法。要求不接受额外字符不接受前导零这个是后来我给自己加的严格要求每个字段范围是0到255。这道题看着简单但写明白不容易。我第一版代码直接用了sscanf几个字段一口气扫出来然后判断范围。代码倒是很短但是经不起细问。面试官如果问“192.168.01.1算合法吗”“1.2.3.4.5怎么处理”“空字符串传进来会怎样”我那个版本就会露怯。第二版老老实实手写解析。先判断非空然后按点切分每段里逐字符检查是否是数字再判断前导零规则最后把值算出来判断0到255。#include stdio.h #include string.h #include stdbool.h int isValidIP(const char *ip) { if (ip NULL) return 0; int len strlen(ip); if (len 0 || len 15) return 0; /* 最短0.0.0.0长度7最长255.255.255.255长度15 */ int num 0, dotCount 0; int segLen 0; for (int i 0; i len; i) { char c ip[i]; if (c 0 c 9) { if (segLen 0 c 0 i 1 len ip[i 1] ! .) { return 0; /* 前导零判定0后面跟数字就非法 */ } num num * 10 (c - 0); segLen; if (num 255) return 0; if (segLen 3) return 0; } else if (c .) { if (i 0 || ip[i - 1] .) return 0; /* 连续点或开头点 */ dotCount; num 0; segLen 0; } else { return 0; /* 非法字符 */ } } if (dotCount ! 3) return 0; /* 必须有且仅有3个点 */ if (ip[len - 1] .) return 0; /* 结尾不允许是点 */ return 1; }这道题踩坑点我复盘了一下主要集中在三个地方。第一用sscanf解析时“1.2.3.4.5”这种输入它只会解析出前四个字段剩下的“5”它不管这就会把一个明显非法的输入判成合法。第二前导零的问题很多网上的示例代码都不做“01”或“001”的拒绝处理。你可以先不要求这一步但机试后老师如果追问“你觉得哪里还能改进”你能说出前导零规则是一个加分项。第三数据类型的细节比如num num * 10 (c - 0)这一步如果不判断segLen 3那么输入一个超长的数字字段num可能直接溢出整型行为就不可控了。2.2 第二题日志频率统计——哈希表的空间换时间题目原型给定一个日志文件每行记录一个访问来源IP统计出现次数最多的 K 个IP按次数降序输出次数相同按IP字典序升序。日志行数最大是10^6IP总数可能达到10^5级别。这道题我在第12天的时候写过一版当时用的是最粗暴的方式二维数组或者字符串数组存下所有IP然后二重循环去数每行出现几次。10^6量级直接跑挂复杂度是O(n^2)我不可能等它跑完。今天重新写换了哈希表加排序的思路。先用哈希表统计每个IP的频率再把哈希表里的键值对搬到数组里按频率排序取前K个。#include stdio.h #include stdlib.h #include string.h #define HASH_SIZE 100003 /* 一个较大的质数 */ typedef struct Node { char ip[16]; int count; struct Node *next; } Node; typedef struct { Node *buckets[HASH_SIZE]; } HashMap; unsigned int hash(const char *ip) { unsigned int h 0; while (*ip) { h h * 131 (unsigned char)(*ip); ip; } return h % HASH_SIZE; } void insert(HashMap *map, const char *ip) { unsigned int idx hash(ip); Node *cur map-buckets[idx]; while (cur) { if (strcmp(cur-ip, ip) 0) { cur-count; return; } cur cur-next; } Node *newNode (Node *)malloc(sizeof(Node)); strcpy(newNode-ip, ip); newNode-count 1; newNode-next map-buckets[idx]; map-buckets[idx] newNode; } typedef struct { char ip[16]; int count; } Item; int cmp(const void *a, const void *b) { Item *ia (Item *)a, *ib (Item *)b; if (ib-count ! ia-count) return ib-count - ia-count; return strcmp(ia-ip, ib-ip); }排序比较函数里先按频率降序频率相同就按IP的字典序升序这一点容易漏。很多人在qsort的比较函数里只写了return ib-count - ia-count结果面试官追加一句“次数相同怎么办”就会愣住。更关键的是复杂度分析。哈希表插入平均O(1)总插入O(n)排序的复杂度如果直接全排序是O(m log m)m是不同IP的数量。如果面试官追问“K很小比如K10但IP种类很多有没有更优做法”那就是要你说堆排序或者优先队列维护一个大小为K的小顶堆遍历哈希表的时候频率比堆顶大就替换这样复杂度是O(n m log K)空间也更有优势。这个追问几乎一定会出现建议提前准备好说辞代码哪怕不写思路要用嘴说得清清楚楚。2.3 第三题判断二叉搜索树——递归与中序遍历的双思路题目原型给一棵二叉树的根节点判断它是否是一棵合法的二叉搜索树。BST定义左子树所有节点值小于根节点右子树所有节点值大于根节点左右子树也必须是BST。这题我在第8天就写过当时用的方法是递归时把每个节点的值限定在一个区间里根节点区间是负无穷到正无穷。但是C语言里INT_MIN和INT_MAX有坑当树里恰好有节点的值是INT_MIN或INT_MAX时边界判断就会出现相等判断的错误。我第一次写就踩了这个问题被2147483647这个值卡了一天。标准做法有两种。第一种是递归限定区间用long long来传上下界避开int边界问题。#include limits.h #include stdbool.h typedef struct TreeNode { int val; struct TreeNode *left; struct TreeNode *right; } TreeNode; bool helper(struct TreeNode *node, long long lower, long long upper) { if (node NULL) return true; if (node-val lower || node-val upper) return false; return helper(node-left, lower, node-val) helper(node-right, node-val, upper); } bool isValidBST(struct TreeNode *root) { return helper(root, LLONG_MIN, LLONG_MAX); }第二种是中序遍历。中序遍历一棵BST得到的结果一定是递增序列所以就写一个中序遍历判断当前节点值是否严格大于前一个节点的值。如果中间出现小于等于的情况就不是BST。这个思路面试时口头讲非常加分因为它展示你对两种经典方法都有理解还能解释为什么中序序列递增等价于BST的合法性。bool isValidBST2(struct TreeNode *root) { long long prev LLONG_MIN; return inorder(root, prev); } bool inorder(struct TreeNode *root, long long *prev) { if (root NULL) return true; if (!inorder(root-left, prev)) return false; if (root-val *prev) return false; *prev root-val; return inorder(root-right, prev); }这道题真正想考察的不是你会不会背模板而是你有没有想清楚BST的定义里“所有左子树节点都小于根”和“递归时节点值的上界/下界是如何传递的”。我见过很多人判断只查了左孩子根、右孩子根遇到下面这种树就直接错了5 / \ 1 6 / \ 4 7只看局部6的右子树没问题但4比5小却出现在右子树里整棵树不是BST。这种测试用例永远是考场上最阴的。3. 网安复试编程和普通机试的差别在哪里3.1 代码风格与安全编码比“能跑”更重要的是“像样”前19天我刷题时有个毛病写代码只图快速AC变量名用a、b、tmp函数能缩成一坨就缩成一坨。到了今天模拟复试我才发现这种习惯在网安方向的复试里非常吃亏。为什么因为网安这个方向的老师看代码的水平可能比看算法水平还高。你天天跟二进制、漏洞、逆向、流量分析打交道别人代码里有没有隐患、有没有糟心的风格一眼就看得出来。我给自己总结了一套“网安复试编程代码自检清单”写的时候按这个来哪怕题目做不出来代码分也不会太差每个函数不超过40行功能单一不写“面条函数”所有可能为空的指针使用前都检查尤其是malloc、fopen之后数组、缓冲区使用前明确最大长度防止越界读写字符串操作优先用strncpy、snprintf这类带长度控制的版本而不是strcpy、sprintfmalloc 和 free 成对出现释放后置空指针对用户的每一种非法输入都有防御性分支坚决不假设输入是干净的。这三个词——防御性、边界感、可读性就是网安复试编程和普通机试最大的差别。打个比方普通机试像你去驾校考科目二只要倒库不压线就行网安复试编程像你实际开车上路不光要不压线还得懂得随时观察后视镜、预判行人和电瓶车。后视镜和预判就是代码里对边界条件和异常输入的敏感度。3.2 面试官真正想从编程题里看到的东西我后来跟一个已经在杭电读研的学长聊过一次他透露了几个很有价值的细节。第一个是复试机试或面试现场给题之后老师会特别留意你拿到题目之后的前几分钟在干什么。如果上来就闷头写边界想都不想大概率是要扣分的。更好的做法是花两三分钟把题目的输入范围、边界情况、数据规模看清楚甚至在草稿纸上列几个特殊测试用例再动笔。这个习惯本身就是一种安全思维——我们做安全的拿到一个程序第一反应就是“它的输入能被怎么篡改”而不是“它正常情况跑起来什么样”。第二个细节是代码写完后面试官特别喜欢追问时间复杂度和空间复杂度以及“如果数据量扩大100倍怎么办”。以前我的做法是刷完题不看解析根本不总结导致被问到复杂度时支支吾吾。今天开始我强制自己给每道题写三行笔记一行写思路、一行写复杂度、一行写特殊用例。这个方法推荐你们也试试。第三个细节是我学长特别强调的他说“代码风格好的考生哪怕算法差一点老师也愿意给机会”。因为研究生阶段导师需要的是能一起改代码、读代码、维护代码的人。你写出来的代码别人读不懂还要导师帮你擦屁股这是大忌。4. Day20暴露的问题清单与修正方案4.1 我在今天训练中踩的三个具体错误我把今天练题过程中真实暴露的问题列出来每一道都值得单独说。第一个错误是在IP校验题里我一开始真的想用sscanf。我当时的理由是“代码短看起来简洁”但写完之后我试着跑了几个特殊用例“192.168.1.01”它认了“1.2.3.4.5”它也认了只解析前四段这个结果对我心态影响很大因为这种“看着对但实际错”的代码在复试现场是最要命的。它不像编译错误会直接提醒你它会安安静静地给你错误结果而你可能交卷前都发现不了。第二个错误是在日志统计题里我第一次写完排序比较函数时竟然把字典序排序写反了。我写的是return strcmp(ib-ip, ia-ip)测试数据一跑完全乱套。细看才发现是我在cmp函数里参考了频率排序的写法两个参数的先后顺序搞混了。这个错误纯属操作熟练度不够说明我平时对qsort比较函数写得少、练得少。第三个错误是在BST题里我口头复述“中序遍历BST等于递增序列”这个结论时一开始说成了“中序遍历等于有序序列”。虽然只是一句口头表述但面试时这种地方被追问一下就很尴尬。导师会接着问“为什么中序遍历就能得到有序序列”其实答案很简单BST的左子树全部小于根、右子树全部大于根中序遍历先走左再走中最后走右相当于从小到大把所有节点访问一遍。我当时虽然懂但没准备好这句话这一课记下了。4.2 错题本的新写法按“为什么错”分类而不是按“题目类型”分类以前我的错题本是按链表、树、字符串这样分类的后来发现这个分类方式价值不大。因为同样一道树题我可能是因为“不会递归”错的也可能是因为“边界值没考虑”错的还可能是因为“遍历顺序理解反了”错的。这三类问题背后的训练方向完全不同。今天我把错题本改成了按错误原因分类分四类第一类数据结构不熟练。比如某个容器、指针操作、节点定义写不清楚。解决办法是回到基础手写三遍标准实现。第二类边界条件考虑不全。比如空输入、最大输入、特殊值、重复元素。解决办法是每次写完代码强制列出至少五个测试用例从“空、最小、最大、非法、常规”五个角度去攻击自己的代码。第三类复杂度说不清。解决办法是把每道题都做一次复杂度推演最好能写出递推过程。第四类口头表达不精准。解决办法是每天模拟5分钟“给一个不懂这道题的人讲思路”录下来自己听。这样的分类训练起来才高效。你不能用一个“多刷题”这个笼统的方案去解决所有问题。数据结构不熟就回去手写边界考虑不全就练用例设计复杂度说不清就练推导表达不准就练复述。5. 第20天之后我的训练路线怎么调5.1 把C语言重新作为第一语言前16天我有一段弯路因为Python写起来快我大量用Python刷题。Python确实香写个哈希表几行就搞定了字符串处理也方便。但是杭电网安复试明显是C/C优先的而且就算你不考C老师对你的底层理解期待也是C视角的。今天开始我的所有核心算法题全部用C重写一遍Python只作为验证想法的辅助工具。这里分享一个我用的笨方法同一道题强制自己用C语言写三遍。第一遍照着思路写错没关系第二遍不看任何参考从头写第三遍隔一天再写然后对比第一遍的代码看看自己到底进步在哪、还有哪些重复的坏习惯。这个方法虽然耗时但对C语言熟练度的提升非常明显尤其是对指针、内存管理和字符串操作这三块。5.2 安全基础与编程训练双线并行编程不能脱离网安的知识背景来练。我给自己定了“编程题背景靠安全场景”的原则。练字符串就练日志清洗、IP解析、端口解析练哈希表就练日志统计、告警聚合练树和图就练目录结构遍历、依赖关系分析练位运算就练掩码计算、子网划分、简单的校验和模拟。同时每天固定一个小时专门看网安的知识提纲。这里我不展开具体的攻击技术只讲一个原则复试准备阶段重心要放在“原理、防御、检测、分析”这些正向能力上比如常见漏洞的成因与修复、网络协议的正常工作流程、主机日志和流量日志的基本分析思路。这些知识和编程题一起复习效率是11大于2的。时间分配上我现在是这样排的早上8点到10点C语言基本功专门练习指针、结构体、字符串、内存管理上午10点到12点核心算法链表、栈、队列、树、图每天两个类型手写三遍法下午2点到4点安全基础看协议分析、日志分析、漏洞原理的时候顺手用今天学的数据结构写一个小工具去处理模拟数据下午4点到5点错题本整理和口头复述把当天所有题目的思路对着镜子讲一遍晚上8点到10点模拟考试掐时间做题严格按“先花3分钟列用例和边界再写代码”的流程来。这个节奏我打算至少保持到复试前。以前我总觉得“时间不够了要多刷题”但Day20这么一次深刻的复盘下来我更确信一句话刷题数量不重要重要的是你能不能稳定地把一道基础题做到“无可挑剔”。网安复试编程考的不是你会不会花活而是你有没有养成安全从业者该有的那种代码洁癖。如果你也在准备杭电网安或者其他学校的网安复试我希望你从Day1就开始建立这个意识每道题不只要写出来还要能讲清楚、能扛得住追问、能对边界条件如数家珍。别像我一样等到第20天才把方向掰回来。现在动起来下一道题就从“空指针检查”开始写起吧。