「數據結構」| 鏈表問題總結

點贊關注,不再迷路,你的支持對我意義重大!

?? Hi,我是丑丑。本文「數據結構 & 算法」| 導讀 —— 登高博見 已收錄,這里有 Android 進階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長。(聯系方式在 GitHub)

前言

  • 鏈表的相關問題,在面試中出現頻率較高,這些問題往往也是解決其他復雜問題的基礎;
  • 在這篇文章里,我將梳理鏈表問題的問題 & 解法。如果能幫上忙,請務必點贊加關注,這真的對我非常重要。
刪除鏈表節點 提示 & 題解
203. 移除鏈表元素
Remove Linked List Elements
【題解】
237. 刪除鏈表中的節點
Delete Node in a Linked List
【題解】
19. 刪除鏈表的倒數第N個節點
Remove Nth Node From End of List
【題解】
86. 分隔鏈表
Partition List
【題解】
328. 奇偶鏈表
Odd Even Linked List
【題解】
83. 刪除排序鏈表中的重復元素
Remove Duplicates from Sorted List
82. 刪除排序鏈表中的重復元素 II
Remove Duplicates from Sorted List II
725. 分隔鏈表
Split Linked List in Parts
(擴展)0876. 鏈表的中間結點
Middle of the Linked List
【題解】
反轉鏈表 提示 & 題解
206. 反轉鏈表
Reverse Linked List
【題解】
92. 反轉鏈表 II
Reverse Linked List II
【題解】
234. 回文鏈表
Palindrome Linked List
【題解】
25. K 個一組翻轉鏈表
Reverse Nodes in k-Group
合并有序鏈表 提示 & 題解
21. 合并兩個有序鏈表
Merge Two Sorted Lists
【題解】
23. 合并K個升序鏈表
Merge k Sorted Lists
【題解】
排序鏈表 提示 & 題解
147. 對鏈表進行插入排序
Insertion Sort List
【題解】
148. 排序鏈表
Sort List
【題解】
環形鏈表 提示 & 題解
160. 相交鏈表
Intersection of Two Linked Lists
【題解】
141. 環形鏈表
Linked List Cycle
【題解】
142. 環形鏈表 II
Linked List Cycle II
【題解】
61. 旋轉鏈表
Rotate List
【題解】
其他 提示 & 題解
24. 兩兩交換鏈表中的節點
Swap Nodes in Pairs
143. 重排鏈表
Reorder List
138. 復制帶隨機指針的鏈表
Copy List with Random Pointer
380. 常數時間插入、刪除和獲取隨機元素
Insert Delete GetRandom O(1)
381. O(1) 時間插入、刪除和獲取隨機元素 - 允許重復
Insert Delete GetRandom O(1) - Duplicates allowed
707. 設計鏈表
Design Linked List
430. 扁平化多級雙向鏈表
Flatten a Multilevel Doubly Linked List
817. 鏈表組件
Linked List Components
【題解】

目錄


1. 概述

1.1 鏈表的定義

鏈表是一種常見的基礎數據結構,是一種線性表。與順序表不同的是,鏈表中的每個節點不是順序存儲的,而是通過節點的指針域指向到下一個節點。

1.2 鏈表的優缺點

對比 優點 缺點
內存管理 充分利用計算機內存空間,更靈活地分配內存空間 指針域增加了內存消耗
操作效率 能在 O(1) 時間內刪除或添加節點(前提是前驅節點已知) 失去了數組隨機訪問的特性,查詢對應位置的節點需要 O(n) 時間
數據容量 需要預先分配內存空間,容量不足需要擴容 不需要預先分配內存空間,不需要擴容
訪問性能 / CPU 緩存行無法提高效率

1.3 鏈表的類型

單鏈表、雙鏈表、循環鏈表、靜態鏈表


2. 刪除鏈表節點

刪除鏈表節點時,考慮到可能刪除的是鏈表的第一個節點(沒有前驅節點),為了編碼方便,可以考慮增加一個 哨兵節點。其中,在刪除鏈表的倒數第 N 個節點問題里,使用快慢指針在一趟掃描里找出倒數第 N 個節點是比較重要的編程技巧。

237. Delete Node in a Linked List 刪除鏈表中的節點 【題解】
203. Remove Linked List Elements 移除鏈表元素 【題解】
不移除野指針
class Solution {
    fun removeElements(head: ListNode?, `val`: Int): ListNode? {
        // 哨兵節點
        val sentinel = ListNode(-1)
        sentinel.next = head

        var pre = sentinel
        var cur: ListNode? = sentinel
        while (null != cur) {
            if (`val` == cur.`val`) {
                // 移除
                pre.next = cur.next
            } else {
                pre = cur
            }
            cur = cur.next
        }
        return sentinel.next
    }
}

