學習鏈表復盤中


鏈表基礎知識

鏈表的分類

鏈表是一種通過指針串聯在一起的線性結構,主要分為單鏈表、雙向鏈表和循環鏈表。

單鏈表

單鏈表中每一個節點是由兩部分組成,一個是數據域、一個是指針域(存放指向下一個節點的指針),最后一個節點的指針域為空。

雙向鏈表

雙向鏈表中的每一個節點有兩個指針域,一個指向下一個節點,一個指向上一個節點。雙向鏈表既可以向前查詢也可以向后查詢。

單向循環鏈表

單向循環鏈表是指首尾相連的單鏈表。

雙向循環鏈表

雙向循環鏈表是指首尾相連的雙向鏈表。

鏈表的定義

我們在刷LeetCode的時候,由於鏈表的節點都默認定義好了,直接用就行了,但是在面試的時候,一旦要手寫鏈表代碼時,節點的定義是大家容易犯錯的地方,所有我們來看一下定義鏈表節點的方式。

#單鏈表
class ListNode(object):
    def __init__(self, x):
        #數據域
        self.val = x
        #指針域
        self.next = None

鏈表的操作

單鏈表的刪除操作

刪除鏈表中的q節點,只需要執行p->next=q->next,即讓p的指針域指向q的下一個節點。

向單鏈表中增加一個節點

在節點p后增加一個節點q,只需要執行q->next=p->next,p->next=q即可,這里一定要注意執行的先后順序。如果先執行p->next=q,那么原鏈表的p->next的信息就丟失了。

解題有妙招

引入啞結點

啞結點也叫做哨兵節點,對於鏈表相關的題目,為了方便處理邊界條件,一般我們都會引入啞結點來方便求解。首先我們來看一下什么是啞結點,啞結點是指數據域為空,指針域指向鏈表頭節點的節點,它是為了簡化邊界條件而引入的。下面我們來看一個具體的例子,例如要刪除鏈表中的某個節點操作。常見的刪除鏈表的操作是找到要刪元素的前一個元素,假如我們記為 pre。我們通過:pre->next=pre->next->next來執行刪除操作。如果此時要刪除的是鏈表的第一個結點呢?就不能執行這個操作了,因為鏈表的第一個結點的前一個節點不存在,為空。如果此時你設置了啞結點,那么第一個結點的前一個節點就是這個啞結點。這樣如果你要刪除鏈表中的任何一個節點,都可以通過pre->next=pre->next->next的方式來進行,這就簡化的代碼的邏輯。

雙指針法

在求解鏈表相關的題目時,雙指針也是非常常用的思想,例如對於求解鏈表有環問題時,我們申請兩個指針,以一快一慢的速度在鏈表上行走,等他們相遇時,就可以知道鏈表是否有環;或者在求鏈表的倒數k個節點時,申請兩個指針,一個指針先走k步,然后兩個指針再同時向后移動,當先走的那個指針走到鏈表的末尾時,另一個指針恰好指向了倒數第k個節點。

該文檔已經更新到github上了,https://github.com/meetalgo/meet_algo

反轉鏈表

問題描述

給定單鏈表的頭節點 head ,請反轉鏈表,並返回反轉后的鏈表的頭節點。
示例:
輸入:head = [1,2,3,4,5]
輸出:[5,4,3,2,1]

分析問題

首先,我們按照題目的要求,先把圖畫出來,然后再分析。

從圖中我們可以看到,反轉前和反轉后指針的指向發生了反轉。所以,我們在實現的過程中,我們可以通過調整鏈表的指針來達到反轉鏈表的目的。

  1. 我們定義兩個指針pre和cur。pre表示已反轉部分的頭結點,cur表示還沒有反轉部分的頭結點。開始時cur=head,pre=None
  2. 每次讓cur->next=pre,實現一次局部反轉。
  3. 局部反轉完成后,cur和pre同時向前移動一位。
  4. 循環上述過程,直到鏈表反轉完成。

代碼實現

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class Solution:
    def reverse(self, head):
        cur = head
        #初始化時,pre為None
        pre = None
        while cur:
            next=cur.next
            cur.next = pre
            pre = cur
            cur = next
        return pre

head=ListNode(1,None)
cur=head
for i in range(2,6):
    tmp=ListNode(i,None)
    cur.next=tmp
    cur=cur.next

s=Solution()
pre=s.reverse(head)

while pre!=None:
    print(pre.val)
    pre=pre.next

合並兩個有序鏈表

問題描述

輸入兩個單調遞增的鏈表,輸出兩個鏈表合成后的鏈表,我們需要合成后的鏈表滿足單調不減規則。

示例:

輸入: {1,3,5},{2,4,6}

返回值: {1,2,3,4,5,6}

分析問題

既然給定的兩個鏈表都是有序的,那么我們可以判斷兩個鏈表的頭結點的值的大小,將較小值的結點添加到結果中,然后將對應鏈表的結點指針后移一位,循環往復,直到有一個鏈表為空為止。

由於鏈表是有序的,所以循環終止時,那個非空的鏈表中的元素都比前面已經合並的鏈表中的元素大,所以,我們只需要簡單地將非空鏈表接在合並鏈表的后面,並返回合並鏈表即可。

首先我們需要先創建一個哨兵節點,然后將prehead指向鏈表l1和l2中比較小的一個。如果相等的話,指向任意一個即可。然后將較小值對應的鏈表的指針后移一位。

我們下面來看一下代碼實現。

