搞懂單鏈表常見面試題

搞懂單鏈表常見面試題

Hello 繼上次的 搞懂基本排序算法,這個一星期,我總結了,我所學習和思考的單鏈表基礎知識和常見面試題,這些題有的來自 《劍指 offer》 ,有的來自《程序員代碼面試指南》,有的來自 leetCode,不是很全面,但都具備必定表明性,相信你們看完之後必定跟我同樣,對面試的時候算法題又多了一份自信。不過文章仍然是又臭又長,但願你們備好咖啡,火腿腸,方便麪之類的,慢慢看,若是我有哪些理解不對的地方,也但願你們能在評論區爲我指出,也算是對我碼這麼多字的承認吧。java

什麼是單鏈表

鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,可是並不會按線性的順序存儲數據,而是在每個節點裏存到下一個節點的指針(Pointer),簡單來講鏈表並不像數組那樣將數組存儲在一個連續的內存地址空間裏,它們能夠不是連續的由於他們每一個節點保存着下一個節點的引用(地址),因此較之數組來講這是一個優點。node

對於單鏈表的一個節點咱們常用下邊這種代碼表示:git

public class Node{
    //節點的值
    int value;
    //指向下一個節點的指針(java 中表現爲下一個節點的引用)
    Node next;
    
    public void Node(int value){
        this.value = value;
    }
}
複製代碼

單鏈表的特色程序員

  1. 鏈表增刪元素的時間複雜度爲O(1),查找一個元素的時間複雜度爲 O(n);
  2. 單鏈表不用像數組那樣預先分配存儲空間的大小,避免了空間浪費
  3. 單鏈表不能進行回溯操做,如:只知道鏈表的頭節點的時候沒法快讀快速鏈表的倒數第幾個節點的值。

單鏈表的基本操做

上一節咱們說了什麼是單鏈表,那麼咱們都知道一個數組它具備增刪改查的基本操做,那麼咱們單鏈表做爲一種常見的數據結構類型也是具備這些操做的那麼咱們就來看下對於單鏈表有哪些基本操做:github

獲取單鏈表的長度

因爲單鏈表的存儲地址不是連續的,鏈表並不具備直接獲取鏈表長度的功能,對於一個鏈表的長度咱們只能一次去遍歷鏈表的節點,直到找到某個節點的下一個節點爲空的時候獲得鏈表的總長度,注意這裏的出發點並非一個空鏈表而後依次添加節點後,而後去讀取已經記錄的節點個數,而是已知一個鏈表的頭結點而後去獲取這個鏈表的長度:面試

public int getLength(Node head){
    
    if(head == null){
        return 0;
    }
    
    int len = 0;
    while(head != null){
        len++;
        head = head.next;
    }  
    return len;  
}
複製代碼

查詢指定索引的節點值或指定值得節點值的索引

因爲鏈表是一種非連續性的存儲結構,節點的內存地址不是連續的,也就是說鏈表不能像數組那樣能夠經過索引值獲取索引位置的元素。因此鏈表的查詢的時間複雜度要是O(n)級別的,這點和數組查詢指定值得元素位置是相同的,由於你要查找的東西在內存中的存儲地址都是不必定的。算法

/** 獲取指定角標的節點值 */
    public int getValueOfIndex(Node head, int index) throws Exception {

        if (index < 0 || index >= getLength(head)) {
            throw new Exception("角標越界!");
        }

        if (head == null) {
            throw new Exception("當前鏈表爲空!");
        }

        Node dummyHead = head;

        while (dummyHead.next != null && index > 0) {
            dummyHead = dummyHead.next;
            index--;
        }

        return dummyHead.value;
    }

    
    /** 獲取節點值等於 value 的第一個元素角標 */
    public int getNodeIndex(Node head, int value) {
    
            int index = -1;
            Node dummyHead = head;
    
            while (dummyHead != null) {
                index++;
                if (dummyHead.value == value) {
                    return index;
                }
                dummyHead = dummyHead.next;
            }
    
            return -1;
    }

複製代碼

鏈表添加一個元素

學過數據結構的朋友必定知道鏈表的插入操做,分爲頭插法,尾插法,隨機節點插入法,固然數據結構講得時候也是針對一個已經構造好的(保存了鏈表頭部節點和尾部節點引用)的狀況下去插入一個元素,這看上去很簡單,若是咱們在只知道一個鏈表的頭節點的狀況下去插入一個元素,就不是那麼簡單了,就對於頭插入法咱們只須要構造一個新的節點,而後將這個節點的 next 指針指向已知鏈表的頭節點就能夠了。數組

一、 在已有鏈表頭部插入一個節點數據結構

public Node addAtHead(Node head, int value){
     Node newHead = new Node(value);
     newHead.next = head;
     return newHead;
}
複製代碼