移除野指針
class Solution {
    fun removeElements(head: ListNode?, `val`: Int): ListNode? {
        // 哨兵節點
        val sentinel = ListNode(-1)
        sentinel.next = head

        var pre = sentinel
        var cur: ListNode? = sentinel
        while (null != cur) {
            val removeNode = if (`val` == cur.`val`) {
                // 移除
                pre.next = cur.next
                cur
            } else {
                pre = cur
                null
            }
            cur = cur.next
            if (null != removeNode) {
                removeNode.next = null
            }
        }
        return sentinel.next
    }
}
19. Remove Nth Node From End of List 刪除鏈表的倒數第N個節點 【題解】

給定一個鏈表,刪除鏈表的倒數第 n 個節點,并且返回鏈表的頭結點。

class Solution {
    fun removeNthFromEnd(head: ListNode, n: Int): ListNode? {
        // 哨兵節點
        val sentinel = ListNode(-1)
        sentinel.next = head

        var fast: ListNode? = sentinel
        var slow: ListNode? = sentinel

        for (index in 0 until n) {
            fast = fast!!.next
        }

        // 找到倒數第 k 個節點的前驅
        while (null != fast!!.next) {
            fast = fast.next
            slow = slow!!.next
        }
        slow!!.next = slow.next!!.next
        return sentinel.next
    }
}

復雜度分析:

  • 時間復雜度:每個節點掃描一次,時間復雜度為 O(n)
  • 空間復雜度:使用了常量級別變量,空間復雜度為 O(1)

類似地,876. Middle of the Linked List 鏈表的中間結點 【題解】 也是通過快慢指針來找到中間節點的:

class Solution {
    fun middleNode(head: ListNode?): ListNode? {
        if (null == head || null == head.next) {
            return head
        }
        var fast = head
        var slow = head

        while (null != fast && null != fast.next) {
            fast = fast.next!!.next
            slow = slow!!.next
        }

        return slow
    }
}
86. Partition List 分隔鏈表 【題解】

刪除鏈表中等于給定值 val 的所有節點。

思路:分隔鏈表無非是先將大于等于 val 的節點從原鏈表中移除到第二個鏈表中,最后再拼接兩個鏈表。

class Solution {
    fun partition(head: ListNode?, x: Int): ListNode? {
        if (null == head) {
            return null
        }

        // 哨兵節點
        val sentinel = ListNode(-1)
        sentinel.next = head
        var pre = sentinel
        // 第二鏈表
        var bigHead : ListNode? = null
        var bigRear = bigHead

        var cur = head
        while (null != cur) {
            if (cur.`val` >= x) {
                // 大于等于:移除
                pre.next = cur.next
                if(null == bigHead){
                    bigHead = cur
                    bigRear = cur
                }else{
                    bigRear!!.next = cur
                    bigRear = cur
                }
            } else {
                pre = cur
            }
            if (null == cur.next) {
                // 拼接
                pre.next = bigHead
                bigRear?.next = null
                break
            }
            cur = cur.next
        }
        return sentinel.next
    }
}

復雜度分析:

  • 時間復雜度:每個節點掃描一次,時間復雜度為 O(n)
  • 空間復雜度:使用了常量級別變量,空間復雜度為 O(1)
328. Odd Even Linked List 奇偶鏈表 【題解】

思路:奇偶鏈表無非是先將奇節點放在一個鏈表里,偶節點放在另一個鏈表里,最后把偶節點接在奇鏈表的尾部

class Solution {
    fun oddEvenList(head: ListNode?): ListNode? {
        if (null == head) {
            return null
        }

        var odd: ListNode = head
        var even = head.next
        val evenHead = even

        while (null != even && null != even.next) {
            // 偶節點
            odd.next = even.next
            odd = odd.next!!
            // 奇節點
            even.next = odd.next
            even = even.next
        }
        odd.next = evenHead
        // 頭節點不動
        return head
    }
}
83. Remove Duplicates from Sorted List 刪除排序鏈表中的重復元素
82. Remove Duplicates from Sorted List II 刪除排序鏈表中的重復元素 II

3. 反轉鏈表

反轉鏈表問題在面試中出現頻率 非常非常高,相信有過幾次面試經驗的同學都會同意這個觀點。在這里,我找出了 4 道反轉鏈表的問題,從簡單延伸到困難,快來試試吧。

206. 反轉鏈表 Reverse Linked List 【題解】

反轉一個單鏈表。

解法1:遞歸

class Solution {
    fun reverseList(head: ListNode?): ListNode? {
        if(null == head || null == head.next){
            return head
        }
        val prefix = reverseList(head.next)
        head.next.next = head
        head.next = null
        return prefix
    }
}

復雜度分析:

  • 時間復雜度:每個節點掃描一次,時間復雜度為 O(n)
  • 空間復雜度:使用了遞歸棧,空間復雜度為 O(n)

