当前位置:   article > 正文

链表算法面试问题看我就够了!

链表算法面试

640?wx_fmt=jpeg

1 引言

单链表的操作算法是笔试面试中较为常见的题目。本文将着重介绍平时面试中常见的关于链表的应用题目。

本文大概 一万五千字 ,建议阅读时间为一个小时,请先收藏再阅读,平时也可以拿出来多看几遍。

2 输出单链表倒数第 K 个节点

2.1 问题描述

题目:输入一个单链表,输出此链表中的倒数第 K 个节点。(去除头结点,节点计数从 1 开始)。

2.2 两次遍历法
2.2.1 解题思想

(1)遍历单链表,遍历同时得出链表长度 N 。

2.2.2 图解过程
640?wx_fmt=png
图 1
2.2.3 代码实现
  1. /*计算链表长度*/
  2. int listLength(ListNode* pHead){
  3.     int count = 0;
  4.     ListNode* pCur = pHead->next;
  5.     if(pCur == NULL){
  6.         printf("error");
  7.     }
  8.     while(pCur){
  9.         count++;
  10.         pCur = pCur->pNext;
  11.     }
  12.     return count;
  13. }
  14. /*查找第k个节点的值*/
  15. ListNodesearchNodeK(ListNode* pHead, int k){
  16.     int i = 0;
  17.     ListNode* pCur = pHead; 
  18.     //计算链表长度
  19.     int len = listLength(pHead);
  20.     if(k > len){
  21.         printf("error");
  22.     }
  23.     //循环len-k+1次
  24.     for(i=0; i < len-k+1; i++){
  25.         pCur  = pCur->next;
  26.     }
  27.     return pCur;//返回倒数第K个节点
  28. }    

采用这种遍历方式需要两次遍历链表,时间复杂度为O(n※2)。可见这种方式最为简单,也较好理解,但是效率低下。

2.3 递归法
2.3.1 解题思想

(1)定义num = k

2.3.2 图解过程
640?wx_fmt=png
图 2
2.3.3 代码实现
  1. int num;//定义num值
  2. ListNodefindKthTail(ListNode* pHead, int k) {
  3.         num = k;
  4.         if(pHead == NULL)
  5.             return NULL;
  6.         //递归调用
  7.         ListNode* pCur = findKthTail(pHead->next, k);
  8.         if(pCur != NULL)
  9.             return pCur;
  10.         else{
  11.             num--;// 递归返回一次,num值减1
  12.             if(num == 0)
  13.                 return pHead;//返回倒数第K个节点
  14.             return NULL;
  15.         }
  16. }

使用递归的方式实现仍然需要两次遍历链表,时间复杂度为O(n※2)。

2.4 双指针法
2.4.1 解题思想

(1)定义两个指针 p1 和 p2 分别指向链表头节点。

2.4.2 图解过程
640?wx_fmt=png
图 3
640?wx_fmt=png
图 4
2.4.3 代码实现
  1. ListNodefindKthTail(ListNode *pHead, int K){
  2.     if (NULL == pHead || K == 0)
  3.         return NULL;
  4.     //p1,p2均指向头节点
  5.     ListNode *p1 = pHead;
  6.     ListNode *p2 = pHead;
  7.     //p1先出发,前进K个节点
  8.     for (int i = 0; i < K; i++) {
  9.         if (p1)//防止k大于链表节点的个数
  10.             p1 = p1->_next;
  11.         else
  12.             return NULL;
  13.     }
  14.     while (p1)//如果p1没有到达链表结尾,则p1,p2继续遍历
  15.     {
  16.         p1 = p1->_next;
  17.         p2 = p2->_next;
  18.     }
  19.     return p2;//当p1到达末尾时,p2正好指向倒数第K个节点
  20. }

可以看出使用双指针法只需遍历链表一次,这种方法更为高效时间复杂度为O(n),通常笔试题目中要考的也是这种方法。

3 链表中存在环问题

3.1 判断链表是否有环

