什么是同向雙指針? 什么是相向雙指針? 雙指針的鼻祖題 —— 兩數之和 Two Sum 鏈表上的快慢指針算法 快速排序 & 歸並排序
同向雙指針 • 相向雙指針 • 幾乎所有 Two Sum 變種 • Partition • Quick Select • 分成兩個部分 • 分成三個部分 • 一些你沒聽過的(但是面試會考的)排序算法
一個典型的相向雙指針問題就是翻轉字符串的問題。
相向雙指針模板1
Python:
""" @param s: a list of characters """ def reverse(s): left, right = 0, len(s)-1 while left < right: s[left], s[right] = s[right], s[left] left += 1 right -= 1
另外一個雙指針的經典練習題,就是回文串的判斷問題。給一個字符串,判斷這個字符串是不是回文串。
我們可以用雙指針的算法輕易的解決:
Python:
def isPalindrome(s): i, j = 0, len(s)-1 while i < j: if s[i] != s[j]: return False i += 1 j -= 1 return True
雙指針的鼻祖:兩數之和
題目描述
給一個整數數組,找到兩個數使得他們的和等於一個給定的數 target。
返回這兩個數。
相向雙指針模板2
Python:
class Solution: def twoSum(self, numbers, target): numbers.sort() L, R = 0, len(numbers)-1 while L < R: if numbers[L]+numbers[R] == target: return (numbers[L], numbers[R]) if numbers[L]+numbers[R] < target: L += 1 else: R -= 1 return None
- 首先我們對數組進行排序。
- 用兩個指針(L, R)從左右開始:
- 如果numbers[L] + numbers[R] == target, 說明找到,返回對應的數。
- 如果numbers[L] + numbers[R] < target, 此時L指針右移,只有這樣才可能讓和更大。
- 反之使R左移。
- L和R相遇還沒有找到就說明沒有解。
同向雙指針
同向雙指針的問題,是指兩根指針都從頭出發,朝着同一個方向前進。我們通過下面 5 個題目來初步認識同向雙指針:
- 數組去重問題 Remove duplicates in an array
- 滑動窗口問題 Window Sum
- 兩數之差問題 Two Difference
- 鏈表中點問題 Middle of Linked List
- 帶環鏈表問題 Linked List Cycle
問題描述
給你一個數組,要求去除重復的元素后,將不重復的元素挪到數組前段,並返回不重復的元素個數。
LintCode 練習地址:http://www.lintcode.com/problem/remove-duplicate-numbers-in-array/
問題分析
這個問題有兩種做法,第一種做法比較容易想到的是,把所有的數扔到 hash 表里,然后就能找到不同的整數有哪些。但是這種做法會耗費額外空間 O(n)O(n)O(n)。面試官會追問,如何不耗費額外空間。
此時我們需要用到雙指針算法,首先將數組排序,這樣那些重復的整數就會被擠在一起。然后用兩根指針,一根指針走得快一些遍歷整個數組,另外一根指針,一直指向當前不重復部分的最后一個數。快指針發現一個和慢指針指向的數不同的數之后,就可以把這個數丟到慢指針的后面一個位置,並把慢指針++。
同向雙指針模板1
# O(nlogn) time, O(1) extra space class Solution: # @param {int[]} nums an array of integers # @return {int} the number of unique integers def deduplication(self, nums): # Write your code here n = len(nums) if n == 0: return 0 nums.sort() result = 1 for i in range(1, n): if nums[i - 1] != nums[i]: nums[result] = nums[i] result += 1 return result
問題描述
求出一個數組每 kkk 個連續整數的和的數組。如 nums = [1,2,3,4]
, k = 2
的話,window sum 數組為 [3,5,7]
。
http://www.lintcode.com/problem/window-sum/
問題分析
這個問題並沒有什么難度,但是如果你過於暴力的用戶 O(n∗k)O(n * k)O(n∗k) 的算法去做是並不合適的。比如當前的 window 是 |1,2|,3,4
。那么當 window 從左往右移動到 1,|2,3|,4
的時候,整個 window 內的整數和是增加了3,減少了1。因此只需要模擬整個窗口在滑動的過程中,整數一進一出的變化即可。這就是滑動窗口問題。
class Solution: # @param nums {int[]} a list of integers # @param k {int} size of window # @return {int[]} the sum of element inside the window at each moving def winSum(self, nums, k): # Write your code here n = len(nums) if n < k or k <= 0: return [] sums = [0] * (n - k + 1) for i in range(k): sums[0] += nums[i]; for i in range(1, n - k + 1): sums[i] = sums[i - 1] - nums[i - 1] + nums[i + k - 1] return sums
兩數之差問題
610. 兩數和 - 差等於目標值
給定一個整數數組,找到兩個數的 差
等於目標值。index1必須小於index2。注意返回的index1和index2不是 0-based。
樣例
例1:
輸入: nums = [2, 7, 15, 24], target = 5
輸出: [1, 2]
解釋:
(7 - 2 = 5)
例2:
輸入: nums = [1, 1], target = 0
輸出: [1, 2]
解釋:
(1 - 1 = 0)
注意事項
保證只有一個答案。
問題分析
作為兩數之和的一個 Follow up 問題,在兩數之和被問爛了以后,兩數之差是經常出現的一個面試問題。
我們可以先嘗試一下兩數之和的方法,發現並不奏效,因為即便在數組已經排好序的前提下,nums[i] - nums[j] 與 target 之間的關系並不能決定我們淘汰掉 nums[i] 或者 nums[j]。
那么我們嘗試一下將兩根指針同向前進而不是相向而行,在 i 指針指向 nums[i] 的時候,j 指針指向第一個使得 nums[j] - nums[i] >= |target| 的下標 j:
- 如果 nums[j] - nums[i] == |target|,那么就找到答案
- 否則的話,我們就嘗試挪動 i,讓 i 向右挪動一位 => i++
- 此時我們也同時將 j 向右挪動,直到 nums[j] - nums[i] >= |target|
可以知道,由於 j 的挪動不會從頭開始,而是一直遞增的往下挪動,那么這個時候,i 和 j 之間的兩個循環的就不是累乘關系而是疊加關系。
同向雙指針模板2
Python:
nums.sort()
target = abs(target)
j = 1 for i in range(len(nums)): while j < len(nums) and nums[j]-nums[i] < target: j += 1 if nums[j]-nums[i] == target: # 找到答案
class Solution: """ @param nums: an array of Integer @param target: an integer @return: [index1 + 1, index2 + 1] (index1 < index2) """ def twoSum7(self, nums, target): # write your code here target = abs(target) nums2 = [(n, i) for i,n in enumerate(nums)] nums2.sort(key=lambda x: x[0]) result = [] j = 1 for i in range(len(nums2)): while j < len(nums2) and nums2[j][0]-nums2[i][0] < target: j += 1 if nums2[j][0]-nums2[i][0] == target: if i != j: result = (nums2[i][1]+1, nums2[j][1]+1) break if result[0] > result[1]: return [result[1], result[0]] return result
相似問題
G家的一個相似問題:找到一個數組中有多少對二元組,他們的平方差 < target(target 為正整數)。
我們可以用類似放的方法來解決,首先將數組的每個數進行平方,那么問題就變成了有多少對兩數之差 < target。
然后走一遍上面的這個流程,當找到一對 nums[j] - nums[i] >= target 的時候,就相當於一口氣發現了:
nums[i + 1] - nums[i]
nums[i + 2] - nums[i]
...
nums[j - 1] - nums[i]
一共 j - i - 1
對滿足要求的二元組。累加這個計數,然后挪動 i 的位置 +1 即可。
鏈表中點問題
問題描述
求一個鏈表的中點
LintCode 練習地址:http://www.lintcode.com/problem/middle-of-linked-list/
228. 鏈表的中點
找鏈表的中點。
樣例
樣例 1:
輸入: 1->2->3
輸出: 2
樣例解釋: 返回中間節點的值
樣例 2:
輸入: 1->2
輸出: 1
樣例解釋: 如果長度是偶數,則返回中間偏左的節點的值。
挑戰
如果鏈表是一個數據流,你可以不重新遍歷鏈表的情況下得到中點么?
同向雙指針模板3--針對鏈表
""" Definition of ListNode class ListNode(object): def __init__(self, val, next=None): self.val = val self.next = next """ class Solution: """ @param head: the head of linked list. @return: a middle node of the linked list """ def middleNode(self, head): # write your code here slow, fast = head, head while fast and fast.next and fast.next.next: slow = slow.next fast = fast.next.next return slow
其中,fast.next.next條件表示可以往前跨兩步。
問題分析
這個問題可能大家會覺得,WTF 這么簡單有什么好做的?你可能的想法是:
先遍歷一下整個鏈表,求出長度 L,然后再遍歷一下鏈表找到第 L/2 的那個位置的節點。
但是在你拋出這個想法之后,面試官會追問你:如果只允許遍歷鏈表一次怎么辦?
可以看到這種 Follow up 並不是讓你優化算法的時間復雜度,而是嚴格的限制了你遍歷整個鏈表的次數。你可能會認為,這種優化有意義么?事實上是很有意義的。因為遍歷一次
這種場景,在真實的工程環境中會經常遇到,也就是我們常說的數據流問題
(Data Stream Problem)。
數據流問題 Data Stream Problem
所謂的數據流問題,就是說,你需要設計一個在線系統,這個系統不斷的接受一些數據,並維護這些數據的一些信息。比如這個問題就是在數據流中維護中點在哪兒。(維護中點的意思就是提供一個接口,來獲取中點)
類似的一些數據流問題還有:
- 數據流中位數 http://www.lintcode.com/problem/data-stream-median/
- 數據流最大 K 項 http://www.lintcode.com/problem/top-k-largest-numbers-ii/
- 數據流高頻 K 項 http://www.lintcode.com/problem/top-k-frequent-words-ii/
這類問題的特點都是,你沒有機會第二次遍歷所有數據
。上述問題部分將在《九章算法強化班》中講解。
用雙指針算法解決鏈表中點問題
我們可以使用雙指針算法來解決鏈表中點的問題,更具體的,我們可以稱之為快慢指針
算法。該算法如下:
Python:
slow, fast = head, head.next
while fast != None and fast.next != None: slow = slow.next fast = fast.next.next return slow
在上面的程序中,我們將快指針放在第二個節點上,慢指針放在第一個節點上,while 循環中每一次快指針走兩步,慢指針走一步。這樣當快指針走到頭的時候,慢指針就在中點了。
快慢指針的算法,在下一小節的“帶環鏈表”中,也用到了。======>這種寫法容易出錯,我的預判能夠走兩步的做法更好!
一個小練習
將上述代碼改為提供接口的模式,即設計一個 class,支持兩個函數,一個是 add(node)
加入一個節點,一個是 getMiddle()
求中間的那個節點。
102. 帶環鏈表
給定一個鏈表,判斷它是否有環。
樣例
```
樣例 1:
輸入: 21->10->4->5, then tail connects to node index 1(value 10).
輸出: true
樣例 2:
輸入: 21->10->4->5->null
輸出: false
```
挑戰
不要使用額外的空間
""" Definition of ListNode class ListNode(object): def __init__(self, val, next=None): self.val = val self.next = next """ class Solution: """ @param head: the head of linked list. @return: a middle node of the linked list """ def middleNode(self, head): # write your code here slow, fast = head, head while fast and fast.next and fast.next.next: slow = slow.next fast = fast.next.next return slow
快速排序(Quick Sort)和歸並排序(Merge Sort)是算法面試必修的兩個基礎知識點。很多的算法面試題,要么是直接問這兩個算法,要么是這兩個算法的變化,要么是用到了這兩個算法中同樣的思想或者實現方式,要么是挑出這兩個算法中的某個步驟來考察。
相向雙指針模板3---中等難度------------不建議使用
注意:都是<=,否則定出錯。
上面的模板我自己用起來並不滿意,還是使用同向雙指針的寫法更直觀。
另外一種直觀的模板寫法,快速排序,使用同向雙指針模板(基本上和前面沒有區別):
# use leftest for pivot index def partition(arr, left, right): pivot = arr[left] """ # 使用中位數作為pivot: mid = (left + right) // 2 pivot = arr[mid] arr[left], arr[mid] = arr[mid], arr[left] """ index = left + 1 # 注意index起始位置是left+1 for i in range(left + 1, right + 1): # 循環的起始位置也是left+1 if arr[i] < pivot: # <= 也是可以的 arr[i], arr[index] = arr[index], arr[i] index += 1 arr[left], arr[index - 1] = arr[index - 1], arr[left] return index-1 # 返回index-1,非常關鍵!!!因為 assert arr[index-1] < pivot and arr[index]>=pivot def qsort(arr, start, end): if start >= end: return index = partition(arr, start, end) qsort(arr, start, index-1) # 注意必須是index-1,因為index這個位置必定排序好了 qsort(arr, index+1, end) # 注意必須是index+1 arr = [] qsort(arr, 0, -1) print arr from random import randint for k in range(1, 100): arr = [randint(0, 100) for i in range(k)] arr2 = (list(arr)) qsort(arr, 0, len(arr) - 1) arr2.sort() for j in range(0, len(arr)): assert arr[j] == arr2[j]
31. 數組划分
給出一個整數數組 nums 和一個整數 k。划分數組(即移動數組 nums 中的元素),使得:
- 所有小於k的元素移到左邊
- 所有大於等於k的元素移到右邊
返回數組划分的位置,即數組中第一個位置 i,滿足 nums[i] 大於等於 k。
樣例
例1:
輸入:
[],9
輸出:
0
例2:
輸入:
[3,2,2,1],2
輸出:1
解釋:
真實的數組為[1,2,2,3].所以返回 1
挑戰
使用 O(n) 的時間復雜度在數組上進行划分。
注意事項
你應該真正的划分數組 nums,而不僅僅只是計算比 k 小的整數數,如果數組 nums 中的所有元素都比 k 小,則返回 nums.length。
同向雙指針寫法:
class Solution: """ @param arr: The integer array you should partition @param k: An integer @return: The index after partition """ def partitionArray(self, arr, k): # write your code here ans = 0 index = 0 for i in range(0, len(arr)): if arr[i] < k: arr[i],arr[index] = arr[index],arr[i] index += 1 ans = index return ans
雙向雙指針做法,寫起來比較痛苦,不建議用:
class Solution: """ @param nums: The integer array you should partition @param k: An integer @return: The index after partition """ def partitionArray(self, nums, k): # write your code here if not nums: return 0 left, right = 0, len(nums)-1 while left <= right: while left <= right and nums[left] < k: left += 1 while left <= right and nums[right] >= k: right -= 1 if left <= right: nums[left], nums[right] = nums[right], nums[left] left += 1 right -= 1 return left
63. 整數排序
給一組整數,按照升序排序,使用選擇排序,冒泡排序,插入排序或者任何 O(n2) 的排序算法。
樣例
樣例 1:
輸入: [3, 2, 1, 4, 5]
輸出: [1, 2, 3, 4, 5]
樣例解釋:
返回排序后的數組。
樣例 2:
輸入: [1, 1, 2, 1, 1]
輸出: [1, 1, 1, 1, 2]
樣例解釋:
返回排好序的數組。
快速排序:
同向雙指針寫法:
class Solution: """ @param A: an integer array @return: nothing """ def sortIntegers(self, A): # write your code here self.qsort(A, start=0, end=len(A)-1) def patition(self, arr, left, right): pivot = arr[left] index = left+1 for i in range(left+1, right+1): if arr[i] < pivot: arr[i],arr[index] = arr[index],arr[i] index += 1 pivot_index = index-1 arr[left], arr[pivot_index] = arr[pivot_index], arr[left] return pivot_index def qsort(self, nums, start, end): if start >= end: return index = self.patition(nums, start, end) self.qsort(nums, start, index-1) self.qsort(nums, index+1, end)
相向雙指針寫法(更復雜):
class Solution: """ @param A: an integer array @return: nothing """ def sortIntegers(self, A): # write your code here self.qsort(A, start=0, end=len(A)-1) def qsort(self, nums, start, end): if start >= end: return left, right = start, end pivot = nums[(left+right)//2] while left <= right: while left <= right and nums[left] < pivot: left += 1 while left <= right and nums[right] > pivot: right -= 1 if left <= right: nums[left], nums[right] = nums[right], nums[left] left += 1 right -= 1 self.qsort(nums, start, right) self.qsort(nums, left, end)
461. 無序數組K小元素
找到一個無序數組中第K小的數
樣例
樣例 1:
輸入: [3, 4, 1, 2, 5], k = 3
輸出: 3
樣例 2:
輸入: [1, 1, 1], k = 2
輸出: 1
挑戰
O(nlogn)的算法固然可行, 但如果你能 O(n) 解決, 那就非常棒了.
class Solution: """ @param k: An integer @param nums: An integer array @return: kth smallest element """ def kthSmallest(self, k, nums): # write your code here return self.find_kth(k-1, nums, left=0, right=len(nums)-1) def patition(self, arr, left, right): pivot = arr[left] index = left+1 for i in range(left+1, right+1): if arr[i] < pivot: arr[i],arr[index] = arr[index],arr[i] index += 1 pivot_index = index-1 arr[left], arr[pivot_index] = arr[pivot_index], arr[left] return pivot_index def find_kth(self, k, nums, left, right): index = self.patition(nums, left, right) if index == k: return nums[k] elif index > k: return self.find_kth(k, nums, left, index-1) else: return self.find_kth(k, nums, index+1, right)
相向雙指針寫法(更復雜):
class Solution: # @param k & A a integer and an array # @return ans a integer def kthLargestElement(self, k, A): if not A or k < 1 or k > len(A): return None return self.partition(A, 0, len(A) - 1, len(A) - k) def partition(self, nums, start, end, k): """ During the process, it's guaranteed start <= k <= end """ if start == end: return nums[k] left, right = start, end pivot = nums[(start + end) // 2] while left <= right: while left <= right and nums[left] < pivot: left += 1 while left <= right and nums[right] > pivot: right -= 1 if left <= right: nums[left], nums[right] = nums[right], nums[left] left, right = left + 1, right - 1 # left is not bigger than right if k <= right: return self.partition(nums, start, right, k) if k >= left: return self.partition(nums, left, end, k) return nums[k]
373. 奇偶分割數組
分割一個整數數組,使得奇數在前偶數在后。
樣例
樣例1:
輸入: [1,2,3,4]
輸出: [1,3,2,4]
樣例2:
輸入: [1,4,2,3,5,6]
輸出: [1,3,5,4,2,6]
挑戰
在原數組中完成,不使用額外空間。
注意事項
答案不唯一。你只需要給出一個合法的答案。
class Solution: """ @param: nums: an array of integers @return: nothing """ def partitionArray(self, nums): # write your code here index = 0 for i in range(0, len(nums)): if nums[i] & 1: nums[index], nums[i] = nums[i], nums[index] index += 1
144. 交錯正負數
給出一個含有正整數和負整數的數組,重新排列成一個正負數交錯的數組。
樣例
樣例 1
輸入 : [-1, -2, -3, 4, 5, 6]
輸出 : [-1, 5, -2, 4, -3, 6]
解釋 : 或者仍和滿足條件的答案
挑戰
完成題目,且不消耗額外的空間。
注意事項
不需要保持正整數或者負整數原來的順序。
沒啥意思的題目:
class Solution: """ @param: A: An integer array. @return: nothing """ def rerange(self, A): # write your code here index = 0 for i in range(0, len(A)): if A[i] > 0: A[index], A[i] = A[i], A[index] index += 1 # A[0:index] > 0, A[index:] < 0 assert A[index] < 0 j = 0 if index == len(A)//2 else 1 while index < len(A) and j < len(A): A[j], A[index] = A[index], A[j] index += 1 j += 2
49. 字符大小寫排序
給定一個只包含字母的字符串,按照先小寫字母后大寫字母的順序進行排序。
樣例
樣例 1:
輸入: "abAcD"
輸出: "acbAD"
樣例 2:
輸入: "ABC"
輸出: "ABC"
挑戰
在原地掃描一遍完成
注意事項
小寫字母或者大寫字母他們之間不一定要保持在原始字符串中的相對位置。
class Solution: """ @param: chars: The letter array you should sort by Case @return: nothing """ def sortLetters(self, chars): # write your code here chars2 = list(chars) index = 0 for i in range(0, len(chars2)): if ord('a') < ord(chars2[i]) < ord('z'): chars2[index], chars2[i] = chars2[i], chars2[index] index += 1 return "".join(chars2)
148. 顏色分類
給定一個包含紅,白,藍且長度為 n 的數組,將數組元素進行分類使相同顏色的元素相鄰,並按照紅、白、藍的順序進行排序。
我們可以使用整數 0,1 和 2 分別代表紅,白,藍。
樣例
樣例 1
輸入 : [1, 0, 1, 2]
輸出 : [0, 1, 1, 2]
解釋 : 原地排序。
挑戰
一個相當直接的解決方案是使用計數排序掃描2遍的算法。
首先,迭代數組計算 0,1,2 出現的次數,然后依次用 0,1,2 出現的次數去覆蓋數組。
你否能想出一個僅使用常數級額外空間復雜度且只掃描遍歷一遍數組的算法?
注意事項
不能使用代碼庫中的排序函數來解決這個問題。
排序需要在原數組中進行。
做兩次 Partition。先把0和非0分開,再把1和非1分開。
class Solution: """ @param nums: A list of integer which is 0, 1 or 2 @return: nothing """ def sortColors(self, nums): # write your code here index = 0 for i in range(0, len(nums)): if nums[i] == 0: nums[i], nums[index] = nums[index], nums[i] index += 1 assert nums[index-1] == 0 assert nums[index] != 0 for i in range(index, len(nums)): if nums[i] == 1: nums[i], nums[index] = nums[index], nums[i] index += 1 assert nums[index-1] == 1 assert nums[index] == 2
計數排序:
class Solution: """ @param nums: A list of integer which is 0, 1 or 2 @return: nothing """ def sortColors(self, nums): # write your code here cnt_map = {0:0, 1:0, 2:0} for n in nums: cnt_map[n] += 1 i = 0 for n in (0, 1, 2): for j in range(0, cnt_map[n]): nums[i] = n i += 1
143. 排顏色 II
給定一個有n個對象(包括k種不同的顏色,並按照1到k進行編號)的數組,將對象進行分類使相同顏色的對象相鄰,並按照1,2,...k的順序進行排序。
樣例
樣例1
輸入:
[3,2,2,1,4]
4
輸出:
[1,2,2,3,4]
樣例2
輸入:
[2,1,1,2,2]
2
輸出:
[1,1,2,2,2]
挑戰
一個相當直接的解決方案是使用計數排序掃描2遍的算法。這樣你會花費O(k)的額外空間。你否能在不使用額外空間的情況下完成?
注意事項
- 不能使用代碼庫中的排序函數來解決這個問題
k
<=n
class Solution: """ @param nums: A list of integer @param k: An integer @return: nothing """ def sortColors2(self, nums, k): # write your code here index = 0 for target in range(1, k+1): for i in range(index, len(nums)): if nums[i] == target: nums[i], nums[index] = nums[index], nums[i] index += 1
539. 移動零
給一個數組 nums 寫一個函數將 0
移動到數組的最后面,非零元素保持原數組的順序
樣例
例1:
輸入: nums = [0, 1, 0, 3, 12],
輸出: [1, 3, 12, 0, 0].
例2:
輸入: nums = [0, 0, 0, 3, 1],
輸出: [3, 1, 0, 0, 0].
注意事項
1.必須在原數組上操作
2.最小化操作數
class Solution: """ @param nums: an integer array @return: nothing """ def moveZeroes(self, nums): # write your code here index = 0 for i in range(0, len(nums)): if nums[i] != 0: nums[index], nums[i] = nums[i], nums[index] index += 1