連結清單問題彙總
- 1 前言
- 2 題目
-
- 2.1 連結清單中倒數第k個結點
-
- 2.1.1 題目
- 2.1.2 思路
- 2.1.3 代碼
- 2.2 反轉連結清單
-
- 2.2.1 題目
- 2.2.2 思路
- 2.2.3 代碼
- 2.3 合并兩個排序的連結清單
-
- 2.3.1 題目
- 2.3.2 思路1
- 2.3.3 代碼1
- 2.3.4 思路2
- 2.3.5 代碼2
- 2.4 兩個連結清單的第一個公共結點
-
- 2.4.1 題目
- 2.4.2 思路
- 2.4.3 代碼
- 2.4.4 圖解
- 2.5 複雜連結清單的複制
-
- 2.5.1 題目
- 2.5.2 思路
- 2.5.3 圖解
- 2.5.4 代碼
- 2.6 連結清單中環的入口結點
-
- 2.6.1 題目
- 2.6.2 思路
- 2.6.3 圖解
- 2.6.4 代碼
- 2.7 删除連結清單中重複的結點
-
- 2.7.1 題目
- 2.7.2 思路1
- 2.7.3 代碼1
- 2.7.4 思路2
- 2.7.5 代碼2
- 參考
1 前言
整理下劍指offer連結清單問題。
2 題目
2.1 連結清單中倒數第k個結點
2.1.1 題目
輸入一個連結清單,輸出該連結清單中倒數第k個結點。
2.1.2 思路
思路:
- 如果輸對外連結表正向的第k個結點會嗎?應該會的!那麼再得到長度,一減,就ok了啊!
- 但是上面思路太麻煩,有一個非常牛逼的思路!
- 牛逼思路:我們可以定義兩個指針。
- 第一個指針從連結清單的頭指針開始周遊向前走k-1,第二個指針保持不動;
- 從第k步開始,第二個指針也開始從連結清單的頭指針開始周遊。
- 由于兩個指針的距離保持在k-1,當第一個(走在前面的)指針到達連結清單的尾結點時,第二個指針(走在後面的)指針正好是倒數第k個結點。
- 圖示:
2.1.3 代碼
- 代碼寫起來!
- 寫代碼發現有報錯,考慮下邊界條件:如果查詢的是最後一個元素咋辦?也就是倒數第一個!
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def FindKthToTail(self, head, k):
# write code here
fast = slow = head
'''
為什麼這種思路ok?
因為是循環k次,也就是距離為k 這時候就不能以.next作為循環終止條件了!而是自己為終止條件!但還是下面一種好思考一些!
'''
for _ in range(k):
if not fast: # 如果fast為空 傳回空
return None
fast = fast.next
while fast:
slow, fast = slow.next, fast.next
return slow
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def FindKthToTail(self, head, k):
# write code here
# 邊界條件!
if head == None or k == 0:
return None
# 初始化指針
phead = head
pbehind = head
# 先走k-1步
for i in range(k-1):
if phead.next == None: # 防止出現連結清單就1個元素 然後說倒數第二個元素 這樣就gg
return None
else:
phead = phead.next
while phead.next != None: # 隻要走在前面的指針的下一步不是指向None 那麼就繼續移動!
phead = phead.next
pbehind = pbehind.next
return pbehind
注意return的結果!!!不要把元素搞出來!看題目的需求!!!是傳回節點!!!現在直接傳回節點就ok!是以審題相當重要!
2.2 反轉連結清單
2.2.1 題目
輸入一個連結清單,反轉連結清單後,輸出新連結清單的表頭。
2.2.2 思路
思路:
- 如何反轉呢?也就是把元素全部倒置過來!如何實作?
- 能不能依次存儲到一個list 然後反轉list?好像不太行吧?因為最後要輸出表頭诶?就是直接輸出一個節點!
- 我好像隻要找到新連結清單的表頭就ok了?是以不需要存儲?那直接周遊到最後一個輸出?不行的!輸出就是反轉的連結清單!
代碼思路:
- 每一步解釋下面都有!
- 值得注意的就是 最後傳回的是pre!
- 一開始pre的定義是None!因為首節點反轉之後要指向None!巧妙!
- 循環體部分包括兩部分:
- 轉換節點方向【先儲存下一個指向資訊 再轉向】
- 移動指針【直接前一個指向後一個 後一個指向之前儲存的next!】
- 注意考慮特殊情況
- 空連結清單
- 隻有1個節點的連結清單的情形!
2.2.3 代碼
代碼:
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
# 傳回ListNode
def ReverseList(self, pHead):
# write code here
if pHead == None:
return None
# 定義兩個指針
pre = None #前一個節點 因為頭節點反轉後需要指向None
cur = pHead # 目前節點
# 開始循環
while cur != None:
# 先保留目前節點的下一個資訊 防止連結清單斷裂
next = cur.next
# 讓目前節點指向前一個節點
cur.next = pre
# 移動指針
pre = cur
cur = next
return pre
2.3 合并兩個排序的連結清單
2.3.1 題目
輸入兩個單調遞增的連結清單,輸出兩個連結清單合成後的連結清單,當然我們需要合成後的連結清單滿足單調不減規則。
2.3.2 思路1
思路:
- 如何合并呢?兩個排序的連結清單?是否要比較連結清單内元素的大小?關鍵應該還是指向的問題!
- 如果逐個元素兩兩比較是不是太麻煩了?應該有其餘的方法!在兩個連結清單上分别設指針?思路沒問題!但是稍微修改下!
代碼思路:
- 非遞歸版本
- 一開始先定義一個空的節點!然後讓pHead指向它!這樣就能很好地解決最後不知道傳回誰的問題!直接傳回pHead.next 逐個列印出來就行了!
- 中間的判斷循環語句和自己一開始的思路一緻 不難!誰元素小指向誰!并且它還要移動!
- 最後需要做一個判斷的就是 有可能一個連結清單中元素為空了!那麼這時候全部指向另一個就ok!
- 中間有一個細節就是
具體原因上面解釋了!tmp = tmp.next
- 最後傳回的是
記住這個trick!pHead.next
2.3.3 代碼1
非遞歸版本 代碼:
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
# 傳回合并後清單
def Merge(self, pHead1, pHead2):
# write code here
# 先考慮特殊情況
#初始化
tmp = ListNode(0)
pHead = tmp # pHead指向tmp
# 開始循環
while pHead1 and pHead2: # 均非空
if pHead1.val < pHead2.val: # 如果1的元素小于2的 改變1
tmp.next = pHead1 # 讓初始tmp指向1
pHead1 = pHead1.next # 移動1
else: # 如果1的元素大于2的 改變2
tmp.next = pHead2 # 讓初始tmp指向2
pHead2 = pHead2.next # 移動2
tmp = tmp.next # 同時tmp每一次循環結束後都要向前移動一次!因為之後都要指向新元素 是以要移動到目前的位置!
# 循環結束 看哪一個為空
if not pHead1: # 如果pHead1為空 那麼tmp直接下一步指向2
tmp.next = pHead2
if not pHead2: # 如果pHead2為空 那麼tmp直接下一步指向1
tmp.next = pHead1
return pHead.next # 最後傳回誰呢?為什麼傳回pHead.next?可以一個一個的列印出來!
2.3.4 思路2
遞歸版本圖解:
2.3.5 代碼2
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
# 傳回合并後清單
def Merge(self, pHead1, pHead2):
# write code here
# 先考慮特殊情況
if pHead1 is None:
return pHead2
if pHead2 is None:
return pHead1
# 開始循環遞歸判斷!
if pHead1.val < pHead2.val: # 1元素小于2
pHead1.next = self.Merge(pHead1.next,pHead2) # 讓1指向除了1+2的新的兩個連結清單
return pHead1 # 最後傳回pHead1?妙!
else:
pHead2.next = self.Merge(pHead1,pHead2.next)
return pHead2
代碼思路:
- 這個遞歸版本思路太妙了!
- 怎麼想呢?就是說每次比較兩個元素之後,不局限于元素之間的指向轉變!而是直接将小的元素指向一個新的兩個連結清單!遞歸實作!
以[1 3 ] [2 4 ] 來示例:
進入循環:
pHead1.next = self.Merge(pHead1.next,pHead2)=[3,5],[2,4,6]
= 進入第二個循環
pHead2.next = self.Merge(pHead1,pHead2.next)=[3,5],[4,6]
= 進入第一個循環
pHead1.next = self.Merge(pHead1.next,pHead2)=[5],[4,6]
= 進入第二個循環
pHead2.next = self.Merge(pHead1,pHead2.next)=[5],[6]
= 進入第一個循環
pHead1.next = self.Merge(pHead1,pHead2.next)=[None],[6]
= 6
- 直接從圖形角度了解吧!逆推推不回去!就想着每次會多出一個元素出來!
- return 1 還是 2 取決于第一次進入的是哪個部分裡面!不要過多糾結于每一步的細節!從圖形角度了解就ok!
2.4 兩個連結清單的第一個公共結點
2.4.1 題目
輸入兩個連結清單,找出它們的第一個公共結點。
2.4.2 思路
思路:
- 直接搜尋!要不要先排序呢?然後二分查找?但是排序的複雜度會比較高吧?是的!
- 先暴力搜尋!先固定一個連結清單的一個元素,然後周遊另外一個連結清單,再換另一個元素!
寫了代碼,但是測試用例有點一臉懵逼,
測試用例說明這個題目的意思是 公共節點之後的元素都一樣!
a = {1,2,3,6,7}
b = {4,5,6,7}
圖解就是:
這時候采用一種思路:
- 先讓把長的連結清單的頭砍掉,讓兩個連結清單長度相同,這樣,同時周遊也能找到公共結點。此時,時間複雜度O(m+n),空間複雜度為O(MAX(m,n))。
2.4.3 代碼
代碼為:
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def FindFirstCommonNode(self, pHead1, pHead2):
# write code here
# 下面這個沒問題 先判斷特殊情況
if pHead1 == None or pHead2 == None:
return None
cur1, cur2 = pHead1, pHead2
while cur1 != cur2:
cur1 = cur1.next if cur1 != None else pHead2
cur2 = cur2.next if cur2 != None else pHead1
return cur1
這種做法代碼看起來其實有點炫,我們來分解一下,如何讓兩個長度不同的連結清單并駕齊驅的?
2.4.4 圖解
其實四張圖就可以解釋!圖解如下:
2.5 複雜連結清單的複制
2.5.1 題目
輸入一個複雜連結清單(每個節點中有節點值,以及兩個指針,一個指向下一個節點,另一個特殊指針指向任意一個節點),傳回結果為複制後複雜連結清單的head。(注意,輸出結果中請不要傳回參數中的節點引用,否則判題程式會直接傳回空)
2.5.2 思路
思路:
- 完全沒有思路,啥意思?複制連結清單?咋複制?而且這個連結清單還很奇怪,兩個指向,一個是下一個節點,一個是任意一個節點?蜜汁操作!什麼叫複制?以及傳回head頭結點?
思路還是比較6的:
我們這裡将複雜連結清單的複制過程分解為三個步驟。在寫代碼的時候我們每一步定義一個函數,這樣每個函數完成一個功能,整個過程的邏輯也就非常清晰明了了。
我們這裡采用三步:
- 第一步:複制複雜指針的label和next。但是這次我們把複制的結點跟在元結點後面,而不是直接建立新的連結清單;
- 第二步:設定複制出來的結點的random。因為新舊結點是前後對應關系,是以也是一步就能找到random;
- 第三步:拆分連結清單。奇數是原連結清單,偶數是複制的連結清單。
2.5.3 圖解
圖解為:
2.5.4 代碼
代碼為:
# -*- coding:utf-8 -*-
# class RandomListNode:
# def __init__(self, x):
# self.label = x
# self.next = None
# self.random = None
class Solution:
# 傳回 RandomListNode
def Clone(self, pHead):
# 邊界條件:如果為空連結清單 傳回空
if not pHead:
return None
# 首先定義一個指針
dummy = pHead
# first step, N' to N next 即 複制連結清單 緊跟它後面!
while dummy: # 爸爸非空即進入循環
# 1 記錄爸爸的next指向
dummynext = dummy.next
# 2 記錄爸爸基因(label),産生兒子
copynode = RandomListNode(dummy.label)
# 3 讓兒子指向爸爸的next指向
copynode.next = dummynext
# 4 讓爸爸指向兒子
dummy.next = copynode
# 5 讓爸爸繼續向前 指向本應該的next
dummy = dummynext
# 複制完成 重新歸一化dummy
dummy = pHead
# second step, random' to random' 即确定random的指向
while dummy:
# 1 先記錄父親的random位置
dummyrandom = dummy.random
# 2 讓兒子指向爸爸的next位置
copynode = dummy.next
# 3 如果存在父親的random位置就讓兒子的random也複制一下
if dummyrandom:
copynode.random = dummyrandom.next
# 4 重點:如何讓父親繼續走?讓父親指向兒子的next節點即可!
dummy = copynode.next
# third step, split linked list 分開list 奇數為真實 偶數為複制的!即如何傳回copynode
# 其實就是确定新的指向!新的連結清單指向順序!
# 複制random完成 重新歸一化dummy
dummy = pHead
# 爸爸的next指向兒子頭結點copyHead!
copyHead = pHead.next
while dummy:
# 隻要爸爸不為空 進入循環!
# 1 産生兒子-爸爸dummy的next
copyNode = dummy.next
# 2 dummynext記錄爸爸實際的next指向-copyNode.next
dummynext = copyNode.next
# 3 讓爸爸跳到實際的next指向上
dummy.next = dummynext
# 4 如果dummynext非空 那麼兒子的下一個指向為dummynext的next指向
if dummynext:
copyNode.next = dummynext.next
else:
copyNode.next = None
# 5 讓父親指向實際的下一個位置 繼續循環!
dummy = dummynext
return copyHead
2.6 連結清單中環的入口結點
2.6.1 題目
給一個連結清單,若其中包含環,請找出該連結清單的環的入口結點,否則,輸出null。
2.6.2 思路
思路:
- 周遊連結清單!
- 什麼情況下會包括環呢?也就是某一個節點的next指向前面的任一節點!這時候輸出該節點?
- 如何判斷某一個節點的next指向前面的任一節點?其實指向後面的節點也ok!就是不指向下一個!如何判斷呢?好難啊!不好想~
牛逼的思路:
- 分為兩部分。一個結論+如何确定連結清單環中元素的個數!
- 結論:如果一個連結清單有環,且環中元素的個數為n,那麼定義兩個指針,一個指針先走n步,另一個指針在原點位置,然後同時開始移動兩個指針,直至兩個指針相遇,相遇的元素即為環的入口節點!
2.6.3 圖解
具體見下圖:
- 如何确定環内元素的個數呢?使用**快慢指針**!定義兩個指針,一前一後,一個移動2步,一個移動1步,兩個指針肯定會相遇,具體見下圖:
- 相遇之後隻使用一個指針開始繼續向前走,一直到又走到這個位置的時候,記錄下走了多少次,即為環内元素個數!
- 如果判斷沒有環呢?如何判斷?那就是快慢指針的時候沒有相遇!此時就沒有環了!
2.6.4 代碼
下面開始寫代碼:
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def EntryNodeOfLoop(self, pHead):
# write code here
if not pHead:
return None
# 1 首先定義快慢指針 确定是否有環以及環内元素個數
# 1.1 定義指針 注意一開始要選擇不同的
slow = pHead
fast = pHead.next
# 1.2 快慢指針開始周遊 直至相遇
while slow != fast:
# 注意考慮邊界情況!就是隻有一個節點!肯定也不會有環!
if slow.next == None or fast.next == None:
return None
else:
# 慢指針一次走一步 快指針一次兩步
slow = slow.next
fast = fast.next.next
# 1.3 相遇 用一個指針去走 直至回來
tmp = slow
n = 1
while tmp.next != slow:
n += 1
tmp = tmp.next
# 1.4 确定了環内元素個數
# 2 再定義兩個指針!确定入口結點
# 2.1 定義兩個指針
cur1 = pHead
cur2 = pHead
# 2.2 先移動cur2往前n步
for i in range(n):
cur2 = cur2.next
# 2.3 現在開始同步移動 直至兩者相等
while cur1 != cur2:
cur1 = cur1.next
cur2 = cur2.next
return cur1
2.7 删除連結清單中重複的結點
2.7.1 題目
在一個排序的連結清單中,存在重複的結點,請删除該連結清單中重複的結點,重複的結點不保留,傳回連結清單頭指針。 例如,連結清單1->2->3->3->4->4->5 處理後為 1->2->5
2.7.2 思路1
這個思路超牛逼!
- 先将每個元素過一遍!然後統計出現的次數!過濾掉元素大于1的!
- 然後再将剩餘的元素組織成連結清單的形式!
- 完美!
2.7.3 代碼1
思路比較清晰!開始寫代碼:
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def deleteDuplication(self, pHead):
# write code here
res=[]
while pHead:
res.append(pHead.val)
pHead=pHead.next
res=list(filter(lambda c:res.count(c)==1,res))
newList=ListNode(0)
pre=newList
for i in res:
node=ListNode(i)
pre.next=node
pre=pre.next
return newList.next
2.7.4 思路2
思路:
- 已經排序了,那就直接周遊每一個元素吧!如果遇到相同的,都删了關鍵就是得确定指向的問題!是以應該至少要有2個指針!保留重複元素的最後一個元素的next指向,讓首個重複元素前面元素指向該next指向即可!遇到重複的就一直往下走!直到不等于!while!
思路更新:
- 其實思路和自己想的基本差不多 隻不過有些細節需要注意
- 首先得建立一個first,作為最初始!其指向head!
- 定義一個指針跟着pHead跑!因為要考慮其前面的元素!!!!
- 進入循環直接考慮兩個元素是否相等,相等取出元素來進行周遊循環,然後讓之前的前一個指針指向它,即在判斷的時候隻用自身以及next來判斷就好!保持前指針last的純粹性!
- 最後傳回是誰也很重要!為什麼不傳回pHead?因為第一個元素無法顯示!沒有指向第一個元素的!而pHead是最後一個。為什麼不傳回last,因為沒有指向第一個元素!隻有first符合條件!
2.7.5 代碼2
代碼2:
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def deleteDuplication(self, pHead):
# write code here
# 特殊情況
if not pHead:
return None
# 得定義一個指針跟着pHead跑!因為要考慮其前面的元素!
first=ListNode(-1)
first.next=pHead
last=first
while pHead and pHead.next:
# 開始一次判斷
if pHead.val == pHead.next.val:
# 如果兩個元素相等 則往後繼續移動-取出值來 做判斷依據
val = pHead.val
while pHead and pHead.val == val:
pHead = pHead.next
# 循環結束 說明此時跳出了重複元素 改變指向
last.next = pHead
else:
# 說明元素不等 繼續往前移動
last = pHead
pHead = pHead.next
return first.next
參考
- https://blog.csdn.net/c406495762/article/details/79247243