前言
鏈表的相關問題,在面試中出現頻率較高,這些問題往往也是解決其他複雜問題的基礎;
在這篇文章裏,我將梳理鏈表問題的問題 & 解法。如果能幫上忙,請務必點贊加關注,這真的對我非常重要。
目錄
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,裏面還有不同方向的自學編程路線、面試題集合/面經、及系列技術文章等。
資源持續更新中,歡迎大家一起學習和探討。