演算法問題總結-連結串列相關

佑闖發表於2015-04-29

作者:YouChuang

本文主要是對劍指offer及平時遇到的連結串列相關的題目做了部分總結



刪除連結串列結點

O(1)時間內刪除連結串列指定結點

給定指定連結串列的頭結點和目標結點,在O(1)的時間內刪除掉該目標結點

  • 思路
    一般思路:
    從頭結點遍歷連結串列,找到指定結點然後刪除,但是這樣會帶來O(n)的時間開銷,不滿足題意。存在的問題是查詢都是從頭結點開始,時間過長。
    優化思路:
    抽象認識問題,刪除結點其實本質上就是刪除該結點中存放的資料,另外刪除結點的本質方法就是找到該結點的前一個結點然後實施刪除。
    基於這兩種認識,提出將當前結點的下一個結點作為目標結點,將兩者的資料交換,就可以得到前一個結點(即之前的目標結點)

  • 邊界條件
    針對目標結點為尾結點的情況,我們無法找到下一個結點來刪除,所以就採用傳統的方法來進行刪除,總體的時間複雜度為((n-1) * 1 + n * 1) / n,還是O(1)的複雜度

刪除重複結點

在一個排序的連結串列中,存在重複的結點,請刪除該連結串列中重複的結點,重複的結點不保留,返回連結串列頭指標。 例如,連結串列1->2->3->3->4->4->5 處理後為 1->2->5

  • 思路
    特別注意,不是隻刪除掉重複的結點,而是隻要該結點出現重複,則所有相關結點都刪掉(包括源結點)
  • 邊界條件
    連結串列為空或只有一個結點
    所有結點都為重複(重複同一個資料或者多種重複資料)
    包括頭結點在內的前幾個結點發生重複
/*
 public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
  ListNode deleteDuplication(ListNode pHead)
  {
        /*
        work、pre雙指標
        */
        if(pHead == null)
            return pHead;

        //用來標記是否出現重複結點
        int tag = 0;
        ListNode ppre, pwork, pnext;
        ppre = null;
        pwork = pHead;
        pnext = pwork.next;

        while(pnext != null)
        {
            if(pwork.val == pnext.val)
            {
                tag = 1;
                pwork.next = pnext.next;
                pnext.next = null;
                pnext = pwork.next;
            }
            else
            {
                if(tag == 1)
                {
                    //說明之前出現了重複結點,如果是在頭結點就出現這種情況需要小心處理
                   if(pwork == pHead)
                    {
                        pwork = pnext;
                        pnext = pnext.next;
                        pHead = pwork;
                    }
                    else
                    {
                        ppre.next = pnext;
                        pwork = pnext;
                        pnext = pnext.next;                        
                    }
                tag = 0;
                }
                else
                {                
                    ppre = pwork;
                    pwork = pnext;
                    pnext = pnext.next;
              }
            }
        }

        //如果最後部分全部重複,整個連結串列都是重複資料,則全部刪除處理
        if(tag == 1)
        {
            if(pHead == pwork)
            {
                pHead = pnext;            
            }
            else
            {
                ppre.next = pnext;
                pwork.next = null;
            }
        }
        return pHead;
  }
}


查詢特定節點

倒數第k個結點

輸入一個連結串列,輸出該連結串列中倒數第k個結點。

  • 思路
    常規思路,兩個工作指標,一前一後出發
  • 邊界條件
    計數k與結點為null兩個條件的控制
/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
  public ListNode FindKthToTail(ListNode head,int k) {

        if((head == null) || (k == 0))
            return null;

        /* 前者先走的步數 */
        int num = k; 
        ListNode ppre;
        ListNode pwork;
        ppre = pwork = head;

        /* pwork先走k步 */
        while((num > 1) && (pwork != null)){
            pwork = pwork.next;
            num--;
        }

        /* k若超過了連結串列的範圍 */
        if(pwork == null){
           return null;
        }

        /* 同時後移 */ 
        while(pwork.next != null){
            pwork = pwork.next;
            ppre = ppre.next;
        }
        return ppre;

    }
}

查詢中間結點

思路:跟上面的類似,只不過兩個工作指標同時出發,步幅不同



環與相交

判斷是否存在環

思路:
若是連結串列存在環的話,則兩個速度不同的工作指標,肯定會在環中相遇。
設定兩個工作指標,同時出發,步幅不同,兩者相遇則證明有環,否則會遍歷結束
具體的實現在下面問題中有程式碼

查詢環的入口

