點贊關注,不再迷路,你的支持對我意義重大!
?? 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 鏈表的優缺點
對比 | 優點 | 缺點 |
---|---|---|
內存管理 | 充分利用計算機內存空間,更靈活地分配內存空間 | 指針域增加了內存消耗 |
操作效率 | 能在 |
失去了數組隨機訪問的特性,查詢對應位置的節點需要 |
數據容量 | 需要預先分配內存空間,容量不足需要擴容 | 不需要預先分配內存空間,不需要擴容 |
訪問性能 | / | 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
}
}
復雜度分析:
- 時間復雜度:每個節點掃描一次,時間復雜度為
- 空間復雜度:使用了常量級別變量,空間復雜度為
類似地,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 個數中取出最小節點的時間復雜度為 。
思路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:小頂堆法
思路:在解法1中,從 k 個數中取出最小節點的時間復雜度為 ,可以使用最小堆(優先隊列)來優化到
。其中,堆內節點始終是 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 的二叉堆建堆時間為
,取堆頂的時間為
,插入一個新節點的時間為
,總體時間復雜度為
- 空間復雜度:二叉堆空間為
5. 排序鏈表
147. Insertion Sort List 對鏈表進行插入排序 |【題解】
148. Sort List 排序鏈表 【題解】
6. 環形鏈表
鏈表相交 & 成環問題可以歸為一類問題,在面試中出現頻率較高;在之前的一篇文章里,我們單獨討論過:《算法面試題 | 鏈表相交 & 成環問題》
創作不易,你的「三連」是丑丑最大的動力,我們下次見!