@Author:Runsen
编程的本质来源于算法,而算法的本质来源于数学,编程只不过将数学题进行代码化。 ---- Runsen
双指针
双指针是一种解决问题的技巧或者思维方式,指在访问一个序列中的数据时使用两个指针进行扫描,两个指针可以是同向的,也可以是反向的。
我们的关注点可以是这两个指针指向的两个元素本身,也可以是两个指针中间的区域。二分法的思想基于这种左右指针的实现。
双指针是一种思想,一种技巧或一种方法,并不是什么特别具体的算法。在区间问题上,暴力的做法的复杂度往往达到 O ( n 2 ) O(n^2) O(n2)复杂度,而双指针的思想挖掘区间“单调”性质将复杂度降到 O ( n ) O(n) O(n)。
常用的双指针思想有:快慢指针、碰撞指针、滑动窗口等。
快慢指针:快慢指针按照某种规律运动。例如,设置快慢两个指针,快指针先移动距离,慢指针跟快指针同时移动,这样快慢指针之间总是保持一段相同的距离。常见的应用场景主要出现在链表中,如:链表的环的判断,求链表的中间节点等操作。
碰撞指针:在排序好的数组中,设置头指针和尾指针,按照规则,分别向中间靠拢。常见的应用场景主要出现在有序数组中:数组的和,二分查找等。这里需要强调的是:对于碰撞指针要用于已排序的区间。
滑动窗口:两个指针,一前一后组成滑动窗口,并计算滑动窗口中的元素的问题。常见问题:字符串匹配问题等,用来解决一些查找满足一定条件的连续区间求值或长度的问题。
LeetCode 第 15题:三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
在Leetcode上第一题是两数之和,使用Hashmap储存,时间复杂度是 O ( n ) O(n) O(n)。
如果三数之和使用该方法,时间复杂度是 O ( n 2 ) O(n^2) O(n2)。题目中说的不可以包含重复的三元组,然后在去去重,这样是非常费时的,很容易超时但是过高的时间复杂度导致代码编译效率极差,我记得很清楚Leetcode不给予通过。这种方法是在面试中实在想不出其他解法时的选择…
双指针思路:采取左右两个指针代替两个for循环,在第一层循环下调节指针的位置,设置判断条件就可以排除很多重复项和不满足条件的组合,最终得到满足题目的三元组,具体的伪代码大致如下:
function fn (list) {
var left = 0;
var right = list.length - 1;
//遍历数组
while (left <= right) {
left++;
// 一些条件判断 和处理
... ...
right--;
}
}
由于本提中给出的数组是未排序,且有重复数据的情况,所以首先需要做排序和去重处理
下面使用排序 + 双指针方法解决:
看到这张概念图后,是不是已经有内味了?
- 首先进行数组排序,时间复杂度
O(nlogn)
- 对数组nums进行遍历,每遍历一个值利用其下标 i,形成一个固定值
nums[i]
- 如果
nums[i]
大于0, 则三数之和必然无法等于0,直接结束循环 - 如果
nums[i] == nums[i-1]
,则说明该数字重复,会导致结果重复,所以应该跳过 - 再使用前指针指向 l = i + 1处,后指针指向
r = nums.length - 1
,也就是结尾处 - 根据
three_sum = nums[i] + nums[l] + nums[r]
结果,判断three_sum
与 0 的大小关系,满足则添加进入结果,此时l+=1
和r-=1
。 如果three_sum < 0
,则l+=1
, 如果 three_sum > 0, 则 `r-=1`` - three_sum === 0 的时候还要考虑结果重复的情况
- nums[l] == nums[l+1] 则会导致结果重复,应该跳过,l++
- nums[r] == nums[r-1] 则会导致结果重复,应该跳过,r–
- 总时间复杂度: O ( n l o g n ) + O ( n 2 ) = O ( n 2 ) O(nlogn) + O(n^2) = O(n^2) O(nlogn)+O(n2)=O(n2)
具体查看代码:
class Solution:
def threeSum(nums):
nums.sort()
# [-4, -1, -1, 0, 1, 2]
res_list = []
# 头部循环查找
for i in range(len(nums)):
if i == 0 or nums[i] > nums[i - 1]:
# 最左端
l = i + 1
# 最右端
r = len(nums) - 1
while l < r: # 正在查找
three_sum = nums[i] + nums[l] + nums[r]
if three_sum == 0:
res_list.append([nums[i], nums[l], nums[r]])
l += 1 # 右移一位
r -= 1 # 左移一位
while l < r and nums[l] == nums[l - 1]:
# 从左往右,相同数值直接跳过
l += 1
while r > l and nums[r] == nums[r + 1]:
# 从右往左,相同数值直接跳过
r -= 1
elif three_sum > 0:
# 大于零,右边数值大,左移
r -= 1
else:
# 小于零,左边数值小,右移
l += 1
return res_list
LeetCode 第 16题:最接近的三数之和
给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
示例:
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
提示:
3 <= nums.length <= 10^3
-10^3 <= nums[i] <= 10^3
-10^4 <= target <= 10^4
本题目因为要计算三个数,如果靠暴力枚举的话时间复杂度会到 O ( n 3 ) O(n^3) O(n3),需要降低时间复杂度,借鉴上面的三数之和,其实两道题的解决思路几乎一样,只不过这道题需要不停着记录三个数的和与 target 之间的差。
- 首先进行数组排序,时间复杂度O(nlogn)
- 在数组nums中,进行遍历,每遍历一个值利用其下标i,形成一个固定值nums[i]
- 再使用前指针指向
j= i + 1
处,后指针指向k= nums.length - 1
处,也就是结尾处 - 根据
sum = nums[i] + nums[start] + nums[end]
的结果,判断sum与目标target的距离,如果更近则更新结果ans - 同时判断sum与target的大小关系,因为数组有序,如果
sum > target
则k--
,如果sum < target 则j++
,如果sum == target
则说明距离为0直接返回结果 - 整个遍历过程,固定值为n次,双指针为n次,时间复杂度为O(n^2)
- 总时间复杂度: O ( n l o g n ) + O ( n 2 ) = O ( n 2 ) O(nlogn) + O(n^2) = O(n^2) O(nlogn)+O(n2)=O(n2)
具体查看代码:
class Solution:
def threeSumClosest(self, nums: List[int], target: int) -> int:
if not nums: return 0
if len(nums) < 3: return sum(nums)
ans = float('inf')
nums.sort()
for i in range(len(nums)):
# 优化点 1
if i > 0 and nums[i] == nums[i-1]:
continue
t = target - nums[i]
j, k = i + 1, len(nums) - 1
while j < k:
# 优化点 2
if t == nums[j] + nums[k]: return target
# 当前 j、k更接近target
if abs(t - nums[j] - nums[k]) < abs(target - ans):
ans = nums[i] + nums[j] + nums[k]
# 移动j | k
if t > nums[j] + nums[k]:
j += 1
else:
k -= 1
return ans
LeetCode 第 27题:移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。
首先讲讲自己做题的思路,用Python做比较简单,遍历数组,如果当前值不等于val,就是i += 1
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
i = 0
for num in nums:
if num != val:
nums[i] = num
i += 1
return i
官方的解法是双指针,我们可以保留两个指针 i 和 j,其中 i 是慢指针,j 是快指针。
public int removeElement(int[] nums, int val) {
int i = 0;
for (int j = 0; j < nums.length; j++) {
if (nums[j] != val) {
nums[i] = nums[j];
i++;
}
}
return i;
}
人生最重要的不是所站的位置,而是内心所朝的方向。只要我在每篇博文中写得自己体会,修炼身心;在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰难,奋勇前行,不忘初心,砥砺前行,人生定会有所收获,不留遗憾 (作者:Runsen )
本文已收录 GitHub,传送门~ ,里面更有大厂面试完整考点,欢迎 Star。