鏈表部分的算法

鏈表部分:

鏈表部分:

[圖片上傳失敗...(image-b2bb7d-1649069467758)]

1.反轉單雙鏈表

//迭代的方式
 private ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }
    //2.反轉鏈表的遞歸方式
    private ListNode reverse(ListNode head){
        if(head == null || head.next == null){
            return head;
        }
        ListNode node = reverse(head.next);
        head.next.next = head;
        head.next = null;
        return node;
    }

2.打印倆有序鏈表的公共部分

方法論:

時間復雜度最低即可

1.額外的數據結構記錄

2.快慢指針的方式

進階

1.回文鏈表

筆試版:

1.借助棧

2.快慢指針

整個流程可以分為以下五個步驟:

  1. 找到前半部分鏈表的尾節點。
  2. 反轉后半部分鏈表。
  3. 判斷是否回文。
  4. 恢復鏈表。
  5. 返回結果。
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
 class Solution {
    public boolean isPalindrome(ListNode head) {
        if (head == null) {
            return true;
        }

        // 找到前半部分鏈表的尾節點并反轉后半部分鏈表
        ListNode firstHalfEnd = endOfFirstHalf(head);
        // ListNode secondHalfStart = reverseList(firstHalfEnd.next);
          ListNode secondHalfStart = reverse(firstHalfEnd.next);

        // 3.判斷是否回文
        ListNode p1 = head;
        ListNode p2 = secondHalfStart;
        boolean result = true;
        while (result && p2 != null) {
            if (p1.val != p2.val) {
                result = false;
            }
            p1 = p1.next;
            p2 = p2.next;
        }        

        // 4.還原鏈表并返回結果
        // firstHalfEnd.next = reverseList(secondHalfStart);
        firstHalfEnd.next = reverse(secondHalfStart);
        return result;
    }
//2.反轉后半部分鏈表。
    private ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }
    //2.反轉鏈表的遞歸方式
    private ListNode reverse(ListNode head){
        if(head == null || head.next == null){
            return head;
        }
        ListNode node = reverse(head.next);
        head.next.next = head;
        head.next = null;
        return node;
    }
//1.找到前半部分鏈表的尾節點。利用快慢指針。
    private ListNode endOfFirstHalf(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }
}

2.將單鏈表按照值劃分為左邊小,右邊大,中間相等的形式

數組Node[N]

玩快排

面試:有限的變量去做

[圖片上傳失敗...(image-76ef68-1649069467758)]

[圖片上傳失敗...(image-c32a99-1649069467758)]

存在 5 既沒有小的也沒有等的也沒有大的所以我們要判斷

防止空指針加入報錯

public static Node listPivot (Node head,int pivot){
    //六個變量保持有序且符合題意
    Node SH = null;//small head
     Node ST = null;//small tail
     Node ET = null;//equal head 
     Node EH = null;//qual tail
     Node MH = null;//big head
     Node MT = null;//big tail
    Node next = null;//save nexxt node
    
    while(head != null){
        //互指用來保存首部尾部
        next = head.next;
        head.next = null;
        
        if(head.value < pivot){
            //如果當前最小頭是新的節點。那么我們初始化最小頭和最小尾都指向該鏈表的頭。
            if(SH == null){
                SH = head;
                ST = head;
            }
             //如果當前節點不是最新的話。把老的Next指向當前節點head。 NBA。把head作為新的最小頭的尾巴。
        }else{
                ST.next = head;
                ST = head;
        }else  if(head.value = pivot){
            if(EH == null){
                EH = head;
                ET = head;
            }       
        }else{
                ET.next = head;
                ET = head;
        } else if(head.value > pivot){
            if(MH == null){
                MH = head;
                MT = head;
            }
             
        }else{
                MH.next = head;
                MT = head;
        }
        head = head.next;
    }
    
    
    //鏈接:
    // ST -> EH 和 ET -> MH;
    //充分討論有沒有大于下雨區域
    if(ST != null){ //如果有小于的區域
        ST.next = EH;
        ET = ET == null ? ST : ET;//誰去來凝結大的區域的頭誰就變成eT
    }
    if(ET != null){
        ET.next = MH;
        }
    //**************
    //最后定返回頭節點
    //**************
    return SH != null ? SH :(EH != null ? EH : MH);      
    }
}