单链表中的环是指链表末尾的节点的 next 指针不为 NULL ,而是指向了链表中的某个节点,导致链表中出现了环形结构。

链表中有环示意图:

640?wx_fmt=png
图 5

链表的末尾节点 8 指向了链表中的节点 3,导致链表中出现了环形结构。

3.1.1 穷举比较法
3.1.1.1 解题思想

(1)遍历链表,记录已访问的节点。

这种穷举比较思想简单,但是效率过于低下,尤其是当链表节点数目较多,在进行比较时花费大量时间,时间复杂度大致在 O(n^2)。这种方法自然不是出题人的理想答案。如果笔试面试中使用这种方法,估计就要跪了,忘了这种方法吧

3.1.2 哈希缓存法

既然在穷举遍历时,元素比较过程花费大量时间,那么有什么办法可以提高比较速度呢?

3.1.2.1 解题思想

(1)首先创建一个以节点 ID 为键的 HashSe t集合,用来存储曾经遍历过的节点。

假设从链表头节点到入环点的距离是 a ,链表的环长是 r 。而每一次 HashSet 查找元素的时间复杂度是 O(1), 所以总体的时间复杂度是 1 * ( a + r ) = a + r,可以简单理解为 O(n) 。而算法的空间复杂度还是 a + r - 1,可以简单地理解成 O(n) 。

3.1.3 快慢指针法
3.1.3.1 解题思想

(1)定义两个指针分别为 slow,fast,并且将指针均指向链表头节点。

3.1.3.2 图解过程

无环过程:

640?wx_fmt=png
图 6
640?wx_fmt=png
图 7
640?wx_fmt=png
图 8

通过图解过程可以看出,若表中不存在环形,fast 与 slow 指针只能在链表末尾相遇。

有环过程:

640?wx_fmt=png
图 9
640?wx_fmt=png
图 10
640?wx_fmt=png
图 11

图解过程可以看出,若链表中存在环,则快慢指针必然能在环中相遇。这就好比在环形跑道中进行龟兔赛跑。由于兔子速度大于乌龟速度,则必然会出现兔子与乌龟再次相遇情况。因此,当出现快慢指针相等时,且二者不为NULL,则表明链表存在环。

3.1.3.3 代码实现
  1. bool isExistLoop(ListNode* pHead)  {  
  2.     ListNode* fast;//慢指针,每次前进一个节点
  3.     ListNode* slow;//快指针,每次前进2个节点 
  4.     slow = fast = pHead ;  //两个指针均指向链表头节点
  5.     //当没有到达链表结尾,则继续前进
  6.     while (slow != NULL && fast -> next != NULL)  {  
  7.         slow = slow -> next ; //慢指针前进一个节点
  8.         fast = fast -> next -> next ; //快指针前进两个节点
  9.         if (slow == fast)  //若两个指针相遇,且均不为NULL则存在环
  10.             return true ;  
  11.     }  
  12.     //到达末尾仍然没有相遇,则不存在环
  13.     return false ;  
  14. }  
3.2 定位环入口

在 3.1 节中,已经实现了链表中是否有环的判断方法。那么,当链表中存在环,如何确定环的入口节点呢?

3.2.1 解题思想

slow 指针每次前进一个节点,故 slow 与 fast 相遇时,slow 还没有遍历完整个链表。设 slow 走过节点数为 s,fast 走过节点数为 2s。设环入口点距离头节点为 a,slow 与 fast 首次相遇点距离入口点为 b,环的长度为 r。

若链表头节点到环的末尾节点度为 L,slow 与 fast 的相遇节点距离环入口节点为 X。

例如图3.1所示链表:

3.2.2 图解过程
640?wx_fmt=png
图 12
640?wx_fmt=png
图 13
3.2.3 代码实现
  1. //找到环中的相遇节点
  2. ListNodegetMeetingNode(ListNode* pHead) // 假设为带头节点的单链表
  3. {
  4.     ListNode* fast;//慢指针,每次前进一个节点
  5.     ListNode* slow;//快指针,每次前进2个节点 
  6.     slow = fast = pHead ;  //两个指针均指向链表头节点
  7.     //当没有到达链表结尾,则继续前进
  8.     while (slow != NULL && fast -> next != NULL){  
  9.         slow = slow -> next ; //慢指针前进一个节点
  10.         fast = fast -> next -> next ; //快指针前进两个节点
  11.         if (slow == fast)  //若两个指针相遇,且均不为NULL则存在环
  12.             return slow;  
  13.     }  
  14.     //到达末尾仍然没有相遇,则不存在环
  15.     return NULL ;
  16. }
  17. //找出环的入口节点
  18. ListNodegetEntryNodeOfLoop(ListNode* pHead){
  19.     ListNode* meetingNode = getMeetingNode(pHead); // 先找出环中的相遇节点
  20.     if (meetingNode == NULL)
  21.         return NULL;
  22.     ListNode* p1 = meetingNode;
  23.     ListNode* p2 = pHead;
  24.     while (p1 != p2) // p1和p2以相同的速度向前移动,当p2指向环的入口节点时,p1已经围绕着环走了n圈又回到了入口节点。
  25.     {
  26.         p1 = p1->next;
  27.         p2 = p2->next;
  28.     }
  29.     //返回入口节点
  30.     return p1;
  31. }
3.3 计算环长度
3.3.1 解题思想

在3.1中找到了 slow 与 fast 的相遇节点,令 solw 与 fast 指针从相遇节点出发,按照之前的前进规则,当 slow 与fast 再次相遇时,slow 走过的长度正好为环的长度。

3.3.2 图解过程
640?wx_fmt=png
图 14
640?wx_fmt=png
图 15
3.3.3 代码实现
  1. int getLoopLength(ListNode* head){
  2.     ListNode* slow = head;
  3.     ListNode* fast = head;
  4.     while ( fast && fast->next ){
  5.         slow = slow->next;
  6.         fast = fast->next->next;
  7.         if ( slow == fast )//第一次相遇
  8.             break;
  9.     }
  10.     //slow与fast继续前进
  11.     slow = slow->next;
  12.     fast = fast->next->next;
  13.     int length = 1;       //环长度
  14.     while ( fast != slow )//再次相遇
  15.     {
  16.         slow = slow->next;
  17.         fast = fast->next->next;
  18.         length ++;        //累加
  19.     }
  20.     //当slow与fast再次相遇,得到环长度
  21.     return length;
  22. }

4 使用链表实现大数加法

4.1 问题描述

两个用链表代表的整数,其中每个节点包含一个数字。数字存储按照在原来整数中相反的顺序,使得第一个数字位于链表的开头。写出一个函数将两个整数相加,用链表形式返回和。

例如:

4.2 代码实现
  1. ListNodenumberAddAsList(ListNode* l1, ListNode* l2) {
  2.         ListNode *ret = l1, *pre = l1;
  3.         int up = 0;
  4.         while (l1 != NULL && l2 != NULL) {
  5.             //数值相加
  6.             l1->val = l1->val + l2->val + up;
  7.             //计算是否有进位
  8.             up = l1->val / 10;
  9.             //保留计算结果的个位
  10.             l1->val %= 10;
  11.             //记录当前节点位置
  12.             pre = l1;
  13.             //同时向后移位
  14.             l1 = l1->next;
  15.             l2 = l2->next;
  16.         }
  17.         //若l1到达末尾,说明l1长度小于l2
  18.         if (l1 == NULL)
  19.             //pre->next指向l2的当前位置
  20.             pre->next = l2;
  21.         //l1指针指向l2节点当前位置
  22.         l1 = pre->next;
  23.         //继续计算剩余节点
  24.         while (l1 != NULL) {
  25.             l1->val = l1->val + up;
  26.             up = l1->val / 10;
  27.             l1->val %= 10;
  28.             pre = l1;
  29.             l1 = l1->next;
  30.         }
  31.         //最高位计算有进位,则新建一个节点保留最高位
  32.         if (up != 0) {
  33.             ListNode *tmp = new ListNode(up);
  34.             pre->next = tmp;
  35.         }
  36.         //返回计算结果链表
  37.         return ret;
  38. }

