算法题---链表篇

本文详细解析了链表相关的算法问题,包括判断两个链表是否相交并找到第一个公共结点、在O(1)时间复杂度内删除链表节点、判断单链表是否存在环以及环的起始位置、单向链表的反转以及两两反转单向链表的方法。通过具体的代码实现,阐述了链表操作的思路和技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 判断俩个链表是否相交,并返回第一个公共结点

链表的结点定义为:

struct ListNode

{

      int       m_nKey;

      ListNode*   m_pNext;

};

分析:这是一道微软的面试题。微软非常喜欢与链表相关的题目,因此在微软的面试题中,链表出现的概率相当高。

如果两个单向链表有公共的结点,也就是说两个链表从某一结点开始,它们的m_pNext都指向同一个结点。但由于是单向链表的结点,每个结点只有一个m_pNext,因此从第一个公共结点开始,之后它们所有结点都是重合的,不可能再出现分叉。所以,两个有公共结点而部分重合的链表,拓扑形状看起来像一个Y,而不可能像X

看到这个题目,第一反应就是蛮力法:在第一链表上顺序遍历每个结点。每遍历一个结点的时候,在第二个链表上顺序遍历每个结点。如果此时两个链表上的结点是一样的,说明此时两个链表重合,于是找到了它们的公共结点。如果第一个链表的长度为m,第二个链表的长度为n,显然,该方法的时间复杂度为O(mn)

接 下来我们试着去寻找一个线性时间复杂度的算法。我们先把问题简化:如何判断两个单向链表有没有公共结点?前面已经提到,如果两个链表有一个公共结点,那么 该公共结点之后的所有结点都是重合的。那么,它们的最后一个结点必然是重合的。因此,我们判断两个链表是不是有重合的部分,只要分别遍历两个链表到最后一 个结点。如果两个尾结点是一样的,说明它们用重合;否则两个链表没有公共的结点。

在上面的思路中,顺序遍历两个链表到尾结点的时候,我们不能保证在两个链表上同时到达尾结点。这是因为两个链表不一定长度一样。但如果假设一个链表比另一个长l个结点,我们先在长的链表上遍历l个结点,之后再同步遍历,这个时候我们就能保证同时到达最后一个结点了。由于两个链表从第一个公共结点考试到链表的尾结点,这一部分是重合的。因此,它们肯定也是同时到达第一公共结点的。于是在遍历中,第一个相同的结点就是第一个公共的结点。

在这个思路中,我们先要分别遍历两个链表得到它们的长度,并求出两个长度之差。在长的链表上先遍历若干次之后,再同步遍历两个链表,知道找到相同的结点,或者一直到链表结束。此时,如果第一个链表的长度为m,第二个链表的长度为n,该方法的时间复杂度为O(m+n)

基于这个思路,我们不难写出如下的代码:

///////////////////////////////////////////////////////////////////////

// Find the first common node in the list with head pHead1 and

// the list with head pHead2

// Input: pHead1 - the head of the first list

//        pHead2 - the head of the second list

// Return: the first common node in two list. If there is no common

//         nodes, return NULL

///////////////////////////////////////////////////////////////////////

ListNodeFindFirstCommonNodeListNode *pHead1ListNode *pHead2)

{

      // Get the length of two lists

      unsigned int nLength1 = ListLength(pHead1);

      unsigned int nLength2 = ListLength(pHead2);

      int nLengthDif = nLength1 - nLength2;

      // Get the longer list

      ListNode *pListHeadLong = pHead1;

      ListNode *pListHeadShort = pHead2;

      if(nLength2 > nLength1)

      {

            pListHeadLong = pHead2;

            pListHeadShort = pHead1;

            nLengthDif = nLength2 - nLength1;

      }

      // Move on the longer list

      for(int i = 0; i < nLengthDif; ++ i)

            pListHeadLong = pListHeadLong->m_pNext;

      // Move on both lists

      while((pListHeadLong != NULL) &&

            (pListHeadShort != NULL) &&

            (pListHeadLong != pListHeadShort))

      {

            pListHeadLong = pListHeadLong->m_pNext;

            pListHeadShort = pListHeadShort->m_pNext;

      }

      // Get the first common node in two lists

      ListNode *pFisrtCommonNode = NULL;

      if(pListHeadLong == pListHeadShort)

            pFisrtCommonNode = pListHeadLong;

      return pFisrtCommonNode;

}

