1. 程式人生 > 實用技巧 >秋招演算法刷題彙總(1) - 連結串列篇

秋招演算法刷題彙總(1) - 連結串列篇

目錄

從尾到頭列印連結串列

題目描述

輸入一個連結串列,按連結串列從尾到頭的順序返回一個ArrayList。

  • 列表反轉:從頭到尾獲取連結串列節點的值並存入list,之後反轉list.

  • 遞迴

    #solution 1 
    
    class Solution:
        # 返回從尾部到頭部的列表值序列,例如[1,2,3]
        
        def printListFromTailToHead(self, listNode):
            l = []
            while listNode:
                l.append(listNode.val)
                listNode = listNode.next
            return l[::-1]
    
    #solution 2
    
    def recu(result,listNode):
        if not listNode:
            return
        recu(result,listNode.next)
        result.append(listNode.val)
    
    class Solution:
        # 返回從尾部到頭部的列表值序列,例如[1,2,3]
        
        def printListFromTailToHead(self, listNode):
            result = []
            recu(result,listNode)
            return result
    

刪除連結串列中重複的節點

題目描述(同leetcode 83) 中等難度

在一個排序的連結串列中,存在重複的結點,請刪除該連結串列中重複的結點,重複的結點不保留,返回連結串列頭指標。 例如,連結串列1->2->3->3->4->4->5 處理後為 1->2->5.

  • 使用兩個指標p、q同時遍歷連結串列,用q與q.next比較判斷需要刪除的節點,之後使用p.next=q.next刪除中間重複的節點。
  • 注意點:
    • 連結串列1->1->2型別,在頭部重複。新增新的連結串列表頭。
    • 連結串列1->2->2->2型別,大於2個重複結點。使用while迴圈找到不相同的節點為止。注意:找到後並不是直接連結不相同的結點,因為後面可能同樣會出現重複的情況。
      如:1->2->2->3->3->4,遇到第一個3時不能直接把p.next指向3,而是通過q指標越過之前重複的元素,然後對新的元素進行比較。
    • 最後可能存在一個尾節點需要新增。重要:沒有這一行會導致p仍然連結一部分原連結串列節點,q的跳躍沒有起到應有的作用。
class Solution:
    def deleteDuplication(self, pHead):
        # new linklist head
        
        head = ListNode(0)
        head.next = pHead
        p=head
        q = head.next
        while q and q.next:
            if q.val == q.next.val:
                #>=2 same value node,jump these nodes
                
                while q.next and q.val == q.next.val:
                    q.next = q.next.next
            else:
                p.next = q
                p = p.next
            q = q.next
        #link last node
        
        p.next = q
        return head.next

連結串列中的倒數第 k 個節點

題目描述

輸入一個連結串列,輸出該連結串列中倒數第k個結點。

  • 兩個指標head、q,q先走k個節點,之後head與q同時向後遍歷,當q指向最後一個節點時head指向倒數第k個節點。
  • 注意:k值可能越界
class Solution:
    def FindKthToTail(self, head, k):
        q = head
        for _ in range(k):
            #note : note q.next
            
            if q:
                q = q.next
            else:
                return None
        while q:
            q = q.next
            head = head.next
        return head

連結串列中環的入口節點

題目描述 劍指Offer 23 中等難度 leetcode 142

給一個連結串列,若其中包含環,請找出該連結串列的環的入口結點,否則,輸出null。

  • 思路一:使用set來儲存遍歷的每一個節點,遇到重複節點則為環的入口。空間複雜度\(O(n)\)
class Solution:
    def EntryNodeOfLoop(self, pHead):
        nodes = set()
        while pHead:
            if pHead in nodes:
                return pHead
            else:
                nodes.add(pHead)
                pHead = pHead.next
        return None
  • 思路二:

    1. 使用一塊一慢兩個指標,如果有環則兩個指標會相遇;
    2. 固定一個指標,單步移動另一個,當其相等時可計算出環的長度\(l\)
    3. 兩個指標位置分別設為0和l,以相同速度向後遍歷,相遇節點即為環入口節點。
    • 注意:各層迴圈的判定條件,以及對空連結串列以及長度小於2的連結串列的判斷。
class Solution:
    def EntryNodeOfLoop(self, pHead):
        if not pHead or not pHead.next or not pHead.next.next:
            return None
        pslow = pHead.next
        pfast = pslow.next
        #if exists loop
        
        while pfast != pslow:
            #使用較快的指標next來判斷是否到連結串列尾
            
            if not pfast.next or not pfast.next.next:
                return None
            pfast = pfast.next.next
            pslow = pslow.next
        #確定存在迴圈,計算環的長度
        
        length = 1
        while pfast.next != pslow:
            pfast = pfast.next
            length += 1
        #兩指標從 0和l處開始遍歷,相遇時為環的入口
        
        pfast = pslow = pHead
        for _ in range(length):
            pfast = pfast.next
        
        while pfast != pslow:
            pfast = pfast.next
            pslow = pslow.next
        return pfast
  • 思路三 floyd演算法
    • Floyd 的演算法被劃分成兩個不同的階段。在第一階段,找出列表中是否有環,如果沒有環,可以直接返回 null 並退出。否則,用相遇節點來找到環的入口。
    • 給定階段 1 找到的相遇點,階段 2將找到環的入口。首先我們初始化額外的兩個指標:ptr1指向連結串列的頭, ptr2 指向相遇點。然後,我們每次將它們往前移動一步,直到它們相遇,它們相遇的點就是環的入口,返回這個節點。