二、在已有鏈表的尾部插入一個節點:函數

public void addAtTail(Node head, int value){
     Node node = new Node(value);
     Node dummyHead = head;
     
     //找到未節點 注意這裏是當元素的下一個元素爲空的時候這個節點即爲未節點
     while( dummyHead.next != null){
        dummyHead = dummyHead.next;
     }
     
     dummyHead.next = node;   
}
複製代碼

三、在指定位置添加一個節點

// 注意這裏 index 從 0 開始
 public Node insertElement(Node head, int value, int index) throws Exception {
   //爲了方便這裏咱們假設知道鏈表的長度
   int length = getLength(head);
   if (index < 0 || index >= length) {
       throw new Exception("角標越界!");
   }

   if (index == 0) {
       return addAtHead(head, value);
   } else if (index == length - 1) {
       addAtTail(head, value);
   } else {

       Node pre = head;
       Node cur = head.next;
       //
       while (pre != null && index > 1) {
           pre = pre.next;
           cur = cur.next;
           index--;
       }

       //循環結束後 pre 保存的是索引的上一個節點 而 cur 保存的是索引值當前的節點
       Node node = new Node(value);
       pre.next = node;
       node.next = cur;
   }
   return head;
}

複製代碼

在指定位置添加一個節點,首先咱們應該找到這個索引所在的節點的前一個,以及該節點,分別記錄這兩個節點,而後將索引所在節點的前一個節點的 next 指針指向新節點,而後將新節點的 next 指針指向插入節點便可。與其餘元素並無什麼關係,因此單鏈表插入一個節點時間複雜度爲 O(1),而數組插入元素就不同了若是將一個元素插入數組的指定索引位置,那麼該索引位置之後元素的索引位置(內存地址)都將發生變化,因此一個數組的插入一個元素的時間複雜度爲 O(n);因此鏈表相對於數組插入的效率要高一些,刪除同理。

鏈表刪除一個元素

因爲上邊介紹了鏈表添加元素的方法這裏對於鏈表刪除節點的方法不在詳細介紹直接給出代碼:

一、 刪除頭部節點 也就是刪除索引爲 0 的節點:

public Node deleteHead(Node head) throws Exception {
        if (head == null) {
            throw new Exception("當前鏈表爲空!");
        }
        return head.next;
    }
複製代碼

二、 刪除尾節點

public void deleteTail(Node head) throws Exception {

        if (head == null) {
            throw new Exception("當前鏈表爲空!");
        }

        Node dummyHead = head;
        while (dummyHead.next != null && dummyHead.next.next != null) {
            dummyHead = dummyHead.next;
        }
        dummyHead.next = null;
    }

複製代碼

三、 刪除指定索引的節點:

public Node deleteElement(Node head, int index) throws Exception {

   int size = getLength(head);
   
   if (index < 0 || index >= size) {
       throw new Exception("角標越界!");
   }

   if (index == 0) {
       return deleteHead(head);
   } else if (index == size - 1) {
       deleteTail(head);
   } else {
       Node pre = head;

       while (pre.next != null && index > 1) {
           pre = pre.next;
           index--;
       }

       //循環結束後 pre 保存的是索引的上一個節點 將其指向索引的下一個元素
       if (pre.next != null) {
           pre.next = pre.next.next;
       }
   }

   return head;
}
複製代碼

由單鏈表的增長刪除能夠看出,鏈表的想要對指定索引進行操做(增長,刪除),的時候必須獲取該索引的前一個元素。記住這句話,對鏈表算法題頗有用。

單鏈表常見面試題

介紹了鏈表的常見操做之後,咱們的目標是學習鏈表常見的面試題目,否則咱們學他幹嗎呢,哈哈~ 開個玩笑那麼咱們就先從簡單的面試題開始:

尋找單鏈表的中間元素

同窗們可能看到這道面試題笑了,咋這麼簡單,拿起筆來就開始寫,遍歷整個鏈表,拿到鏈表的長度len,再次遍歷鏈表那麼位於 len/2 位置的元素就是鏈表的中間元素。

咱也不能說這種方法不對,想一想一下一個騰訊的面試官坐在對面問這個問題,這個回答顯然連本身這一關都很難過去。那麼更漸快的方法是什麼呢?或者說時間複雜度更小的方法如何實現此次查找?這裏引出一個很關鍵的概念就是 快慢指針法,這也是面試官想考察的。

假如咱們設置 兩個指針 slow、fast 起始都指向單鏈表的頭節點。其中 fast 的移動速度是 slow 的2倍。當 fast 指向末尾節點的時候,slow 正好就在中間了。想一想一下是否是這樣假設一個鏈表長度爲 6 , slow 每次一個節點位置, fast 每次移動兩個節點位置,那麼當fast = 5的時候 slow = 2 正好移動到 2 的節點的位置。