解法2:迭代

class Solution {
    fun reverseList(head: ListNode?): ListNode? {
        var cur: ListNode? = head
        var headP: ListNode? = null

        while (null != cur) {
            val tmp = cur.next
            cur.next = headP
            headP = cur
            cur = tmp
        }
        return headP
    }
}

復雜度分析:

  • 時間復雜度:每個節點掃描一次,時間復雜度為 O(n)
  • 空間復雜度:使用了常量級別變量,空間復雜度為 O(1)
92. 反轉鏈表 II Reverse Linked List II 【題解】

給定一個鏈表,旋轉鏈表,將鏈表每個節點向右移動 k 個位置,其中 k 是非負數。

class Solution {
    fun reverseBetween(head: ListNode?, m: Int, n: Int): ListNode? {
        if (null == head || null == head.next) {
            return head
        }

        // 哨兵節點
        val sentinel = ListNode(-1)
        sentinel.next = head
        var rear = sentinel

        // 1. 找到反轉開始位置前驅節點
        var cur = sentinel
        for (index in 0 until m - 1) {
            cur = cur.next!!
            rear = cur
        }

        // 2. 反轉指定區域
        rear.next = reverseList(rear.next!!, n - m + 1)
        return sentinel.next
    }

    /**
     * 反轉指定區域
     * @param size 長度
     */
    fun reverseList(head: ListNode, size: Int): ListNode? {
        var cur: ListNode? = head
        var headP: ListNode? = null
        // 反轉的起始點需要連接到第 n 個節點
        val headTemp = head

        var count = 0
        while (null != cur && count < size) {
            val tmp = cur.next
            cur.next = headP
            headP = cur
            cur = tmp

            count++
        }

        // 連接到第 n 個節點
        headTemp.next = cur
        return headP
    }
}

復雜度分析:

  • 時間復雜度:每個節點掃描一次,時間復雜度為 O(n)
  • 空間復雜度:使用了常量級別變量,空間復雜度為 O(1)
234. Palindrome Linked List 回文鏈表 【題解】

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

思路:使用快慢指針找到中間節點,反轉后半段鏈表(基于反轉鏈表 II),比較前后兩段鏈表是否相同,最后再反轉回復到原鏈表。

class Solution {
    fun isPalindrome(head: ListNode?): Boolean {
        if (null == head || null == head.next) {
            return true
        }

        // 1. 找到右邊中節點(右中節點)
        var fast = head
        var slow = head

        while (null != fast && null != fast.next) {
            slow = slow!!.next
            fast = fast.next!!.next
        }

        // 2. 反轉后半段
        val reverseP = reverseList(slow!!)

        // 3. 比較前后兩段是否相同
        var p = head
        var q: ListNode? = reverseP
        var isPalindrome = true

        while (null != p && null != q) {
            if (p.`val` == q.`val`) {
                p = p.next
                q = q.next
            } else {
                isPalindrome = false
                break
            }
        }

        // 4. 恢復鏈表
        reverseList(reverseP)
        return isPalindrome
    }

    /**
     * 反轉鏈表
     */
    private fun reverseList(head: ListNode): ListNode {
        // 略,見上一節...
    }
}

復雜度分析:

  • 時間復雜度:每個節點掃描兩次,時間復雜度為 O(n)
  • 空間復雜度:使用了常量級別變量,空間復雜度為 O(1)
25. K 個一組翻轉鏈表 Reverse Nodes in k-Group

給你一個鏈表,每 k 個節點一組進行翻轉,請你返回翻轉后的鏈表。


4. 合并有序鏈表

合并有序鏈表問題在面試中出現頻率 較高,其中,合并兩個有序鏈表 是比較簡單的,而它的進階版 合并K個升序鏈表 要考慮的因素更全面,難度也有所增強,快來試試吧。

21. Merge Two Sorted Lists 合并兩個有序鏈表 【題解】

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

class Solution {
    fun mergeTwoLists(l1: ListNode?, l2: ListNode?): ListNode? {
        if (null == l1) return l2
        if (null == l2) return l1

        // 哨兵節點
        val sentinel = ListNode(-1)
        var rear = sentinel

        var p = l1
        var q = l2

        while (null != p && null != q) {
            if (p.`val` < q.`val`) {
                rear.next = p
                rear = p
                p = p.next
            } else {
                rear.next = q
                rear = q
                q = q.next
            }
        }
        rear.next = if (null != p) p else q

        return sentinel.next
    }
}

復雜度分析:

  • 時間復雜度:每個節點掃描一次,時間復雜度為 O(m + n)
  • 空間復雜度:使用了常量級別變量,空間復雜度為 O(1)
23. Merge k Sorted Lists 合并K個升序鏈表 【題解】