3.復制含有隨機指針節點的鏈表

[圖片上傳失敗...(image-82b98b-1649069467758)]

Hash表來存儲

<key,value> 老節點對應的新節點

[圖片上傳失敗...(image-e21780-1649069467758)]

不用HashMap

1.跟隨設置,復制節點不要設置

[圖片上傳失敗...(image-1fcb63-1649069467758)]

2.跟隨的next位置設置

[圖片上傳失敗...(image-28d6df-1649069467758)]

3.分離出來

public static Node copyListWithRand(Node head){
    if(head == null){
        return null;
    }
    Node cur = head;
    Node next = null;
    //1. 在每一個原始節點后面跟隨設置他的copy節點。例如1 - > 1"
    while(cur !=null){
        next = cur.next;
        cur.next = new Node(cur.val);
        cur.next.next = next;
        cur = next;
    }
    //重置car為head即將當前的節點,作為該鏈表的頭。再來一次便利。
    cur = head;
    Node curCopy = null;
    //2.按照該鏈表原來的方式設置。它的Next和Random。
    while(cur != null){
        next = cur.next.next;
        curCopy = cur.next;
        curCopy.rand = cur.rand != null ? cur.rand.next : null;
        cur = next;
    }
    Node res = head.next;
    cur = head;
    
    //將拷貝節點全部切分出去。
    while(cur != null){
        next = cur.next.next;
        curCopy = cur.next;
        cur.next = next;
        curCopy.next = next != null? next.next : null;
        cur = next;
    }
    return res;
}

4.單鏈表相交

[圖片上傳失敗...(image-24cb9e-1649069467758)]

利用HashSet來遍歷

有沒有重復

[圖片上傳失敗...(image-b6a312-1649069467758)]

快慢指針經典

相遇后:

快指針回到head 以后每次走一步,就一定會在入口相遇

鏈表題目練習

鏈表

提綱

鏈表相關的核心點

  • null 異常處理
  • dummy node 啞巴節點
  • 快慢指針
  • 插入一個節點到排序鏈表
  • 從一個鏈表中移除一個節點
  • 翻轉鏈表
  • 合并兩個鏈表
  • 找到鏈表的中間節點

基本操作

鏈表刪除

刪除排序鏈表中的重復元素

83. 刪除排序鏈表中的重復元素

給定一個排序鏈表,刪除所有重復的元素,使得每個元素只出現一次。

public ListNode deleteDuplicates(ListNode head) {
    ListNode p = head;
    while (p != null) {
        // 全部刪除完再移動到下一個元素
        while (p.next != null && p.val == p.next.val) {
            p.next = p.next.next;
        }
        p = p.next;
    }
    return head;
}

刪除排序鏈表中的重復元素 II

82. 刪除排序鏈表中的重復元素 II

給定一個排序鏈表,刪除所有含有重復數字的節點,只保留原始鏈表中 沒有重復出現的數字。

思路:鏈表頭結點可能被刪除,所以用 dummy node 輔助刪除

public ListNode deleteDuplicates(ListNode head) {
    if (head == null) {
        return null;
    }
    ListNode newHead = new ListNode(-1, head);
    ListNode p = newHead;
    int n = 0;
    while (p.next != null && p.next.next != null) {
        if (p.next.val == p.next.next.val) {
            // 記錄已經刪除的值,用于后續節點判斷
            n = p.next.val;
            while (p.next != null && p.next.val == n) {
                p.next = p.next.next;
            }
        } else {
            p = p.next;
        }
    }
    return newHead.next;
}

注意點

  • A->B->C 刪除 B,A.next = C
  • 刪除用一個 Dummy Node 節點輔助(允許頭節點可變)
  • 訪問 X.next 、X.value 一定要保證 X != nil

鏈表反轉

反轉鏈表

206. 反轉鏈表

反轉一個單鏈表。

思路:用一個 prev 節點保存向前指針,temp 保存向后的臨時指針

public ListNode reverseList(ListNode head) {
    ListNode pre = null, p = head;
    while (p != null) {
        // 保存當前head.Next節點,防止重新賦值后被覆蓋
        // 一輪之后狀態:nil<-1 2->3->4
        //               prev p
        ListNode temp = p.next;
        p.next = pre;
        // pre 移動
        pre = p;
        // p 移動
        p = temp;
    }
    return pre;
}