class Solution:
    def detectCycle(self, head: ListNode) -> ListNode:
        if not head or not head.next or not head.next.next:
            return None
        slow,fast = head.next,head.next.next
        while slow != fast:
            if not fast.next or not fast.next.next:
                return None
            slow = slow.next
            fast = fast.next.next
        fast = head
        while fast != slow:
            fast = fast.next
            slow = slow.next
        return slow

反轉連結串列

題目描述

輸入一個連結串列,反轉連結串列後,輸出新連結串列的表頭。

  • 初始化一個表頭,遍歷連結串列時使用頭插入法插入到新表頭後面,注意要使用臨時變數儲存,避免破壞pHead正向的遍歷過程。
class Solution:
    # 返回ListNode
    
    def ReverseList(self, pHead):
        result = ListNode(0)
        while pHead:
            temp = pHead
            pHead = pHead.next
            
            temp.next = result.next
            result.next = temp
        return result.next

合併兩個排序的連結串列

題目描述

輸入兩個單調遞增的連結串列,輸出兩個連結串列合成後的連結串列,當然我們需要合成後的連結串列滿足單調不減規則。

  • 先兩個同時遍歷 知道有一個空為止,之後把非空的連線到新的連結串列後。
class Solution:
    # 返回合併後列表
    
    def Merge(self, pHead1, pHead2):
        result = ListNode(0)
        p3 = result
        while pHead1 and pHead2:
            if pHead1.val < pHead2.val:
                p3.next = pHead1
                pHead1 = pHead1.next
            else:
                p3.next = pHead2
                pHead2 = pHead2.next
            p3 = p3.next
        p3.next = pHead1 if pHead1 else pHead2
        return result.next

複雜連結串列的複製

題目描述 劍指Offer 35 中等難度

輸入一個複雜連結串列(每個節點中有節點值,以及兩個指標,一個指向下一個節點,另一個特殊指標random指向任意一個節點),返回結果為複製後複雜連結串列的head。(注意,輸出結果中請不要返回引數中的節點引用,否則判題程式會直接返回空)

  • 簡單思路:先複製只帶next指標的連結串列,之後對每個節點逐個遍歷為其設定特殊指標的指向,時間複雜度為\(O(n^2)\)
  • 優化:三步法
    • 在原連結串列的基礎上覆制連結串列,在每個節點後插入一個複製節點;
    • 遍歷合成的連結串列,複製random的指向關係;
    • 將合成的連結串列拆成兩個新連結串列。

class Solution:
    # 返回 RandomListNode
    
    def Clone(self, pHead):
        if not pHead:
            return None
        #依據next指標拷貝連結串列
        
        p = pHead
        while p:
            node = RandomListNode(p.label)
            node.next = p.next
            p.next = node
            p = node.next
        #從前往後連結random指標 注意random指標可能為空
        
        p = pHead
        while p:
            p.next.random = p.random.next if p.random else None
            p = p.next.next
        #分離兩個連結串列  注意迴圈體裡的做法。兩個連結串列進行分離時的過程
        
        p=pHead
        result = p.next
        q = result
        while p:
            p.next = q.next
            p = p.next
            if not p:
                break
            q.next = p.next
            q = q.next
        return result
  • 解法二:遞迴
class Solution:
    def Clone(self, head):
        if not head: return
        newNode = RandomListNode(head.label)
        newNode.random = head.random
        newNode.next = self.Clone(head.next)
        return newNode

兩個連結串列的第一個公共節點

題目描述 劍指Offer 52

輸入兩個連結串列,找出它們的第一個公共結點。(注意因為傳入資料是連結串列,所以錯誤測試資料的提示是用其他方式顯示的,保證傳入資料是正確的)

  • 思路一:公共結點之後兩個連結串列相交,計算兩個連結串列的長度差l,然後較長的連結串列指標先走l步,之後兩指標一塊遍歷,當其相等時就是公共節點。
class Solution:
    def FindFirstCommonNode(self, pHead1, pHead2):
        if not pHead1 or not pHead2:
            return None
        #計算長度
        
        len1,len2 = 0,0
        p,q = pHead1,pHead2
        while p:
            p = p.next
            len1 += 1
        while q:
            q = q.next
            len2 += 1
        if len2 > len1 : 
            pHead1,pHead2 = pHead2,pHead1
        #一個先走len1-len2步 之後同步遍歷
        
        p,q = pHead1,pHead2
        for _ in range(len1-len2):
            p = p.next
        while p and q and p != q:
            p = p.next
            q = q.next
        return p if p else None
  • 思路二:依照棧的後進先出機制,使用兩個棧儲存連結串列,在同步彈出,找到不相同的前一個節點為公共節點。
    • python的棧 直接使用list,帶有append pop函式。
    • 佇列:可以使用list的append pop(0)但效率低\(O(n)\),collections有deque模組,有相應的append、pop、popleft等函式可用作棧、佇列。