因此求解鏈表中間元素的解題思路是:

public Node getMid(Node head){
      if(head == null){
         return null;
      }
      
      Node slow = head;
      Node fast = head;
      
      // fast.next = null 表示 fast 是鏈表的尾節點
      while(fast != null && fast.next != null){
         fast = fast.next.next;
         slow = slow.next;
      }
      return slow;
    }

複製代碼

判斷一個鏈表是不是循環鏈表

首先此題也是也是考察快慢指針的一個題,也是快慢指針的第二個應用。先簡單說一下什麼循環鏈表,循環鏈表其實就是單鏈表的尾部指針指向頭指針,構建成一個環形的鏈表,叫作循環鏈表。 如 1 -> 2 - > 3 -> 1 -> 2 .....。爲何快慢指針再循環鏈表中總能相遇呢?你能夠想象兩我的在賽跑,A的速度快,B的速度慢,通過必定時間後,A老是會和B相遇,且相遇時A跑過的總距離減去B跑過的總距離必定是圈長的n倍。這也就是 Floyd判環(圈)算法

那麼如何使用快慢指針去判斷一個鏈表是否爲環形鏈表呢:

private static boolean isLoopList(Node head){

        if (head == null){
            return false;
        }

        
        Node slow = head;
        Node fast = head.next;
        
        //若是不是循環鏈表那麼必定有尾部節點 此節點 node.next = null
        while(slow != null && fast != null && fast.next != null){
            if (fast == slow || fast.next == slow){
                return true;
            }
            // fast 每次走兩步  slow 每次走一步
            fast =fast.next.next;
            slow = slow.next;
        }
        //若是不是循環鏈表返回 false
        return false;
    }

複製代碼

已知一個單鏈表求倒數第 N 個節點

爲何這個題要放在快慢指針的後邊呢,由於這個題的解題思想和快慢指針類似,咱們能夠想一下:若是咱們讓快指針先走 n-1 步後,而後讓慢指針出發。快慢指針每次都只移動一個位置,當快指針移動到鏈表末尾的時候,慢指針是否就正處於倒數第 N 個節點的位置呢。

是這裏把這兩個指針稱之爲快慢指針是不正確的,由於快慢指針是指一個指針移動的快一個指針移動的慢,而此題中 快指針只是比慢指針先移動了 n-1 個位置而已,移動速度是相同的。

若是上邊的講解很差理解,這裏提供另一種思路,就是想象一下,上述快慢指針的移動過程,是否就至關於一個固定窗口大小爲 n 的滑動窗口:

  1. n = 1 fast 指針不移動 fast 到達最後一個節點 即 fast.next 的時候 slow 也到達尾部節點滿條件
  2. n = len fast 指針移動 n-1(len -1 ) 次 fast 到達最後一個節點 slow 位於頭節點不變 知足條件 兩個臨界值均知足咱們這種假設。
  3. 1< n < len 的時候咱們假設 n = 2 ,那麼 fast 比 slow 先移動一步,也就是窗口大小爲 2, 那麼當 fast.next = null 即 fast 已經指向鏈表最後一個節點的時候,slow 就指向了 倒數第二個節點。

下面咱們來看下函數實現:

/**
     * 注意咱們通常說倒數第 n 個元素 n 是從 1 開始的
     */
    private Node getLastIndexNode(Node head, int n) {

        // 輸入的鏈表不能爲空,而且 n 大於0
        if (n < 1 || head == null) {
            return null;
        }

        n = 10;
        // 指向頭結點
        Node fast = head;
        // 倒數第k個結點與倒數第一個結點相隔 n-1 個位置
        // fast 先走 n-1 個位置
        for (int i = 1; i < n; i++) {
            // 說明還有結點
            if (fast.next != null) {
                fast = fast.next;
            }else {
                // 已經沒有節點了,可是i尚未到達k-1說明k太大,鏈表中沒有那麼多的元素
                return null;
            }
        }

        Node slow = head;
        // fast 尚未走到鏈表的末尾,那麼 fast 和 slow 一塊兒走,
        // 當 fast 走到最後一個結點即,fast.next=null 時,slow 就是倒數第 n 個結點
        while (fast.next != null) {
            slow = slow.next;
            fast = fast.next;
        }
        // 返回結果
        return slow;
}
複製代碼

刪除單鏈表的倒數第 n 個節點

看到這個題時候樂了,這考察的知識點不就是一道求解倒數第 n 個節點的進化版麼。可是咱們也說過,若是想操做鏈表的某個節點(添加,刪除)還必須知道這個節點的前一個節點。因此咱們刪除倒數第 n 個元素就要找到倒數第 n + 1 個元素。而後將倒數第 n + 1個元素 p 的 next 指針 p.next 指向 p.next.next

