鏈表問題總結 (上)

【聲明】
歡迎轉載,但請保留文章原始出處→_→
文章來源:http://www.lxweimin.com/p/08d085b34b2c
聯系方式:zmhg871@gmail.com

【正文】
鏈表是非常基礎和靈活的數據結構,在面試中出現的頻率非常高。以下是我在學習《劍指offer》過程中對鏈表問題的總結,希望對大家復習有幫助。
(Java實現)

【目錄】

  1. 單鏈表的創建和遍歷
  2. 求單鏈表中節點的個數
  3. 查找單鏈表中的倒數第k個結點
  4. 查找單鏈表中的中間結點
  5. 合并兩個有序的單鏈表,合并之后的鏈表依然有序
  6. 反轉鏈表
  7. 從尾到頭打印單鏈表
  8. 刪除鏈表結點

【提示】
當我們用一個指針遍歷鏈表不能解決問題的時候,可以嘗試用兩個指針來遍歷鏈表,可以讓其中一個指針遍歷的速度快一些(比如一次在鏈表中走兩步),或者讓它先在鏈表上走幾步。

  • 求鏈表的倒數K個節點
  • 求鏈表的中間節點
  • 求鏈表中是否有環

  1. 單鏈表的創建和遍歷:
public class LinkList {
 
     //Linked List Node
     static class LinkNode{
         int value;                // value for this node
         LinkNode next = null;     // Pointer to next node
 
         LinkNode(int data){
             value = data;
         }
 
         LinkNode(){}
     }
 
     private LinkNode header = null;
 
     //方法:向鏈表中添加數據
     public void add(int value){
         if(header == null) header = new LinkNode();
 
         LinkNode node = new LinkNode(value);
         node.next = header.next;
         header.next  = node;
     }
 
    //方法:遍歷鏈表
     public void print(){
         if(header == null) return;
 
         LinkNode temp = header.next;
         while(temp != null){
             System.out.println("value : " + temp.value);
             temp = temp.next;
         }
     }
 
     public static void main(String[] args) {       
         LinkList list = new LinkList();
         //向LinkList中添加數據
         for (int i = 0; i < 7; i++) {
              list.add(i);
          }
 
         list.print();
         System.out.println(list.getSize());
      }
}
  1. ****求單鏈表中節點的個數:時間復雜度為O(n)****
//方法:獲取單鏈表節點的個數
 public int getSize(){
     if(header == null) return 0;
     
     LinkNode temp = header.next;
     int size = 0;
     
     while(temp != null){
         size++;
         temp = temp.next;
     }
     
     return size;
 }
  1. 查找單鏈表中的倒數第k個結點

題目描述:輸入一個單向鏈表,輸出該鏈表中倒數第k個節點,鏈表的倒數第1個節點為鏈表的尾指針。 (劍指offer,題15)

3.1 普通思路:(遍歷鏈表2次)
首先計算出鏈表的長度size,然后輸出第(size-k)個節點就可以了。
(注意鏈表為空,k為0,k大于鏈表中節點個數時的情況)。

public int getLastNode(int index){
     if(header == null || index == 0) return -1;
     
     int size = getSize();
     if(index > size) return -1;
     
     LinkNode temp = header.next;
     for(int i =1;i<= size-index;i++){
         temp = temp.next;
     }
     return temp.value;
 }

3.2 改進思路:(遍歷鏈表1次)
聲明兩個指針:first和second,首先讓first和second都指向第一個結點,然后讓first結點往后挪k-1個位置,此時first和second就間隔了k-1個位置,然后整體向后移動這兩個節點,直到first節點走到最后一個結點的時候,此時second節點所指向的位置就是倒數第k個節點的位置。

  public int getLastNode(int k){
     if(header == null || k<= 0) return -1;
     
     LinkNode first = header;
     LinkNode second = header;
     
     //讓first結點往后挪k-1個位置
     for(int i =0;i<k-1;i++){
         if(first.next != null){
             first = first.next;
         }else{
             return -1;
         }
     }
     while(first.next != null){
         first = first.next;
         second = second.next;
     }
     return second.value;
}
  1. 查找單鏈表中的中間結點