class Solution:
    def FindFirstCommonNode(self, pHead1, pHead2):
        if not pHead1 or not pHead2:
            return None
        stack1,stack2 = [],[]
        while pHead1:
            stack1.append(pHead1)
            pHead1 = pHead1.next
        while pHead2:
            stack2.append(pHead2)
            pHead2 = pHead2.next
        first = None
        while stack1 != [] and stack2 != []:
            val1 = stack1.pop()
            val2 = stack2.pop()
            if val1 == val2:
                first = val2
        return first if first else None

leetcode 206: 反轉連結串列

反轉一個單鏈表。

示例:

輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
進階:
你可以迭代或遞迴地反轉連結串列。你能否用兩種方法解決這道題.

  • 迭代法見上。
  • 遞迴法:先尋找到最後一個節點,將其連線在head後。
class Solution(object):
	def reverseList(self, head):
		# 遞迴終止條件是當前為空,或者下一個節點為空
        
		if(head==None or head.next==None):
			return head
		# 這裡的cur就是最後一個節點
        
		cur = self.reverseList(head.next)
        
		# 如果連結串列是 1->2->3->4->5,那麼此時的cur就是5
        
		# 而head是4,head的下一個是5,下下一個是空
        
		# 所以head.next.next 就是5->4
        
		head.next.next = head
		# 防止連結串列迴圈,需要將head.next設定為空
        
		head.next = None
		# 每層遞迴函式都返回cur,也就是最後一個節點
        
		return cur

leetcode 234: 迴文連結串列

請判斷一個連結串列是否為迴文連結串列。

輸入: 1->2
輸出: false

輸入: 1->2->2->1
輸出: true
進階:
你能否用 O(n) 時間複雜度和 O(1) 空間複雜度解決此題?

  • 解法一:使用額外空間儲存連結串列值再進行比較,時\(O(n)\),空\(O(n)\)
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        vals = []
        while head:
            vals.append(head.val)
            head = head.next
        return vals == vals[::-1]
  • 解法二:遞迴,使用遞迴可以從尾向頭進行遍歷,結合一個從head開始從頭到尾的指標進行比較判斷是否是迴文。時\(O(n)\),空\(O(n)\)
    • 我們要理解計算機如何執行遞迴函式,在一個函式中呼叫一個函式時,計算機需要在進入被呼叫函式之前跟蹤它在當前函式中的位置(以及任何區域性變數的值),通過執行時存放在堆疊中來實現(堆疊幀)。在堆疊中存放好了資料後就可以進入被呼叫的函式。在完成被呼叫函式之後,他會彈出堆疊頂部元素,以恢復在進行函式呼叫之前所在的函式。在進行迴文檢查之前,遞迴函式將在堆疊中建立 n 個堆疊幀,計算機會逐個彈出進行處理。所以在使用遞迴時要考慮堆疊的使用情況。
#front從前向後,遞迴從尾到前,逐個比較

class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        self.front = head
        def recursive_check(currentNode):
            if currentNode is not None:
                if not recursive_check(currentNode.next):
                    return False
                if currentNode.val != self.front.val:
                    return False
                self.front = self.front.next
            return True
        return recursive_check(head)
  • 解法三:避免使用 O(n) 額外空間的方法就是改變輸入。

    我們可以將連結串列的後半部分反轉(修改連結串列結構),然後將前半部分和後半部分進行比較。比較完成後我們應該將連結串列恢復原樣。雖然不需要恢復也能通過測試用例,因為使用該函式的人不希望連結串列結構被更改。

    • 找到前半部分連結串列的尾節點。可以使用快慢指標在一次遍歷中找到:慢指標一次走一步,快指標一次走兩步,快慢指標同時出發。當快指標移動到連結串列的末尾時,慢指標到連結串列的中間。通過慢指標將連結串列分為兩部分。
    • 反轉後半部分連結串列。
    • 判斷是否為迴文。當後半部分到達末尾則比較完成,可以忽略計數情況中的中間節點。
    • 恢復連結串列。
    • 返回結果。
class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        if head is None:
            return True
        #get mid
        
        pslow,pfast = head,head
        while pfast.next and pfast.next.next:
            pslow = pslow.next
            pfast = pfast.next.next
        second_half_start = self.reverseList(pslow.next)

        #compare
        
        second_position = second_half_start
        while second_position:
            if head.val != second_position.val:
                return False
            head = head.next
            second_position = second_position.next

        #restore
        
        pslow.next = self.reverseList(second_half_start)
        return True
    
    def reverseList(self,head):
        result = None
        while head:
            nextNode = head.next
            head.next = result
            result = head
            head = nextNode
        return result

leetcode 237: 刪除連結串列中的節點

請編寫一個函式,使其可以刪除某個連結串列中給定的(非末尾)節點,你將只被給定要求被刪除的節點。

輸入: head = [4,5,1,9], node = 5
輸出: [4,1,9]