一個連結串列中包含環,請找出該連結串列的環的入口結點。

  • 思路
    經典問題,相關問題還有幾個,稍後補上,再總結一下
    需要數學推導,簡單說下過程:
    假設非環連結串列長度為K(長度即為結點個數,非環連結串列不包含環的入口結點),環的長度為L,兩個工作指標ppre、pnext,ppre的速度為spre,pnext的速度為snext;
    假設兩個指標同時從頭結點(不計入長度之內)出發,向前走了x次,即ppre走了spre*x步,pnext走了spnext*x步,則兩者在環中相交時(這裡有兩個問題,判斷連結串列是否有環以及預設兩個結點是在環中的第一圈即相遇,稍後解決)
    (ppre*x - K) % L = (pnext*x - K) % L
    ((pnext - ppre) * X) % L = 0
    假設pnext的速度為2,ppre為1
    則X % L = 0,即X = L
    則兩者相交的地方在X-K=L-K的地方,即環中距離環入口處L-K個位置的地方
    這時可以看出相交處再次到達環入口的距離為K,與連結串列頭結點到環入口的距離相同
    稍後補上圖片來做說明,可能會更好理解一下
    則本題目的思路也就出來了
    先讓兩個指標ppre、pnext分別以1、2的速度同時遍歷連結串列,知道兩者相遇
    然後ppre指標重回頭結點,pnext不變,接著ppre和pnext同時以速度1繼續遍歷,直到再次相遇,則相遇的地方即為環的入口結點

以下圖為例
K為3,L為7,則X=L=7,即ppre指標向前走了7步,pnext向前走了14步,兩者第一次相聚在X-K=4,距離入口4步的位置,然後再繼續前行3步即可再次達到環的入口處

這裡寫圖片描述

  • 邊界條件
    連結串列本身即為環
/*
 public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {

    ListNode EntryNodeOfLoop(ListNode pHead)
    {

        if(pHead == null || pHead.next == null)
            return null;

        ListNode pwork = pHead;
        ListNode pa, pb;
        pa = pb = pHead;

        /** 找到兩個結點在環中相遇的結點 */
        while(pa != null && pa.next != null)
        {
            pa = pa.next;
            pb = pb.next.next;

            if(pa == pb)
            {
                //若是相遇,則跳出迴圈繼續處理
                break;
            }
        }

        //若兩者在環的入口處相遇,則連結串列本身為環
        if(pa == pHead)
            return pa;

        if(pa != null)
        {
          pa = pHead;
            while(pa != null && pa.next != null)
            {
                pa = pa.next;
                pb = pb.next;

               if(pa == pb)
               {
                  //若是相遇,則跳出迴圈繼續處理
                  return pa;
              }
            }
        }
        return pa;
    }
}

兩個連結串列是否相交

兩個單連結串列,判斷是否相交

  • 思路
    首先明確一點,若是兩個連結串列相交,則兩個連結串列的尾結點必定相同,頭結點不同
    最基本思路,對連結串列1中的每一個結點遍歷連結串列2
    優化思路一,轉化成環的問題,將連結串列2的頭部接在連結串列1後面,若是兩個連結串列相交,則連結串列2變成環,直接判斷連結串列2是否為滿環即可(遍歷一遍是否回到起點)
    優化思路二,因為兩個相交連結串列的尾結點必定相同,則直接分別找到兩個連結串列的尾結點,然後比較即可

如果連結串列中可能有環,判斷是否相交

  • 思路
    若是有環的連結串列相交,由於相交連結串列尾結點必定相同,則兩個連結串列的環必定重合
    那麼就可以直接判斷連結串列一中環的入口結點是否在另一個連結串列上出現即可。

相交的第一個結點

兩個單連結串列

  • 思路
    基於優化思路一,則找到新的帶環連結串列的環入口結點即為相交的第一個結點
    基於優化思路二,則需先確定兩個連結串列長度,然後基於短連結串列,遍歷長連結串列結點到相同的位置,然後兩個連結串列同時遍歷並比較結點是否相同,直至末尾,程式碼見下方
/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {   
        /** 
         * 邊界條件處理,其中一個連結串列為空或者只有一個結點則不成立,兩個連結串列頭部不能重合,不然就完全重合
         * 連結串列1的末尾指標、工作指標,連結串列2的末尾、工作指標
         */
        ListNode pwork1, pwork2, pwork;
        pwork1 = pHead1;
        pwork2 = pHead2;

        int len1 = 0, len2 = 0, tmp = 0;

        /** 獲取連結串列長度,比較,然後遍歷對齊 */
        while(pwork1 != null){
            len1++;
            pwork1 = pwork1.next;
        }
        while(pwork2 != null){
            len2++;
            pwork2 = pwork2.next;
        }
        pwork1 = pHead1;
        pwork2 = pHead2;
        tmp = len1 - len2;
        if(tmp > 0){
            while(tmp > 0){
                pwork1 = pwork1.next;
                tmp--;
            }
        }else if(tmp < 0){
            while(tmp < 0){
                pwork2 = pwork2.next;
                tmp++;
            }
        }

        /** 開始遍歷並判斷 */
        while((pwork1 != pwork2) && (pwork1 != null)){
            pwork1 = pwork1.next;
            pwork2 = pwork2.next;
        }

        return pwork1;

    }

}


