算法工程師面試必考項(xiàng)——鏈表
AI作者:田旭
AI編輯:田旭
1 知識點(diǎn)
1.1 什么是鏈表
提到鏈表,我們大家都不陌生,在平時的編碼中我們也或多或少地使用過這個數(shù)據(jù)結(jié)構(gòu)。算法(第4版) (豆瓣)一書中對鏈表的定義如下:
鏈表是一種遞歸的數(shù)據(jù)結(jié)構(gòu),它或者為空(null),或者是指向一個結(jié)點(diǎn)(node)的引用,該節(jié)點(diǎn)還有一個元素和一個指向另一條鏈表的引用。
鏈表(Linked list)是一種常見的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),是一種線性表,但是并不會按線性的順序存儲數(shù)據(jù),而是在每一個節(jié)點(diǎn)里存到下一個節(jié)點(diǎn)的指針(Pointer)。由于不必須按順序存儲,鏈表在插入的時候可以達(dá)到O(1)的復(fù)雜度,比另一種線性表順序表快得多,但是查找一個節(jié)點(diǎn)或者訪問特定編號的節(jié)點(diǎn)則需要O(n)的時間,而順序表相應(yīng)的時間復(fù)雜度分別是O(logn)和O(1)。
使用鏈表結(jié)構(gòu)可以克服數(shù)組鏈表需要預(yù)先知道數(shù)據(jù)大小的缺點(diǎn),鏈表結(jié)構(gòu)可以充分利用計(jì)算機(jī)內(nèi)存空間,實(shí)現(xiàn)靈活的內(nèi)存動態(tài)管理。但是鏈表失去了數(shù)組隨機(jī)讀取的優(yōu)點(diǎn),同時鏈表由于增加了結(jié)點(diǎn)的指針域,空間開銷比較大。
1.2 鏈表結(jié)構(gòu)
1.2.1 單向鏈表
鏈表中最簡單的一種是單向鏈表,它包含兩個域,一個信息域和一個指針域。這個鏈接指向列表中的下一個節(jié)點(diǎn),而最后一個節(jié)點(diǎn)則指向一個空值。

一個單向鏈表包含兩個值: 當(dāng)前節(jié)點(diǎn)的值和一個指向下一個節(jié)點(diǎn)的鏈接
一個單向鏈表的節(jié)點(diǎn)被分成兩個部分。第一個部分保存或者顯示關(guān)于節(jié)點(diǎn)的信息,第二個部分存儲下一個節(jié)點(diǎn)的地址。單向鏈表只可向一個方向遍歷。
1.2.2 雙向鏈表
一種更復(fù)雜的鏈表是“雙向鏈表”或“雙面鏈表”。每個節(jié)點(diǎn)有兩個連接:一個指向前一個節(jié)點(diǎn),(當(dāng)此“連接”為第一個“連接”時,指向空值或者空列表);而另一個指向下一個節(jié)點(diǎn),(當(dāng)此“連接”為最后一個“連接”時,指向空值或者空列表)。

一個雙向鏈表有三個整數(shù)值: 數(shù)值, 向后的節(jié)點(diǎn)鏈接, 向前的節(jié)點(diǎn)鏈接
雙向鏈表也叫雙鏈表。雙向鏈表中不僅有指向后一個節(jié)點(diǎn)的指針,還有指向前一個節(jié)點(diǎn)的指針。這樣可以從任何一個節(jié)點(diǎn)訪問前一個節(jié)點(diǎn),當(dāng)然也可以訪問后一個節(jié)點(diǎn),以至整個鏈表。一般是在需要大批量的另外儲存數(shù)據(jù)在鏈表中的位置的時候用。雙向鏈表也可以配合下面的其他鏈表的擴(kuò)展使用。
1.2.3 循環(huán)鏈表
在一個 循環(huán)鏈表中, 首節(jié)點(diǎn)和末節(jié)點(diǎn)被連接在一起。這種方式在單向和雙向鏈表中皆可實(shí)現(xiàn)。要轉(zhuǎn)換一個循環(huán)鏈表,你開始于任意一個節(jié)點(diǎn)然后沿著列表的任一方向直到返回開始的節(jié)點(diǎn)。再來看另一種方法,循環(huán)鏈表可以被視為“無頭無尾”。這種列表很利于節(jié)約數(shù)據(jù)存儲緩存, 假定你在一個列表中有一個對象并且希望所有其他對象迭代在一個非特殊的排列下。
指向整個列表的指針可以被稱作訪問指針。