解釋: 給定你連結串列中值為 5 的第二個節點,那麼在呼叫了你的函式之後,該連結串列應變為 4 -> 1 -> 9.

輸入: head = [4,5,1,9], node = 1
輸出: [4,5,9]

解釋: 給定你連結串列中值為 1 的第三個節點,那麼在呼叫了你的函式之後,該連結串列應變為 4 -> 5 -> 9.

說明:

連結串列至少包含兩個節點。
連結串列中所有節點的值都是唯一的。
給定的節點為非末尾節點並且一定是連結串列中的一個有效節點。
不要從你的函式中返回任何結果。

  • 給定的是節點而不是head指標,無法尋找其前面的節點,因此選擇交換下一個結點的值,然後刪除下一個節點。
class Solution:
    def deleteNode(self, node):
        node.val,node.next.val = node.next.val,node.val
        node.next = node.next.next

leetcode 83: 刪除排序連結串列中重複的節點

給定一個排序連結串列,刪除所有重複的元素,使得每個元素只出現一次。

輸入: 1->1->2
輸出: 1->2

輸入: 1->1->2->3->3
輸出: 1->2->3
  • 思路:同之前的刪除重複演算法,是其簡化版。
class Solution:
    def deleteDuplicates(self, head: ListNode) -> ListNode:
        if not head:
            return head
        p = head
        while p:
            if p.next and p.val == p.next.val:
                while p.next and p.val == p.next.val:
                    p.next = p.next.next
            p = p.next
        return head

leetcode 203: 移除連結串列元素

刪除連結串列中等於給定值 val 的所有節點。

輸入: 1->2->6->3->4->5->6, val = 6
輸出: 1->2->3->4->5
  • 思路:當要刪除的節點在中間時可以很方便地通過連結串列操作完成,但當連結串列開始有一個或多個val存在時刪除起來比較麻煩,因此可以新增一個新的連結串列頭,最後返回時再取其next。
class Solution:
    def removeElements(self, head: ListNode, val: int) -> ListNode:
        if head is None:
            return None
        result = ListNode(0)
        result.next = head      
        pre,cur = result,result.next
        while cur:
            if cur.val == val:
                pre.next = cur.next
            else:
                pre = cur
            cur = cur.next
        return result.next

*leetcode 92:反轉連結串列2

[**中等 **] 反轉從位置 m 到 n 的連結串列。請使用一趟掃描完成反轉。

說明:
1 ≤ m ≤ n ≤ 連結串列長度。

輸入: 1->2->3->4->5->NULL, m = 2, n = 4
輸出: 1->4->3->2->5->NULL
  • 思路一:使用兩個指標分別指向m和n位置,然後逐個進行值的交換。而在n處的指標無法向前移動,因而採用遞迴的方法。使用left,right兩個指標在遞迴過程中移動,在遞迴函式中使用頭遞迴的思路,前半段不斷移動left和right指標指到m和n的位置,遞迴後是對left和right位置資料的交換。

class Solution:
    def reverseBetween(self, head, m, n):
        if not head:
            return None

        left, right = head, head
        stop = False
        def recurseAndReverse(right, m, n):
            #重要,保證在right隨著遞迴更新時left也得到更新 而不是產生作用範圍內的值
            
            nonlocal left, stop

            # base case. Don't proceed any further
            
            #n = 1 , m = 1 ,指向同樣的位置,不用交換
            
            if n == 1:
                return
			#forward過程,left和right指標分別走向對應的m、n位置
            
            # Keep moving the right pointer one step forward until (n == 1)
            
            right = right.next

            # Keep moving left pointer to the right until we reach the proper node
            
            # from where the reversal is to start.
            
            if m > 1:
                left = left.next

            # Recurse with m and n reduced.
            
            #上面right走了一步,m和n對應的位置減一
            
            recurseAndReverse(right, m - 1, n - 1)

            # In case both the pointers cross each other or become equal, we
            
            # stop i.e. don't swap data any further. We are done reversing at this
            
            # point.
            
            #right到n處節點位置,開始進行交換操作。判斷是不是已經完成交換可以中止。
            
            if left == right or right.next == left:
                stop = True

            # Until the boolean stop is false, swap data between the two pointers  
            
            if not stop:
                left.val, right.val = right.val, left.val
                # Move left one step to the right.
                
                # The right pointer moves one step back via backtracking.
                
                left = left.next           

        recurseAndReverse(right, m, n)
        return head
  • 思路二:使用迭代法改變next指標域,為方便連線,需要額外的指標con來儲存m-1處的地址,最後使用con連線n處的節點,也需要tail指標指向m來完成連線n+1處的節點,而在m到n處節點反轉的過程可以通過幾個指標從前往後迭代完成。
class Solution:
    def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:
        if head is None:
            return None
        prev,curr = None,head
        #迭代使curr指向第一個要反轉的節點m處
        
        while m > 1:
            prev = curr
            curr = curr.next
            m,n = m-1,n-1
        #con用來連線反轉後的頭prev,tail.next連線n+1處節點
        
        con,tail = prev,curr
        #cur應走到n+1的位置方便連線
        
        while n > 0:
            nex = curr.next
            curr.next = prev
            prev = curr
            curr = nex
            n -= 1
        #建立con與新的子連結串列頭prev以及tail與cur之間的連線
        
        #m = 1 condition
        
        if con:
            con.next = prev
        else:
            head = prev
        tail.next = curr
        return head