///////////////////////////////////////////////////////////////////////

// Get the length of list with head pHead

// Input: pHead - the head of list

// Return: the length of list

///////////////////////////////////////////////////////////////////////

unsigned int ListLength(ListNodepHead)

{

      unsigned int nLength = 0;

      ListNodepNode = pHead;

      while(pNode != NULL)

      {

            ++ nLength;

            pNode = pNode->m_pNext;

      }

      return nLength;

}

PS:两个有公共结点而部分重合的链表,拓扑形状看起来像一个Y,而不可能像X。即从公共节点开始之后的所有都相同!求出链表长度差,遍历差数目长的链表,之后同步遍历两链表,第一个相同的节点即是结果。

二 删除链表结点(时间复杂度为O(1)))

题目:给定链表的头指针和一个结点指针,在O(1)时间删除该结点。链表结点的定义如下:
struct ListNode
{
      int        m_nKey;
      ListNode*  m_pNext;
};
函数的声明如下:
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted);
分析:这是一道广为流传的Google面试题,能有效考察我们的编程基本功,还能考察我们的反应速度,更重要的是,还能考察我们对时间复杂度的理解。
   在链表中删除一个结点,最常规的做法是从链表的头结点开始,顺序查找要删除的结点,找到之后再删除。由于需要顺序查找,时间复杂度自然就是O(n) 了。我们之所以需要从头结点开始查找要删除的结点,是因为我们需要得到要删除的结点的前面一个结点。我们试着换一种思路。

    我们可以从给定的结点得到它的下一个结点。这个时候我们实际删除的是它的下一个结点,由于我们已经得到实际删除的结点的前面一个结点,因此完全是可以实现的。当然,在删除之前,我们需要需要把给定的结点的下一个结点的数据拷贝到给定的结点中。此时,时间复杂度为O(1)。
   上面的思路还有一个问题:如果删除的结点位于链表的尾部,没有下一个结点,怎么办?我们仍然从链表的头结点开始,顺序遍历得到给定结点的前序结点,并完成删除操作。这个时候时间复杂度是O(n)。
     那题目要求我们需要在O(1)时间完成删除操作,我们的算法是不是不符合要求?实际上,假设链表总共有n个结点,我们的算法在n-1总情况下时间复杂度是O(1),只有当给定的结点处于链表末尾的时候,时间复杂度为O(n)。那么平均时间复杂度[(n-1)*O(1)+O(n)]/n,仍然为O(1)。
基于前面的分析,我们不难写出下面的代码。
参考代码:
///////////////////////////////////////////////////////////////////////
// Delete a node in a list
// Input: pListHead - the head of list
//        pToBeDeleted - the node to be deleted