def mergeTwoLists(self, l1, l2):
        #合並后鏈表的哨兵結點
        head=ListNode(-1,None)
        pre=head
        #循環遍歷,將兩個鏈表中的較小值插入到合並后的鏈表中
        while l1 and l2:
            if l1.val <= l2.val:
                pre.next=l1
                l1=l1.next
            else:
                pre.next=l2
                l2=l2.next
            pre=pre.next
        #將剩余的非空鏈表插入到合並鏈表的后面
        if l1:
            pre.next=l1
        else:
            pre.next=l2

        return head.next

其實,我們這里也可以使用遞歸的方式來實現。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class Solution:
    def mergeTwoLists(self, l1, l2):
        #鏈表l1為空,不需要合並,直接返回l2
        if(l1==None):
            return l2
        #同理,l2為空,直接返回l1即可    
        if(l2==None):
            return l1

        if(l1.val<=l2.val):
            l1.next=self.mergeTwoLists(l1.next,l2)
            return l1
        else:
            l2.next=self.mergeTwoLists(l1,l2.next)
            return l2

問題升級

下面,我們來把問題升級一下,將兩個有序鏈表合並改成多個有序鏈表合並,我們來看一下題目。

給定一個有序鏈表, 其中每個節點也表示有一個有序鏈表。結點包含兩個類型的指針:

  1. 指向主鏈表中下一個結點的指針。
  2. 指向以此結點為頭的鏈表。

示例如下所示:

  4 ->  9 -> 15 -> 19
  |     |     |     |
  7    13    18    28
  |           |     |
  8          21    37
  |                
  20               
  
 實現函數flatten(),該函數用來將鏈表扁平化成單個鏈表。例如上面的鏈表,輸出鏈表為
 
  4 -> 7 -> 8 -> 9 -> 13 -> 15 -> 18 ->19 -> 20 -> 21 -> 28 -> 37 

題目要求我們把二維有序鏈表合並成一個單鏈表,我們來把問題簡化一下,假設主鏈表只有兩個節點,即這個二維鏈表變成如下所示。

 4 ->  9 
  |     |     
  7    13           旋轉一下         4 -> 7 -> 8 -> 20
  |               ---------->       |
  8                                 9 -> 13
  |                
  20   

這不就是我們上面講的兩個有序鏈表合並嗎?那如果主鏈表有多個節點呢?我們可以使用歸並的思想,逐個去合並就好了,如下圖所示。

下面我們來看一下代碼如何實現。

class ListNode:
    def __init__(self, val=0, right=None, down=None):
        self.val = val
        self.right = right
        self.down = down

class Solution:
    def mergeTwoLists(self, l1, l2):
        #如果有一個鏈表為空,則合並后的鏈表就是另外一個
        if(l1==None):
            return l2
        if(l2==None):
            return l1

        if(l1.val<=l2.val):

            l1.down=self.mergeTwoLists(l1.down,l2)
            return l1
        else:
            l2.down=self.mergeTwoLists(l1,l2.down)
            return l2


    def flatten(self,root):
        if root== None or root.right == None:
            return root
        #把root->right 看作是已經有序的單鏈表,
        #然后通過遞歸來進行歸並
        return self.mergeTwoLists(root, self.flatten(root.right))

鏈表中的節點每k個一組翻轉

問題描述

將給出的鏈表中的節點每 k 個一組翻轉,返回翻轉后的鏈表。如果鏈表中的節點數不是 k 的倍數,將最后剩下的節點保持原樣。你不能更改節點中的值,只能更改節點本身。

例如:

給定的鏈表是:1 -> 2 -> 3 -> 4 -> 5

對於 k=2,你應該返回 2 -> 1 -> 4 -> 3 -> 5

對於 k=3, 你應該返回 3 -> 2 -> 1 -> 4 -> 5

分析問題

我們把這個問題進行拆分。

  1. 我們首先將鏈表按照k個一組進行分組。對於最后一組,有可能元素個數不滿足k個。

  2. 對於每一個分組,我們去判斷元素的個數是否為k,如果是k的話,我們進行反轉,否則不需要進行反轉。

我們下面來看一下代碼實現。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class Solution:
    #反轉鏈表,並且返回鏈表頭和尾
    def reverse(self, head, tail):
        prev = tail.next
        p = head
        while prev != tail:
            next = p.next
            p.next = prev
            prev = p
            p = next
        return tail, head

    def reverseKGroup(self, head, k):
        #初始化一個哨兵節點,避免臨界條件復雜的判斷
        prehead = ListNode(0)
        #哨兵節點指向頭結點
        prehead.next = head
        pre = prehead

        while head:
            tail = pre
            #查看剩余部分長度是否大於等於k
            for i in range(k):
                tail = tail.next
                #如果剩余長度小於k,則不需要反轉,直接返回
                if not tail:
                    return prehead.next
            #tail指向子鏈表的尾部
            #所以next指向下一個子鏈表的頭部
            next = tail.next
            #將鏈表進行反轉,並返回鏈表頭和尾
            head, tail = self.reverse(head, tail)
            #把子鏈表重新接回原鏈表
            pre.next = head
            tail.next = next
            pre = tail
            head = tail.next
            
        return prehead.next

判斷鏈表是否有環

問題描述

LeetCode141. 環形鏈表

給定一個鏈表,判斷鏈表中是否有環。如果鏈表中有某個節點,可以通過連續跟蹤 next 指針再次到達,則鏈表中存在環。 為了表示給定鏈表中的環,我們使用整數 pos 來表示鏈表尾連接到鏈表中的位置(索引從 0 開始)。 如果 pos 是 -1,則在該鏈表中沒有環。注意:pos 不作為參數進行傳遞,僅僅是為了標識鏈表的實際情況。