leetcode 86: 連結串列劃分

[中等]Given a linked list and a value x, partition it such that all nodes less than x come before nodes greater than or equal to x.

You should preserve the original relative order of the nodes in each of the two partitions.

Input: head = 1->4->3->2->5->2, x = 3
Output: 1->2->2->4->3->5
  • 思路:使用兩個指標,分別連線小於x和大於等於x的節點,之後再將兩個連結串列進行連線。
    • 注意點:為方便處理開頭大於x的情況,使用空頭節點。
    • 在較大值連結串列最後需要指定next=None,不然會導致無限迴圈致使超時。
class Solution(object):
    def partition(self, head, x):
        before = before_head = ListNode(0)
        after = after_head = ListNode(0)

        while head:
            if head.val < x:
                before.next = head
                before = before.next
            else:
                after.next = head
                after = after.next
            head = head.next

        # Last node of "after" list would also be ending node of the reformed list
        
        after.next = None
        # 連線兩個連結串列
        
        before.next = after_head.next
        return before_head.next

leetcode 2: 兩數相加

You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.

You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Example:

Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8
Explanation: 342 + 465 = 807
  • 兩個連結串列同時遍歷,逐個相加,注意用一個變數來計算進位值。
  • 注意:迴圈結束條件:兩個連結串列都為空且進位為零
class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        q = result = ListNode(0)
        carry = 0
        while l1 or l2 or carry!=0 :
            val1 = l1.val if l1 else 0
            val2 = l2.val if l2 else 0
            q.next = ListNode((val1+val2+carry)%10)
            q = q.next
            carry = (val1+val2+carry)//10
            l1 = l1.next if l1 else None
            l2 = l2.next if l2 else None
        return result.next

leetcode 445: 兩數相加2

  • 相較於上一題,變成了高位在前,使用棧儲存資料在同時出棧計算,使用頭插入法插入連結串列節點。
class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        stack1 = []
        stack2 = []
        while l1:
            stack1.append(l1.val)
            l1 = l1.next
        while l2:
            stack2.append(l2.val)
            l2 = l2.next
        result = ListNode(0)
        carry = 0
        while len(stack1)>0 or len(stack2) > 0 or carry != 0 :
            val1 = stack1.pop() if len(stack1) > 0 else 0
            val2 = stack2.pop() if len(stack2) > 0 else 0
            node = ListNode((val1+val2+carry)%10)
            node.next = result.next
            result.next = node
            carry = (val1+val2+carry)//10
        return result.next

*leetcode 148:排序連結串列

[中等較難]Sort a linked list in O(n log n) time using constant space complexity.

Input: 4->2->1->3
Output: 1->2->3->4

Input: -1->5->3->4->0
Output: -1->0->3->4->5
  • 思路:根據時空複雜度的要求,選用歸併排序,因為要求\(O(1)\)的空間複雜度,不能使用遞迴方法。(二分的遞迴方法空間複雜度\(O(logn)\))因而選用從底到頂直接合並的方法。

  • 附:使用遞迴方法的連結串列歸併排序方法:

    • 分割 cut 環節: 找到當前連結串列中點,並從中點將連結串列斷開(以便在下次遞迴 cut 時,連結串列片段擁有正確邊界);
      • 我們使用fast,slow 快慢雙指標法,奇數個節點找到中點,偶數個節點找到中心左邊的節點。
      • 找到中點 slow 後,執行 slow.next = None 將連結串列切斷。
      • 遞迴分割時,輸入當前連結串列左端點 head 和中心節點 slow 的下一個節點 tmp(因為連結串列是從 slow 切斷的)。
      • cut 遞迴終止條件: 當head.next == None時,說明只有一個節點了,直接返回此節點。
    • 合併 merge 環節: 將兩個排序連結串列合併,轉化為一個排序連結串列
      • 雙指標法合併,建立輔助ListNode h 作為頭部。
      • 設定兩指標 left, right 分別指向兩連結串列頭部,比較兩指標處節點值大小,由小到大加入合併連結串列頭部,指標交替前進,直至新增完兩個連結串列。
      • 返回輔助ListNode h 作為頭部的下個節點 h.next。
      • 時間複雜度 O(l + r),l, r 分別代表兩個連結串列長度。
      • 當題目輸入的 head == None 時,直接返回None。
    class Solution:
        def sortList(self, head: ListNode) -> ListNode:
            if not head or not head.next:
                return head
            #快慢指標尋找中間結點,這個中間結點不必是嚴格定義的中間
            
            slow,fast = head,head.next
            while fast and fast.next:
                fast = fast.next.next
                slow = slow.next
            #cut 分治
            
            mid,slow.next = slow.next,None
            left,right = self.sortList(head),self.sortList(mid)
            #排序 插入到新連結串列
            
            h = result = ListNode(0)
            while left and right:
                if left.val < right.val:
                    h.next = left
                    left = left.next
                else:
                    h.next = right
                    right = right.next
                h = h.next
            h.next = left if left else right
            return result.next
    
  • 迭代法:

    • 以inv=1為步長,每次找兩個inv長度的子連結串列進行歸併。注意要通過遍歷來得到兩個子連結串列的頭h1、h2,以及其對應的長度len1、len2,不能用next指標來判斷子連結串列的結束。
    • 到達連結串列末尾後,inv*=2,模擬第二輪歸併過程。