題目描述:求鏈表的中間節點,如果鏈表的長度為偶數,返回中間兩個節點的任意一個,若為奇數,則返回中間節點。

4.1 普通思路:(遍歷鏈表2次)
首先計算出鏈表的長度size,然后輸出第(size/2)個節點就可以了。

     public int getMiddleNode(){
         if(header == null)
             return -1;
         
         //第1次遍歷獲取節點數
         LinkNode temp = header.next;
         int size = 0;
         while(temp != null){
             size++;
             temp = temp.next;
         }
         if(size == 0) return -1;
         
        //第2次遍歷查找中間節點
         temp = header.next;
         for(int i=0;i<size/2;i++){
             temp = temp.next;
         }
         return temp.value;
     }

4.2 改進思路:(遍歷鏈表1次)
聲明兩個指針:first和second,同時從鏈表頭節點開始,一個指針每次移動兩步,另一個每次移動一步,當走的快的指針走到鏈表的末尾時,走的慢的指針正好在鏈表的中間。

     public LinkNode getMiddleNode(){
         if(header == null)
             return null;
         
         LinkNode first = header; //快指針
         LinkNode second = header;
         
         while(first!=null && first.next!=null){
             first = first.next.next;
             second = second.next;
         }
         return second;
     }
  1. 合并兩個有序的單鏈表,合并之后的鏈表依然有序

題目描述:輸入兩個遞增排序的鏈表,合并這兩個鏈表并使新鏈表中的結點仍然是按照遞增排序的。 (劍指offer,題17)
例如:
鏈表1: 1->3->5->7
鏈表2: 2->4->6->8
合并之后:1->2->3->4->5->6->7->8

解題思路 : 類似于歸并排序
首先分析合并兩個鏈表的過程。鏈表1的頭結點的值小于鏈表2的頭結點的值,因此鏈表1的頭結點將是合并后鏈表的頭結點。
我們繼續合并兩個鏈表中剩余的結點。在兩個鏈表中剩下的結點依然是排序的,因此合并這兩個鏈表的步驟和前面的步驟是一樣的。我們還是比較兩個頭結點的值。此時鏈表2的頭結點的值小于鏈表1的頭結點的值,因此鏈表2的頭結點的值將是合并剩余結點得到的鏈表的頭結點。我們把這個結點和前面合并鏈表時得到的鏈表的尾節點鏈接起來。
當我們得到兩個鏈表中值較小的頭結點并把它鏈接到已經合并的鏈表之后,兩個鏈表剩余的結點依然是排序的,因此合并的步驟和之前的步驟是一樣的。這就是典型的遞歸的過程,我們可以定義遞歸函數完成這一合并過程。

 //兩個參數代表的是兩個鏈表的頭結點,返回合并后的頭結點
 public static LinkNode Merge(LinkNode head1,LinkNode head2){
     if(head1 == null) return head2;
     if(head2 == null) return head1;
     
     LinkNode mergeHead = null;
     if(head1.value <= head2.value){
         mergeHead = head1;
         mergeHead.next = Merge(head1.next,head2);
     }else{
         mergeHead = head2;
         mergeHead.next = Merge(head1,head2.next);
     }
     return mergeHead;
 }

注意問題:
1)鏈表不能斷開,且仍為遞增順序;
2)代碼魯棒性,考慮鏈表為空的情況;

//遍歷的方式
public static LinkNode Merge(LinkNode head1,LinkNode head2){
         
         //預判斷
         if(head1 == null && head2 == null){
             return null;
         }
         if(head1 == null){
             return head2;
         }
         if(head2 == null){
             return head1;
         }
         
         LinkNode head;//新的頭結點
         LinkNode temp;
         
         //確定新的頭結點
         if(head1.value <= head2.value){
             head = head1;
             temp = head1;
             head1 = head1.next;
         }else{
             head = head2;
             temp = head2;
             head2 = head2.next;
         }
         //合并
         while(head1 != null && head2!=null){
             if(head1.value <= head2.value){
                 temp.next = head1;
                 temp = temp.next;
                 head1 = head1.next;
             }else{
                 temp.next = head2;
                 temp = temp.next;
                 head2 = head2.next;
             }
         }
         //合并剩余的元素
         if(head1 != null){
             temp.next = head1;
         }
         if(head2 != null){
             temp.next = head2;;
         }
         
         return head;
     }
  1. 查找單鏈表中的倒數第k個結點