///////////////////////////////////////////////////////////////////////
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted)
{
      if(!pListHead || !pToBeDeleted)
            return;
 
      // if pToBeDeleted is not the last node in the list
      if(pToBeDeleted->m_pNext != NULL)
      {
            // copy data from the node next to pToBeDeleted
            ListNode* pNext = pToBeDeleted->m_pNext;  // 获得待删除结点的下一个结点B
            pToBeDeleted->m_nKey = pNext->m_nKey;   //  获得结点B的value值
            pToBeDeleted->m_pNext = pNext->m_pNext; // 获得结点B的下一个结点
  
            // delete the node next to the pToBeDeleted
            delete pNext;
            pNext = NULL;
      }
      // if pToBeDeleted is the last node in the list
      else
      {
            // get the node prior to pToBeDeleted
            ListNode* pNode = pListHead;
            while(pNode->m_pNext != pToBeDeleted)
            {
                  pNode = pNode->m_pNext;             
            }
 
            // deleted pToBeDeleted (此时的pNode结点是最后一个结点,固有如下:)
            pNode->m_pNext = NULL;
            delete pToBeDeleted;
            pToBeDeleted = NULL;
      }

值得注意的是,为了让代码看起来简洁一些,上面的代码基于两个假设:(1)给定的结点的确在链表中;(2)给定的要删除的结点不是链表的头结点。不考虑第一个假设对代码的鲁棒性是有影响的。至于第二个假设,当整个列表只有一个结点时,代码会有问题。但这个假设不算很过分,因为在有些链表的实现中,会创建一个虚拟的链表头,并不是一个实际的链表结点。这样要删除的结点就不可能是链表的头结点了。当然,在面试中,我们可以把这些假设和面试官交流。这样,面试官还是会觉得我们考虑问题很周到的。

PS:假设要删除A节点,A.next==B。则删除B,并将B的值赋给A,这样就等于删除了A节点。

三  判断一个单链表是否有环,如果有,找出环的起始位置 (即环的入口)

     方法1从单链表head开始,每遍历一个,就把那个node放在hashset里。走到下一个的时候,把该node放在hashset里查找,如果有相同的,就表示有环,如果走到单链表最后一个node,在hashset里都没有重复的node,就表示没有环。 这种方法需要O(n)的空间和时间。


     方法2:  设置两个指针指向单链表的head, 然后开始遍历,第一个指针走一步,第二个指针走两步,如果没有环,它们会直接走到底,如果有环,这两个指针一定会相遇。

四  反转单向链表

给一个单向链表,把它从头到尾反转过来。比如: a -> b -> c ->d 反过来就是 d -> c -> b -> a 。这里讲解两种方法:

方法1 :就是把每个Node按照顺序存入到一个stack里面,这样,最后面一个就在最上面了。然后,把每一个再取出来,这样顺序就换过来了。

[java]  view plain copy
  1. public static Node reverse(Node head) {  
  2.     Stack<Node> stack = new Stack<Node>();  
  3.       
  4.     // put all the nodes into the stack  
  5.     while (head != null) {  
  6.         stack.add(head);  
  7.         head = head.next();  
  8.     }  
  9.       
  10.     //reverse the linked list  
  11.     Node current = stack.pop();  
  12.     head = current;  
  13.     while (stack.empty() != true) {  
  14.         Node next = stack.pop();  
  15.         //set the pointer to null, so the last node will not point to the first node.  
  16.         next.setNext(null);  
  17.         current.setNext(next);  
  18.         current = next;  
  19.     }  
  20.       
  21.     return head;      
  22. }  
方法2 :就是利用两个指针,分别指向前一个节点和当前节点,每次做完当前节点和下一个节点的反转后,把两个节点往下移,直到到达最后节点。

[java]  view plain copy
  1. public static Node reverse(Node head) {  
  2.     Node previous = null;  
  3.   
  4.     while (head != null) {  
  5.         Node nextNode = head.next();  
  6.         head.setNext(previous);  
  7.         previous = head;  
  8.         head = nextNode;  
  9.     }  
  10.           
  11.     return previous;      
  12. }  

五 俩俩反转单向链表

两两反转单项链表就是把每两个数反转一次。如下:

A -> B -> C ->D -> E -> F ->G -> H -> I  两两反转后变为 B -> A -> D ->C -> F -> E ->H -> G -> I

分析:

我们需要两个“指针”指着当前要反转的两个值。两两反转后,我们还需要记录下一个的值,换句话说,我们反转 A 和 B 后, 需要记录 C 值,我们才能够不断向下走,直到到达链表末端,所以,我们还需要另一个指向下一个值的“指针”。

反转以后,A的下一个是C, 但是,实际上,A的下一个应该是D,所以,每次反转时,我们需要更新前一个值的下一个值,也就是说把 A -> C 改成 A -> D。

所以,要完成这个操作,我们总共需要4个“指针”。

代码:

[java]  view plain copy
  1. class Node {  
  2.     char value;  
  3.     Node next;    
  4. }  

[java]  view plain copy
  1. public static Node reverseInPair(Node current) {  
  2.     if (current == null || current.next == nullreturn current;  
  3.       
  4.     Node head = current.next;//save the head of the list  
  5.     Node previousNode = null;  
  6.       
  7.     while(current != null && current.next != null) {  
  8.         //get the current node's next and "nextnext" node  
  9.         Node nextNode = current.next;  
  10.         Node nextNextNode = nextNode.next;  
  11.           
  12.         //exchange the "next" node  
  13.         nextNode.next = current;  
  14.         current.next = nextNextNode;  
  15.           
  16.         //update the "next" value of the previous node   
  17.         if (previousNode != null) {  
  18.             previousNode.next = nextNode;  
  19.         }  
  20.         previousNode = current;  
  21.         current = nextNextNode;  
  22.     }  
  23.       
  24.     return head;  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值