用單向鏈表構(gòu)建的循環(huán)鏈表
1.3鏈表相關(guān)的核心點(diǎn)
null/nil 異常處理 dummy node 啞巴節(jié)點(diǎn) 快慢指針 插入一個節(jié)點(diǎn)到排序鏈表 從一個鏈表中移除一個節(jié)點(diǎn) 翻轉(zhuǎn)鏈表 合并兩個鏈表 找到鏈表的中間節(jié)點(diǎn)
2 常見題型
83. 刪除排序鏈表中的重復(fù)元素
給定一個排序鏈表,刪除所有重復(fù)的元素,使得每個元素只出現(xiàn)一次。
思路:直接法,遇到重復(fù)就跳過
class Solution:def deleteDuplicates(self, head: ListNode) -> ListNode:cur = headwhile cur and cur.next:if cur.val == cur.next.val:cur.next = cur.next.nextelse:cur = cur.nextreturn head
82. 刪除排序鏈表中的重復(fù)元素 II
給定一個排序鏈表,刪除所有含有重復(fù)數(shù)字的節(jié)點(diǎn),只保留原始鏈表中 沒有重復(fù)出現(xiàn) 的數(shù)字。
思路:定義啞節(jié)點(diǎn)指向鏈表頭,定義雙指針,一個指向當(dāng)前節(jié)點(diǎn),一個指向前一個節(jié)點(diǎn)。當(dāng)有重復(fù)的元素時,cur指向下一位,直到下一位與當(dāng)前節(jié)點(diǎn)不相等,用flag標(biāo)記表示有重復(fù)數(shù)字,當(dāng)循環(huán)完畢,pre的下一位指向cur的下一位,flag標(biāo)記歸位
當(dāng)沒有重復(fù)數(shù)字,pre指向cur
class Solution:def deleteDuplicates(self, head: ListNode) -> ListNode:if not head or not head.next: # 空鏈表或單即節(jié)點(diǎn)鏈表return headdummy = ListNode(0) # 定義啞節(jié)點(diǎn)dummy.next = headpre = dummy # 啞節(jié)點(diǎn)指針cur = head # 鏈表指針flag = False # 標(biāo)記是否重復(fù)while cur:while cur.next and pre.next.val == cur.next.val:cur = cur.next # 將所有重復(fù)節(jié)點(diǎn)標(biāo)記為Trueflag = Trueif flag is True: # 重復(fù)節(jié)點(diǎn)就跳過pre.next = cur.nextflag = Falseelse:pre = cur # 不重復(fù)就下一個節(jié)點(diǎn)cur = cur.nextreturn dummy.next
206. 反轉(zhuǎn)鏈表
反轉(zhuǎn)一個單鏈表。
思路1:迭代,每次存儲前一個節(jié)點(diǎn),并讓當(dāng)前節(jié)點(diǎn)的next指針指向前一個節(jié)點(diǎn)
思路2:遞歸,假設(shè)我子節(jié)點(diǎn)下的所有節(jié)點(diǎn)都已經(jīng)反轉(zhuǎn)好了,現(xiàn)在就剩我和我的子節(jié)點(diǎn)沒有完成最后的反轉(zhuǎn)了,所以反轉(zhuǎn)一下我和我的子節(jié)點(diǎn)。
# 迭代class Solution:def reverseList(self, head: ListNode) -> ListNode:pre = Nonecur = headwhile cur:temp = cur.next # 存儲節(jié)點(diǎn)的下一位cur.next = pre # 指向前驅(qū)節(jié)點(diǎn)pre = curcur = temp # cur指向原來的下一位return pre
# 遞歸class Solution:def reverseList(self, head: ListNode) -> ListNode:if not head or not head.next:return headp = self.reverseList(head.next)head.next.next = headhead.next = Nonereturn p
92. 反轉(zhuǎn)鏈表 II
反轉(zhuǎn)從位置 m 到 n 的鏈表。請使用一趟掃描完成反轉(zhuǎn)。
思路:用雙指針法,現(xiàn)將慢指針移動到要反轉(zhuǎn)的部分的前一個節(jié)點(diǎn),用另一個指針指向慢指針后面的節(jié)點(diǎn),然后兩兩交換節(jié)點(diǎn)
class Solution:def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:dummy = ListNode(None)dummy.next = headpre = dummyfor i in range(1, m):pre = pre.next # 前驅(qū)指針移動到位置mcur = pre.nextfor i in range(m, n): # 兩兩交換節(jié)點(diǎn)temp = cur.nextcur.next = temp.next # cur一直不變,只修改指針到下一個位置temp.next = pre.next # temp.next指向pre的next,也就是最新的第m個位置pre.next = temp # 更新temp為最新的第m個位置return pre
21. 合并兩個有序鏈表
將兩個升序鏈表合并為一個新的 升序 鏈表并返回。新鏈表是通過拼接給定的兩個鏈表的所有節(jié)點(diǎn)組成的。
思路:建立新鏈表,依次按升序鏈接兩個鏈表的元素
class Solution:def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:dummy = ListNode(None)pre = dummywhile l1 and l2:if l1.val < l2.val:pre.next = l1l1 = l1.nextelse:pre.next = l2l2 = l2.nextpre = pre.nextpre.next = l1 if l1 else l2return dummy.next
86. 分隔鏈表
給定一個鏈表和一個特定值 x,對鏈表進(jìn)行分隔,使得所有小于 x 的節(jié)點(diǎn)都在大于或等于 x 的節(jié)點(diǎn)之前。
思路:創(chuàng)建雙鏈表,一個存儲小于x的值,一個存儲大于x的值
class Solution:def partition(self, head: ListNode, x: int) -> ListNode:before = befornHead = ListNode(None)after = afterHead = ListNode(None)while head:if head.val < x:before.next = headbefore = before.nextelse:after.next = headafter = after.nexthead = head.nextafter.next = Nonebefore.next = afterHead.nextreturn befornHead.next
148. 排序鏈表
在 O(n log n) 時間復(fù)雜度和常數(shù)級空間復(fù)雜度下,對鏈表進(jìn)行排序。
思路:歸并排序,找中點(diǎn)和合并操作
# 歸并排序(遞歸),空間復(fù)雜度O(n)class Solution:def sortList(self, head: ListNode) -> ListNode:if not head or not head.next:return headslow, fast = head, head.nextwhile fast and fast.next:slow, fast = slow.next, fast.next.nextmid = slow.nextslow.next = Noneleft = self.sortList(head)right = self.sortList(mid)h = res = ListNode(0)while left and right:if left.val < right.val:h.next, left = left, left.nextelse:h.next, right = right, right.nexth = h.nexth.next = left if left else rightreturn res.next
# 快速排序class Solution:def sortList(self, head: ListNode) -> ListNode:if head is None:return head# 分成三個鏈表,分別是比軸心數(shù)小,相等,大的數(shù)組成的鏈表big = Nonesmall = Noneequal = Nonecur = headwhile cur:temp = curcur = cur.nextif temp.val < head.val:temp.next = smallsmall = tempelif temp.val > head.val:temp.next = bigbig = tempelse:temp.next = equalequal = temp# 拆完各自排序即可,equal 無需排序big = self.sortList(big)small = self.sortList(small)ret = ListNode(None)cur = ret# 將三個鏈表組合成一起# 可以同時返回鏈表的頭指針和尾指針加速鏈表的合并。for p in [small, equal, big]:while p is not None:cur.next = pp = p.nextcur = cur.nextcur.next = Nonereturn ret.next
143. 重排鏈表
給定一個單鏈表 L:L0→L1→…→L**n-1→Ln , 將其重新排列后變?yōu)椋?em>L0→Ln→L1→Ln-1→L2→Ln-2→…
思路:找到中點(diǎn)斷開,翻轉(zhuǎn)后面部分,然后合并前后兩個鏈表
class Solution:def reorderList(self, head: ListNode) -> None:"""Do not return anything, modify head in-place instead."""if not head or not head.next:return headslow, fast = head, head# 找到鏈表中點(diǎn)while fast.next and fast.next.next:slow = slow.nextfast = fast.next.next# 反轉(zhuǎn)后半部分print(slow.next)right = Nonecur = slow.nextwhile cur:temp = cur.nextcur.next = rightright = curcur = temp# 將反轉(zhuǎn)的后半部分插入到前半部分left = headwhile left and right:slow.next = right.nextright.next = left.nextleft.next = rightleft = right.nextright = slow.next
141. 環(huán)形鏈表
給定一個鏈表,判斷鏈表中是否有環(huán)。
思路1:哈希表,統(tǒng)計(jì)每個元素出現(xiàn)的次數(shù)
思路2:快慢指針,快慢指針相同則有環(huán),證明:如果有環(huán)每走一步快慢指針距離會減 1
# 哈希表class Solution:def hasCycle(self, head: ListNode) -> bool:dic = {}while head:if head in dic:return Trueelse:dic[head] = 1head = head.nextreturn False
# 快慢指針class Solution:def hasCycle(self, head: ListNode) -> bool:if head is None or head.next is None:return Falseslow = headfast = slow.nextwhile fast and fast.next:fast = fast.next.nextslow = slow.nextif fast == slow:return Truereturn False
142. 環(huán)形鏈表 II
給定一個鏈表,返回鏈表開始入環(huán)的第一個節(jié)點(diǎn)。如果鏈表無環(huán),則返回
null。
思路:快慢指針,快慢相遇之后,慢指針回到頭,快慢指針步調(diào)一致一起移動,相遇點(diǎn)即為入環(huán)點(diǎn)
class Solution:def detectCycle(self, head: ListNode) -> ListNode:fast, slow = head, headwhile True:if not (fast and fast.next):returnslow = slow.nextfast = fast.next.nextif slow == fast:breakfast = headwhile fast != slow:fast = fast.nextslow = slow.nextreturn fast
138. 復(fù)制帶隨機(jī)指針的鏈表
給定一個鏈表,每個節(jié)點(diǎn)包含一個額外增加的隨機(jī)指針,該指針可以指向鏈表中的任何節(jié)點(diǎn)或空節(jié)點(diǎn)。
要求返回這個鏈表的 深拷貝。
思路:1、hash 表存儲指針,2、復(fù)制節(jié)點(diǎn)跟在原節(jié)點(diǎn)后面
class Solution:def copyRandomList(self, head: 'Node') -> 'Node':if not head:returndic = {}dic[None] = Nonecur = headwhile cur:if cur not in dic:dic[cur] = Node(cur.val) # 將當(dāng)前未拷貝的節(jié)點(diǎn)放入字典中if cur.random not in dic:dic[cur.random] = Node(cur.random.val) # 將當(dāng)前未拷貝的random指針放入字典中dic[cur].random = dic[cur.random]if cur.next not in dic:dic[cur.next] = Node(cur.next.val) # 將當(dāng)前未拷貝的next指針放入字典中dic[cur].next = dic[cur.next]cur = cur.nextreturn dic[head]
234. 回文鏈表
請判斷一個鏈表是否為回文鏈表。
思路1:將值復(fù)制到數(shù)組中,時間復(fù)雜度O(n),空間復(fù)雜度O(n)
思路2:快慢指針,先找到鏈表中點(diǎn),然后反轉(zhuǎn)后半部分,再與前半部分比較,時間復(fù)雜度O(n),空間復(fù)雜度O(1)
# 將值復(fù)制到數(shù)組中class Solution:def isPalindrome(self, head: ListNode) -> bool:visit = []cur = headwhile cur:visit.append(cur.val)cur = cur.nextreturn visit == visit[::-1]
# 找中點(diǎn) -> 反轉(zhuǎn) -> 比較class Solution:def isPalindrome(self, head: ListNode) -> bool:pre = Noneslow, fast = head, headwhile fast and fast.next:slow = slow.next # 使慢指針指向中間節(jié)點(diǎn)fast = fast.next.nextwhile slow: # 反轉(zhuǎn)后半部分,pre指向反轉(zhuǎn)部分temp = slow.nextslow.next = prepre = slowslow = tempwhile head and pre: # 比較兩部分是否相同if head.val != pre.val:return Falsehead = head.nextpre = pre.nextreturn True
如果覺得不錯,請關(guān)注一下公眾號,也請為小編點(diǎn)個在看!
推薦閱讀
算法工程師面試必選項(xiàng):動態(tài)規(guī)劃
機(jī)器學(xué)習(xí)算法工程師
? ??? ? ? ? ? ? ? ? ? ? ? ??????????????????一個用心的公眾號