如果鏈表中存在環,則返回 true 。 否則,返回 false 。

示例:

輸入:head = [-1,-7, 7,-4, 9, 6, -5, -2], pos = 3

輸出:true

解釋:鏈表中有一個環,其尾部連接到第二個節點。

分析問題

拿到這個問題,我們最直觀的想法就是在遍歷結點的過程中去標記一下這個結點是否已經被訪問過。如果被訪問過,那么就代表該鏈表有環,直接返回。如果沒有被訪問過,我們就標記一下,然后接着去遍歷下一個結點,直到遍歷完整個鏈表為止。下面我們來看一下代碼的實現。

def hasCycle(self, head):
    tags = set()
    while head:
        #表示已經被訪問過了,代表有環
        if head in tags:
            return True
        tags.add(head)
        head = head.next
    return False

我們可以知道該算法的時間復雜度和空間復雜度都是O(n)。那我們有更好的解法嗎?

優化

你可以這么思考,當有兩名同學在操場上以不同的速度進行跑步,它們最終肯定會相遇。因為操場是環形的,如果在直線上跑,那他們肯定不會相遇。

我們假設同學1以速度1在跑,同學2以速度2在跑。

下面我們來看一下代碼如何實現。

def hasCycle(self, head):
    #如果鏈表為空或者鏈表只有一個結點
    #直接返回false,因為不可能有環
    if not head or not head.next:
        return False
    #快慢指針
    slow = fast = head
    start = True

    while slow != fast || start:
        start=False
        if not fast or not fast.next:
            return False
        slow = slow.next
        fast = fast.next.next

    return True

我們這里引入了一個變量start表示是否是起跑。

可以看到該算法的空間復雜度降低為O(1)。

鏈表中環的入口結點

問題描述

LeetCode 劍指 Offer II 022. 鏈表中環的入口節點

給定一個鏈表,返回鏈表開始入環的第一個節點。 如果鏈表無環,則返回 null。

為了表示給定鏈表中的環,我們使用整數 pos 來表示鏈表尾連接到鏈表中的位置(索引從 0 開始)。 如果 pos 是 -1,則在該鏈表中沒有環。注意,pos 僅僅是用於標識環的情況,並不會作為參數傳遞到函數中。

說明:不允許修改給定的鏈表。

分析問題

拿到這個問題,我們最直觀的想法就是在遍歷結點的過程中去標記一下這個結點是否已經被訪問過。如果被訪問過,就代表這個結點是鏈表中環的入口點,我們直接返回就好。如果沒有被訪問過,我們就標記一下,然后接着去遍歷下一個結點,直到遍歷完整個鏈表為止。下面我們來看一下代碼的實現。

def EntryNodeOfLoop(self, pHead):
        tags = set()
        while pHead:
            #表示已經被訪問過了,代表有環
            if pHead in tags:
                return pHead
            tags.add(pHead)
            pHead = pHead.next
        return None

我們可以看到該算法的時間復雜度和空間復雜度都是O(n)。

優化

我們這里也可以采用快慢指針來求解,就和上一題的解法類似,我們來看一下。

我們可以使用兩個指針fast和slow。他們都從鏈表的頭部開始出發,slow每次都走一步,即slow=slow->next,而fast每次走兩步,即fast=fast->next->next。如果鏈表中有環,則fast和slow最終會在環中相遇。

我們假設鏈表中環外的長度為a,show指針進入環后又走了b的距離與fast相遇,此時fast指針已經繞着環走了n圈。所以快指針一共走了a+n(b+c)+b=a+(n+1)b+nc的距離,我們知道快指針每次走2步,而慢指針每次走一步,所以,我們可以得出快指針走的距離是慢指針的兩倍,即a+(n+1)b+nc=2(a+b),所以a=c+(n-1)(b+c)。這里你會發現:從相遇點到入環的距離c,再加上n-1圈的環長,恰好等於從鏈表頭部到入環點的距離。

因此,當發現slow和fast相遇時,我們再額外使用一個指針ptr指向鏈表頭部,然后它和slow指針每次都向后移動一個位置。最終,他們會在入環點相遇。

Tips: 你也許會有疑問,為什么慢指針在第一圈沒走完就會和快指針相遇呢?我們來看一下,首先,快指針會率先進入環內。然后,當慢指針到達環的入口時,快指針在環中的某個位置,我們假設此時快指針和慢指針的距離為x,若x=0,則表示在慢指針剛入環時就相遇了。我們假設環的長度為n,如果看成快指針去追趕慢指針,那么快指針需要追趕的距離為n-x。因為快指針每次都比慢指針多走一步,所以一共需要n-x次就能追上慢指針,在快指針遇上慢指針時,慢指針一共走了n-x步,其中x>=0,所以慢指針走的路程小於等於n,即走不完一圈就會相遇。

下面,我們來看一下代碼實現。

def detectCycle(head):
    if not head:
        return None
    #快慢指針
    slow = head
    fast = head
    while True:
        if not fast or not fast.next:
            return None
        fast=fast.next.next
        slow=slow.next
        #相遇時,跳出循環
        if fast == slow:
            break

    ptr = head
    while ptr != slow:
        ptr=ptr.next
        slow=slow.next
    return ptr

該算法的時間復雜度是O(n),空間復雜度是O(1)。

刪除鏈表倒數第n個節點

問題描述

LeetCode 劍指 Offer II 021. 刪除鏈表的倒數第 n 個結點