給你一個鏈表數組,每個鏈表都已經按升序排列。請你將所有鏈表合并到一個升序鏈表中,返回合并后的鏈表。

解法1:暴力法

思路1:與合并兩個有序鏈表類似,每輪從 k 個鏈表中取出最小的節點,并插入結果鏈表中。其中,從 k 個數中取出最小節點的時間復雜度為 O(k)

思路2:這個思路與上個思路類似,時間復雜度和空間復雜度頁相同,即:依次將 k 個鏈表與結果鏈表合并。

復雜度分析:

  • 時間復雜度:O(nk * k)
  • 空間復雜度:O(1)

解法2:排序法

思路:用一個數組保存所有節點之后,進行快速排序,隨后將數組輸出單鏈表。

class Solution {
    fun mergeKLists(lists: Array<ListNode?>): ListNode? {
        if (lists.isNullOrEmpty()) {
            return null
        }
        // 1. 用一個數組保存所有節點
        val array = ArrayList<ListNode>()
        for (list in lists) {
            var cur = list
            while (null != cur) {
                array.add(cur)
                cur = cur.next
            }
        }
        // 2. 快速排序
        array.sortWith(Comparator { node1, node2 -> node1.`val` - node2.`val` })
        // 3. 輸出為鏈表
        val newHead = ListNode(-1)
        var rear = newHead
        for (node in array) {
            rear.next = node
            rear = node
        }
        return newHead.next
    }
}

復雜度分析:

  • 時間復雜度:合并節點時間 O(nk),快速排序時間 O(nk*lgnk),輸出單鏈表時間 O(nk),總體時間復雜度 O(nk*lgnk)
  • 空間復雜度:使用數組空間 O(nk)

解法3:歸并法

思路:將 k 組鏈表分為兩部分,然后遞歸地處理兩組鏈表,最后再合并起來。

class Solution {

    // 合并 k 個有序鏈表
    fun mergeKLists(lists: Array<ListNode?>): ListNode? {
        if (lists.isNullOrEmpty()) {
            return null
        }
        return mergeKLists(lists, 0, lists.size - 1)
    }

    fun mergeKLists(lists: Array<ListNode?>, left: Int, right: Int): ListNode? {
        if (left == right) {
            return lists[left]
        }
        // 歸并
        val mid = (left + right) ushr 1
        return mergeTwoLists(
            mergeKLists(lists, left, mid),
            mergeKLists(lists, mid + 1, right)
        )
    }

    // 合并兩個有序鏈表
    fun mergeTwoLists(l1: ListNode?, l2: ListNode?): ListNode? {
        // 略,見上一節...
    }
}

復雜度分析:

  • 時間復雜度:時間主要在合并鏈表的操作上,從遞歸樹可以看出,遞歸樹每一層的節點個數都是 nk,而遞歸樹的高度 h = lgk,因此總的時間復雜度為 O(nk*lgk)
  • 空間復雜度:使用了遞歸棧,空間復雜度為 O(lgk)

解法4:小頂堆法

思路:在解法1中,從 k 個數中取出最小節點的時間復雜度為 O(k),可以使用最小堆(優先隊列)來優化到 O(lgk)。其中,堆內節點始終是 k 個鏈表的未處理部分的表頭。

class Solution {

    // 合并 k 個有序鏈表
    fun mergeKLists(lists: Array<ListNode?>): ListNode? {
        if (lists.isNullOrEmpty()) {
            return null
        }

        // 最小堆
        val queue = PriorityQueue<ListNode>(lists.size) { node1, node2 -> node1.`val` - node2.`val` }

        // 1. 建堆
        for (list in lists) {
            if (null != list) {
                queue.offer(list)
            }
        }

        val sentinel = ListNode(-1)
        var rear = sentinel

        // 2. 出隊
        while (queue.isNotEmpty()) {
            val node = queue.poll()!!
            // 輸出到結果鏈表
            rear.next = node
            rear = node
            // 存在后繼節點,加入堆中
            if (null != node.next) {
                queue.offer(node.next)
            }
        }
        return sentinel.next
    }
}

復雜度分析:

  • 時間復雜度:大小為 k 的二叉堆建堆時間為 O(k),取堆頂的時間為 O(1),插入一個新節點的時間為 O(lgk),總體時間復雜度為 O(nk?lgk)
  • 空間復雜度:二叉堆空間為 O(k)

5. 排序鏈表

147. Insertion Sort List 對鏈表進行插入排序 |【題解】
148. Sort List 排序鏈表 【題解】

6. 環形鏈表

鏈表相交 & 成環問題可以歸為一類問題,在面試中出現頻率較高;在之前的一篇文章里,我們單獨討論過:《算法面試題 | 鏈表相交 & 成環問題》


創作不易,你的「三連」是丑丑最大的動力,我們下次見!

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