常见链表面试题总结

1.  从无序链表中删除重复元素      来源 Cracking the Code Interview (2nd Edition) Ch2 Q1.

【解答】:题目已经明确说明是无序列表,对这类面试题,一般最差的复杂度是 O(NlogN),也就是需要一次排序过程,所以给出的答案最好要优于O(NlogN)。

方法一:除去重复元素,首先想到利用hash。将List迭代一遍,每拿出一个元素,放到hashtable里,并判断是否重复,如果重复则从List中删除该元素。这个算法的时间复杂度是O(N),但需要O(N)的空间建立hashtable去存放元素。

方法二:利用runner pointer. 这个技巧也是解答LinkedList Problem的重要技巧之一。迭代当前链表,每次迭代利用一个runner去访问之后的元素,删除重复元素。算是naive approach,时间复杂度O(N^2),空间复杂度O(1)。面试中最先可能想到的应该是这种解答,然后可以优化到第一种方法。

2. 给定单链表,检测是否有环。如果有环,则求出进入环的第一个节点。

         判断单向链表是否有环,可以采用快指针与慢指针的方式来解决。即定义一个快指针fast和一个慢指针slow,使得fast每次跳跃两个节点,slow每次跳跃一个节点。如果链表没有环的话,则slow与fast永远不会相遇(这里链表至少有两个节点);如果有环,则fast与slow将会在环中相遇。判断出链表有环以后,则需要算出进入环的第一个节点。在fast与slow第一次相遇后,设置一个节点pNode从链表的头部开始遍历,每次只遍历一个节点。这样,当fast与slow再次相遇时,pNode所处的位置便是环的首部。以下该问题的实现源码(C语言描述):

[cpp]  view plain copy
  1. LNode* GetLoopNode(LNode* head)  
  2. {  
  3.     //前置条件的判断  
  4.     if (!head)  
  5.     {  
  6.         return NULL;  
  7.     }  
  8.   
  9.     //定义一个快指针和一个慢指针  
  10.     LNode* fast = head;  
  11.     LNode* slow = head;  
  12.     while (fast && (fast->next))  
  13.     {  
  14.         fast = fast->next->next;  
  15.         slow = slow->next;  
  16.   
  17.         if (fast == slow)  
  18.         {  
  19.             //如果有环,则返回环的第一个节点  
  20.             slow = head;  
  21.             while (1)  
  22.             {  
  23.                 fast = fast->next;  
  24.                 slow = slow->next;  
  25.   
  26.                 if (fast == slow)  
  27.                 {  
  28.                     break;  
  29.                 }  
  30.             }  
  31.   
  32.             return slow;  
  33.         }  
  34.     }  
  35.   
  36.     return NULL;  
  37. }  

 

3. 给定两个单链表,检测两个链表是否有交点,如果有返回第一个交点。

        检测两个单链表是否有交点是很容易的,因为如果两个链表有交点,那么这两个链表的最后一个节点必须相同,所以只需比较最后一个节点即可。但是这种方案是无法求出第一个交点的,所以需要另辟蹊径。另外,判断是否有交点可以转换成链表是否有环的问题。让一个链表的最后一个节点指向第二个链表的头结点,这样的话,如果相交,则新的链表是存在环的,并且交点便是环的入口点。

ListNode* GetFirstCommonNode(ListNode * pHead1, ListNode * pHead2)  
{  
    if(pHead1 == NULL || pHead2 == NULL)  
        return NULL;  
  
    int len1 = 1;  
    ListNode * pTail1 = pHead1;  
    while(pTail1->m_pNext != NULL)  
    {  
        pTail1 = pTail1->m_pNext;  
        len1++;  
    }  
  
    int len2 = 1;  
    ListNode * pTail2 = pHead2;  
    while(pTail2->m_pNext != NULL)  
    {  
        pTail2 = pTail2->m_pNext;  
        len2++;  
    }  
  
    if(pTail1 != pTail2) // 不相交直接返回NULL  
        return NULL;  
  
    ListNode * pNode1 = pHead1;  
    ListNode * pNode2 = pHead2;  
        // 先对齐两个链表的当前结点,使之到尾节点的距离相等  
    if(len1 > len2)  
    {  
        int k = len1 - len2;  
        while(k--)  
            pNode1 = pNode1->m_pNext;  
    }  
    else  
    {  
        int k = len2 - len1;  
        while(k--)  
            pNode2 = pNode2->m_pNext;  
    }  
    while(pNode1 != pNode2)  
    {  
        pNode1 = pNode1->m_pNext;  
        pNode2 = pNode2->m_pNext;  
    }  
        return pNode1;  
}