class Solution:
    def sortList(self, head: ListNode) -> ListNode:
        if not head or not head.next:
            return head
        result = ListNode(0)
        result.next = head
        q = head
        length = 0
        while q:
            q = q.next
            length += 1
        inv = 1
        #最後一輪 整個連結串列長度為inv 結束迴圈
        
        while inv < length:
            #pre在歸併過程中充當每次歸併的頭節點
            
            pre = result
            h = result.next
            #用inv不斷切分原連結串列(每次兩個子連結串列)並進行歸併
            
            while h:
                #h1為第一個歸併子連結串列表頭,i用來計運算元連結串列長度inv
                
                h1 = h
                i = inv
                while h and i:
                    h = h.next
                    i -= 1
                #還未達到inv長度時上面的迴圈中止,本輪歸併完成
                
                if i: 
                    break
                h2 = h
                #接下來繼續向下,計算第二個歸併子連結串列長度,並方便下一組歸併子連結串列。
                
                i = inv
                while h and i:
                    h = h.next
                    i -= 1
                #兩個歸併子連結串列的長度,這裡不能用指標為空來判定,因而上面需要計算出兩個子連結串列長度
                
                len1,len2 = inv,inv-i
                #對兩個歸併子連結串列進行排序
                
                while len1 and len2:
                    if h1.val < h2.val:
                        pre.next = h1
                        h1 = h1.next
                        len1 -= 1
                    else:
                        pre.next = h2
                        h2 = h2.next
                        len2 -= 1
                    pre = pre.next
                pre.next = h1 if len1 else h2
                #接下來將pre指向下一輪歸併子連結串列的位置
                
                while len1 > 0 or len2 > 0 :
                    pre = pre.next
                    len1 -= 1
                    len2 -= 1
                #連結原連結串列
                
                pre.next = h
            inv *= 2
        return result.next

leetcode 328: 奇偶連結串列

  • 類似於上面的連結串列劃分題,使用兩個啞節點分別連線對應的奇偶節點,最後連線。
class Solution:
    def oddEvenList(self, head: ListNode) -> ListNode:
        if not head or not head.next :
            return head
        odd_head = odd = ListNode(0)
        even_head = even = ListNode(0)
        val = 1
        p = head
        while p:
            if val % 2 != 0:
                odd_head.next = p
                odd_head = odd_head.next
            else:
                even_head.next = p
                even_head = even_head.next
            p = p.next
            val += 1
        even_head.next = p
        odd_head.next = even.next
        return odd.next

leetcode 24: 兩兩交換連結串列中的節點

Given a linked list, swap every two adjacent nodes and return its head.

You may not modify the values in the list's nodes, only nodes itself may be changed.

Example:

Given 1->2->3->4, you should return the list as 2->1->4->3.

  • 設立一個啞節點,遍歷連結串列並每兩個進行一次交換。
class Solution:
    def swapPairs(self, head: ListNode) -> ListNode:
        if not head or not head.next:
            return head

        res = result = ListNode(0)
        res.next = head
        while res.next and res.next.next:
            a,b = res.next,res.next.next
            res.next = b
            a.next = b.next
            b.next = a
            res = res.next.next
        return result.next

leetcode 147: 對連結串列進行插入排序

Sort a linked list using insertion sort.

Example 1:
Input: 4->2->1->3
Output: 1->2->3->4
Example 2:
Input: -1->5->3->4->0
Output: -1->0->3->4->5
  • 新增啞節點,然後逐個節點遍歷,並與之前排好序的進行比較。
  • 為節省時間(序列呈升序時),使用tail指標指向已排好序的連結串列尾,先與尾節點進行比較,小於尾節點時再從頭進行比較。
  • 遇到連結串列交換問題時,注意最後一個結點之後連線的還是不是None,可能導致迴圈超時。
class Solution:
    def insertionSortList(self, head: ListNode) -> ListNode:
        while not head or not head.next:
            return head
        res = ListNode(0)
        res.next = head
        #加一個tail指標,指向排好序的隊尾,對升序序列節約時間,且可最後設定tail.next = None
        tail = head
        head = head.next
    
        while head:
            if head.val >= tail.val:
                tail.next = head
                tail = tail.next
                head = head.next
            else:
                #兩個指標 用於比較和插入
                p,q = res,res.next
                while q.val <= head.val and q!=tail:
                    p,q = p.next,q.next
                #insert after p
                next_node = head.next
                p.next = head
                head.next = q
                head = next_node
        tail.next = None
        return res.next

leetcode 19: 刪除連結串列的倒數第 N 個節點

Given a linked list, remove the n-th node from the end of list and return its head.