給定一個鏈表,刪除鏈表的倒數第 n個結點,並且返回鏈表的頭結點。

示例:

輸入:head = [1,2,3,4,5], n = 2

輸出:[1,2,3,5]

分析問題

這個問題最簡單的求解方式就是遍歷一遍鏈表,獲取到鏈表的長度m,然后求出倒數第n個結點的位置m-n+1,然后再遍歷一次鏈表,找到第m-n+1的位置,刪掉這個結點就好。其實,我們這里可以使用雙指針法,只需要遍歷一次鏈表就可以解決問題。

首先,我們可以設置兩個指針slow和fast都指向頭結點,然后讓fast先走n步,之后slow和fast一起走,直到fast.next為空為止,這是slow指向的就是倒數第n+1個結點,我們通過slow.next=slow.next.next就可以把倒數第n個結點刪掉。

下面我們來看一下代碼的實現。

    def removeNthFromEnd(self,head,n):    
        #左右指針指向頭結點
        slow = fast = head
        #fast先走n步
        while n>0 and fast:
            fast = fast.next
            n=n-1
            
        if not fast:
            return head.next
        
        while fast.next:
            slow = slow.next
            fast = fast.next
        slow.next = slow.next.next
        return head

該算法只遍歷一遍鏈表,所以時間復雜度是O(n),空間復雜度是O(1)。

兩個鏈表的第一個公共結點

問題描述

LeetCode 劍指 Offer 52. 兩個鏈表的第一個公共節點

輸入兩個無環的單向鏈表,找出它們的第一個公共結點,如果沒有公共節點則返回空。

要求:空間復雜度是O(1),時間復雜度是O(m+n)。

示例:

分析問題

這個問題最直觀的想法就是遍歷鏈表headA,然后把headA中的所有每個節點都加入到集合中。然后再循環遍歷鏈表headB,判斷結點是否在集合中,如果在,則返回該結點,該結點就代表第一個公共結點。如果不在,繼續遍歷,直到結束。如果headB的所有結點都不在集合中,則表明不相交,直接返回null。

def getIntersectionNode(headA,headB):
    nodes=set()
    while headA:
        nodes.add(headA)
        headA=headA.next
    while headB:
        if nodes.__contains__(headB):
            return headB
        headB=headB.next
    return None

該算法的時間復雜度是O(m+n),空間復雜度是O(n)。其中m和n分別是鏈表headA和headB的長度。

由於題目要求時間復雜度是O(m+n),空間復雜度是O(1)。我們這里可以使用雙指針法將空間復雜度降低到O(1)。我們分別用兩個指針p1和p2分別指向headA和headB,然后同時移動指針p1和p2。當p1到達headA的末尾時,讓p1指向headB,當p2到達headB的末尾時,讓p2指向headA,這樣,當它們相遇時,所指的節點就是第一個公共結點。

Tips:假設headA不相交的部分是a,headB不相交的部分是b,公共部分是c,那么headA的長度為a+c,headB的長度為b+c,當a等於b時,可以知道p1和p2指針同時到達第一個公共結點;當a不等於b時,在p1移動了a+b+c時,即p1走完headA,又在headB上走了b時,p2也走了a+b+c,即p2走完headB,然后又在headA上走了a,此時p1和p2正好相遇,且指向了第一個公共結點。

def getIntersectionNode(headA,headB):
    p1 = headA
    p2 = headB

    while p1 != p2:
        if p1:
            p1=p1.next
        else:
            p1=headB
        if p2:
            p2=p2.next
        else:
            p2=headA
    return p1

兩個鏈表生成相加鏈表

問題描述

LeetCode 劍指 Offer II 025. 鏈表中的兩數相加

假設鏈表中每一個節點的值都在 0 - 9 之間,那么鏈表整體就可以代表一個整數。給定兩個這種鏈表,請生成代表兩個整數相加值的結果鏈表。

示例:

輸入:[9,3,7],[6,3]

返回值:{1,0,0,0}

分析問題

由於兩個數字相加是從個位數開始,然后再十位數、百位數。對於的鏈表中,我們需要將兩個鏈表進行右端對齊,然后從右往左進行計算。

要想讓兩個鏈表右端對齊,我們有兩種實現方式。

  1. 將兩個鏈表進行反轉,然后直接求和。
  2. 借助棧這種先進后出的特性,來實現鏈表的右端對齊。

我們先來看一下如何使用鏈表反轉來實現。

class Solution(object):
    def reverse(self, head):
        cur = head
        #初始化時,pre為None
        pre = None
        while cur:
            next=cur.next
            cur.next = pre
            pre = cur
            cur = next
        return pre
    def addTwoNumbers(self, l1, l2):
        #將兩個鏈表翻轉
        l1 = self.reverse(l1)
        l2 = self.reverse(l2)
        head=ListNode(0)
        pre=head
        #代表是否進位
        carray=0
        while l1 or l2:
           v1=l1.val if l1 else 0
           v2=l2.val if l2 else 0
           sum=v1+v2+carray
           #進位數
           carray=int(sum/10)
           tmp=sum%10
           node=ListNode(tmp)
           pre.next=node
           pre=pre.next
           if l1:
               l1=l1.next
           if l2:
               l2=l2.next
        if carray==1:
            node=ListNode(carray)
            pre.next=node
        
        return self.reverse(head.next)

下面我們來看一下如何使用棧來求解。我們首先將兩個鏈表從頭到尾放入兩個棧中,然后每次同時出棧,就可以實現鏈表的右端對齊相加,我們來看一下代碼如何實現。