4. 只给定单链表中某个结点p(并非最后一个结点,即p->next!=NULL)指针,删除该结点。

        由于没有给定链表的头结点,所以无法获取需要删除节点的前驱结点,这样就无法使用常规的方法去解决。在《编程之美》这本书里面给出一个非常精妙的解法,也就是采用了狸猫换太子的思想。以下是实现源码:

[cpp]  view plain copy
  1. void DeleteNode(LNode* pNode)  
  2. {  
  3.     //如果pNode是最后一个节点,则该问题误解  
  4.     if (!pNode || !(pNode->next))  
  5.     {  
  6.         return;  
  7.     }  
  8.   
  9.     //获取pNode的后驱节点  
  10.     LNode* qNode = pNode->next;  
  11.   
  12.     //将qNode的值赋给pNode,并删除qNode  
  13.     pNode->data = qNode->data;  
  14.     pNode->next = qNode->next;  
  15.     free(qNode);  
  16. }  

 5. 给定单链表头结点,删除链表中倒数第k个结点

        这个问题的关键是需要获取倒数第k个节点。如何获取呢?这里,需要两个指针p和q,p指向头结点,q指向距离头结点为k的节点。这样p和q每次遍历一个节点,当q遍历到末尾的时候,p指向的节点即为倒数第k个节点。然后删除即可。以下是实现源码(这里获取的是倒数第k+1个节点,因为需要删除的是倒数第k个节点):

[cpp]  view plain copy
  1. void DelLastKNode(unsigned int k, LNode* head)  
  2. {  
  3.     //前置条件判断  
  4.     if (!head || k <= 0)  
  5.     {  
  6.         return;  
  7.     }  
  8.   
  9.     LNode* pNode = head;  
  10.     LNode* qNode = head;  
  11.   
  12.     //将qNode遍历到正数第k + 1个节点  
  13.     unsigned int i;   
  14.     for (i = 0; i < k; i++)  
  15.     {  
  16.         qNode = qNode->next;  
  17.     }  
  18.   
  19.     //获取倒数第k + 1个节点  
  20.     while (qNode->next)  
  21.     {  
  22.         pNode = pNode->next;  
  23.         qNode = qNode->next;  
  24.     }  
  25.   
  26.     //删除倒数第k个节点  
  27.     LNode* temp = pNode->next;  
  28.     pNode->next = temp->next;  
  29.     free(temp);  
  30. }  

 

6.  判断链表是不是回文链表。来源 Cracking the Code Interview (2nd Edition) Ch2 Q7.

【解答】:经典面试题了。回文就是 0-->1-->2-->1-->0 奇数个节点, 或者 0-->1-->2-->2-->1-->0 偶数个节点。

方法一:很自然的想法就是把链表反转,然后对比反转以后的链表和原链表的节点值,并且只要比较反转链表和原链表一半的元素就可以。

方法二:利用runner pointer。很多的题目都可以利用这个方法来解决。Runner pointer和递归是解决链表问题最常用的两个方法。

定义两个指针,slow runner和fast runner,fast以2倍于slow的速度向链表尾部移动。如有奇数个节点:当fast到达链表尾部时,slow恰好到达链表中间。如有偶数个节点:fast第一次为null时,slow恰好完成了一半节点的访问。把slow访问过的节点元素值压入一个stack里。此后slow继续向链表尾部访问,同时stack里的元素开始出栈(奇数个元素的情况下要跳过中间元素),比较两个元素值,一旦出现不等,则不是回文,否则是。该解法的时间复杂度是O(N),因为需要一次迭代,同时需要O(N)的空间用作stack来存放链表元素。

7.反转单链表

#include <iostream>
using namespace std;
struct Node
{
    int data;
    Node* next;
}*head;

// 非递归写法
Node* InverseLinkedList(Node* head)
{
    if(head == NULL)
        return NULL;
    Node* newHead = NULL;
    while(head != NULL)
    {
        Node* nextNode = head->next;
        head->next = newHead;
        newHead = head;
        head = nextNode;
    }
    return newHead;
}

// 递归写法
Node* InverseLinkedListRecur(Node* head)
{
    if(head == NULL)
        return NULL;
    if(head->next == NULL)
        return head;
    Node* newHead = InverseLinkedListRecur(head->next);
    Node *tmp = newHead;
    while(tmp->next != NULL)
    {
        tmp = tmp->next;
    }
    tmp->next = head;
    head->next = NULL;
    return newHead;
}