題目描述:輸入一個單鏈表的頭結點,反轉該鏈表并輸出反轉后的頭結點。 (劍指offer,題16)
例如:
鏈表反轉前: 1->3->5->7
鏈表反轉后: 7->5->3->1

解題思路 : 從頭到尾遍歷原鏈表,使用三個節點pNode、pPrev、pPost 記錄當前節點,前一個節點和后一個節點。

public static LinkNode ReverseList(LinkNode header){
    if(header == null) return null;
    
    LinkNode pNode = header;
    LinkNode pPrev = null;   //記錄前一個節點
    LinkNode pPost = null;   //記錄后一個節點
    while(pNode != null){
        pPost = pNode.next;
        pNode.next = pPrev;
        pPrev = pNode;
        pNode = pPost;
    }
    return pPrev;
}

注意問題:鏈表為空和只有1個節點的問題。

  1. ****從尾到頭打印單鏈表****

題目描述:輸入一個鏈表的頭結點,從尾到頭反過來打印出每個節點的值. (劍指offer,題5)

7.1 解法1:在允許修改鏈表的結構的情況下,可以先反轉鏈表,然后從頭到尾輸出。

7.2 解法2:在不允許修改鏈表的結構的情況下,可以使用棧實現。
遍歷的順序是從頭到尾的順序,可輸出的順序卻是從尾到頭。也就是說第一個遍歷到的節點最后一個輸出,而最后一個遍歷到的節點第一個輸出。這就是典型的“后入先出”,我們可以用棧來實現這種順序。

 public static void reversePrint(LinkNode header){
     if(header == null) return;
     
     Stack<LinkNode> stack  = new Stack<>();
     LinkNode temp = header;
     
     while(temp!=null){
         stack.push(temp);
         temp = temp.next;
     }
     
     while(!stack.empty()){
         System.out.println(stack.pop().value);
     }
 }

7.3 解法3:在不允許修改鏈表的結構的情況下,可以使用遞歸實現。(遞歸的本質上就是一個棧結構)
要想實現反過來輸出鏈表,每訪問一個節點的時候,先遞歸輸出它后面的節點,再輸出節點自身,這樣鏈表的輸出結果就反過來了。

 public static void reversePrint(LinkNode header){
     if(header == null) return;
     
     reversePrint(header.next);
     System.out.println(header.value);
 }

代碼簡潔,但是鏈表非常長的情況下可能會導致函數調用棧溢出。所以顯示使用棧基于循環實現的代碼的魯棒性會更好。

  1. 刪除鏈表結點

題目描述:給定鏈表的頭指針和一個節點指針,在O(1)時間刪除該節點。 (劍指offer,題13)

8.1 普通思路:平均時間復雜度O(n)
從鏈表的頭結點開始,順序遍歷要刪除的節點,并在鏈表中刪除該節點。

 public void delete(LinkNode head,LinkNode toBeDeleted){
     
     if(head == null || toBeDeleted == null)
         return;
     
     LinkNode prev = head;
     LinkNode temp = head.next;
     
     while( temp != null ){
         if(temp.value == toBeDeleted.value){
             prev.next = temp.next;
             break;
         }else{
             temp = temp.next;
             prev = prev.next;
         }
     }
     
 }

8.2 改進思路:平均時間復雜度O(1)
前一種方法之所以要從頭開始查找,是因為我們需要得到被刪除節點的前一個節點,但是想要刪除節點并不一定非要找到前一個節點。由于在單鏈表中可以很方便的得到刪除節點的下一個節點,如果我們把下一個節點的內容復制到需要刪除的節點上覆蓋原有的內容,再把下一個節點刪除,就相當于把需要刪除的節點刪除了。