def addTwoNumbers(l1, l2):
    #申請兩個棧
    stack1=[]
    stack2=[]
    #l1入棧
    while l1:
        stack1.append(l1.val)
        l1 = l1.next
    while l2:
        stack2.append(l2.val)
        l2 = l2.next

    head = None
    carry = 0

    while stack1 and stack2:
        num = stack1.pop() + stack2.pop() + carry
        #求進位數
        carry=int(num/10)
        tmp=num%10
        node = ListNode(tmp)
        node.next = head
        head = node


    s = stack1 if stack1 else stack2
    while s:
        num = s.pop() + carry
        carry = int(num / 10)
        tmp = num % 10
        node = ListNode(tmp)
        node.next = head
        head = node

    if carry==1:
        node = ListNode(carry)
        node.next = head
        head = node
    return head

單鏈表的排序

問題描述

LeetCode 148. 排序鏈表

給定一個節點數為n的無序單鏈表,對其按升序排序。

要求:空間復雜度 O(n),時間復雜度 O(nlogn)。

示例:

輸入:[-1,0,-2]

返回值:{-2,-1,0}

分析問題

由於題目要求時間復雜度是O(nlogn),那時間復雜度是O(nlogn)的排序算法有歸並排序、快速排序和堆排序,其中最適合鏈表的排序算法是歸並排序。

歸並排序是基於分治思想,最容易想到的就是自頂向下的遞歸實現。自頂向下的遞歸實現主要包括二個環節。

  1. 分割環節

    • 找到鏈表的中點,以中點為分界,將鏈表拆分成兩個子鏈表。尋找鏈表的中點可以使用快慢指針法,快指針每次移動2步,慢指針每次移動1步,當快指針走到鏈表的末尾時,慢指針恰好指向了鏈表的中點位置。

    • 找到中點后,將鏈表在中點處分割成兩個子鏈表。

    • 然后遞歸的進行分割,直到分割后的鏈表只有一個節點或者為Null。這時,分割的子鏈表都是有序的,因為只包含一個節點。

  2. 合並環節

    • 將兩個有序的鏈表合並成一個有序鏈表。我們可以采用雙指針法求解。
    • 遞歸執行,直到合並完成。

class Solution:
    def sortList(self, head):
        #如果鏈表為空或者只包含一個節點,遞歸終止
        if not head or not head.next:
            return head
        #使用快慢指針法來尋找鏈表的中點
        slow=head
        fast=head.next
        while fast and fast.next:
            fast=fast.next.next
            slow=slow.next
        #slow指向的就是鏈表的中點,將鏈表在中點處進行分割
        head2=slow.next
        slow.next=None
        #遞歸的切割分割鏈表
        left = self.sortList(head)
        right = self.sortList(head2)
        #合並鏈表,使用雙指針法
        tmp = res = ListNode(0)
        while left and right:
            if left.val < right.val:
                tmp.next=left
                left=left.next
            else:
                tmp.next=right
                right=right.next
            tmp=tmp.next
        if left:
            tmp.next=left
        else:
            tmp.next=right
        return res.next

該算法的時間復雜度是O(n)。由於自頂向下是通過遞歸來實現的,如果考慮遞歸調用棧的棧空間,那么該算法的空間復雜度是O(logn)。

優化

我們也可以采用自底向上的方法來求解。

首先,我們求出鏈表的長度length。然后將鏈表拆分成子鏈表進行合並。

  1. 我們用sublength表示每次需要排序的子鏈表的長度,初始時sublength=1。
  2. 每次將鏈表拆分成若干個長度為sublength的子鏈表(最后一個子鏈表的長度可能小於sublength),按照每兩個子鏈表一組進行合並,合並后即可以得到若干個長度為sublength * 2的有序子鏈表(最后一個子鏈表的長度可能小於sublength * 2)。
  3. 將sublength的值加倍,重復第二步,然后對更長的有序子鏈表進行合並,直到有序子鏈表的長度大於或等於鏈表的長度,這樣整個鏈表的排序就完成了。

我們來看一下代碼的實現。

class Solution:
    def sortList(self, head):
        #合並兩個有序鏈表
        def merge(head1, head2):
            #哨兵節點
            dummyHead = ListNode(0)
            temp=dummyHead

            while head1 and head2:
                if head1.val <= head2.val:
                    temp.next = head1
                    head1 = head1.next
                else:
                    temp.next = head2
                    head2 = head2.next
                temp = temp.next
            if head1:
                temp.next = head1
            else:
                temp.next = head2

            return dummyHead.next

        #如果鏈表為空,直接返回
        if not head:
            return head
        #遍歷一遍鏈表,求出鏈表的長度
        length = 0
        node = head
        while node:
            length += 1
            node = node.next

        #創建一個哨兵節點,指向鏈表頭
        dummyHead = ListNode(0)
        dummyHead.next=head

        #初始時,子鏈表的長度為1
        subLength = 1

        while subLength < length:

            prev=dummyHead
            cur=dummyHead.next

            while cur:
                #截取長度為subLength的子鏈表head1
                head1 = cur
                for i in range(1, subLength):
                    if cur.next:
                        cur = cur.next
                    else:
                        break

                head2 = cur.next
                cur.next = None

                #截取長度為subLength的子鏈表head2
                cur = head2
                for i in range(1, subLength):
                    if cur and cur.next:
                        cur = cur.next
                    else:
                        break

                #截取完后剩余的鏈表節點
                surplus_head = None
                if cur:
                    surplus_head = cur.next
                    cur.next = None
                #將兩個有序鏈表進行合並
                merged = merge(head1, head2)
                #將排好序的鏈表插入到新生成的鏈表里
                prev.next = merged

                #將指針移動到鏈表的末尾
                while prev.next:
                    prev = prev.next

                #繼續合並剩余的節點
                cur=surplus_head
            subLength = subLength * 2

        return dummyHead.next

