链表的快慢指针(面试常考)

本文介绍了链表与数组的区别,详细讲解了单链表的快慢指针概念,以及如何利用快慢指针解决链表中判断环、找中间节点和倒数第N个节点的问题。通过快慢指针,可以在遍历链表时减少遍历次数,降低空间复杂度,提高查找效率。

1.链表和数组的区别

链表和数组都是基本的数据结构,但是二者的存储方式不同。

数组:
数组 在内存中存储,需要一块连续的内存空间,对内存的要求较高,比如需要200MB大小的存储空间,那么这200MB必须是连续的。

链表:
链表的存储不需要连续的内存空间,它是通过指针来指定相应的内存空间,也就是通过“指针”,将一组零散的内存块串联起来。所以完全不用担心连续内存空间以及扩容等问题。

链表中的每个结点,都存储了下一个结点的内存地址的指针。所以链表在声明时,不需要和数组一样连续的内存空间,但是它需要额外的空间来存储与之关联的结点指针,通常就会认为,链表比数组会消耗更多的内存空间。

但是其实在我们正常的编码过程中,链表中存储的每个结点,其实是远大于指针存储所消耗的空间,这点消耗,基本上是可以忽略不计的。

2.链表的类型

链表根据指向的不同以及每个节点存储的指针关系,可分为:

  1. 单链表:每个结点存在一个 next 指针,称为后续指针, next 指针指向后一个结点,尾结点的 next 指针,指向 NULL。
  2. 单向循环链表:和单链表类似,但是尾结点的 next 指针,指向头结点。
  3. 双向链表:在单链表的基础上,为每个结点增加一个 prev 指针,指向该结点在链表中的前驱结点。
  4. ·双向循环链表:和双向链表类似,但是头结点的 prev 指向尾结点,而尾结点的 next 指向头结点,以此形成一个环。

单链表是最基本的链表,也是面试题中最常考的链表,通过单链表可以衍生出很多的面试题,而快慢指针就是常考的一类。

3.单链表的快慢指针

对于单链表,每个结点只有一个存储下一结点的 next 指针。当我们在查找某个结点的时候,无法和数组一样通过下标快速定位到待查询的结点,只能从头结点开始,通过每个结点的 next 指针,一个个结点的匹配查找。

这就代表了单链表循环的一个特点,它在遍历上无法后悔,遍历走过了,就是过了,无法回头(当然这里可以使用遍历两次的方法,但快慢指针可以只遍历一次)。除非再用一个数据结构去记录已经遍历的结点,那就不再是一个单纯的单链表了。

这就引发出一种解决方案,就是快慢指针,一个指针无法完成任务,那就再引入一个指针。它通过两个指针 fast 和 slow 指针,让两个指针保持一定的间隔规律,达到我们查找的目的。

4.快慢指针的例子

4.1判断链表中是否存在环

题目:已知一个单链表的 head,判断当前链表是否存在环。

在链表中,如果是在头节点就已经确定是个环的时候,只需要在循环的时候,判断尾结点的 next 是否指向头结点即可。

但是如果在环的入口不确定,它可能从任意一个结点开始形成了环的情况下就不这么简单了,这里我们就可以用快慢指针来进行判断。

fast 每次走两步而 slow 每次走一步,如果单链表中存在环,fast 以两倍于 slow 的速度前进,那么两个指针,最终一定会进入环,也一定会在环中相遇。