咱們找到倒數第 n 個節點的時候,先讓 fast 先走了 n-1 步,那麼咱們刪除倒數第 n 個節點的時候就須要 讓 fast 先走 n 步,構建一個 n+1 大小的窗口,而後 fast 和 slow 總體平移到鏈表尾部,slow 指向的節點就是 倒數第 n+1 個節點。

這裏咱們還可使用滑動窗口的思想來考慮臨界值:

  1. n = 1 的時候咱們須要構建的窗口爲 2,也就是當 fast.next = null 的時候 slow 在的倒數第二個節點上,那麼可想而知是知足咱們的條件的。

  2. 當 1 < n < len 的時候咱們老是能構建出這樣的一個 len + 1大小的窗口,n 最大爲 len -1 的時候,slow 位於頭節點,fast 位於未節點,刪除倒數第 n 個元素,即刪除正數第二個節點,slow.next = slow.next.next 便可。

  3. 當 n > len 的時候可想而知,咱們要找的倒數第 n 個元素不存在,此時返回 頭節點就行了

  4. n = len 的時候比較特殊,循環並無由於倒數第 len 個元素不存在而終止,並進行了 fast = fast.next; 循環結束後 fast 指向 null , 且此時 slow 位於頭節點,因此咱們要刪除的節點是頭節點,只須要在循環結束後判斷 若是 fast == null 返回 head.next 便可

下面咱們來看解法:

/**
 * 刪除卻是第 n 個節點 咱們就要找到倒數第 n + 1 個節點, 若是 n > len 則返回原列表
 */
private Node deleteLastNNode(Node head, int n) {

   if (head == null || n < 1) {
       return head;
   }

   Node fast = head;
   
   //注意 咱們要構建長度爲 n + 1 的窗口 因此 i 從 0 開始
   for (int i = 0; i < n; i++) {
       //fast 指針指向倒數第一個節點的時候,就是要刪除頭節點
       if (fast == null) {
           return head;
       } else {
           fast = fast.next;
       }
   }

   // 因爲 n = len 再循環內部沒有判斷直接前進了一個節點,臨界值 n = len 的時候 循環完成或 fast = null
   if (fast == null){
       return head.next;
   }

   //此時 n 必定是小於 len 的 且 fast 先走了 n 步
   Node pre = head;

   while (fast.next != null) {
       fast = fast.next;
       pre = pre.next;
   }

   pre.next = pre.next.next;

   return head;
}
複製代碼

旋轉單鏈表

題目:給定一個鏈表,旋轉鏈表,使得每一個節點向右移動k個位置,其中k是一個非負數。 如給出鏈表爲 1->2->3->4->5->NULL and k = 2, return 4->5->1->2->3->NULL.

作完,刪除倒數第 n 個節點的題,咱們在看着道題是否是很簡單了,這道題的本質就是,找到 k 位置節點 將其變成尾節點,而後原來鏈表的尾節點指向原來的頭節點

private Node rotateList(Node head, int n) {

   int start = 1;

   Node fast = head;

   //先讓快指針走 n 給個位置
   while (start < n && fast.next != null) {
       fast = fast.next;
       start++;
   }


   //循環結束後若是 start < n 表示 n 整個鏈表還要長 旋轉後仍是原鏈表
   //若是 fast.next = null 表示 n 正好等於原鏈表的長度此時也不須要旋轉
   if (fast.next == null || start < n) {
       return head;
   }

   //倒數第 n + 1個節點
   Node pre = fast;
   //旋轉後的頭節點
   Node newHead = fast.next;

   while (fast.next != null) {
       fast = fast.next;
   }
   //原鏈表的最後一個節點指向原來的頭節點
   fast.next = head;
   //將旋轉的節點的上一個節點變爲尾節點
   pre.next = null;

   return newHead;
}

複製代碼

翻轉單鏈表

翻轉一個單鏈表,要求額外的空間複雜度爲 O(1)

翻轉單鏈表是我感受比較難的基礎題,那麼先來屢一下思路:一個節點包含指向下一節點的引用,翻轉的意思就是對要原來指向下一個節點引用指向上一個節點

  1. 找到當前要反轉的節點的下一個節點並用變量保存由於下一次要反轉的是它
  2. 而後讓當前節點的 next 指向上一個節點, 上一個節點初始 null 由於頭結點的翻轉後變爲尾節點
  3. 當前要反轉的節點變成了下一個要比較元素的上一個節點,用變量保存
  4. 當前要比較的節點賦值爲以前保存的未翻轉前的下一個節點
  5. 當前反轉的節點爲 null 的時候,保存的上一個節點即翻轉後的鏈表頭結點