該算法的時間復雜度是O(nlogn),空間復雜度是O(1)。

判斷一個鏈表是否為回文結構

問題描述

LeetCode 劍指 Offer II 027. 回文鏈表

給定一個鏈表,請判斷該鏈表是否為回文結構。回文是指該字符串正序逆序完全一致。

示例:

輸入:{1,2,2,1}

輸出:true

說明:1 -> 2 -> 2 -> 1

分析問題

回文串是指正讀反讀都一樣的字符串,最簡單的是使用雙指針法。但是對於鏈表這種數據結構來說,指針只能向一個方向移動,也就是說只能找到后繼節點,沒辦法找到前驅節點。所以沒辦法使用雙指針法,要想使用雙指針,我們就需要把鏈表元素放入一個數組中,然后再去判斷是否是回文,這需要O(n)的空間復雜度,這里就不在贅述。大家可以去看第44題。

我們可以這么考慮,將鏈表的后半部分進行反轉,然后將前半部分和后半部分進行比較,如果相同就代表是回文鏈表,否則不是回文鏈表。尋找鏈表的中點我們可以使用快慢指針的方式。

  1. 快慢指針尋找鏈表中點。

  1. 對鏈表的后半部分進行翻轉

  1. 前半部分和后半部分進行比較。

class Solution:
    def isPalindrome(self, head) -> bool:
        #鏈表為空,直接返回true
        if head is None:
            return True

        #找到鏈表的中點
        middle_point = self.middle_point(head)
        second_start = self.reverse_list(middle_point.next)

        #判斷前半部分和后半部分是否相等
        result = True
        first = head
        second = second_start
        while result and second is not None:
            if first.val != second.val:
                result = False
            first = first.next
            second = second.next

        #還原鏈表並返回結果
        middle_point.next = self.reverse_list(second_start)
        return result

    #快慢指針尋找中點
    def middle_point(self, head):
        fast = head
        slow = head
        while fast.next is not None and fast.next.next is not None:
            fast = fast.next.next
            slow = slow.next
        return slow

    #翻轉鏈表
    def reverse_list(self, head):
        previous = None
        current = head
        while current is not None:
            next_node = current.next
            current.next = previous
            previous = current
            current = next_node
        return previous

鏈表內指定區間反轉

問題描述

LeetCode 92. 反轉鏈表 II

給你單鏈表的頭指針 head 和兩個整數 left 和 right ,其中 left <= right 。請你反轉從位置 left 到位置 right 的鏈表節點,返回反轉后的鏈表 。

示例:

輸入:head = [1,2,3,4,5], left = 2,right = 4

輸出:[1,4,3,2,5]

分析問題

對於鏈表相關的題目,我們一定要先想清楚思路,搞懂指針移動的先后順序。

對於這道題,我們可以采用頭插法來求解。在鏈表反轉的區間內,當我們每次遍歷到一個節點時,就讓該節點插入到反轉部分的起始位置。如下圖所示:

具體來說,我們定義三個指針pre、cur、next變量。

  1. 我們首先將pre移動到第一個要反轉的節點的前面,即pre->next=left
  2. 然后將指針cur移動到第一個要反轉的節點位置上,即cur=left,
  3. 然后將 cur->next 賦值給變量next。
  4. 將cur的下一個節點指向next的下一個節點,即cur->next=next->next
  5. 然后將next的下一個節點指向pre的下一個節點,即next->next=pre->next
  6. 然后將next插入到鏈表頭部,即pre->next=next。
  7. 然后循環往復執行3、4、5、6,直到反轉完鏈表區間內的節點。

下面我們來看一下代碼如何實現。

class Solution:
    def reverseBetween(self, head, left, right):
        #設置哨兵節點,對於鏈表相關的問題,我們通過設置哨兵節點
        #可以省去邊界條件的判斷
        dummynode = ListNode(-1)
        #哨兵節點指向鏈表頭
        dummynode.next = head
        pre = dummynode
        #遍歷,使得pre指向鏈表反轉部分
        #的第一個結點left
        for _ in range(left - 1):
            pre = pre.next
        #cur指向鏈表反轉部分的第一個節點
        cur = pre.next
        for _ in range(right - left):
            next = cur.next
            cur.next = next.next
            next.next = pre.next
            pre.next = next
        return dummynode.next

該算法的時間復雜度是O(N),其中 N 是鏈表總節點數。最多只遍歷了鏈表一次,就完成了反轉。空間復雜度是O(1),只用到了常數個變量。

刪除有序鏈表中重復的元素-I

問題描述

LeetCode 83. 刪除排序鏈表中的重復元素

刪除給出鏈表中的重復元素(鏈表中元素從小到大有序),使鏈表中的所有元素都只出現一次。

示例:

輸入:{1,1,2}

輸出:{1,2}

分析問題

因為給定的鏈表是排好序的,所以我們可以知道重復的元素在鏈表中出現的位置一定是連續的,因此我們只需要對鏈表進行一次遍歷,就可以刪除重復的元素。

開始時,我們定義一個指針cur指向鏈表的頭結點,然后開始對鏈表進行遍歷。如果cur.val==cur.next.val時,我們就將cur.next這個節點從鏈表中移除;如果不相同,我們將指針cur后移,即cur=cur.next。當遍歷完整個鏈表之后,我們返回鏈表的頭結點即可。

