鏈表類型題
本博文僅用作個人學習的記錄,包含個人學習過程的一些思考,想到啥寫啥,因此有些東西闡述的很羅嗦,邏輯可能也不清晰,看不懂的且當作是作者的囈語,自行跳過即可。
單序鏈表
首先我們得清楚鏈表是什麼,也不用把它想象的多高大上,多專業化,它其實就是一個結構,一個包含有兩個部分的結構:
val | next |
---|
它包含當前的值和下一個鏈表結構的索引,從定義可以看出:
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
所以也就是說有一個鏈表 pHead
, 或許它很長,但是你只知道 pHead
此時的值以及它指向下一個的索引,對它進行賦值也只會影響當前這個節點的值以及它指向的下一個節點的索引,而不會改變原鏈表的其它節點,最多是當前節點 pHead
丟失了指向原鏈表的索引,不再能夠由 pHead.next
將原鏈表遍歷出來。
講題吧,從題目入手好理解一些,敲敲小黑板!!
逆序打印鏈表
代表題型:劍指offer 第3題
-
題目描述
輸入一個鏈表,按鏈表從尾到頭的順序返回一個ArrayList。 -
代碼
class Solution:
# 返回從尾部到頭部的列表值序列,例如[1,2,3]
def printListFromTailToHead(self, listNode):
# 新建一個列表存放遍歷得到的鏈表值
l = []
head = listNode
while head:
# 在列表首部,即0位置處插入鏈表值
l.insert(0, head.val)
# 鏈表指向下一個值
head = head.next
return l
- 解析
這裏是逆序打印鏈表,可以分成三步:
1.將鏈表遍歷一遍,取出每個節點的值。
2.將值存放到列表中
3.將列表逆序
這裏的代碼是將2,3兩步結合了,直接將值逆序存放到列表中。
倒數第k個結點
代表題型:劍指offer 第14題
-
題目描述
輸入一個鏈表,輸出該鏈表中倒數第k個結點。 -
代碼
class Solution:
def FindKthToTail(self, head, k):
# 判斷輸入是否符合規範
if head==None or k<=0:
return None
# 選用兩個鏈表結構
p1 = p2 = head
while p1.next!=None:
# p2要比p1晚遍歷k-1個值
if k>1:
k-=1
else:
p2 = p2.next
p1 = p1.next
# 判斷k值是否符合規範
if k>1:
return None
# 如果符合規範,就返回p2
else:
return p2
- 解析
本題難點在於鏈表無法預知長度,因此也無法從後往前取值。
方法一: 我們最容易想到的是,將鏈表整個完成遍歷,然後將所有的值都順序(或者逆序)存入列表中,然後取出倒數(順數)第k個值。因爲列表是可以知道長度的,並且可以按索引取值,這個也剛好在逆序打印的基礎上添加代碼即可。但是需要注意,它不是打印倒數第k個結點的值,而是需要返回一個鏈表:
class Solution:
def FindKthToTail(self, head, k):
if head==None or k<=0:
return None
l = []
while head:
# 順序保存
l.append(head.val)
head = head.next
if k<=len(l):
# 定義兩個相同的鏈表結點
p1 = p2 = ListNode(0)
# 取得倒數第k個結點的值,並將之後的所有值生成一個鏈表返回
for item in l[(len(l)-k):]:
p1.next = ListNode(item)
p1 = p1.next
# 返回鏈表頭,p1已經丟失了鏈表頭,p2依舊指向的是原鏈表
return p2.next
else:
return None
可以看出這個方法還是很麻煩的,需要鏈表->列表->鏈表,藉助了多箇中間量。
這裏用到了python列表的可變性,下文單列一節來講吧,私以爲還是很重要的一個點。
方法二: 也就是最開始的那個代碼,主要思路是利用快慢指針,同時進行兩次遍歷,快指針剛好比慢指針快 k-1
個結點,那麼當快指針遍歷完成時,慢指針剛好遍歷到鏈表的倒數第 k
個結點。
鏈表反轉
代表題型:劍指offer 第15題
這一題剛開始的時候我總是和逆序打印鏈表傻傻分不清楚。。。
鏈表反轉和逆序打印最大的區別在於,鏈表反轉的返回值是一個 linkedlist
,而逆序打印鏈表返回的是一個 list
。
-
題目描述
輸入一個鏈表,反轉鏈表後,輸出新鏈表的表頭。 -
代碼
class Solution:
# 返回ListNode
def ReverseList(self, pHead):
p1 = pHead
p2 = None
while p1:
tem = p1.next
p1.next = p2
p2 = p1
p1 = tem
return p2
- 解析
那麼同樣可以有多種方法。
方法一: 在逆序打印的基礎上,將list
轉換爲linkedlist
。
class Solution:
# 返回ListNode
def ReverseList(self, pHead):
l = []
while pHead:
# 逆序保存
l.insert(0, pHead.val)
pHead = pHead.next
# 定義兩個相同的鏈表結點
p1 = p2 = ListNode(0)
# 取得倒數第k個結點的值,並將之後的所有值生成一個鏈表返回
for item in l:
p1.next = ListNode(item)
p1 = p1.next
# 返回鏈表頭,p1已經丟失了鏈表頭,p2依舊指向的是原鏈表
return p2.next
同樣是很麻煩,需要鏈表->列表->鏈表,而且代碼也不簡潔。
方法二: 逐個遍歷鏈表結點,調轉結點的指向,代碼如開始所示。
首先新建一個 None
值。
對於輸入鏈表的第一個結點,把它指向的下一個結點由第二個結點轉換爲 None
。
p1.next = p2
但是這樣你就丟失了第二個結點和之後結點的地址,因此在這個操作之前需要先將第二個節點賦值給另外一個臨時變量 tem
。
tem = p1.next
那麼此時你手裏的三個變量:
p1
包含原鏈表的第一個結點,並且下一個節點指向 None
。
p2
依舊是一個 None
值。
tem
包含原鏈表的第二個結點,並且逐個指向之後的所有原鏈表結點。
通過這一次操作可以看出,我們已經將一個結點的指向調轉了,目前存放在 p1
這個參數裏,原鏈表之後的結點放在了 tem
參數裏。而 p1
代表的是原鏈表,p2
才代表反轉之後的鏈表,因此我們需要把參數再調整回來。
p2 = p1
p1 = tem
如此遍歷完整個原鏈表,p2
代表的就是反轉之後的鏈表表頭。
排序鏈表合併
代表題型:劍指offer 第16題
這題的幾個解法受益很多,讓我輩渣渣唯有一句臥槽,心中生出只管磕頭之念!
-
題目描述
輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,當然我們需要合成後的鏈表滿足單調不減規則。(這裏的不減應該就是與原來單調遞增一樣,單調性不改變,可以有相等的情況) -
代碼
循環
def Merge(self, l1, l2):
# 同樣是定義兩個鏈表,一個存表頭一個用來修改鏈表數據,防止改完之後找不到表頭
dummy = cur = ListNode(0)
# 這個and用的好呀
while l1 and l2:
# 鏈表值小的就存入到cur中,並且往後走一節。
if l1.val < l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
# 這個or用的是真真好,和開頭那個and一樣,避免了寫多行判斷賦值語句
cur.next = l1 or l2
return dummy.next
遞歸
def Merge(self, l1, l2):
# 方法開頭進行判斷是否有空鏈表,並返回非空鏈表
if not l1 or not l2:
return l1 or l2
# 對於值小的結點,把它的下一個結點和值大的結點再次執行本方法,並且賦值給值小結點指向的下一個結點
if l1.val < l2.val:
l1.next = self.Merge(l1.next, l2)
# 返回值小結點的表頭
return l1
else:
l2.next = self.Merge(l1, l2.next)
return l2
遞歸方法真是奇妙呀,這感覺妙不可言。
遞歸優化
def Merge(self, pHead1, pHead2):
if pHead1 and pHead2:
# p1更大就交換結點
if pHead1.val > pHead2.val:
pHead1, pHead2 = pHead2, pHead1
pHead1.next = self.Merge(pHead1.next, pHead2)
return pHead1 or pHead2
這裏需要注意的一點是:
return pHead1 or pHead2
當有一個是空值的時候,返回的是另外一個非空值;當兩個都是非空值時,返回的是第一個值。
這與 or
的判斷機制有關係,當判斷第一個非空後,就不會判斷第二個值了。
因此,這裏只能把 pHead1
寫在 pHead2
前面,因爲小值按順序排放在 pHead1
這個結點裏,所以最後要返回的必須是 pHead1
。
- 總結
鏈表一個節點一個節點順序操作的時候,需要一個額外的參數記錄表頭。
正向的鏈表操作感覺更適合使用遞歸,遞歸便是一層層執行,然後再一層層返回數據,這樣就不需要多餘參數記錄表頭。
Python 的字符串不變性
不可變性值的是:變量在新建之後就不可以被修改。
在Python中,數字(number)、字符串(string)、元組(tuple)是不可變的,集合(set)、列表(list)和字典(dictionary)可變。
但是我們還是經常能夠看到,對上面這些類型的參數進行修改呀,而且也並沒有報錯,這是爲什麼呢?
對不可變類型的參數進行賦值時,並不是直接修改這個參數內存區域指向的值,而是新開闢了另外一個內存區域,並將當前參數指向了這個新的內存區域。
看個對比:
a = b = 1
b = 2
print(a) # 此時a的值還是1,並沒有被改變,而是b指向了2所在的內存區域
a = b = [0]
b[0] = 1
print(a) # 此時a的值已經變爲了[1],因爲列表可以被修改
在Python中,標準庫並沒有實現鏈表,鏈表是由列表來自定義實現的一種數據結構,因此它是具有可變性的,所以可以採用兩個參數(一個記錄表頭,一個修改鏈表)對鏈表實行操作。
Java 的 String
也是不可變的。