ok,不知道按照上邊我寫的步驟可否理解一個鏈表的翻轉過程。若是不理解本身動手畫一下可能更好理解哈,注意在畫的時候一次只考慮一個節點,且不要考慮已經翻轉完的鏈表部分。

下面咱們來看下實現過程:

public Node  reverseList(Node head){
   //頭節點的上一個節點爲 null
   Node pre = null;
   Node next = null;
   
   while(head != null){
       next = head.next;
       head.next = pre;
       pre = head;
       head = next;
   }
}
複製代碼

翻轉部分單鏈表

題目要求:要求 0 < from < to < len 若是不知足則不翻轉

這類題還有一類進階題型,就是翻轉鏈表 from 位置到 to 位置的節點,其實翻轉過程是類似的,只是咱們須要找到位於 from 的前一個節點,和 to 的下一個節點 翻轉完 from 和 to 部分後將 from 的上一個節點的 next 指針指向翻轉後的to,將翻轉後 from 節點的 next 指針指向 to 節點下一個節點。

  1. 遍歷整個鏈表 遍歷過程須要統計鏈表的長度 len ,from 節點的前一個節點 fPosPre , 翻轉開始的節點 from ,翻轉結束的節點 to ,節點to 節點的後一個節點 tPosNext 。
  2. 循環後判斷條件 0 < from < to < len 的條件是否知足,若是不知足返回 head
  3. 進行 from 到 to 節點翻轉
  4. 翻轉完後判斷 若是翻轉的起點不是 head 則返回 head,若是反轉的鏈表是起點,那麼翻轉後 toPos 就是頭結點。

下面咱們開看代碼(你可能有更簡便的解法,省去幾個變量,可是下面的解法應該是最好理解的);

private Node reversePartList(Node head, int from, int to) {
        
        Node dummyHead = head;

        int len = 0;

        Node fPosPre = null;
        Node tPosNext = null;
        Node toPos = null;
        Node fromPos = null;

        while (dummyHead != null) {
            //由於 len = 0 開始的因此 len 先作自增一
            len++;

            if (len == from) {
                fromPos = dummyHead;
            } else if (len == from - 1) {
                fPosPre = dummyHead;
            } else if (len == to + 1) {
                tPosNext = dummyHead;
            } else if (len == to) {
                toPos = dummyHead;
            }

            dummyHead = dummyHead.next;
        }

        //不知足條件不翻轉鏈表
        if (from > to || from < 0 || to > len || from > len) {
            return head;
        }


        Node cur = fromPos;
        Node pre = tPosNext;
        Node next = null;

        while (cur != null && cur != tPosNext) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        // 若是翻轉的起點不是 head 則返回 head
        if (fPosPre != null) {
            fPosPre.next = pre;
            return head;
        }
        // 若是反轉的鏈表是起點,那麼翻轉後 toPos 就是頭結點
        return toPos;
    }
複製代碼

單鏈表排序

在個人上一篇文章中說到了數組基本的排序方法 搞懂基本排序方法,對於鏈表來講也有上述幾種排序方法,若是感興趣的朋友也可使用冒泡排序,選擇排序,快速排序去實現單鏈表的排序,因爲鏈表的不可回溯行,對於鏈表來講歸併排序是個不錯的排序方法。咱們知道歸併經過遞歸,能夠實現,那麼對於單鏈表來講也是能夠的。

單鏈表的歸併排序

歸併的中心思想在於在於已知兩個鏈表的時候,若是按順序歸併這兩個鏈表。其實這也是一道面試題按照元素的大小合併兩個鏈表那麼咱們就先看下如何合併兩個鏈表 咱們稱這個過程爲 merge 。

private Node merge(Node l, Node r) {

   //建立臨時空間
   Node aux = new Node();
   Node cur = aux;

   //因爲鏈表不能方便的拿到鏈表長度 因此通常使用 while l == null 表示鏈表遍歷到尾部
   while (l != null && r != null) {
       if (l.value < r.value) {
           cur.next = l;
           cur = cur.next;
           l = l.next;
       } else {
           cur.next = r;
           cur = cur.next;
           r = r.next;
       }
   }
   //當有一半鏈表遍歷完成後 另一個鏈表必定只剩下最後一個元素(鏈表爲基數)
   if (l != null) {
       cur.next = l;
   } else if (r != null) {
       cur.next = r;
   }

   return aux.next;
}
複製代碼

返回的 Node 節點爲歸併完成後的鏈表頭節點。那麼歸併排序的核心過程也完成了,想一想咱們想要歸併一個數組還須要一個劃分操做 中心節點 mid 是誰,看到這裏是否是笑了,以前咱們已經講過如何尋找一個鏈表的中間元素,那麼是否是萬事具有了,ok 咱們來實現鏈表的歸併排序:

private Node mergeSort(Node head) {

   //遞歸退出的條件 當歸並的元素爲1個的時候 即 head.next 退出遞歸
   if (head == null || head.next == null) {
       return head;
   }

   Node slow = head;
   Node fast = head;

   //尋找 mid 值
   while (fast.next != null && fast.next.next != null) {
       slow = slow.next;
       fast = fast.next.next;
   }

   Node left = head;
   Node right = slow.next;

   //拆分兩個鏈表 若是設置鏈表的最後一個元素指向 null 那麼 left 永遠等於 head 這鏈表 也就沒法排序
   slow.next = null;
   
   //遞歸的劃分鏈表
   left = mergeSort(left);
   right = mergeSort(right);

   return merge(left, right);
}
複製代碼

單鏈表的插入排序

回想一下數組的插入排序,咱們從第二個數開始遍歷數組,若是當前考察的元素值比下一個元素的值要大,則下一個元素應該排列排列在當前考察的元素以前,因此咱們從已經排序的元素序列中從後向前掃描,若是該元素(已排序)大於新元素,將該元素移到下一位置(賦值也好,交換位置也好)。可是因爲鏈表的不可回溯性,咱們只能從鏈表的頭節點開始找,這個元素應該要在的位置。

咱們來看下代碼實現:

public Node insertionSortList(Node head) {
        if (head == null || head.next == null) return head;

        Node dummyHead = new Node(0);
        Node p = head;
        dummyHead.next = head;
      //p 的值不小於下一節點元素考察下一節點
        while (p.next != null) {
            if (p.value <= p.next.value) { 
                p = p.next;
            } else {
                //p 指向 4
                Node temp = p.next;
                Node q = dummyHead;
                p.next = p.next.next;

                //從頭遍歷鏈表找到比當前 temp 值小的第一個元素插入其後邊 整個位置必定在 頭節點與 q 節點之間
                while (q.next.value < temp.value && q.next != q)
                    q = q.next;

                temp.next = q.next;
                //從新鏈接鏈表 注意 else 的過程並無改變 p 指針的位置
                q.next = temp;
            }
        }
        return dummyHead.next;
    }
複製代碼

劃分鏈表

題目 : 按某個給定值將鏈表劃分爲左邊小於這個值,右邊大於這個值的新鏈表 如一個鏈表 爲 1 -> 4 -> 5 -> 2 給定一個數 3 則劃分後的鏈表爲 1-> 2 -> 4 -> 5

此題不是很難,就是遍歷一遍鏈表,就能夠完成,咱們新建一兩個鏈表,若是遍歷過程當中,節點值比給定值小則劃在左鏈表中,反之放在右鏈表中,遍歷完成後拼接兩個鏈表就好。不作過多解釋直接看代碼。

private Node partition(Node head , int x){
    if(head == null){
        return = null;
    }
    
    Node left = new Node(0);
    Node right = new Node(0);
    
    Node dummyLeft = left;
    Node dummyRight = right;
    
    while(head != null){
        if(head.value < x){
            dummyLeft.next = head;
            dummyLeft = dummyLeft.next;
        }else{
            dummyRight.next = head;
            dummyRight = dummyRight.next;
        }
        head = head.next;
    }
    
    dummyLeft.next = right.next;
    right.next = null;
    
    return left.next;
 }
複製代碼

鏈表相加求和

題目: 假設鏈表中每個節點的值都在 0-9 之間,那麼鏈表總體能夠表明一個整數。 例如: 9->3->7 能夠表明 937 給定兩個這樣的鏈表,頭節點爲 head1 head2 生成鏈表相加的新鏈表。 如 9->3->7 和 6 -> 3 生成的新鏈表應爲 1 -> 0 -> 0 -> 0

此題若是明白題意的狀況並不難解決,首先理解怎麼取加兩個鏈表,即鏈表按照,尾節點往前的順序每一位相加,若是有進位則在下一個節點相加的時候算上,每一位加和爲新鏈表的一個結點。這看上去跟數學加法同樣。因此咱們的解題思路爲:

  1. 翻轉要相加的兩個鏈表,這樣就能夠從原鏈表的尾節點開始相加。
  2. 同步遍歷兩個逆序鏈表,每個節點的值相加,經過是要使用變量記錄是否進位。
  3. 當鏈表遍歷完成後 判斷是否還有進位 若是有再添加一個結點,
  4. 再次翻轉兩個鏈表使其復原,並翻轉新鏈表,則獲得的題解。