改變連結串列順序

反轉連結串列

輸入一個連結串列,反轉連結串列後,輸出反轉連結串列後頭節點

  • 思路
    非遞迴思路:比較簡單的思路,三個工作指標
    遞迴思路:注意如何結束遞迴
  • 邊界條件
    注意不要讓null指標再取next

非遞迴方式

/*public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode ReverseList(ListNode head) {

        if((head == null) || (head.next == null))
            return head;
        ListNode ppre = null;
        ListNode pwork = null;
        ListNode pnext = null;
        pwork = head;
        //pnext = head.next;
        while(pwork != null){
            /* 
            如果按照下面這樣編碼的話,則會出現一種情況就是pwork不為null,pnext已經為null,然後最後一句再對pnext取next則會出錯。
            pnext = head.next;
          while(pwork != null){
              pwork.next = ppre;
              ppre = pwork;
              pwork = pnext;
              pnext = pnext.next;
           }
            */
            pnext = pwork.next;
            pwork.next = ppre;
            ppre = pwork;
            pwork = pnext;

        }
        //head = ppre;
        return ppre;

    }
}

遞迴方式

public class Solution {
    public ListNode ReverseList(ListNode head) {
        /** 遞迴結束條件 */
        if((head == null) || (head.next == null))
            return head;

        /** 逆置兩個結點 */
        ListNode pNewHead = ReverseList(head.next);
        head.next.next = head;
        head.next = null;
        return pNewHead;
    }
}

其它操作

複製複雜連結串列

複製複雜連結串列,連結串列有next指標和random指標,random指向任意結點

  • 思路
    普通思路:
    先複製next指標的連結串列,再逐個結點遍歷random指標,並對找到的對應結點重新遍歷連結串列記錄其所在位置,最後更改新連結串列對應結點的random指標。存在的問題,random結點的定位比較難
    巧妙思路:
    將新連結串列和舊連結串列的結點之間建立一個對映關係,當遍歷random時,將原結點和對應的random結點分別對映到新連結串列的對應結點中即可。存在的問題,需要專門的儲存空間來存放對映關係
    更為巧妙的思路:
    將對映關係與直接用連結串列的next指標來儲存,一個指標多用,舊連結串列的結點與新連結串列的結點的對映關係就是next關係。建立完畢後,建立相應的random指標關係,最後再拆分為新舊連結串列。

  • 邊界條件

/*
public class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;

    RandomListNode(int label) {
        this.label = label;
    }
}
*/

public class Solution {
  public RandomListNode Clone(RandomListNode pHead)
  {       
        /** 邊界條件判斷 */
        if(pHead == null){
            return null;
        }
        RandomListNode pHeadWork = null;
        pHeadWork = pHead;
        /**
         * 先原地複製next指標連結串列,建立新舊連結串列的同時建立對映關係
         */
        while(pHeadWork != null){
            RandomListNode pNewNode = new RandomListNode(pHeadWork.label);
            //pNewNode.label = pHeadWork.label;
            pNewNode.random = null;
            pNewNode.next = pHeadWork.next;
            pHeadWork.next = pNewNode;
            pHeadWork = pNewNode.next;
        }

        /**
         * 基於對映關係,建立random連結串列關係
         */
        pHeadWork = pHead;
        while(pHeadWork != null){
            if(pHeadWork.random != null){
                pHeadWork.next.random = pHeadWork.random.next;
            }
            pHeadWork = pHeadWork.next.next;
        }

        /**
         * 解開連結串列成兩個連結串列
         */
        RandomListNode pNewHead = null, pNewHeadWork = null;
        pNewHead = pHead.next;
        pNewHeadWork = pNewHead;
        pHeadWork = pHead;
        pHeadWork.next = pHeadWork.next.next;
        pHeadWork = pHeadWork.next;
        while(pHeadWork != null){
            pNewHeadWork.next = pHeadWork.next;
            pNewHeadWork = pNewHeadWork.next;
            pHeadWork.next = pHeadWork.next.next;
            pHeadWork = pHeadWork.next;

        }
        return pNewHead;

    }
}

參考

http://wuchong.me/blog/2014/03/25/interview-link-questions/
http://blog.csdn.net/heyabo/article/details/7610732
http://blog.csdn.net/liuhuiyi/article/details/8742571

相關文章