Python 鏈表相關題型

鏈表類型題

本博文僅用作個人學習的記錄,包含個人學習過程的一些思考,想到啥寫啥,因此有些東西闡述的很羅嗦,邏輯可能也不清晰,看不懂的且當作是作者的囈語,自行跳過即可。

單序鏈表

首先我們得清楚鏈表是什麼,也不用把它想象的多高大上,多專業化,它其實就是一個結構,一個包含有兩個部分的結構:

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 也是不可變的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章