private Node addLists(Node head1, Node head2) {
        head1 = reverseList(head1);
        head2 = reverseList(head2);
        //進位標識
        int ca = 0;
        int n1 = 0;
        int n2 = 0;
        int sum = 0;

        Node addHead = new Node(0);
        Node dummyHead = addHead;

        Node cur1 = head1;
        Node cur2 = head2;

        while (cur1 != null || cur2 != null) {
            n1 = cur1 == null ? 0 : cur1.value;
            n2 = cur2 == null ? 0 : cur2.value;

            sum = n1 + n2 + ca;

            Node node = new Node(sum % 10);
            System.out.println( sum % 10);
            ca = sum / 10;

            dummyHead.next = node;

            dummyHead = dummyHead.next;

            cur1 = cur1 == null ? null : cur1.next;
            cur2 = cur2 == null ? null : cur2.next;
        }

        if (ca > 0) {
            dummyHead.next = new Node(ca);
        }

        head1 = reverseList(head1);
        head2 = reverseList(head2);

        addHead = addHead.next;
        return reverseList(addHead);
    }
    
    private  Node reverseList(Node head) {
        Node cur = head;
        Node pre = null;
        Node next = null;

        while (cur != null) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        //注意這裏返回的是賦值當前比較元素
        return pre;
    }

複製代碼

刪除有序/無序鏈表中重複的元素

刪除有序鏈表中的重複元素

刪除有序鏈表中的重複元素比較簡單,由於鏈表自己有序,因此若是元素值重複,那麼一定相鄰,因此刪除重複元素的方法爲:

如一個鏈表爲 36 -> 37 -> 65 -> 76 -> 97 -> 98 -> 98 -> 98 -> 98 -> 98 刪除重複元素後爲: 36 -> 37 -> 65 -> 76 -> 97 -> 98

private void delSortSame(Node head) {
        if (head == null || head.next == null) {
            return;
        }

        Node dummy = head;
        while (dummy.next != null) {
            if (dummy.value == dummy.next.value) {
                dummy.next = dummy.next.next;
            } else {
                dummy = dummy.next;
            }
        }
    }
複製代碼

刪除無序鏈表中的重複元素

刪除無序鏈表中的重複元素,就要求咱們必須使用一個指針記住當前考察元素 cur 的上一個元素 pre ,並以此遍歷考察元素以後的全部節點,若是有重複則將 pre 指針的 next 指針指向當 cur.next; 重複遍歷每一個節點,直至鏈表結尾。

如一個鏈表刪除重複元素前爲: 0 -> 0 -> 3 -> 5 -> 3 -> 0 -> 1 -> 4 -> 5 -> 7 刪除重複元素後爲: 0 -> 3 -> 5 -> 1 -> 4 -> 7

private void delSame(Node head) {

   if (head == null || head.next == null) {
       return;
   }
   
   Node pre = null;
   Node next = null;
   Node cur = head;

   while (cur != null) {
       //當前考察的元素的前一個節點
       pre = cur;
       //當前考察元素
       next = cur.next;
       //從遍歷剩餘鏈表刪除重複元素
       while (next != null) {
           if (cur.value == next.value) {
               //刪除相同元素
               pre.next = next.next;
           }else {
               //移動指針
               pre = next;
           }
           //移動指針
           next = next.next;
       }
       //考察下一個元素
       cur = cur.next;
   }
}

複製代碼

重排鏈表

其實這也是一系列的題目,主要考察了咱們對於額外空間複雜度爲O(1) 的鏈表操做。咱們先看第一道題:

按照左右半區的方式從新排列組合單鏈表

題目 給定一個單鏈表L: L0→L1→…→Ln-1→Ln, 從新排列後爲 L0→Ln→L1→Ln-1→L2→Ln-2→… 要求必須在不改變節點值的狀況下進行原地操做。

咱們先來分析一下題目,要想重排鏈表,必須先找到鏈表的中間節點,而後分離左右兩部鏈表,而後按左邊一個,右邊一個的順序排列鏈表。咱們假設鏈表爲基數的時候, N/2 位置的節點算左半鏈表, 那麼右半鏈表就會比左半鏈表多一個節點。當左半鏈表爲最後一個節點的時候咱們只須要將剩餘的右半鏈表設爲其下一個節點便可。 N 爲偶數的時候就好說了,N/2 + 1 爲右半鏈表的開始,重拍最後只須要將左半鏈表爲最後一個節點指向 null,恰巧此時右半鏈表爲 null 因此重拍最後一步就是 left.next = right 下面咱們來看題解:

private void relocate1(Node head) {
   //若是鏈表長度小於2 則不須要從新操做
   if (head == null || head.next == null) {
       return;
   }

   //使用快慢指針 遍歷鏈表找到鏈表的中點
   Node mid = head;
   Node right = head.next;

   while (right.next != null && right.next.next != null) {
       mid = mid.next;
       right = right.next.next;
   }

   //拆分左右半區鏈表
   right = mid.next;
   mid.next = null;

   //按要求合併
   mergeLR(head, right);

}