public class Test {
 /** 
  * 鏈表結點 
  */  
 public static class ListNode {  
     int value; // 保存鏈表的值  
     ListNode next; // 下一個結點  
 } 
 /** 
  * 【注意1:這個方法和文本上的不一樣,書上的沒有返回值,這個因為JAVA引用傳遞的原因, 
  *   如果刪除的結點是頭結點,如果不采用返回值的方式,那么頭結點永遠刪除不了】 
  * 【注意2:輸入的待刪除結點必須是待鏈表中的結點,否則會引起錯誤,這個條件由用戶進行保證】 
  * 
  * @param head        鏈表表的頭 
  * @param toBeDeleted 待刪除的結點 
  * @return 刪除后的頭結點 
  **/
 public static ListNode deleteNode(ListNode head,ListNode toBeDeleted){
     //預判斷
     if(head == null)
         return null;
     if(toBeDeleted == null){
         return head;
     }
     // 如果刪除的是頭結點,直接返回頭結點的下一個結點  
     if(head.value == toBeDeleted.value){
         return head.next;
     }
     // 在多個節點的情況下,如果刪除的是最后一個元素  
     if(toBeDeleted.next == null){
         // 找待刪除元素的前驅  
         ListNode temp = head;
         while(temp.next.value != toBeDeleted.value){
             temp = temp.next;
         }
         // 刪除待結點  
         temp.next = null;
     }else{
         // 在多個節點的情況下,如果刪除的是某個中間結點  
         toBeDeleted.value =  toBeDeleted.next.value;
         toBeDeleted.next  = toBeDeleted.next.next;
     }
    // 返回刪除節點后的鏈表頭結點  
     return head;
 }
 
 /** 
  * 輸出鏈表的元素值 
  * 
  * @param head 鏈表的頭結點 
  */  
 public static void printList(ListNode head) {  
     while (head != null) {  
         System.out.print("  value :" + head.value);  
         head = head.next;  
     }  
     System.out.println();  
 } 
 
 public static void main(String[] args) {  
     ListNode head1 = new ListNode();  
     head1.value = 1;  
     ListNode head2 = new ListNode();  
     head2.value = 2;
     ListNode head3 = new ListNode();  
     head3.value = 3;  
     ListNode head4 = new ListNode();  
     head4.value = 4;  
    
     head1.next = head2;
     head2.next = head3;
     head3.next = head4;
     
     printList(head1);  
     ListNode head = head1;  
     // 刪除頭結點  
     head = deleteNode(head, head1);
     printList(head);  
     
     // 刪除尾結點  
     head = deleteNode(head, head4);
     printList(head);  
     
     // 刪除中間結點  
     head = deleteNode(head, head3);
     printList(head);  
     
     ListNode node = new ListNode();  
     node.value = 12;  
     // 刪除的結點不在鏈表中  
     head = deleteNode(head, node);
     printList(head);  
 }  
}

考察思維創新能力,打破常規。當我們需要刪除一個節點時,并不一定要刪除這個節點本身,可以先把下一個節點的內容復制過來覆蓋原來需要被刪除節點的內容,然后把下一個節點刪除。


參考資料:


[2015-9-10]

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內容

  • 轉載請注明出處:http://www.lxweimin.com/p/c65d9d753c31 在上一篇博客《數據結構...
    Alent閱讀 3,522評論 4 74
  • 大學的時候不好好學習,老師在講臺上講課,自己在以為老師看不到的座位看小說,現在用到了老師講的知識,只能自己看書查資...
    和玨貓閱讀 1,463評論 1 3
  • B樹的定義 一棵m階的B樹滿足下列條件: 樹中每個結點至多有m個孩子。 除根結點和葉子結點外,其它每個結點至少有m...
    文檔隨手記閱讀 13,278評論 0 25
  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,114評論 0 12
  • 第一章 緒論 什么是數據結構? 數據結構的定義:數據結構是相互之間存在一種或多種特定關系的數據元素的集合。 第二章...
    SeanCheney閱讀 5,795評論 0 19