點(diǎn)贊關(guān)注,不再迷路,你的支持對(duì)我意義重大廉油!
?? Hi惠险,我是丑丑。本文「數(shù)據(jù)結(jié)構(gòu) & 算法」| 導(dǎo)讀 —— 登高博見 已收錄抒线,這里有 Android 進(jìn)階成長路線筆記 & 博客班巩,歡迎跟著彭丑丑一起成長。(聯(lián)系方式在 GitHub)
前言
- 鏈表的相關(guān)問題嘶炭,在面試中出現(xiàn)頻率較高抱慌,這些問題往往也是解決其他復(fù)雜問題的基礎(chǔ);
- 在這篇文章里眨猎,我將梳理鏈表問題的問題 & 解法遥缕。如果能幫上忙,請(qǐng)務(wù)必點(diǎn)贊加關(guān)注宵呛,這真的對(duì)我非常重要。
刪除鏈表節(jié)點(diǎn) |
提示 & 題解 |
---|---|
203. 移除鏈表元素 Remove Linked List Elements |
【題解】 |
237. 刪除鏈表中的節(jié)點(diǎn) Delete Node in a Linked List |
【題解】 |
19. 刪除鏈表的倒數(shù)第N個(gè)節(jié)點(diǎn) Remove Nth Node From End of List |
【題解】 |
86. 分隔鏈表 Partition List |
【題解】 |
328. 奇偶鏈表 Odd Even Linked List |
【題解】 |
83. 刪除排序鏈表中的重復(fù)元素 Remove Duplicates from Sorted List |
|
82. 刪除排序鏈表中的重復(fù)元素 II Remove Duplicates from Sorted List II |
|
725. 分隔鏈表 Split Linked List in Parts |
|
(擴(kuò)展)0876. 鏈表的中間結(jié)點(diǎn) Middle of the Linked List |
【題解】 |
反轉(zhuǎn)鏈表 |
提示 & 題解 |
---|---|
206. 反轉(zhuǎn)鏈表 Reverse Linked List |
【題解】 |
92. 反轉(zhuǎn)鏈表 II Reverse Linked List II |
【題解】 |
234. 回文鏈表 Palindrome Linked List |
【題解】 |
25. K 個(gè)一組翻轉(zhuǎn)鏈表 Reverse Nodes in k-Group |
合并有序鏈表 |
提示 & 題解 |
---|---|
21. 合并兩個(gè)有序鏈表 Merge Two Sorted Lists |
【題解】 |
23. 合并K個(gè)升序鏈表 Merge k Sorted Lists |
【題解】 |
排序鏈表 |
提示 & 題解 |
---|---|
147. 對(duì)鏈表進(jìn)行插入排序 Insertion Sort List |
【題解】 |
148. 排序鏈表 Sort List |
【題解】 |
環(huán)形鏈表 |
提示 & 題解 |
---|---|
160. 相交鏈表 Intersection of Two Linked Lists |
【題解】 |
141. 環(huán)形鏈表 Linked List Cycle |
【題解】 |
142. 環(huán)形鏈表 II Linked List Cycle II |
【題解】 |
61. 旋轉(zhuǎn)鏈表 Rotate List |
【題解】 |
其他 |
提示 & 題解 |
---|---|
24. 兩兩交換鏈表中的節(jié)點(diǎn) Swap Nodes in Pairs |
|
143. 重排鏈表 Reorder List |
|
138. 復(fù)制帶隨機(jī)指針的鏈表 Copy List with Random Pointer |
|
380. 常數(shù)時(shí)間插入夕凝、刪除和獲取隨機(jī)元素 Insert Delete GetRandom O(1) |
|
381. O(1) 時(shí)間插入宝穗、刪除和獲取隨機(jī)元素 - 允許重復(fù) Insert Delete GetRandom O(1) - Duplicates allowed |
|
707. 設(shè)計(jì)鏈表 Design Linked List |
|
430. 扁平化多級(jí)雙向鏈表 Flatten a Multilevel Doubly Linked List |
|
817. 鏈表組件 Linked List Components |
【題解】 |
目錄
1. 概述
1.1 鏈表的定義
鏈表是一種常見的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),是一種線性表码秉。與順序表不同的是逮矛,鏈表中的每個(gè)節(jié)點(diǎn)不是順序存儲(chǔ)的,而是通過節(jié)點(diǎn)的指針域指向到下一個(gè)節(jié)點(diǎn)转砖。
1.2 鏈表的優(yōu)缺點(diǎn)
對(duì)比 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|
內(nèi)存管理 | 充分利用計(jì)算機(jī)內(nèi)存空間须鼎,更靈活地分配內(nèi)存空間 | 指針域增加了內(nèi)存消耗 |
操作效率 | 能在 時(shí)間內(nèi)刪除或添加節(jié)點(diǎn)(前提是前驅(qū)節(jié)點(diǎn)已知) | 失去了數(shù)組隨機(jī)訪問的特性,查詢對(duì)應(yīng)位置的節(jié)點(diǎn)需要 時(shí)間 |
數(shù)據(jù)容量 | 需要預(yù)先分配內(nèi)存空間府蔗,容量不足需要擴(kuò)容 | 不需要預(yù)先分配內(nèi)存空間晋控,不需要擴(kuò)容 |
訪問性能 | / | CPU 緩存行無法提高效率 |
1.3 鏈表的類型
單鏈表、雙鏈表姓赤、循環(huán)鏈表赡译、靜態(tài)鏈表
2. 刪除鏈表節(jié)點(diǎn)
刪除鏈表節(jié)點(diǎn)時(shí),考慮到可能刪除的是鏈表的第一個(gè)節(jié)點(diǎn)(沒有前驅(qū)節(jié)點(diǎn))不铆,為了編碼方便蝌焚,可以考慮增加一個(gè) 哨兵節(jié)點(diǎn)。其中誓斥,在刪除鏈表的倒數(shù)第 N 個(gè)節(jié)點(diǎn)問題里只洒,使用快慢指針在一趟掃描里找出倒數(shù)第 N 個(gè)節(jié)點(diǎn)是比較重要的編程技巧。
237. Delete Node in a Linked List 刪除鏈表中的節(jié)點(diǎn) 【題解】
203. Remove Linked List Elements 移除鏈表元素 【題解】
不移除野指針
class Solution {
fun removeElements(head: ListNode?, `val`: Int): ListNode? {
// 哨兵節(jié)點(diǎn)
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? {
// 哨兵節(jié)點(diǎn)
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 刪除鏈表的倒數(shù)第N個(gè)節(jié)點(diǎn) 【題解】
給定一個(gè)鏈表劳坑,刪除鏈表的倒數(shù)第 n 個(gè)節(jié)點(diǎn)毕谴,并且返回鏈表的頭結(jié)點(diǎn)。
class Solution {
fun removeNthFromEnd(head: ListNode, n: Int): ListNode? {
// 哨兵節(jié)點(diǎn)
val sentinel = ListNode(-1)
sentinel.next = head
var fast: ListNode? = sentinel
var slow: ListNode? = sentinel
for (index in 0 until n) {
fast = fast!!.next
}
// 找到倒數(shù)第 k 個(gè)節(jié)點(diǎn)的前驅(qū)
while (null != fast!!.next) {
fast = fast.next
slow = slow!!.next
}
slow!!.next = slow.next!!.next
return sentinel.next
}
}
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:每個(gè)節(jié)點(diǎn)掃描一次,時(shí)間復(fù)雜度為
- 空間復(fù)雜度:使用了常量級(jí)別變量析珊,空間復(fù)雜度為
類似地羡鸥,876. Middle of the Linked List 鏈表的中間結(jié)點(diǎn) 【題解】 也是通過快慢指針來找到中間節(jié)點(diǎn)的:
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 的所有節(jié)點(diǎn)。
思路:分隔鏈表無非是先將大于等于 val 的節(jié)點(diǎn)從原鏈表中移除到第二個(gè)鏈表中忠寻,最后再拼接兩個(gè)鏈表惧浴。
class Solution {
fun partition(head: ListNode?, x: Int): ListNode? {
if (null == head) {
return null
}
// 哨兵節(jié)點(diǎn)
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
}
}
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:每個(gè)節(jié)點(diǎn)掃描一次,時(shí)間復(fù)雜度為
- 空間復(fù)雜度:使用了常量級(jí)別變量奕剃,空間復(fù)雜度為
328. Odd Even Linked List 奇偶鏈表 【題解】
思路:奇偶鏈表無非是先將奇節(jié)點(diǎn)放在一個(gè)鏈表里衷旅,偶節(jié)點(diǎn)放在另一個(gè)鏈表里,最后把偶節(jié)點(diǎn)接在奇鏈表的尾部
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) {
// 偶節(jié)點(diǎn)
odd.next = even.next
odd = odd.next!!
// 奇節(jié)點(diǎn)
even.next = odd.next
even = even.next
}
odd.next = evenHead
// 頭節(jié)點(diǎn)不動(dòng)
return head
}
}
83. Remove Duplicates from Sorted List 刪除排序鏈表中的重復(fù)元素
82. Remove Duplicates from Sorted List II 刪除排序鏈表中的重復(fù)元素 II
3. 反轉(zhuǎn)鏈表
反轉(zhuǎn)鏈表問題在面試中出現(xiàn)頻率 非常非常高纵朋,相信有過幾次面試經(jīng)驗(yàn)的同學(xué)都會(huì)同意這個(gè)觀點(diǎn)柿顶。在這里,我找出了 4 道反轉(zhuǎn)鏈表的問題操软,從簡單延伸到困難嘁锯,快來試試吧。
206. 反轉(zhuǎn)鏈表 Reverse Linked List 【題解】
反轉(zhuǎn)一個(gè)單鏈表聂薪。
解法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
}
}
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:每個(gè)節(jié)點(diǎn)掃描一次家乘,時(shí)間復(fù)雜度為
- 空間復(fù)雜度:使用了遞歸棧,空間復(fù)雜度為
解法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
}
}
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:每個(gè)節(jié)點(diǎn)掃描一次藏澳,時(shí)間復(fù)雜度為
- 空間復(fù)雜度:使用了常量級(jí)別變量仁锯,空間復(fù)雜度為
92. 反轉(zhuǎn)鏈表 II Reverse Linked List II 【題解】
給定一個(gè)鏈表,旋轉(zhuǎn)鏈表翔悠,將鏈表每個(gè)節(jié)點(diǎn)向右移動(dòng) k 個(gè)位置业崖,其中 k 是非負(fù)數(shù)。
class Solution {
fun reverseBetween(head: ListNode?, m: Int, n: Int): ListNode? {
if (null == head || null == head.next) {
return head
}
// 哨兵節(jié)點(diǎn)
val sentinel = ListNode(-1)
sentinel.next = head
var rear = sentinel
// 1. 找到反轉(zhuǎn)開始位置前驅(qū)節(jié)點(diǎn)
var cur = sentinel
for (index in 0 until m - 1) {
cur = cur.next!!
rear = cur
}
// 2. 反轉(zhuǎn)指定區(qū)域
rear.next = reverseList(rear.next!!, n - m + 1)
return sentinel.next
}
/**
* 反轉(zhuǎn)指定區(qū)域
* @param size 長度
*/
fun reverseList(head: ListNode, size: Int): ListNode? {
var cur: ListNode? = head
var headP: ListNode? = null
// 反轉(zhuǎn)的起始點(diǎn)需要連接到第 n 個(gè)節(jié)點(diǎ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 個(gè)節(jié)點(diǎn)
headTemp.next = cur
return headP
}
}
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:每個(gè)節(jié)點(diǎn)掃描一次蓄愁,時(shí)間復(fù)雜度為
- 空間復(fù)雜度:使用了常量級(jí)別變量双炕,空間復(fù)雜度為
234. Palindrome Linked List 回文鏈表 【題解】
請(qǐng)判斷一個(gè)鏈表是否為回文鏈表。
思路:使用快慢指針找到中間節(jié)點(diǎn)撮抓,反轉(zhuǎn)后半段鏈表(基于反轉(zhuǎn)鏈表 II)雄家,比較前后兩段鏈表是否相同,最后再反轉(zhuǎn)回復(fù)到原鏈表胀滚。
class Solution {
fun isPalindrome(head: ListNode?): Boolean {
if (null == head || null == head.next) {
return true
}
// 1. 找到右邊中節(jié)點(diǎn)(右中節(jié)點(diǎn))
var fast = head
var slow = head
while (null != fast && null != fast.next) {
slow = slow!!.next
fast = fast.next!!.next
}
// 2. 反轉(zhuǎn)后半段
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. 恢復(fù)鏈表
reverseList(reverseP)
return isPalindrome
}
/**
* 反轉(zhuǎn)鏈表
*/
private fun reverseList(head: ListNode): ListNode {
// 略趟济,見上一節(jié)...
}
}
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:每個(gè)節(jié)點(diǎn)掃描兩次,時(shí)間復(fù)雜度為
- 空間復(fù)雜度:使用了常量級(jí)別變量咽笼,空間復(fù)雜度為
25. K 個(gè)一組翻轉(zhuǎn)鏈表 Reverse Nodes in k-Group
給你一個(gè)鏈表顷编,每 k 個(gè)節(jié)點(diǎn)一組進(jìn)行翻轉(zhuǎn),請(qǐng)你返回翻轉(zhuǎn)后的鏈表剑刑。
4. 合并有序鏈表
合并有序鏈表問題在面試中出現(xiàn)頻率 較高媳纬,其中双肤,合并兩個(gè)有序鏈表 是比較簡單的,而它的進(jìn)階版 合并K個(gè)升序鏈表 要考慮的因素更全面钮惠,難度也有所增強(qiáng)茅糜,快來試試吧。
21. Merge Two Sorted Lists 合并兩個(gè)有序鏈表 【題解】
將兩個(gè)升序鏈表合并為一個(gè)新的 升序 鏈表并返回素挽。新鏈表是通過拼接給定的兩個(gè)鏈表的所有節(jié)點(diǎn)組成的蔑赘。
class Solution {
fun mergeTwoLists(l1: ListNode?, l2: ListNode?): ListNode? {
if (null == l1) return l2
if (null == l2) return l1
// 哨兵節(jié)點(diǎn)
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
}
}
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:每個(gè)節(jié)點(diǎn)掃描一次,時(shí)間復(fù)雜度為
- 空間復(fù)雜度:使用了常量級(jí)別變量预明,空間復(fù)雜度為
23. Merge k Sorted Lists 合并K個(gè)升序鏈表 【題解】
給你一個(gè)鏈表數(shù)組缩赛,每個(gè)鏈表都已經(jīng)按升序排列。請(qǐng)你將所有鏈表合并到一個(gè)升序鏈表中撰糠,返回合并后的鏈表酥馍。
解法1:暴力法
思路1:與合并兩個(gè)有序鏈表類似,每輪從 k 個(gè)鏈表中取出最小的節(jié)點(diǎn)阅酪,并插入結(jié)果鏈表中旨袒。其中,從 k 個(gè)數(shù)中取出最小節(jié)點(diǎn)的時(shí)間復(fù)雜度為 术辐。
思路2:這個(gè)思路與上個(gè)思路類似峦失,時(shí)間復(fù)雜度和空間復(fù)雜度頁相同,即:依次將 k 個(gè)鏈表與結(jié)果鏈表合并术吗。
略
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:
- 空間復(fù)雜度:
解法2:排序法
思路:用一個(gè)數(shù)組保存所有節(jié)點(diǎn)之后,進(jìn)行快速排序帆精,隨后將數(shù)組輸出單鏈表较屿。
class Solution {
fun mergeKLists(lists: Array<ListNode?>): ListNode? {
if (lists.isNullOrEmpty()) {
return null
}
// 1. 用一個(gè)數(shù)組保存所有節(jié)點(diǎn)
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
}
}
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:合并節(jié)點(diǎn)時(shí)間 ,快速排序時(shí)間 卓练,輸出單鏈表時(shí)間 隘蝎,總體時(shí)間復(fù)雜度
- 空間復(fù)雜度:使用數(shù)組空間
解法3:歸并法
思路:將 k 組鏈表分為兩部分,然后遞歸地處理兩組鏈表襟企,最后再合并起來嘱么。
class Solution {
// 合并 k 個(gè)有序鏈表
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)
)
}
// 合并兩個(gè)有序鏈表
fun mergeTwoLists(l1: ListNode?, l2: ListNode?): ListNode? {
// 略,見上一節(jié)...
}
}
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:時(shí)間主要在合并鏈表的操作上顽悼,從遞歸樹可以看出曼振,遞歸樹每一層的節(jié)點(diǎn)個(gè)數(shù)都是 ,而遞歸樹的高度 蔚龙,因此總的時(shí)間復(fù)雜度為
- 空間復(fù)雜度:使用了遞歸棧冰评,空間復(fù)雜度為
解法4:小頂堆法
思路:在解法1中,從 k 個(gè)數(shù)中取出最小節(jié)點(diǎn)的時(shí)間復(fù)雜度為 木羹,可以使用最小堆(優(yōu)先隊(duì)列)來優(yōu)化到 甲雅。其中解孙,堆內(nèi)節(jié)點(diǎn)始終是 k 個(gè)鏈表的未處理部分的表頭。
class Solution {
// 合并 k 個(gè)有序鏈表
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. 出隊(duì)
while (queue.isNotEmpty()) {
val node = queue.poll()!!
// 輸出到結(jié)果鏈表
rear.next = node
rear = node
// 存在后繼節(jié)點(diǎn)抛人,加入堆中
if (null != node.next) {
queue.offer(node.next)
}
}
return sentinel.next
}
}
復(fù)雜度分析:
- 時(shí)間復(fù)雜度:大小為 k 的二叉堆建堆時(shí)間為 弛姜,取堆頂?shù)臅r(shí)間為 ,插入一個(gè)新節(jié)點(diǎn)的時(shí)間為 妖枚,總體時(shí)間復(fù)雜度為
- 空間復(fù)雜度:二叉堆空間為
5. 排序鏈表
147. Insertion Sort List 對(duì)鏈表進(jìn)行插入排序 |【題解】
148. Sort List 排序鏈表 【題解】
6. 環(huán)形鏈表
鏈表相交 & 成環(huán)問題可以歸為一類問題廷臼,在面試中出現(xiàn)頻率較高;在之前的一篇文章里盅惜,我們單獨(dú)討論過:《算法面試題 | 鏈表相交 & 成環(huán)問題》
創(chuàng)作不易中剩,你的「三連」是丑丑最大的動(dòng)力,我們下次見抒寂!