下面我們來看一下代碼的實現。

class Solution:
    def deleteDuplicates(self, head):
        #如果鏈表為空,直接返回
        if not head:
            return head
        #cur指向頭結點
        cur = head
        #當cur.next不為空時
        while cur.next:
            #如果值相同,刪除cur.next節點
            if cur.val == cur.next.val:
                cur.next = cur.next.next
            #否則cur后移
            else:
                cur = cur.next
        return head

該算法的時間復雜度是O(N),空間復雜度是O(1)。

刪除有序鏈表中重復的元素-II

問題描述

LeetCode 82. 刪除排序鏈表中的重復元素 II

存在一個按升序排列的鏈表,給你這個鏈表的頭節點 head ,請你刪除鏈表中所有存在數字重復情況的節點,只保留原始鏈表中沒有重復出現的數字。返回同樣按升序排列的結果鏈表。

示例:

輸入:head = [1,2,3,3,4,5]

輸出:[1,2,5]

分析問題

由於給定的鏈表是有序的,所以鏈表中重復的元素在位置上肯定是相鄰的。因此,我們可以對鏈表進行一次遍歷,就可以刪除重復的元素。

這里需要注意一點,由於可能會刪除頭結點head,我們引入了一個哨兵節點dummy,讓dummy.next=head,這樣即使head被刪除了,那么也會操作dummy.next指向新的鏈表頭結點,所以最終返回dummy.next即可。

首先,我們讓cur指針指向鏈表的哨兵節點,然后開始對鏈表進行遍歷。如果cur.next與cur.next.next對應的元素值相同,那么我們就需要將cur.next以及后面所有值相同的鏈表節點全部刪除,直到cur.next為空節點或者其元素值與其不同。如果cur.next與cur.next.next對應的元素值不相同,那么我們就可以將cur 指向 cur.next。

下面我們來看一下代碼的實現。

class Solution:
    def deleteDuplicates(self, head):
        #如果鏈表為空,直接返回
        if not head:
            return head
        #創建一個哨兵節點
        dummy = ListNode(0)
        dummy.next=head

        #開始時,將cur指向哨兵節點
        cur = dummy
        while cur.next and cur.next.next:
            #如果cur.next.val和cur.next.next.val相同,刪除重復元素
            if cur.next.val == cur.next.next.val:
                data = cur.next.val
                while cur.next and cur.next.val == data:
                    cur.next = cur.next.next
            else:
                cur = cur.next

        return dummy.next

該算法的時間復雜度是O(n),空間復雜度是O(1)。

鏈表的奇偶重排

問題描述

LeetCode 328. 奇偶鏈表

給定一個單鏈表,把所有的奇數節點和偶數節點分別排在一起。請注意,這里的奇數節點和偶數節點指的是節點編號的奇偶性,而不是節點的值的奇偶性。

示例:

輸入:{1,2,3,4,5,6}

輸出:{1,3,5,2,4,6}

分析問題

要想把一個鏈表的奇數節點和偶數節點分別排在一起,我們可以先分離鏈表,把一個鏈表拆分成兩個鏈表,其中一個是奇數鏈表,另一個是偶數鏈表,最后將偶數鏈表拼接在奇數鏈表后即可。

我們都知道,對於一個鏈表來說,相鄰節點的奇偶性是不同的。原始鏈表的頭節點head是奇數鏈表的頭節點,head的后一個節點是偶數鏈表的頭節點,我們假設是evenHead ,則evenHead = head.next。我們維護兩個指針odd和even分別指向奇數鏈表和偶數鏈表的最后一個節點,初始時 odd=head,even=evenHead。通過不斷的更新odd和even,將鏈表分割成奇數鏈表和偶數鏈表。

  1. 更新奇數節點,我們令 odd.next=even.next,此時奇數鏈表的最后一個節點指向了偶數節點even的后一個節點。然后令odd=odd.next,此時odd變成了even的后一個節點。
  2. 更新偶數節點,我們令 even.next=odd.next,此時偶數節點的最后一個節點指向了奇數節點odd的后一個節點。然后令even=even.next,此時even變成了odd的后一個節點。

通過以上兩步,我們就完成了對一個奇數節點和一個偶數節點的分離。重復上述操作,直到全部節點分離完成。最后令 odd.next = evenHead,將偶數鏈表連接在奇數鏈表之后,即完成了奇數鏈表和偶數鏈表的合並。

下面我們來看一下代碼的實現。

class Solution:
    def oddEvenList(self, head):
        #如果鏈表為空,直接返回
        if not head:
            return head
        #evenHead指向偶數鏈表的頭節點
        evenHead = head.next
        #指向奇數鏈表和偶數鏈表的末尾節點
        odd = head
        even = evenHead
        while even and even.next:
            #奇數鏈表的末尾節點指向偶數節點的下一個節點
            odd.next = even.next
            #奇數鏈表末尾節點后移
            odd = odd.next
            #偶數鏈表的末尾節點指向奇數節點的下一個節點
            even.next = odd.next
            #偶數鏈表末尾節點后移
            even = even.next

        #偶數鏈表連接到奇數鏈表的末尾
        odd.next = evenHead
        return head

該算法的時間復雜度是O(n),空間復雜度是O(1)。

重排鏈表

問題描述

LeetCode 143. 重排鏈表

給定一個單鏈表 L 的頭節點head ,單鏈表 L 表示為:L0 -> L1 -> ... -> Ln-1 -> Ln,將其重新排列后變成 L0 -> Ln -> L1 -> Ln-1 -> ...。不能只是單純的改變節點的值,而需要實際的進行節點的交換。

