面試必看算法題|鏈表問題總結

前言

鏈表的相關問題,在面試中出現頻率較高,這些問題往往也是解決其他複雜問題的基礎;
在這篇文章裏,我將梳理鏈表問題的問題 & 解法。如果能幫上忙,請務必點贊加關注,這真的對我非常重要。


目錄


1. 概述

1.1 鏈表的定義

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

1.2 鏈表的優缺點

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
    }
}

複雜度分析:

類似地,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
    }
}

複雜度分析:

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
    }
}
複製代碼

複雜度分析:

解法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
    }
}

複雜度分析:

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
    }
}

複雜度分析:

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 {
        // 略,見上一節...
    }
}

複雜度分析:

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
    }
}

複雜度分析:

23. Merge k Sorted Lists 合併K個升序鏈表 【題解】

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

解法1:暴力法

思路1:與合併兩個有序鏈表類似,每輪從 k 個鏈表中取出最小的節點,並插入結果鏈表中。其中,從 k 個數中取出最小節點的時間複雜度爲 <math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mo stretchy="false">(</mo><mi>k</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">O(k)</annotation></semantics></math>O(k)。

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

複雜度分析:

解法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
    }
}

複雜度分析:

解法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? {
        // 略,見上一節...
    }
}

複雜度分析:

解法4:小頂堆法

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
    }
}

複雜度分析:


5. 排序鏈表

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

6. 環形鏈表

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

最後

國內互聯網面試的流程逐漸在向國外靠攏,像字節跳動、BAT 等大廠,手撕算法題已經成爲了必選動作。這也是公司面試篩選人的低成本辦法,如果你寫出了算法並且通過了,要麼你聰明要麼你勤奮(刷題了)。

希望大家引起重視,好好準備。

另外,我把自己這段時間整理的Android最重要最熱門的學習方向資料放在了我的GitHub,裏面還有不同方向的自學編程路線、面試題集合/面經、及系列技術文章等。

資源持續更新中,歡迎大家一起學習和探討。

原文地址:https://juejin.cn/post/6882370280946302983

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章