Example:
Given linked list: 1->2->3->4->5, and n = 2.
After removing the second node from the end, the linked list becomes 1->2->3->5.
Note:
Given n will always be valid.
  • 新增啞節點(刪除的是第一個節點),然後使用雙指標同時遍歷
  • 注意:進行刪除時需要知道倒數n+1個節點,因此兩個指標的距離不是n而是n+1
class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        res = ListNode(0)
        res.next = head
        p,q = res,res
        for i in range(n+1):
            #若是刪除第一個節點時q會直接到None,而n==i
            if not q:
                return head if i!=n else head.next
            q = q.next
        while q:
            p,q = p.next,q.next
        p.next = p.next.next
        return res.next

leetcode 61: 旋轉連結串列

Given a linked list, rotate the list to the right by k places, where k is non-negative.

Example 1:
Input: 1->2->3->4->5->NULL, k = 2
Output: 4->5->1->2->3->NULL
Explanation:
rotate 1 steps to the right: 5->1->2->3->4->NULL
rotate 2 steps to the right: 4->5->1->2->3->NULL

Example 2:
Input: 0->1->2->NULL, k = 4
Output: 2->0->1->NULL
Explanation:
rotate 1 steps to the right: 2->0->1->NULL
rotate 2 steps to the right: 1->2->0->NULL
rotate 3 steps to the right: 0->1->2->NULL
rotate 4 steps to the right: 2->0->1->NULL
  • 思路一:按數學的思路,把右移操作化為左移
    • 先迴圈計算出連結串列長度l,並把連結串列變為迴圈連結串列;
    • 根據需要把尾部放到頭部的操作次數計算出從頭部到新的頭部的操作次數:length - k%length,其前一個節點為新的尾節點;
    • 尾節點設定next指標域,並返回新的頭節點。
class Solution:
    def rotateRight(self, head: ListNode, k: int) -> ListNode:
        if not head or not head.next:
            return head
        length = 1
        q = head
        while q.next:
            q = q.next
            length += 1
        q.next = head

        #得到移位後的尾節點,下一個節點為新頭節點
        dist = length - k%length -1
        for _ in range(dist):
            head = head.next
        next_node = head.next
        head.next = None
        return next_node
  • 思路二:雙指標遍歷找到需要擷取的位置,然後把後面截掉的部分放在前面組合為新連結串列。
  • 不合適:k的值可能大於length,雙鏈表法沒法直接獲取連結串列長度,需單獨寫一個遍歷獲得長度,略顯麻煩。
class Solution:
    def rotateRight(self, head: ListNode, k: int) -> ListNode:
        if not head or not head.next:
            return head
        q , length= head,0
        while q:
            q,length =q.next,length+1
        rel_k = k % length
        p,q = head,head
        #p in new end , set next = None
        for _ in range(rel_k):
            q = q.next
        #終止條件用q.next,這樣最終q會在最後一個節點,q再需要截斷的結點之前。
        while q.next:
            p ,q = p.next,q.next
        q.next = head
        next_node = p.next
        p.next = None
        return next_node

leetcode 143: 重排連結串列

Given a singly linked list

L: L0→L1→…→Ln-1→Ln,
reorder it to: L0→Ln→L1→Ln-1→L2→Ln-2→…

You may not modify the values in the list's nodes, only nodes itself may be changed.

Example 1:
Given 1->2->3->4, reorder it to 1->4->2->3.
Example 2:
Given 1->2->3->4->5, reorder it to 1->5->2->4->3.
  • 思路一 迭代配合遞迴,左邊left迭代,右邊使用遞迴,直到相等。
  • 注意:和上面交換結點的題不一樣,在有奇數和偶數節點時有不同的處理。
class Solution:
    def reorderList(self, head: ListNode) -> None:
        """
        Do not return anything, modify head in-place instead.
        """
        res = result = ListNode(0)
        left,right = head,head
        stop = False

        def recursive_reorder(right):
            nonlocal left,stop,res
            if not right:
                return
            
            recursive_reorder(right.next)

            #偶數個節點 中止
            if right.next == left:
                stop = True
            #奇數個節點,把該節點連線上
            if left == right:
                stop = True
                res.next = left
                res = res.next
                
            if not stop:
                res.next = left
                left = left.next
                res.next.next = right
                res = res.next.next
            res.next = None
        recursive_reorder(head)
  • 思路二:後半部分入棧,和前半部分同步進行插入。同樣需要注意奇數時最中間結點的問題。
  • 不推薦,使用快慢指標需要嚴格找到中間節點。
  • 更新版思路:對全部連結串列入棧,同時可以計算出連結串列的長度l,一邊遍歷原連結串列一邊出棧進行\(floor(l/2)\)次,然後如果\(l\)是奇數多出棧一個元素。