示例:

輸入: head = [1,2,3,4]

輸出: [1,4,2,3]

分析問題

首先,我們來觀察一下鏈表重置前和重置后的變化。如下圖所示:

我們可以知道,重置后的鏈表是將原鏈表的左半端和反轉后的右半段進行節點交叉合並而成的,所有我們可以分三步來求解。

  1. 找到原鏈表的中點,將鏈表分割成左右兩部分。
  2. 對后半部分進行反轉。
  3. 建原鏈表的左半部分和反轉后的右半部分進行合並。
class Solution:
    def reorderList(self, head):
        #如果鏈表為空,直接返回
        if not head:
            return
        #尋找鏈表的中點,將鏈表分割成左右兩部分
        mid = self.middleNode(head)
        left = head
        right = mid.next
        mid.next = None
        #對右半部分的鏈表進行反轉
        right = self.reverseList(right)
        #左右鏈表進行合並
        self.mergeList(left, right)

    def middleNode(self, head):
        #使用快慢指針法求中點
        slow = fast = head
        while fast.next and fast.next.next:
            slow = slow.next
            fast = fast.next.next
        return slow

    #對鏈表進行反轉
    def reverseList(self, head):
        prev = None
        curr = head
        while curr:
            tmp = curr.next
            curr.next = prev
            prev = curr
            curr = tmp
        return prev

    #對兩個鏈表進行合並操作
    def mergeList(self, l1, l2):
        #l1和l2的節點數量相差不會超過一個
        #所以直接合並就好
        while l1 and l2:
            tmp1 = l1.next
            tmp2 = l2.next

            l1.next = l2
            l1 = tmp1

            l2.next = l1
            l2 = tmp2

該算法的時間復雜度是O(N),空間復雜度是O(1)。

鏈表中倒數最后k個節點

問題描述

LeetCode 劍指 Offer 22. 鏈表中倒數第k個節點

輸入一個鏈表,輸出該鏈表中倒數第k個節點。為了符合大多數人的習慣,本題從1開始計數,即鏈表的尾節點是倒數第1個節點。例如,一個鏈表有 6 個節點,從頭節點開始,它們的值依次是 1、2、3、4、5、6。這個鏈表的倒數第 3 個節點是值為 4 的節點。

示例:

輸入:{1,2,3,4,5},2

輸出:{4,5}

分析問題

這道題比較簡單,我們可以直接使用快慢指針來求解,開始時申請兩個指針同時指向鏈表的頭結點,記為slow和fast,然后讓fast先移動k步,再然后讓兩個指針slow和fast同時移動,使得fast和slow在移動的過程中總是相隔k個單位,當fast指針走到末尾的時候,此時slow正好指向的是倒數第k個位置。

下面我們來看一下代碼的實現。

class Solution:
    def FindKthToTail(self, pHead, k):
        #快慢指針同時執行頭結點
        slow=fast=pHead
        #快指針移動k步
        for i in range(0,k):
            #如果還沒走完k步就走到了鏈表尾,直接返回None
            if not fast:
                ‘return None
            fast = fast.next
        #兩個指針同時移動,直到fast為空
        while fast:
            slow=slow.next
            fast=fast.next

        return slow

划分鏈表

問題描述

LeetCode 面試題 02.04. 分割鏈表

給你一個鏈表的頭節點 head 和一個特定值 x ,請你對鏈表進行分隔,使得所有 小於 x 的節點都出現在 大於或等於 x 的節點之前。並且兩個部分之內的節點之間與原來的鏈表要保持相對順序不變。

示例:

輸入:head = [1,4,3,2,5,2], x = 3

輸出:[1,2,2,4,3,5]

分析問題

簡單來說,我們只需要維護兩個鏈表small和large即可,使得small鏈表按順序存儲所有小於x的節點,large鏈表按順序存儲所有大於等於x的節點。當我們遍歷完鏈表之后,只需要將small鏈表的尾節點指向large鏈表的頭結點即可完成分割鏈表的操作。

首先,我們創建兩個節點smallHead和largeHead分別為兩個鏈表的啞結點,這是為了方便的處理頭結點為空的邊界條件,同時smallTail和largeTail分別指向兩個鏈表的末尾節點。開始時,smallHead=smallTail,largeHead=largeTail,然后從前往后遍歷鏈表head,判斷當前節點的值是否小於x,如果小於x,就將smallTail的next指針指向該節點,否則將largeTail的next指針指向該節點。

遍歷結束后,我們將largeTail的next指針置為空,這是因為當前節點復用的是原鏈表的節點,而其 next 指針可能指向一個小於 x的節點,我們需要切斷這個引用。然后將smallTail的next指針指向largeHead的next指針指向的節點,來將兩個鏈表合並起來,最后返回smallHead.next即可。

下面我們來看一下代碼實現。

class Solution(object):
    def partition(self, head, x):
        #創建兩個啞結點
        smallHead = smallTail = ListNode(0)
        largeHead = largeTail = ListNode(0)
        #遍歷鏈表
        while head:
            #如果head.val<x,將當前節點插入small中,否則插入large中
            if head.val < x:
                smallTail.next=head
                smallTail=smallTail.next
            else:
                largeTail.next=head
                largeTail=largeTail.next
            head=head.next
        largeTail.next=None
        #合並兩個鏈表
        smallTail.next=largeHead.next
        return smallHead.next

該算法的時間復雜度是O(n),空間復雜度是O(1)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM