--- title: "刷題模式: 快慢指針(Fast-slow Pointers)" description: "Patterns for Coding Questions: Fast-slow Pointers." image: https://i.imgur.com/lMvTf2e.png author: Hsins tags: LeetCode, Coding Interview robots: breaks: GA: disqus: --- # 快慢指針(Fast-slow Pointers) <p style="text-align: center"> <img src="https://i.imgur.com/lMvTf2e.png" height=200/> </p> > 本篇內容主要為 [Grokking the Coding Interview: Patterns for Coding Questions](https://www.educative.io/courses/grokking-the-coding-interview) 的翻譯與整理,行有餘力建議可以購買該專欄課程閱讀並在上面進行練習。 ## 重點整理 - 快慢指針(Fast-slow Pointers)又稱龜兔演算法(Hare & Tortoise Algorithm) - 策略:使用兩個不同速度的指針遍歷線性結構(陣列、序列、鏈結串列) - 題型: - 環狀鏈表、環狀陣列 - 鏈表判斷迴圈、鏈表判斷長度、鏈表迴圈操作 ## 題目匯總 - `0141` [[英]](https://leetcode.com/problems/linked-list-cycle/) [[中]](https://leetcode-cn.com/problems/linked-list-cycle/) Linked List Cycle - `0142` [[英]](https://leetcode.com/problems/linked-list-cycle-ii/) [[中]](https://leetcode-cn.com/problems/linked-list-cycle-ii/) Linked List Cycle II - `0143` [[英]](https://leetcode.com/problems/reorder-list/) [[中]](https://leetcode-cn.com/problems/reorder-list/) Recorder List - `0202` [[英]](https://leetcode.com/problems/happy-number/) [[中]](https://leetcode-cn.com/problems/happy-number/) Happy Number - `0234` [[英]](https://leetcode.com/problems/palindrome-linked-list/) [[中]](https://leetcode-cn.com/problems/palindrome-linked-list/) Palindrome Linked List - `0457` [[英]](https://leetcode.com/problems/circular-array-loop/) [[中]](https://leetcode-cn.com/problems/circular-array-loop/) Circular Array Loop - `0876` [[英]](https://leetcode.com/problems/middle-of-the-linked-list/) [[中]](https://leetcode-cn.com/problems/middle-of-the-linked-list/) Middle of the Linked List ## [例題] 環形鏈表(Linked-List Cycle) ### 問題 給定一個單向鏈結串列(Singly Linked-list)的頭部節點,撰寫一支函數判斷這個鏈結串列是否存在迴圈(cycle)。 <p style="text-align: center"> <img src="https://i.imgur.com/QZESX46.png" /> </p> ### 題解 想像有兩位跑者在圓形操場上奔跑,如果其中一位跑者的速度比另外一位跑者要來的快,那麼勢必會趕上並從後面越過較慢的跑者。這樣的思路可以用來設計一種演算法,來確定鏈結串列是否存在迴圈。 分別採用慢速指針 `slow` 和快速指針 `fast` 遍歷鏈結串列,每次執行時讓慢速指標移動一步,快速指標移動兩步。可以得到以下事實: 1. 如果鏈結串列沒有迴圈,則 `fast` 會先抵達鏈結串列的末端 2. 如果鏈結串列沒有迴圈,則 `slow` 永遠不會與 `fast` 相遇 如果鏈結串列存在迴圈,則 `fast` 和 `slow` 先後進入該迴圈。並且在此之後,兩個指針會在迴圈中無限移動。如果在一階段兩個指針相遇了,就可以得到「鏈結串列存在迴圈」的結論。 接下來讓我們分析一下這兩個指針是否有可能相遇。當 `fast` 從後面接近 `slow` 時,有兩種可能性: 1. `fast` 比 `slow` 慢一步 2. `fast` 比 `slow` 慢兩步 快指標和慢速指標之間的所有其他距離將減少到這兩種可能性之一。讓我們分析一下這些場景,考慮到快速指標總是先移動: - 如果 `fast` 比 `slow` 慢一步:`fast` 移動兩步,`slow` 移動一步,彼此相遇 - 如果 `fast` 比 `slow` 慢兩步:`fast` 移動兩步,`slow` 移動一步,等同上一場景,意味兩個指針將在下一次反覆運算中相遇 由上述分析可知:如果鏈接串列中存在迴圈,則兩個指針必定會相遇。上述討論的直觀表示如圖所示: <p style="text-align: center"> <img src="https://i.imgur.com/qApx8VZ.png" /> </p> ### 代碼 #### C++ ``` cpp class LinkedListCycle { public: static bool hasCycle(ListNode *head) { ListNode *slow = head; ListNode *fast = head; while (fast != nullptr && fast-> != nullptr) { fast = fast->next->next; slow = slow->next; if (slow == fast) return true; } return false; } } ``` #### Java ``` java class LinkedListCycle { public static boolean hasCycle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; if (slow == fast) return true; } return false; } } ``` #### JavaScript ``` javascript const findCycleLength = (head) => { let slow = head; let fast = head; while (fast && fast.next) { fast = fast.next.next; slow = slow.next; if (slow === fast) return true; } return false; } ``` #### Python ``` python def has_cycle(head): slow, fast = head, head while fast and fast.next: fast = fast.next.next slow = slow.next if slow == fast: return True return False ``` ### 分析 - **時間複雜度**:$\mathcal{O}(n)$,其中 $n$ 為 Linked-List 中的節點總數 - **空間複雜度**:$\mathcal{O}(1)$ ## [補充] 環狀鏈表的迴圈長度(Linked-List Cycle Length) ### 題目 給定一個單向鏈結串列(Singly Linked-list)的頭部節點,撰寫一支函數獲取這個鏈結串列的迴圈長度。 ### 題解 1. 先透過 `slow` 和 `fast` 指針找到相遇位置 2. 紀錄相遇位置並讓 `slow` 繼續走,同時記錄步數,當其返回原點時,經過的步數即為迴圈長度 ### 代碼 #### C++ ``` cpp class LinkedListCycleLength { public: static bool findCycleLength(ListNode *head) { ListNode *slow = head; ListNode *fast = head; while (fast != nullptr && fast-> != nullptr) { fast = fast->next->next; slow = slow->next; if (slow == fast) return calculateLength(slow); } return false; } private: static int calculateLength(ListNode *slow) { ListNode *curr = slow; int cycleLength = 0; do { curr = curr->next; cycleLength++; } while (curr != slow); return cycleLength; } } ``` #### Java ```java class LinkedListCycleLength { public static int findCycleLength(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; if (slow == fast) return calculateLength(slow); } return 0; } private static int calculateLength(ListNode slow) { ListNode curr = slow; int cycleLength = 0; do { curr = curr.next; cycleLength++; } while (curr != slow); return cycleLength; } } ``` #### JavaScript ``` javascript const findCycleLength = (head) => { let slow = head; let fast = head; while (fast && fast.next) { fast = fast.next.next; slow = slow.next; if (slow === fast) return calculateCycleLength(slow); } } const calculateCycleLength = (slow) => { let curr = slow; let cycle_length = 0; while (true) { curr = curr.next; cycle_length += 1; if (curr === slow) break; } return cycle_length; } ``` #### Python ``` python def find_cycle_length(head): slow, fast = head, head while fast and fast.next: fast = fast.next.next slow = slow.next if slow == fast: return calculate_cycle_length(slow) return False def calculate_cycle_length(slow): curr = slow cycle_length = 0 while True: curr = curr.next cycle_length += 1 if curr == slow: break return cycle_length ``` ### 分析 - **時間複雜度**:$\mathcal{O}(n)$,其中 $n$ 為 Linked-List 中的節點總數 - **空間複雜度**:$\mathcal{O}(1)$ ## [例題] 鏈表的環狀入口(Start of Linked-List Cycle) ### 問題 給定一個單向鏈結串列(Singly Linked-list)的頭部節點,撰寫一支函數獲取這個鏈結串列的迴圈入口結點。 <p style="text-align: center"> <img src="https://i.imgur.com/jAFCJpZ.png" /> </p> ### 題解 如果我們能夠知道鏈結串列的迴圈長度,可以透過以下步驟找到迴圈入口: 1. 取兩個指針分別為 `pointer1` 和 `pointer2` 2. 初始化指針指向鏈結串列的起始點 3. 透過 **快慢指針(fast-slow pointers)** 方法獲取迴圈長度 $K$ 4. 使 `pointer2` 向前移動 $K$ 個結點 5. 再讓 `pointer1` 和 `pointer2` 同步向前移動直到相遇 6. 由於 `pointer1` 與 `pointer2` 相差 $K$ 個結點,當兩者相遇時 `pointer2` 已經走完了一圈,他們相遇的點就是迴圈入口 <p style="text-align: center"> <img src="https://i.imgur.com/G2qzd4B.png" /> </p> ### 代碼 #### C++ ``` cpp class LinkedListCycleStart { public: static ListNode *findCycleStart(ListNode *head) { int cycleLength = 0; ListNode *slow = head; ListNode *fast = head; while (fast != nullptr && fast->next != nullptr) { slow = slow->next; fast = fast->next->next; if (slow == fast) { cycleLength = calculateCycleLength(slow); break; } } return findStart(head, cycleLength); } private: static int calculateCycleLength(ListNode *slow) { ListNode *curr = slow; int cycleLength = 0; do { curr = curr->next; cycleLength++; } while (curr != slow); return cycleLength; } static ListNode *findStart(ListNode *head, int cycleLength) { ListNode *pointer1 = head; ListNode *pointer2 = head; while (cycleLength > 0) { pointer2 = pointer2->next; cycleLength--; } while (pointer1 != pointer2) { pointer1 = pointer1->next; pointer2 = pointer2->next; } return pointer1; } } ``` #### Java ``` java class LinkedListCycleStart { public static ListNode findCycleStart(ListNode head) { int cycleLength = 0; ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; if (slow === fast) { cycleLength = calculateCycleLength(slow); break } } return findStart(head, cycleLength); } private static int calculateCycleLength(ListNode slow) { ListNode curr = slow; int cycleLength = 0; do { curr = curr.next; cycleLength++; } while (curr != slow); return cycleLength; } private static ListNode findStart(ListNode head, int cycleLength) { ListNode pointer1 = head; ListNode pointer2 = head; while (cycleLength > 0) { pointer2 = pointer2.next; cycleLength--; } while (pointer1 != pointer2) { pointer1 == pointer1.next; pointer2 == pointer2.next; } return pointer1; } } ``` #### JavaScript ``` javascript const find_cycle_start = (head) => { let cycle_length = 0; let slow = head; let fast = head; while (fast && fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) { cycle_length = calculate_cycle_length(slow); break; } } return find_start(head, cycle_length); } const calculate_cycle_length = (slow) => { let curr = slow; let cycle_length = 0; while (true) { curr = curr.next; cycle_length += 1; if (curr === slow) break; } return cycle_length; } const find_start = (head, cycle_length) { let pointer1 = head; let pointer2 = head; while (cycle_length > 0) { pointer2 = pointer2.next; cycle_length -= 1; } while (pointer1 !== pointer2) { pointer1 = pointer1.next; pointer2 = pointer2.next; } return pointer1; } ``` #### Python ``` python def find_cycle_start(head): cycle_length = 0 slow, fast = head, head while (fast and fast.next): fast = fast.next.next slow = slow.next if slow == fast: cycle_length = calculate_cycle_length(slow) break return find_start(head, cycle_length) def calculate_cycle_length(slow): curr = slow cycle_length = 0 while True: curr = curr.next cycle_length += 1 if curr == slow: break return cycle_length def find_start(head, cycle_length): pointer1 = head pointer2 = head while cycle_length > 0: pointer2 = pointer2.next cycle_length = cycle_length - 1 while pointer1 != pointer2; pointer1 = pointer1.next pointer2 = pointer2.next return pointer1 ``` ### 分析 - **時間複雜度**:$\mathcal{O}(n)$ - **空間複雜度**:$\mathcal{O}(1)$ ## [例題] 快樂數(Happy Number) ### 問題 如果一個數字 **反覆取其各位數字的平方和後結果為 $1$** 稱其為 [快樂數(Happy Number)](https://en.wikipedia.org/wiki/Happy_number);其他不符合的數字則會被困於一個不包括 $1$ 的迴圈當中。 **Examples** ``` Input: 23 Output: true (23 is a happy number) ``` $$ \begin{align*} 2^2 + 3^2 &= 13 \\ 1^2 + 3^2 &= 10 \\ 1^2 + 0^2 &= 1 \end{align*} $$ ``` Input: 12 Output: false (12 is not a happy number) ``` $$ \begin{align*} 1^2 + 2^2 &= 5 \\ 5^2 &= 25 \\ 2^2 + 5^2 &= 29 \\ 2^2 + 9^2 &= 85 \\ 8^2 + 5^2 &= 89 \\ 8^2 + 9^2 &= 145 \\ 1^2 + 4^2 + 5^2 &= 42 \\ 4^2 + 2^2 &= 20 \\ 2^2 + 0^2 &= 4 \\ 4^2 &= 16 \\ 1^2 + 6^2 &= 37 \\ 3^2 + 7^2 &= 58 \\ 5^2 + 8^2 &= 89 \\ \end{align*} $$ 寫一支程式判斷一個數字是不是快樂數字。 ### 題解 如同題目中所述,如果一個數字是快樂數,在反覆計算的過程中會停駐在數字 $1$ 上(下次計算仍得到 $1$ 這個結果);如果一個數字不是快樂數,在反覆計算的過程中,會不斷地在特定數字重複,比如第二個範例的 $89$。 因此我們可以建立一個鏈結串列儲存計算過程所得到的數字,並透過 **快慢指針(fast-slow pointers)** 找到迴圈的入口,檢查該數字是否為 $1$ 或是其他數字。 ### 代碼 #### C++ ``` cpp class HappyNumber { public: static bool find(int num) { int slow = num, fast = num; do { slow = findSquareSum(slow); fast = findSquareSum(findSquareSum(fast)); } while (slow != fast); return slow == 1; } private: static int findSquareSum(int num) { int sum = 0, digit; while (num > 0) { digit = num % 10; sum += digit * digit; sum /= 10; } return sum; } ``` #### Java ``` java class HappyNumber { public static boolean find(int num) { int slow = num; int fast = num; do { slow = findSquareSum(slow); fast = findSquareSum(findSquareSum(fast)); } while (slow != fast); return slow == 1; } private static int findSquareSum(int num) { int sum = 0; int digit; while (num > 0) { digit = num % 10; sum += digit * digit; num /= 10; } return sum; } } ``` #### JavaScript ``` javascript const find_happy_number = (num) => { let slow = num; let fast = num; while (true) { slow = find_square_sum(slow); fast = find_square_sum(find_square_sum(fast)); if (slow === fast) break; } return slow === 1; } const find_square_sum = (num) => { let sum = 0; while (num) { sum = sum + Math.pow(num % 10, 2); num = Math.floor(num / 10); } return sum; } ``` #### Python ``` python def find_happy_number(num): slow, fast = num, num while True: slow = find_square_sum(slow) fast = find_square_sum(find_square_sum(fast)) if slow == fast: break return slow == 1 def find_square_sum(num): res = 0 while (num > 0): digit = num % 10 res = res + digit * digit num = num // 10 return res ``` ### 分析 - **時間複雜度**:$\mathcal{O}(\log{n})$ - 數字 $n$ 的位數 $M = \log{} (n+1)$ - 其平方和最多為 $81M$(當所有位數都是 $9$ 時) - $n_{\text{next}} < 81 \log(n+1)$ - **空間複雜度**:$\mathcal{O}(1)$ ## [例題] 鏈表的中間結點(Middle of the Linked-List) ### 問題 給定一個單向鏈結串列(Singly Linked-list)的頭部節點,撰寫一支函數獲取鏈結串列的中間結點。 **Example** ``` Input: 1 -> 2 -> 3 -> 4 -> 5 -> null Output: 3 Input: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> null Output: 4 Input: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> null Output: 4 ``` ### 題解 一種直觀的策略是先遍歷鏈結串列獲取長度後,在第二次反覆運算中找到中間結點。除此之外,我們也能透過快慢指針(fast-slow pointers)方法,使 `fast` 指針的步長始終是 `slow` 指針的兩倍,如此一來當 `fast` 抵達鏈結串列的末端時,恰好 `slow` 就位在中間結點上。 ### 代碼 #### C++ ``` cpp class MiddleOfLinkedList { public: static ListNode *findMiddle(ListNode *head) { ListNode *slow = head; ListNode *fast = head; while (fast != nullptr && fast->next != nullptr) { slow = slow->next; fast = fast->next->next; } return slow; } } ``` #### Java ``` java class MiddleOfLinkedList { public static ListNode findMiddle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } return slow; } } ``` #### JavaScript ``` javascript const findMiddleOfLinkedList = (head) => { let slow = head; let fast = head; while (fast && fast.next) { slow = slow.next; fast = afst.next.next; } return slow; } ``` #### Python ``` python def find_middle_of_linked_list(head): slow = head fast = head while fast and fast.next: slow = slow.next fast = fast.next.next return slow ``` ### 分析 - **時間複雜度**:$\mathcal{O}(n)$,其中 $n$ 為 Linked-List 中的節點總數 - **空間複雜度**:$\mathcal{O}(1)$ ## 參考資料 - [Coding Patterns: Fast & Slow Pointers | emre.me](https://emre.me/coding-patterns/fast-slow-pointers/) <!-- Widget: License --> {%hackmd @Hsins/widget-license %}