private void mergeLR(Node left, Node right) {
   Node temp = null;
   while (left.next != null) {
       temp = right.next;

       right.next = left.next;
       left.next = right;

       //這裏每次向後移動兩個位置 也就是原來的 left.next
       left = right.next;
       right = temp;
   }
   left.next = right;
}
複製代碼

今日頭條的一個重排鏈表題目

給定一個鏈表 1 -> 92 -> 8 -> 86 -> 9 -> 43 -> 20 鏈表的特徵是奇數位升序,偶數位爲降序,要求從新排列鏈表並保持鏈表總體爲升序

這道題和左右半區重排鏈表相似,其實這能夠理解爲一個已經進行重排後的鏈表,如今要執行上一道重排的逆過程。要知足這個條件,咱們必須假設偶數位最小的節點大於奇數位最大的元素。我想出題人也是這意思。若是不是的話也不麻煩上邊咱們也講了歸併排序的方法,只是一次歸併而已。下面來看知足數位最小的節點大於奇數位最大的元素的解法:

此題考察了面試者對鏈表的基本操做以及如何翻轉一個鏈表

private Node relocate2(Node head) {

        //新建一個左右連個鏈表的頭指針
        Node left = new Node();
        Node right = new Node();


        Node dummyLeft = left;
        Node dummyRight = right;

        int i = 0;
        while (head != null) {
            //由於 i 從0 開始 鏈表的頭節點算是奇數位因此 i 先自增 再比較
            i++;
            if (i % 2 == 0) {
                dummyRight.next = head;
                dummyRight = dummyRight.next;
            } else {
                dummyLeft.next = head;
                dummyLeft = dummyLeft.next;
            }
            //每次賦值後記得將下一個節點置位 null
            Node next = head.next;
            head.next = null;
            head = next;
        }

        right = reverseList(right.next);
        dummyLeft.next = right;

        return left.next;
    }
複製代碼

判斷兩個單鏈表(無環)是相交

題目: 判斷兩個無環鏈表是否相交,若是相交則返回第一個相交節點,若是不想交返回 null 。

咱們來分析一下這道題,咱們假設兩個單鏈表相交,那從相交的節點開始到結束,一直到兩個鏈表都結束,那麼後邊這段鏈表至關因而共享的。咱們還能夠知道若是將這兩個鏈表的末尾對齊,這兩個鏈表的尾節點必定是相等的,因此咱們的解題思路以下:

  1. 想讓一個鏈表遍歷一遍,並記錄其長度
  2. 在遍歷另外一個鏈表,遍歷過程當中 n 每次自減一
  3. 遍歷結束後,指針 cur1 指向鏈表 head1 的最後一個節點,同理指針 cur2 指向 head2 的最後一個節點,若是此時 cur1 != cur2 那麼根據題意這兩個鏈表不想交。
  4. 遍歷結束後,咱們假設 hea1 要比 head2 長,那麼 n 必定爲正數,表明了 head1 頭節點指針若是向右移動 n 個數 剩餘鏈表的長度將和 head2 同樣長
  5. 此後 point1 和 point2 一塊兒走那麼這兩個 point 指向的節點總會相等,第一次相等的點即爲兩個鏈表相交的點。
private Node intersect(Node head1, Node head2) {
        if (head1 == null || head2 == null) {
            return null;
        }

        Node cur1 = head1;
        Node cur2 = head2;

        int n = 0;

        while (cur1.next != null) {
            n++;
            cur1 = cur1.next;
        }

        while (cur2.next != null) {
            n--;
            cur2 = cur2.next;
        }

        if (cur1 != cur2) {
            return null;
        }

        //令 cur1 指向 較長的鏈表,cur2 指向較短的鏈表
        if (n > 0) {
            cur1 = head1;
            cur2 = head2;
        } else {
            cur1 = head2;
            cur2 = head1;
        }

        n = Math.abs(n);

        //較長的鏈表先走 n 步
        while (n != 0) {
            cur1 = cur1.next;
        }

        //兩個鏈表一塊兒走 第一次相等節點即爲相交的第一個節點
        while (cur1 != cur2) {
            cur1 = cur1.next;
            cur2 = cur2.next;
        }
        
        return cur1;
    }
複製代碼

總結

上篇文章搞懂排序算法評論有人說,文章太長了。沒想到這篇文章寫着寫着又這麼長了。還請你們耐下心來看,每到題本身耐下心來作一遍。等你們都搞懂之後,相信你們也就差很少無所畏懼單鏈表的面試題了。

歡迎你們關注個人我的博客地址,本文算法題也上傳到個人 github上了。NodePractice 後續我將開始學習數組,和字符串的算法題。相信不久未來又能見到個人又臭又長的文章了。

最後 願天不負有心人。

相關文章
相關標籤/搜索