什么是同向双指针? 什么是相向双指针? 双指针的鼻祖题 —— 两数之和 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