8.链表排序

 链表排序最好使用归并排序算法。堆排序、快速排序这些在数组排序时性能非常好的算法,在链表只能“顺序访问”的魔咒下无法施展能力;但是归并排序却如鱼得水,非但保持了它O(nlogn)的时间复杂度,而且它在数组排序中广受诟病的空间复杂度在链表排序中也从O(n)降到了O(1)。真是好得不得了啊,哈哈。以上程序是递推法的程序,另外值得一说的是看看那个时间复杂度,是不是有点眼熟?对!这就是分治法的时间复杂度,归并排序又是divide and conquer。 

9. 从尾到头打印单链表

对于这种颠倒顺序的问题,我们应该就会想到栈,后进先出。所以,这一题要么自己使用栈,要么让系统使用栈,也就是递归。注意链表为空的情况。时间复杂度为O(n)。参考代码如下:

自己使用栈: 

 

1 // 从尾到头打印链表,使用栈  
 2 void RPrintList(ListNode * pHead)  
 3 {  
 4     std::stack<ListNode *> s;  
 5     ListNode * pNode = pHead;  
 6     while(pNode != NULL)  
 7     {  
 8         s.push(pNode);  
 9         pNode = pNode->m_pNext;  
10     }  
11     while(!s.empty())  
12     {  
13         pNode = s.top();  
14         printf("%d\t", pNode->m_nKey);  
15         s.pop();  
16     }  
17 }  

使用递归函数:

1 // 从尾到头打印链表,使用递归  
 2 void RPrintList(ListNode * pHead)  
 3 {  
 4     if(pHead == NULL)  
 5     {  
 6         return;  
 7     }  
 8     else  
 9     {  
10         RPrintList(pHead->m_pNext);  
11         printf("%d\t", pHead->m_nKey);  
12     }  
13 }  

 

10. 已知两个单链表pHead1 和pHead2 各自有序,把它们合并成一个链表依然有序

这个类似归并排序。尤其注意两个链表都为空,和其中一个为空时的情况。只需要O(1)的空间。时间复杂度为O(max(len1, len2))。参考代码如下:

1 // 合并两个有序链表  
 2 ListNode * MergeSortedList(ListNode * pHead1, ListNode * pHead2)  
 3 {  
 4     if(pHead1 == NULL)  
 5         return pHead2;  
 6     if(pHead2 == NULL)  
 7         return pHead1;  
 8     ListNode * pHeadMerged = NULL;  
 9     if(pHead1->m_nKey < pHead2->m_nKey)  
10     {  
11         pHeadMerged = pHead1;  
12         pHeadMerged->m_pNext = NULL;  
13         pHead1 = pHead1->m_pNext;  
14     }  
15     else  
16     {  
17         pHeadMerged = pHead2;  
18         pHeadMerged->m_pNext = NULL;  
19         pHead2 = pHead2->m_pNext;  
20     }  
21     ListNode * pTemp = pHeadMerged;  
22     while(pHead1 != NULL && pHead2 != NULL)  
23     {  
24         if(pHead1->m_nKey < pHead2->m_nKey)  
25         {  
26             pTemp->m_pNext = pHead1;  
27             pHead1 = pHead1->m_pNext;  
28             pTemp = pTemp->m_pNext;  
29             pTemp->m_pNext = NULL;  
30         }  
31         else  
32         {  
33             pTemp->m_pNext = pHead2;  
34             pHead2 = pHead2->m_pNext;  
35             pTemp = pTemp->m_pNext;  
36             pTemp->m_pNext = NULL;  
37         }  
38     }  
39     if(pHead1 != NULL)  
40         pTemp->m_pNext = pHead1;  
41     else if(pHead2 != NULL)  
42         pTemp->m_pNext = pHead2;  
43     return pHeadMerged;  
44 } 


也有如下递归解法:

1 ListNode * MergeSortedList(ListNode * pHead1, ListNode * pHead2)  
 2 {  
 3     if(pHead1 == NULL)  
 4         return pHead2;  
 5     if(pHead2 == NULL)  
 6         return pHead1;  
 7     ListNode * pHeadMerged = NULL;  
 8     if(pHead1->m_nKey < pHead2->m_nKey)  
 9     {  
10         pHeadMerged = pHead1;  
11         pHeadMerged->m_pNext = MergeSortedList(pHead1->m_pNext, pHead2);  
12     }  
13     else  
14     {  
15         pHeadMerged = pHead2;  
16         pHeadMerged->m_pNext = MergeSortedList(pHead1, pHead2->m_pNext);  
17     }  
18     return pHeadMerged;  
19 }  


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值