class Solution:
    def reorderList(self, head: ListNode) -> None:
        """
        Do not return anything, modify head in-place instead.
        """
        stack = []
        p,length = head,0
        while p:
            stack.append(p)
            p , length = p.next , length + 1
        res = ListNode(0)
        for _ in range(length//2):
            res.next = head
            head = head.next
            res.next.next = stack.pop()
            res = res.next.next
        if length % 2 == 1:
            res.next = stack.pop()
            res = res.next
        res.next = None

*leetcode 23: 合併 k 個有序連結串列

[困難]Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.

Example:

Input:
[
  1->4->5,
  1->3->4,
  2->6
]
Output: 1->1->2->3->4->4->5->6
  • 思路一 暴力法:遍歷k個數組,將其值存入一個數組中,再進行排序,之後將排序後的值對一個新的連結串列進行賦值。 \(O(NlogN),O(N)\)
  • 當要求in-place進行排序,不能新建連結串列節點時不能使用。
class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        vals = []
        for li in lists:
            while li:
                vals.append(li.val)
                li = li.next
        vals.sort()
        res = result = ListNode(0)
        for val in vals:
            result.next = ListNode(val)
            result = result.next
        return res.next
  • 思路二 類似於兩個排序連結串列的合併,同時對k個進行合併,每輪比較k個值取最小的。\(O(kN)\)
  • 注意:停止判定不能用指標為空,而應該以該輪是不是有新的節點插入為終止條件。
  • 測試樣例k過大,每個連結串列一個元素,退化為\(O(N^2)\),演算法超時
class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        res = result = ListNode(0)
        loop = True
        while loop:
            min_val = 1000000
            min_idx = -1
            for i,h in enumerate(lists):
                if h and h.val < min_val:
                    min_val = h.val
                    min_idx = i
            #add minist to res
            if min_idx != -1:
                res.next = lists[min_idx]
                lists[min_idx] = lists[min_idx].next
                res = res.next
            else:
                loop = False
                res.next = None
        return result.next
  • 思路三 優先佇列
  • 優先佇列:根據優先順序來判斷是否先出。相當於對list的每個元素加了一個優先順序的關鍵詞。
  • 相關庫:heapq,預設為小根堆,函式:heapq.heappush和heap.heappop
  • queue.PriorityQueue 函式:put , get.
  • 優先佇列接受內容:元組,(priority,content) 注意:元組兩個值都要可比較才行,第二個元素應該存lists的索引
  • 演算法流程:
    • 把所有的連結串列都放入優先佇列中,使用最小堆來高效完成k個值的比較,
    • 將頭節點值最小的連結串列彈出,
    • 將該節點連線到排好序的節點後面,然後把下一個節點在放入優先佇列。
class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        from queue import PriorityQueue
        res = result = ListNode(0)
        q = PriorityQueue()
        for i in range(len(lists)):
            if lists[i]:
                q.put((lists[i].val,i))
        while not q.empty():
            val,i = q.get()
            res.next = lists[i]
            res = res.next
            lists[i] = lists[i].next
            if lists[i]:
                q.put((lists[i].val,i))
        return result.next
  • 思路四 分治
    • 將k個連結串列配對並將同一對中的連結串列合併。
    • 第一輪合併以後,k 個連結串列被合併成了 \(\frac{k}{2}\)個連結串列,平均長度為 \(\frac{2N}{k}\),然後是 \(\frac{k}{4}\)個連結串列, \(\frac{k}{8}\)個連結串列等等。
    • 重複這一過程,直到我們得到了最終的有序連結串列。
  • 注意迴圈範圍的寫法
class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        inter = 1
        amount = len(lists)
        while inter < amount:
            for i in range(0,amount - inter,inter*2):
                lists[i] = self.merge(lists[i],lists[i+inter])
            inter *= 2
        return lists[0] if amount > 0 else lists

    def merge(self,l1,l2):
        head = point = ListNode(0)
        while l1 and l2:
            if l1.val <= l2.val:
                point.next = l1
                l1 = l1.next
            else:
                point.next = l2
                l2 = l1
                l1 = point.next.next
            point = point.next
        if not l1:
            point.next=l2
        else:
            point.next=l1
        return head.next

*leetcode 25: k 個一組翻轉連結串列

困難Given a linked list, reverse the nodes of a linked list k at a time and return its modified list.

k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k then left-out nodes in the end should remain as it is.

Example:
Given this linked list: 1->2->3->4->5
For k = 2, you should return: 2->1->4->3->5
For k = 3, you should return: 3->2->1->4->5

Note:

  • Only constant extra memory is allowed.
  • You may not alter the values in the list's nodes, only nodes itself may be changed.

  • 思路:由於要求不需要額外的空間,棧和遞迴方法不能使用。因此使用雙指標,一邊遍歷,每達到k個節點進行一次頭插入。
class Solution(object):
    def reverseKGroup(self, head, k):
        if not head :
            return head
        res = result = ListNode(0)
        res.next = head
        
        while res:
            pre = res
            count = k
            while res and count:
                res = res.next
                count -= 1
            #還夠一次反轉的k個值
            
            if res:
                stop = res.next if res.next else None
                #儲存反轉後的尾節點,便於和之後的連結串列連線
                
                new_tail = pre.next
                q = pre.next
                while q != stop:
                    next_node = q.next
                    q.next = pre.next
                    pre.next = q
                    q = next_node
                #連線之後的節點
                
                new_tail.next = stop
                #重設下一輪的節點遍歷,為上一輪反轉後的尾節點
                
                res = new_tail
        return result.next