ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

链表刷题与Web工程双线并进:从虚拟头到Spring Boot的实战复盘

链表刷题与Web工程双线并进:从虚拟头到Spring Boot的实战复盘 1. 第27天算法与工程同时推进带来的双重压迫感今天是连续刷题和跟课的第27天也是我决定同时推进两条线的一天。上午把hot100链表的题单重新翻了一遍下午继续跟黑马web课程168到182集的进度。不少人问我为什么项目课都跟到一百多集了还要回头刷链表这种“老知识”我当时没细想写完今天的总结后有了一个更清楚的答案链表题训练的是对引用、状态转移和边界条件的控制力而web课程这一阶段刚好开始讲工程结构、配置文件和部署这两者表面上不搭底层却共用同一套能力。先说点背景。我在刷题记录前加的“0x3f”没什么特殊意义就是早年写C时0x3f3f3f3f用习惯了拿来当个人标记。第27天之所以值得单独写一篇是因为它正好卡在两个节奏的中间hot100链表这批题开始从“能做出来”转向“做得干净、不丢指针”而web课程168到182集也在从“跑通项目”转向“把项目按企业级工程的方式组织”。一天之内同时面对这两种挑战人是会被逼着成长的——前提是节奏安排对。这篇记录打算写清楚四件事链表的基本功到底该夯实到什么程度、hot100链表题的解题套路、链表代码里最容易翻车的几个点、以及web课程这个阶段和算法训练如何互相成就。目标读者是正在双线作战的同行一边跟项目课、一边刷算法题时间永远不够用但两边都不想放。我说的都是自己实际操作中的做法和踩过的坑没有“标准答案”但你可以直接拿去参考。2. 刷hot100链表前先把这几种链表结构彻底分清2.1 带头结点和不带头结点的区别链表刷题翻车十次里有八次出在头结点处理上。带头结点的链表头结点本身不存有效数据只是固定起点。它的最大好处是操作统一无论插入还是删除都不用格外考虑“我操作的是不是第一个结点”。不带头结点的链表head指向真正的第一个数据结点一旦要删除或插入到头部就必须手动维护head的指向。hot100里有不少题给的输入是不带头结点的链表但你在代码里可以自己造一个虚拟头结点dummy node把问题转换成带头结点的场景。这个技巧几乎贯穿链表题的所有中等题不理解带头结点的价值就很难理解为什么每道题解的代码里都多出来一个new ListNode(0)。理解这件事靠生活类比最快带头结点的链表像火车多挂了一节不载客的守车你不管从哪节车厢接新车厢都不用担心整列车头要不要换不带头结点的链表像单节小货车你要是在车头前面再加一节车头的标识就得换。2.2 指定位置插入建立单链表的基本功热词里有一个很常见的说法叫“在指定位置插入建立单链表”这里其实包含两层意思一是会建立链表二是能在指定位置插入。刷hot100不需要从零手写整个链表类但你必须能把“插入到第i个位置”的每一步说清楚。以Java为例带头结点的单链表中在位置pos插入值为val的新结点核心代码是public void insert(ListNode head, int pos, int val) { ListNode cur head; // 走到pos位置的前一个结点 for (int i 0; i pos cur.next ! null; i) { cur cur.next; } ListNode newNode new ListNode(val); // 先连后继再改前驱 newNode.next cur.next; cur.next newNode; }这段代码有两个细节值得反复强调。第一先执行newNode.next cur.next再执行cur.next newNode顺序不能反。顺序反了之后cur.next已经指向新结点原本后面的那段链表就找不回来了。第二循环条件是cur.next ! null这意味着如果pos超出了链表长度这个插入会落在链表末尾这在很多题目里其实是期望行为。2.3 遍历、清空、逆置的三个细节链表遍历是热词里另一个常被提到的基础操作。很多人觉得遍历有什么好说的但就是这里最容易犯迷糊判断条件是while (cur ! null)还是while (cur.next ! null)?前者能进到最后一个结点并读取它的值适合“遍历所有结点”的场景后者会停在倒数第二个结点适合“修改前驱指针”的场景。这两种写法没有对错用错位置才是问题。清空链表比想象中讲究。如果只是把head.next置空Java靠GC能回收但在C/C里必须逐个释放结点。热词里提到“单链表的清空”在C语言写法中我会用一个临时指针遍历逐个delete不直接让头结点脱钩了事void clearList(LNode* head) { LNode* cur head-next; while (cur ! NULL) { LNode* tmp cur; cur cur-next; free(tmp); } head-next NULL; }逆置链表是hot100的高频动作也是后面许多中等题的共同子问题。迭代法需要同时维护pre、cur、next三个指针。核心逻辑用next记录cur的下一个结点把cur.next指向pre然后pre和cur整体后移。这件事练不顺后面做K个一组翻转链表基本做不下去。至于循环单链表和双链表hot100里出场率没那么高但也不能完全不看。循环单链表的判断条件不是“某结点的next为null”而是“next是否等于head”。双链表的删除要同时维护prev和next两个指针和单链表相比只是多一条指针操作思路完全一样。热词里把这些都列出来了说明大家搜的时候确实容易混淆建议按“带头/不带头、单向/双向、是否循环”三个维度把六种形态在纸上画一遍。3. hot100链表题拆解虚拟头、快慢指针、双指针与哨兵3.1 虚拟头结点让删除头结点不再特殊hot100里的链表题我最推荐先攻克“虚拟头结点”这个套路因为一半的题目都可以靠它简化边界判断。以“删除链表的倒数第N个结点”为例。直接做要分两步先遍历得到链表长度再走length - n步找到目标结点的前驱。但用快慢指针加虚拟头可以一次遍历完成public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy new ListNode(0); dummy.next head; ListNode slow dummy, fast dummy; for (int i 0; i n; i) { fast fast.next; } while (fast.next ! null) { slow slow.next; fast fast.next; } slow.next slow.next.next; return dummy.next; }注意最后返回的是dummy.next不是head。如果n刚好等于链表长度删除的就是原头结点此时head已经不再指向链表开头了只有dummy.next才是新链表头。这个坑我踩过很多次凡是用了dummy返回时一律写dummy.next。3.2 快慢指针判环与找中点的通用算法“环形链表”是hot100的必考题快慢指针是标准解法。快指针每次走两步慢指针每次走一步如果链表有环快指针一定会在环里追上慢指针。public boolean hasCycle(ListNode head) { ListNode slow head, fast head; while (fast ! null fast.next ! null) { slow slow.next; fast fast.next.next; if (slow fast) { return true; } } return false; }这段代码看起来很平常但几乎每个新手都会问为什么while条件要写成fast ! null fast.next ! null因为快指针一步要跨两个结点如果fast已经到末尾了fast.next是null再访问fast.next.next就是空指针。记住这个判断条件是快慢指针题不翻车的前提。同样的套路也能用来找链表的中间结点快指针到末尾时慢指针正好停在中间。hot100里有一道“链表的中间结点”本质就是快慢指针的应用。3.3 双指针合并合并两个有序链表“合并两个有序链表”是链表双指针的入门题它的逻辑很像两个有序队列的归并谁小谁先出队。public ListNode mergeTwoLists(ListNode list1, ListNode list2) { ListNode dummy new ListNode(0); ListNode cur dummy; while (list1 ! null list2 ! null) { if (list1.val list2.val) { cur.next list1; list1 list1.next; } else { cur.next list2; list2 list2.next; } cur cur.next; } cur.next list1 null ? list2 : list1; return dummy.next; }最后那行“cur.next list1 null ? list2 : list1”不是偷懒而是利用了链表有序的性质剩下的那段链表本身有序直接挂上去一定不会错。看题解时不要跳过这一步它的存在意味着归并排序中“合并”这一步可以做到O(n)的时间和O(1)的额外空间。3.4 反转链表迭代与递归的差异反转链表是链表的“Hello World”也是最容易在细节上翻车的题。迭代写法就是前面说的三指针法public ListNode reverseList(ListNode head) { ListNode pre null, cur head; while (cur ! null) { ListNode next cur.next; // 关键先保存下一个结点 cur.next pre; pre cur; cur next; } return pre; }递归写法代码更少但空间复杂度更高public ListNode reverseList(ListNode head) { if (head null || head.next null) { return head; } ListNode newHead reverseList(head.next); head.next.next head; head.next null; return newHead; }我的建议是两个版本都练。迭代版用来保证面试时能写出O(1)空间的最优解递归版用来理解“递推”的本质。但初学阶段如果只想掌握一个优先迭代。3.5 哈希辅助相交链表这类“找相同元素”的题相交链表这道题标准解法是双指针A走完换到BB走完换到A最终会在交点相遇。但第一次接触时我建议先用哈希表理解问题模型public ListNode getIntersectionNode(ListNode headA, ListNode headB) { SetListNode seen new HashSet(); while (headA ! null) { seen.add(headA); headA headA.next; } while (headB ! null) { if (seen.contains(headB)) { return headB; } headB headB.next; } return null; }注意哈希表存的是结点引用不是结点值。两个结点值相同不代表是同一个结点相交的判断标准是“同一个对象”。理解了这一点再去看双指针解法就会明白它优化的不过是空间复杂度。把hot100链表的套路按场景归类我整理了一个小表刷题时直接对照套路典型题目关键条件易错点虚拟头结点删除倒数第N个结点返回dummy.next返回了旧的head快慢指针环形链表、中间结点fast与fast.next判空快指针越界双指针合并合并两个有序链表剩余段直接挂上忘记处理剩余段反转三指针反转链表先保存后继断链或死循环哈希辅助相交链表比较引用而非值用equals比较值4. 链表题最常见的事故现场断链、空指针与边界判定4.1 先改next导致后半段链表丢失第27天上午复盘“指定位置插入”时我重新踩了一个特别基础的坑新结点的next还没赋值我就先把前一个结点的next指向了新结点导致原链表后半截彻底找不回来。排查方法不是看报错因为这种逻辑错误根本不会报错。我用三个结点的链表在纸上推演了一遍把每一步的指针变化画出来才明白问题出在“引用的引用被覆盖”上。修复口诀我已经刻在脑子里先连后继再改前驱。所有涉及链表插入的题这一条都适用。4.2 while条件写错导致空指针或越界链表遍历的while条件是区分新手和老手的一个分水岭。while (cur ! null)能进入最后一个结点while (cur.next ! null)会在倒数第二个结点停下。两种条件服务于不同目的用混了很容易埋下空指针。最常见的崩法是这样先写了while (cur.next ! null)又在循环体里执行cur cur.next.next。假设当前cur是倒数第二个结点cur.next不为null但cur.next.next已经是null下一轮循环访问cur.next时直接空指针。我在写快慢指针题时栽过好多次后来养成一个习惯只要一次跳两个结点就先确认cur.next和cur.next.next都非空。4.3 用了dummy却在结尾返回了head这是虚拟头结点套路里最隐蔽的错误。删除头结点后head仍然指向旧头结点如果返回head得到的是一个已经被删除、甚至已经和链表脱钩的结点。正确的返回永远是dummy.next。这里我的习惯是看到dummy出现就顺手把末尾的return改掉不给自己留犹豫的机会。4.4 完整排查链反转链表出现死循环下午写完反转链表我用一个五结点链表测试日志输出的是同一个结点的值明显死循环。排查过程走了三步第一步猜测问题出在指针更新于是只在循环里打印pre、cur、next三个变量的引用地址。第二步发现cur的地址始终没变说明循环体里cur根本没有后移。第三步回头查代码果然是少了一行next cur.next导致cur.next被改成pre之后cur自己再也没有办法前进。这个问题看别人代码很难发现因为代码结构长得跟正确版本几乎一样。但只要亲手踩过一次就会明白“反转链表第一步永远是保存当前结点的后继”这句话为什么是铁律。4.5 边界样例是过滤80%错误的过滤器链表刷题有个经验大半错误集中在空链表、单结点、双结点三种输入上。所以每道链表题写完我不急着提交先按三个规模自测再跑四种边界操作删除头结点、删除尾结点、插入最前面、插入最后面。这套动作听起来琐碎实际能省下大量反复提交的冤枉时间。5. web课程168到182集的阶段复盘与链表刷题怎么互相成就5.1 这一阶段课程在讲什么黑马web课程到168到182集基本已经进入工程化阶段。按我手头这个系列视频的节奏这个阶段的核心是Spring Boot集成、yml配置和项目部署相关的内容。热词里出现的“spring boot 集成web socket yml配置”、“idea2024版本创建web项目”、“nginx高性能web服务器实战教程”都落在这个阶段的范围里。我的实操流程是打开IDEA 2024新建Spring Boot项目确认依赖版本能对上否则yml里的配置很容易生效不了接着把application.yml中的端口、数据源、上下文路径写清楚然后演示WebSocket集成时先跑通一个最简的广播消息不写业务逻辑只验证通道是否建立最后用Maven打成可执行包放到本地Nginx后面做反向代理。整个流程走完才算把课程内容内化。5.2 环境报错的真实复盘dsh web authentication required热词里有一句我印象深刻“dsh web authentication required; reopen the url printed by dsh web.”第27天下午调WebSocket时界面死活打不开报的正是这种认证类问题。解决方式很简单去启动日志里找到它打印出来的那一行URL在浏览器重新打开并完成认证然后继续运行。这类报错的优先级永远排在改配置之前因为它通常是令牌过期不是配置错误。另一个是“failed to load plugins web boot: 2 entries did not activate”这类插件加载失败的问题我按三步处理先看哪个插件没激活再到对应目录确认版本是否匹配最后把无关插件先禁用逐个排除。方法论很朴素插件问题永远先看日志不看日志的排查都是瞎猜。5.3 web安全方向的顺带了解热词里有“web安全”还有“polar ctf web 签到题”、“ctf web解题 找flag夺旗赛”。我在168到182集这个阶段没有深入研究CTF只是补了web安全的基础知识重点看了身份认证、输入校验、会话管理这些正向防护的方向。这些内容改变了我的一个习惯写后端接口时之前只关心能不能跑通现在会多想一层“这个参数进来会不会让程序进入异常分支”。有意思的是这个习惯反过来帮助了链表刷题。写链表的边界条件时我开始用同一套思维空输入会怎样只有一个结点会怎样两个结点会怎样两种状态下系统会不会崩溃从web安全学到的“验证输入”思维迁移到算法题里就是“验证边界”。5.4 链表思想在Java工程里的影子这个阶段让我确信链表不是只在面试里出现的数据结构。JDK自带的LinkedList就是带头结点的双向链表HashMap在哈希冲突时会用链表链表过长又转红黑树任务队列、缓冲区、中间件的责任链全都有链式结构的影子。所以hot100链表刷完之后再回来看Spring、Nginx的源码我对“当前结点”“下一个结点”“头尾维护”这些概念会特别敏感。刷算法不只是为了面试更是在给读框架源码打底。这个体会可能是我第27天最大的收获之一。5.5 双线并行的一次交叉验证课程讲yml配置时我遇到一个参数在调用链上反复被覆盖的问题。排查到后面发现这个问题可以抽象成一条链表几个配置来源按优先级排成链最终生效值取决于最后一个节点。我直接用链表遍历的顺序思维把每个配置来源逐个判定很快定位到问题出在最后一层覆盖。这件事让我彻底认可了双线并行的价值。算法题训练出的抽象能力在web项目里同样能用上只是换了一层皮。6. 从第27天回头看双线作战稳住节奏的几个笨办法6.1 固定时间块把一天切成三段我的安排很机械上午算法复盘、下午课程、晚上新题。这样做的好处是到点就知道该干什么不用反复纠结“现在干嘛”。双线作战最怕的不是累是每次坐下都要花时间决定先做哪件事。固定时间块能消除这种决策成本。6.2 新旧交替隔天和隔三天复习不管刷到第几天我坚持每天重写一遍昨天那道链表题的代码隔三天再重写一遍三天前那道。算法题的遗忘速度快得惊人尤其是套路性很强、但细节很多的链表题。隔天回顾能保证短期记忆隔三天回顾能把套路变成长期记忆。这个方法比刷题数量更关键因为它针对的是“做过又忘了”这个核心痛点。6.3 写不出来可以看题解但必须闭卷重写我有一段时间很抗拒看题解总觉得看了就是作弊。后来想明白了中等以上的算法题能写出最优解的人大概率都看过同类型的题解差别只是看得够不够多。所以现在的策略是写不出来就正常看题解看完必须合上题解闭卷重写一遍再用边界样例自测。这和web课程里“先跑通再优化”是一个道理——先有正确实现才谈得上理解和优化。6.4 学习记录本身就是复习工具“0x3f 第27天”这种标题对我来说就是一个时间戳。写学习记录不是给别人看的是给一周后的自己看的。记录里写下当天最想不通的那个点一周后回看往往会发现那个点已经成了常识。这种正反馈比刷题数量的增长更能支撑人走下去。第27天结束的时候我手边的草稿纸上画满了反转链表的三指针状态电脑上是刚跑通的Spring Boot项目。两个看起来毫无关联的进度在同一个晚上都有了推进。这大概就是双线作战最舒服的状态——不追求一天做完多少事只追求每一天都能看到自己在往前走。
返回列表