代码:

 static boolean linkHasCircle(Node head){
   
   
 Node fast 
链表数据结构中非常基础且重要的部分,常被用于各类编程面试中。以下是链表相关的常见面试题目,涵盖删除、反转、查找中间节点、倒数第k个节点、合并有序链表、分割链表、判断回文结构、查找公共节点、判断是否有环以及环的入口节点等多个方面。 ### 删除链表中等于给定值 val 的所有节点 该问题要求删除链表中所有值为 `val` 的节点。可以通过遍历链表,将不等于 `val` 的节点连接到新链表中,最终返回新链表的头节点。例如: ```c struct ListNode* removeElements(struct ListNode* head, int val) { struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode)); dummy->next = head; struct ListNode* current = dummy; while (current->next != NULL) { if (current->next->val == val) { struct ListNode* temp = current->next; current->next = temp->next; free(temp); } else { current = current->next; } } return dummy->next; } ``` 该方法通过虚拟头节点的方式简化边界条件的处理[^1]。 ### 反转一个单链表 链表反转是经典问题,要求将链表的节点顺序反转。可以通过迭代方式实现,使用三个指针分别记录当前节点、前一个节点和下一个节点: ```c struct ListNode* reverseList(struct ListNode* head) { struct ListNode* prev = NULL; struct ListNode* current = head; while (current != NULL) { struct ListNode* next = current->next; current->next = prev; prev = current; current = next; } return prev; } ``` 该方法的时间复杂度为 O(n),空间复杂度为 O(1)[^1]。 ### 返回链表的中间结点 如果链表有奇数个节点,返回中间节点;如果有偶数个节点,返回第二个中间节点。可以使用快慢指针法,快指针每次走两步,慢指针每次走一步: ```c struct ListNode* middleNode(struct ListNode* head) { struct ListNode* slow = head; struct ListNode* fast = head; while (fast != NULL && fast->next != NULL) { slow = slow->next; fast = fast->next->next; } return slow; } ``` 这种方法避免了遍历两次链表的开销。 ### 输出链表中倒数第k个结点 该问题要求找到链表中倒数第k个节点。可以使用双指针法,先让快指针走k步,然后快慢指针一起移动,当快指针到达末尾时,慢指针指向倒数第k个节点: ```c struct ListNode* findKthToTail(struct ListNode* head, int k) { struct ListNode* fast = head; struct ListNode* slow = head; for (int i = 0; i < k; i++) { if (fast == NULL) return NULL; fast = fast->next; } while (fast != NULL) { fast = fast->next; slow = slow->next; } return slow; } ``` 这种方法避免了计算链表长度的额外遍历。 ### 合并两个有序链表 将两个有序链表合并为一个新的有序链表,并返回其头节点。可以通过创建虚拟头节点并依次比较两个链表节点的大小来实现: ```c struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) { struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode)); struct ListNode* tail = dummy; while (list1 != NULL && list2 != NULL) { if (list1->val < list2->val) { tail->next = list1; list1 = list1->next; } else { tail->next = list2; list2 = list2->next; } tail = tail->next; } if (list1 != NULL) tail->next = list1; if (list2 != NULL) tail->next = list2; return dummy->next; } ``` 该方法时间复杂度为 O(m + n),空间复杂度为 O(1)[^4]。 ### 分割链表(以x为基准) 以给定值 x 为基准将链表分割成两部分,所有小于 x 的节点排在大于或等于 x 的节点之前,且顺序不变。可以使用两个辅助链表分别存储小于x和大于等于x的节点,最后合并: ```c struct ListNode* partition(struct ListNode* head, int x) { struct ListNode less_head, greater_head; struct ListNode* less_tail = &less_head; struct ListNode* greater_tail = &greater_head; while (head != NULL) { if (head->val < x) { less_tail->next = head; less_tail = less_tail->next; } else { greater_tail->next = head; greater_tail = greater_tail->next; } head = head->next; } less_tail->next = greater_head.next; greater_tail->next = NULL; return less_head.next; } ``` 该方法避免了节点的频繁移动,提高了效率[^3]。 ### 判断链表是否回文结构 判断链表是否为回文结构,可以借助快慢指针找到中间节点,然后反转后半部分链表并与前半部分比较: ```c bool isPalindrome(struct ListNode* head) { if (head == NULL || head->next == NULL) return true; struct ListNode* slow = head; struct ListNode* fast = head; while (fast != NULL && fast->next != NULL) { slow = slow->next; fast = fast->next->next; } struct ListNode* prev = NULL; struct ListNode* curr = slow; while (curr != NULL) { struct ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } struct ListNode* left = head; struct ListNode* right = prev; while (right != NULL) { if (left->val != right->val) return false; left = left->next; right = right->next; } return true; } ``` 该方法时间复杂度为 O(n),空间复杂度为 O(1)[^2]。 ### 查找两个链表的公共节点 输入两个链表,找出它们的第一个公共节点。可以通过双指针法,让两个指针分别从两个链表头开始遍历,当其中一个指针到达末尾时,切换到另一个链表的头部继续遍历: ```c struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB) { if (headA == NULL || headB == NULL) return NULL; struct ListNode* pA = headA; struct ListNode* pB = headB; while (pA != pB) { pA = (pA == NULL) ? headB : pA->next; pB = (pB == NULL) ? headA : pB->next; } return pA; } ``` 该方法巧妙利用了两个链表长度的差异,无需额外空间[^2]。 ### 判断链表是否有环 使用快慢指针法,如果链表中存在环,则快指针会追上慢指针: ```c bool hasCycle(struct ListNode* head) { struct ListNode* slow = head; struct ListNode* fast = head; while (fast != NULL && fast->next != NULL) { slow = slow->next; fast = fast->next->next; if (slow == fast) return true; } return false; } ``` 该方法时间复杂度为 O(n),空间复杂度为 O(1)[^2]。 ### 返回链表开始入环的第一个节点 在判断链表是否有环的基础上,找到环的入口节点。当快慢指针相遇后,再引入一个指针从头节点出发,与慢指针同步移动,直到相遇: ```c struct ListNode* detectCycle(struct ListNode* head) { struct ListNode* slow = head; struct ListNode* fast = head; while (fast != NULL && fast->next != NULL) { slow = slow->next; fast = fast->next->next; if (slow == fast) { struct ListNode* entry = head; while (entry != slow) { entry = entry->next; slow = slow->next; } return entry; } } return NULL; } ``` 该方法同样无需额外空间,仅通过指针操作即可实现[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值