5 有序链表合并

5.1 问题描述

题目:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:

5.2 一般方案
5.2.1 解题思想

(1)对空链表存在的情况进行处理,假如 pHead1 为空则返回 pHead2 ,pHead2 为空则返回 pHead1。(两个都为空此情况在pHead1为空已经被拦截)

5.2.2 代码实现
  1. ListNodemergeTwoOrderedLists(ListNode* pHead1, ListNode* pHead2){
  2.     ListNode* pTail = NULL;//指向新链表的最后一个结点 pTail->next去连接
  3.     ListNode* newHead = NULL;//指向合并后链表第一个结点
  4.     if (NULL == pHead1){
  5.         return pHead2;
  6.     }else if(NULL == pHead2){
  7.         return pHead1;
  8.     }else{
  9.         //确定头指针
  10.         if ( pHead1->data < pHead2->data){
  11.             newHead = pHead1;
  12.             pHead1 = pHead1->next;//指向链表的第二个结点
  13.         }else{
  14.             newHead = pHead2;
  15.             pHead2 = pHead2->next;
  16.         }
  17.         pTail = newHead;//指向第一个结点
  18.         while ( pHead1 && pHead2) {
  19.             if ( pHead1->data <= pHead2->data ){
  20.                 pTail->next = pHead1;  
  21.                 pHead1 = pHead1->next;
  22.             }else {
  23.                 pTail->next = pHead2;
  24.                 pHead2 = pHead2->next;
  25.             }
  26.             pTail = pTail->next;
  27.         }
  28.         if(NULL == pHead1){
  29.             pTail->next = pHead2;
  30.         }else if(NULL == pHead2){
  31.             pTail->next = pHead1;
  32.         }
  33.         return newHead;
  34. }
5.3 递归方案
5.3.1 解题思想

(1)对空链表存在的情况进行处理,假如 pHead1 为空则返回 pHead2 ,pHead2 为空则返回 pHead1。

5.3.2 代码实现
  1. ListNodemergeTwoOrderedLists(ListNode* pHead1, ListNode* pHead2){
  2.     ListNode* newHead = NULL;
  3.     if (NULL == pHead1){
  4.         return pHead2;
  5.     }else if(NULL ==pHead2){
  6.         return pHead2;
  7.     }else{
  8.         if (pHead1->data < pHead2->data){
  9.             newHead = pHead1;
  10.             newHead->next = mergeTwoOrderedLists(pHead1->next, pHead2);
  11.         }else{
  12.             newHead = pHead2;
  13.             newHead->next = mergeTwoOrderedLists(pHead1, pHead2->next);
  14.          }
  15.         return newHead;
  16.     }   
  17. }

6 删除链表中节点,要求时间复杂度为O(1)

6.1 问题描述

给定一个单链表中的表头和一个等待被删除的节点。请在 O(1) 时间复杂度删除该链表节点。并在删除该节点后,返回表头。

示例:

6.2 解题思想

在之前介绍的单链表删除节点中,最普通的方法就是遍历链表,复杂度为O(n)。

示例

如果删除的节点的是头节点,把头结点指向 NULL。

6.3 图解过程
640?wx_fmt=png
图 16
6.4 代码实现
  1. void deleteNode(ListNode **pHead, ListNode* pDelNode) {
  2.         if(pDelNode == NULL)
  3.             return;
  4.         if(pDelNode->next != NULL){
  5.             ListNode *pNext = pDelNode->next;
  6.             //下一个节点值赋给待删除节点
  7.             pDelNode->val   =  pNext->val;
  8.             //待删除节点指针指后面第二个节点
  9.             pDelNode->next  = pNext->next;
  10.             //删除待删除节点的下一个节点
  11.             delete pNext;
  12.             pNext = NULL;
  13.         }else if(*pHead == pDelNode)//删除的节点是头节点
  14.          {
  15.             delete pDelNode;
  16.             pDelNode= NULL;
  17.             *pHead = NULL;
  18.         } else//删除的是尾节点
  19.         {
  20.             ListNode *pNode = *pHead;
  21.             while(pNode->next != pDelNode) {
  22.                 pNode = pNode->next;
  23.             }
  24.             pNode->next = NULL;
  25.             delete pDelNode;
  26.             pDelNode= NULL;
  27.         }
  28.     }

7 从尾到头打印链表

7.1 问题描述

输入一个链表,按链表值从尾到头的顺序返回一个 ArrayList 。

7.2 解法

初看题目意思就是输出的时候链表尾部的元素放在前面,链表头部的元素放在后面。这不就是 先进后出,后进先出 么。

什么数据结构符合这个要求?

640?wx_fmt=gif
动画 2
7.2.1 代码实现
  1. class Solution {
  2. public:
  3.     vector<int> printListFromTailToHead(ListNode* head) {
  4.         vector<int> value;
  5.         ListNode *p=NULL;
  6.         p=head;
  7.         stack<int> stk;
  8.         while(p!=NULL){
  9.             stk.push(p->val);
  10.             p=p->next;
  11.         }
  12.         while(!stk.empty()){
  13.             value.push_back(stk.top());
  14.             stk.pop();
  15.         }
  16.         return value;
  17.     }
  18. };
7.3 解法二

第二种方法也比较容易想到,通过链表的构造,如果将末尾的节点存储之后,剩余的链表处理方式还是不变,所以可以使用递归的形式进行处理。

7.3.1 代码实现
  1. class Solution {
  2. public:
  3.     vector<int> value;
  4.     vector<int> printListFromTailToHead(ListNode* head) {
  5.         ListNode *p=NULL;
  6.         p=head;
  7.         if(p!=NULL){
  8.             if(p->next!=NULL){
  9.                 printListFromTailToHead(p->next);
  10.             }
  11.             value.push_back(p->val);
  12.         }
  13.         return value;
  14.     }
  15. };

8 反转链表

8.1 题目描述

反转一个单链表。

示例:

  1. 输入: 1->2->3->4->5->NULL
  2. 输出: 5->4->3->2->1->NULL

进阶:

8.2 解题思路

设置三个节点precurnext

  • (1)每次查看cur节点是否为NULL,如果是,则结束循环,获得结果

  • (2)如果cur节点不是为NULL,则先设置临时变量nextcur的下一个节点

  • (3)让cur的下一个节点变成指向pre,而后pre移动curcur移动到next

  • (4)重复(1)(2)(3)


8.3 动画演示

8.4 代码实现
8.4.1 迭代方式
  1. class Solution {
  2. public:
  3.     ListNodereverseList(ListNode* head) {
  4.         ListNode* pre = NULL;
  5.         ListNode* cur = head;
  6.         while(cur != NULL){
  7.             ListNode* next = cur->next;
  8.             cur->next = pre;
  9.             pre = cur;
  10.             cur = next;
  11.         }
  12.         return pre;
  13.     }
  14. };
8.4.2 递归的方式处理
  1. class Solution {
  2. public:
  3.     ListNodereverseList(ListNode* head) {
  4.         // 递归终止条件
  5.         if(head == NULL || head->next == NULL)
  6.             return head;
  7.         ListNode* rhead = reverseList(head->next);
  8.         // head->next此刻指向head后面的链表的尾节点
  9.         // head->next->next = head把head节点放在了尾部
  10.         head->next->next = head;
  11.         head->next = NULL;
  12.         return rhead;
  13.     }
  14. };

End


今日问题:


通过本文的学习,以后遇到有关链表的面试问题我们有哪些思路去解决?

打卡格式:

打卡 X 天,答:xxx 。

640?wx_fmt=png

欢迎关注这个程序员?

如果文章有帮助,点个“好看”吧!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/629012
推荐阅读
相关标签
  

闽ICP备14008679号