反轉鏈表 II

92. 反轉鏈表 II

反轉從位置 mn 的鏈表。請使用一趟掃描完成反轉。

思路:先遍歷到 m 處,翻轉,再拼接后續,注意指針處理

public ListNode reverseBetween(ListNode head, int m, int n) {
    // 思路:先遍歷到m處,翻轉,再拼接后續,注意指針處理
    // 輸入: 1->2->3->4->5->null, m = 2, n = 4
    ListNode newHead = new ListNode(0, head);
    ListNode p = newHead;
    // 最開始:0(p)->1->2->3->4->5->null
    for (int i = 0; i < m-1; i++) {
        p = p.next;
    }
    // 遍歷之后: 0->1(p)->2(cur)->3->4->5->null
    ListNode pre = null;
    ListNode cur = p.next;
    for (int i = m; i <= n; i++) {
        ListNode next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    // 循環結束:0->1(p)->2->null 5(cur)->null 4(pre)->3->2->null
    p.next.next = cur;
    p.next = pre;
    return newHead.next;
}

鏈表合并

合并兩個有序鏈表

21. 合并兩個有序鏈表

將兩個升序鏈表合并為一個新的升序鏈表并返回。新鏈表是通過拼接給定的兩個鏈表的所有節點組成的。

思路:通過 dummy node 鏈表,連接各個元素

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode head = new ListNode(0);
    ListNode p = head;
    while (l1 != null && l2 != null) {
        if (l1.val < l2.val) {
            p.next = l1;
            l1 = l1.next;
        } else {
            p.next = l2;
            l2 = l2.next;
        }
        p = p.next;
    }
    // 連接未處理完節點
    p.next = l1 == null ? l2 : l1;
    return head.next;
}

合并K個升序鏈表

23. 合并K個升序鏈表

給你一個鏈表數組,每個鏈表都已經按升序排列。

請你將所有鏈表合并到一個升序鏈表中,返回合并后的鏈表。

思路:使用分治的方法兩個兩個地合并鏈表

public ListNode mergeKLists(ListNode[] lists) {
    return merge(lists, 0, lists.length - 1);
}

public ListNode merge(ListNode[] lists, int begin, int end) {
    if (begin == end) return lists[begin];
    if (begin > end) return null;
    int mid = (begin + end) >> 1;
    return mergeTwoLists(merge(lists, begin, mid), merge(lists, mid + 1, end));
}

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    // 同上
}

快慢指針

鏈表中點

使用兩個指針變量,慢指針每次前進一步,快指針每次前進兩步。這樣當快指針到達鏈表末尾時,慢指針恰好在鏈表的中間位置。要注意鏈表長度為偶數的情況。

876. 鏈表的中間結點

給定一個頭結點為 head 的非空單鏈表,返回鏈表的中間結點。

如果有兩個中間結點,則返回第二個中間結點。

public ListNode middleNode(ListNode head) {
    ListNode p = head;
    ListNode q = head;
    while (q != null && q.next != null) {
        p = p.next;
        q = q.next.next;
    }
    return p;
}

重排鏈表

143. 重排鏈表
給定一個單鏈表 L:L0→L1→…→Ln-1→Ln ,

將其重新排列后變為: L0→Ln→L1→Ln-1→L2→Ln-2→…

不能只是單純的改變節點內部的值,而是需要實際的進行節點交換。

public void reorderList(ListNode head) {
    if (head == null || head.next == null) return;
    // 通過快慢指針找中點
    ListNode slow = head, fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    ListNode p = head;
    // 反轉鏈表
    ListNode q = reverseList(slow.next);
    slow.next = null;
    while (p != null && q != null) {
        ListNode qNext = q.next;
        q.next = p.next;
        p.next = q;
        p = q.next;
        q = qNext;
    }
}

回文鏈表

234. 回文鏈表

請判斷一個鏈表是否為回文鏈表。

public boolean isPalindrome(ListNode head) {
     // fast如果初始化為head.Next則中點在slow.Next
    // fast初始化為head,則中點在slow
    ListNode slow = head, fast = head, pre = null;
    // 這里順便做了反轉鏈表的操作
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        ListNode next = slow.next;
        slow.next = pre;
        pre = slow;
        slow = next;
    }
    if (fast != null){
        slow = slow.next;
    }
    // 與另一半鏈表依次比較
    while (slow != null) {
        if (slow.val != pre.val) return false;
        slow = slow.next;
        pre = pre.next;
    }
    return true;
}

