《链表面试基础看点:这里不止“快慢指针”的完美实现,更懂“哨兵节点”的巧妙运用》

《链表面试基础看点:这里不止“快慢指针”的完美实现,更懂“哨兵节点”的巧妙运用》

🔥@晨非辰Tong:个人主页 

👀专栏:《C语言》《数据结构与算法》《数据结构与算法刷题集》

💪学习阶段:C语言、数据结构与算法初学者

⏳“人理解迭代,神理解递归。”


引言:链表刷题进行时。寻找中间结点,看似简单,但你的解法是否考虑了所有边界情况?本文手把手带你用“快慢指针”写出完美解,以及“哨兵节点”对合并链表的简化实现。

目录

1.  876. 链表的中间结点 - 力扣(LeetCode)(快慢指针)

2.  21. 合并两个有序链表 - 力扣(LeetCode)


1.  876. 链表的中间结点 - 力扣(LeetCode)(快慢指针)

方法一:遍历链表计算总大小,算出mid,将首节点指针向后mid个节点。(容易想到)

方法二:使用快、慢指针,慢指针动一步、快指针动两步。

/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ typedef struct ListNode ListNode; struct ListNode* middleNode(struct ListNode* head) { //创建快慢指针 ListNode* fast, *slow; //首先都指向头节点 fast = head; slow = head; //循环条件将两种情况都包含 while(fast != NULL && fast->next != NULL)//条件换顺序? { //慢指针移动一节点 slow = slow->next; //快指针移动两个节点 fast = fast->next->next; } //返回slow return slow; }
--为什么慢指针移动一个节点,快指针移动两个节点,是固定的吗?
        :很好理解,因为是找 mid ,以慢指针所在节点为关注,那么快指针移动的是慢指针的两倍—>当快指针移动到尾节点或空,那么慢指针就指向 mid 。

--循环条件为什么不是或的关系?
        :在循环体中实现的的是奇数或偶数个节点链表的寻找,那么只要两个条件只有都满足才能进行。

    

--上面提到,循环条件可以换位置吗?

        :对偶数个节点进行分析


2.  21. 合并两个有序链表 - 力扣(LeetCode)(“哨兵”位)

  • 方法一:创建新链表,比较大小,小的尾插新链表
/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ typedef struct ListNode ListNode; struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) { //判断两个链表是否是空链表 if(list1 == NULL) { return list2;//直接返回list2 } if(list2 ==NULL) { return list1;//直接返回list1 } //创建空链表:空的首节点 ListNode* newhead, *newtail; newhead = newtail = NULL; //两个指向指针 ListNode* l1 = list1; ListNode* l2 = list2; //遍历原链表 //先将相同长度内的节点进行比较 while(l1 != NULL && l2 != NULL) { if(l1->val < l2->val) { //l1尾插 //第一回尾插,链表为空 if(newhead == NULL) { newhead = newtail = l1;//为头、为尾 } //后续尾插 else { newtail->next = l1; newtail = newtail->next; //尾后移 } l1 = l1->next;//链表1后移 } else { //l2尾插 //第一回尾插,链表为空 if(newhead == NULL) { newhead = newtail = l2;//为头、为尾 } //后续尾插 else { newtail->next = l2; newtail = newtail->next; //尾后移 } l2 = l2->next; } //跳出循环,代表有链表走到了空 if(l1) { newtail->next = l1; } if(l2) { newtail->next = l2; } } return newhead; }
--前面进行的单独判断为空:防止后续解引用空指针。

--最后面的是判断哪个链表走到了空,直接将另一个链表链接到尾节点。
  • 改良:减少代码冗余
--发现上面的两串代码重复性很高,为什么??

        :初始时,设置的头节点、尾节点为空,导致尾插都要判断是否为空——>创建非空链表?——>可以!
typedef struct ListNode ListNode; struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) { //判断两个链表是否是空链表 if(list1 == NULL) { return list2;//直接返回list2 } if(list2 ==NULL) { return list1;//直接返回list1 } //创建非空链表:申请一份节点的空间 ListNode* newhead, *newtail; newhead = newtail = (ListNode*)malloc(sizeof(ListNode)); //两个指向指针 ListNode* l1 = list1; ListNode* l2 = list2; //遍历原链表 //先将相同长度内的节点进行比较 while(l1 != NULL && l2 != NULL) { if(l1->val < l2->val) { //l1尾插 newtail->next = l1; newtail = newtail->next; //尾后移 l1 = l1->next;//链表1后移 } else { //l2尾插 newtail->next = l2; newtail = newtail->next; //尾后移 l2 = l2->next; } //跳出循环,代表有链表走到了空 if(l1) { newtail->next = l1; } if(l2) { newtail->next = l2; } } //手动申请的空间自己要释放 //释放前,保存节点 ListNode* retnewhead = newhead->next; free(newhead); newhead = NULL; return retnewhead; }
改良:申请一份节点大小的空间,让头、尾节点直接指向这个空间(“哨兵节点”),就不需要后面的判断是否为空链表了。



--不是遇到重复代码,可以封装函数吗?

       
:没错,但只是代码的呈现形式改变了而已,实际的逻辑没有变。
--后面虽然结束后,会自动释放,但是还要保持良好的习惯。

--后面新创建的 retnewhead 指向的是 newhead 的下一个节点(不要倒在小错误上!)。

回顾:

《一招吃透链表操作:三指针反转法,面试遇到链表题再也不慌!》

《算法面试“必杀技”:双指针法高效解决数组原地操作》
结语:掌握“快慢指针”这一基础工具,就能优雅地解决“中间结点”问题,而“哨兵节点”的应用更为我们后续攻克“双向链表”等复杂题目打下了坚实基础下一题刷题之道,在于积跬步以致千里。,再见~~

Read more

【算法通关指南:数据结构与算法篇】二叉树相关算法题:1.二叉树深度 2.求先序排列

【算法通关指南:数据结构与算法篇】二叉树相关算法题:1.二叉树深度 2.求先序排列

🔥小龙报:个人主页 🎬作者简介:C++研发,嵌入式,机器人方向学习者 ❄️个人专栏:《算法通关指南》 ✨ 永远相信美好的事情即将发生 文章目录 * 前言 * 一、二叉树深度 * 2.1题目 * 2.2 算法原理 * 2.3代码 * 二、 求先序排列 * 3.1题目 * 3.2 算法原理 * 3.3代码 * 总结与每日励志 前言 本专栏聚焦算法题实战,系统讲解算法模块:以《c++编程》,《数据结构和算法》《基础算法》《算法实战》 等几个板块以题带点,讲解思路与代码实现,帮助大家快速提升代码能力ps:本章节题目分两部分,比较基础笔者只附上代码供大家参考,其他的笔者会附上自己的思考和讲解,希望和大家一起努力见证自己的算法成长 一、二叉树深度 2.

By Ne0inhk
【STL】stack/queue 底层模拟实现与典型算法场景实践

【STL】stack/queue 底层模拟实现与典型算法场景实践

前言 STL 中 stack 与 queue 本质是容器适配器,基于基础容器封装实现特定操作逻辑。本文先介绍容器适配器及二者核心概念,再手动模拟实现,最后通过几道算法题展示其应用,助力夯实 STL 设计思想与数据结构基础。 目录  ------------容器适配器------------ 1、什么是容器适配器? 2、为啥容器配置器不支持迭代器  ---------------stack--------------- 1、stack介绍 2、stack模拟实现 问题:为啥 stack 不用提供默认成员函数? ---------------queue-------------- 1、queue介绍 2、queue模拟实现 --------------算法题-------------- 1、最小栈 2、栈的压入、弹出序列 3、逆波兰表达式求值 4、用栈实现队列 5、用队列实现栈  ------------容器适配器------------ 1、什么是容器适配器? 适配器可以理解为“

By Ne0inhk
Flutter 三方库 conduit_password_hash 的鸿蒙化适配指南 - 实现企业级安全密码加盐哈希、支持 Argon2, PBKDF2 与 BCrypt 算法集成

Flutter 三方库 conduit_password_hash 的鸿蒙化适配指南 - 实现企业级安全密码加盐哈希、支持 Argon2, PBKDF2 与 BCrypt 算法集成

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 conduit_password_hash 的鸿蒙化适配指南 - 实现企业级安全密码加盐哈希、支持 Argon2, PBKDF2 与 BCrypt 算法集成 前言 在进行 Flutter for OpenHarmony 的全栈开发时,用户的账户安全是压倒一切的需求。尤其是在构建鸿蒙端侧的本地认证服务或配套的 Dart 服务端时,绝不能以明文存储密码。conduit_password_hash 是一个源自 Conduit 框架的高性能加密库,它提供了多种符合工业安全标准的哈希算法。本文将探讨如何在鸿蒙端利用该库构建牢不可破的密码保护体系。 一、原理解析 / 概念介绍 1.1 基础原理 conduit_password_hash 采用了“慢哈希(Slow

By Ne0inhk
Linux Socket编程核心:深入解析sockaddr数据结构族

Linux Socket编程核心:深入解析sockaddr数据结构族

Linux Socket编程核心:深入解析sockaddr数据结构族 * 引言:网络编程的基石 * 一、sockaddr:通用套接字地址结构 * 1.1 基本定义与设计哲学 * 1.2 为什么需要这样的设计? * 二、sockaddr家族成员详解 * 2.1 IPv4专用结构:sockaddr_in * 2.2 IPv6专用结构:sockaddr_in6 * 2.3 本地通信结构:sockaddr_un * 2.4 其他重要成员 * 三、字节序:网络编程的隐形陷阱 * 3.1 大端序 vs 小端序 * 3.2 常见错误示例 * 四、实际应用案例 * 4.1 创建TCP服务器

By Ne0inhk