結構判斷

環形鏈表

141. 環形鏈表

給定一個鏈表,判斷鏈表中是否有環。

如果鏈表中存在環,則返回 true。 否則,返回false

思路:快慢指針,快慢指針相同則有環,證明:如果有環每走一步快慢指針距離會減 1
[圖片上傳失敗...(image-8b9f58-1649069467758)]

public boolean hasCycle(ListNode head) {
    ListNode p = head, q = head;
    // 思路:快慢指針 快慢指針相同則有環,證明:如果有環每走一步快慢指針距離會減1
    while (p != null && q != null && q.next != null) {
        p = p.next;
        q = q.next.next;
        // 比較指針是否相等(不要使用val比較)
        if (p == q) {
            return true;
        }
    }
    return false;
}

環形鏈表 II

142. 環形鏈表 II

給定一個鏈表,返回鏈表開始入環的第一個節點。 如果鏈表無環,則返回 null

思路:快慢指針,快慢相遇之后,慢指針回到頭,快慢指針步調一致一起移動,相遇點即為入環點
[圖片上傳失敗...(image-b266f5-1649069467758)]

public ListNode detectCycle(ListNode head) {
    // 思路:快慢指針,快慢相遇之后,慢指針回到頭,快慢指針步調一致一起移動,相遇點即為入環點
    ListNode p = head, q = head;
    while (p != null && q != null && q.next != null) {
        p = p.next;
        q = q.next.next;
        if (p == q) {
            // 指針重新從頭開始移動
            ListNode m = head;
            // 比較指針對象(不要比對指針Val值)
            while (m != p) {
                m = m.next;
                p = p.next;
            }
            return p;
        }
    }
    return null;
}

其他

19. 刪除鏈表的倒數第 N 個結點

給你一個鏈表,刪除鏈表的倒數第 n 個結點,并且返回鏈表的頭結點。(嘗試使用一趟掃描實現)

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode newHead = new ListNode(0, head);
    ListNode p1 = newHead;
    ListNode p2 = newHead;
    // 提前前進n個位置
    while (n >= 0) {
        p2 = p2.next;
        n--;
    }
    while (p2 != null) {
        p1 = p1.next;
        p2 = p2.next;
    }
    p1.next = p1.next.next;
    return newHead.next;
}

138. 復制帶隨機指針的鏈表

給定一個鏈表,每個節點包含一個額外增加的隨機指針,該指針可以指向鏈表中的任何節點或空節點。

要求返回這個鏈表的 深拷貝。

思路:1、hash 表存儲指針,2、復制節點跟在原節點后面

public Node copyRandomList(Node head) {
    if (head == null) {
        return null;
    }
    // 復制節點,緊挨到到后面
    // 1->2->3  ==>  1->1'->2->2'->3->3'
    Node cur = head;
    while (cur != null) {
        Node cloneNode = new Node(cur.val);
        cloneNode.next = cur.next;
        Node temp = cur.next;
        cur.next = cloneNode;
        cur = temp;
    }
    // 處理random指針
    cur = head;
    while (cur != null) {
        if (cur.random != null) {
            cur.next.random = cur.random.next;
        }
        cur = cur.next.next;
    }
    // 分離兩個鏈表
    cur = head;
    Node cloneHead = cur.next;
    while (cur != null && cur.next != null) {
        Node temp = cur.next;
        cur.next = cur.next.next;
        cur = temp;
    }
    // 原始鏈表頭:head 1->2->3
    // 克隆的鏈表頭:cloneHead 1'->2'->3'
    return cloneHead;
}

總結

鏈表必須要掌握的一些點,通過下面練習題,基本大部分的鏈表類的題目都是手到擒來~

  • null 異常處理
  • dummy node 啞巴節點
  • 快慢指針
  • 插入一個節點到排序鏈表
  • 從一個鏈表中移除一個節點
  • 翻轉鏈表
  • 合并兩個鏈表
  • 找到鏈表的中間節點
  • 鏈表的末尾指針最后指向null
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容