LeetCode题型归纳——数组双指针
目录
一、双指针算法基础
所谓双指针算法,就是指的是在遍历的过程中,不是普通的使用单个指针进行循环访问,而是使用两个相同方向或者相反方向的指针进行扫描,从而达到相应的目的。双指针法充分使用了数组有序这一特征,从而在某些情况下能够简化一些运算,降低时间复杂度.
二、双指针算法分类
包含:
- 相向双指针
- 同向双指针 - 快慢指针
- 同向双指针 - 滑动窗口
- 分离双指针
下面分别介绍:
2.1 相向双指针
是指在有序数组中,将指向最左侧的索引定义为左指针 (left),最右侧的定义为右指针 (right),然后从两头向中间进行数组遍历 。(前提:数组有序,若无序,先排序)
2.1.1 两数之和(1)
方法一:暴力枚举
枚举数组中的每一个数 x,寻找数组中是否存在 target - x。当我们使用遍历整个数组的方式寻找 target - x 时,需要注意到每一个位于 x 之前的元素都已经和 x 匹配过,因此不需要再进行匹配。而每一个元素不能被使用两次,所以我们只需要在 x 后面的元素中寻找 target - x。
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
for i in range(len(nums) - 1):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
class Solution {
public int[] twoSum(int[] nums, int target) {
for (int i = 0; i < nums.length - 1; i++){
for (int j = i + 1; j < nums.length; j++){
if (nums[i] + nums[j] == target){
return new int[] {i, j};
}
}
}
return new int[0];
}
}
方法二:哈希表解法
创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,若不存在则将 x 插入到哈希表中,即可保证不会让 x 和自己匹配,若存在,则返回 。
例如:
初始状态:
第一步:哈希表中没有11,所以把2插入,对应的索引0作为其值,
第二步:哈希表中没有6,所以把7插入,对应的索引1作为其值,
第三步:哈希表中有2,所以返回2对应的索引0和11对应的索引2
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
# 创建一个哈希表
res = {}
# 遍历
for i in range(len(nums)):
if target - nums[i] not in res:
res[nums[i]] = i
else:
return [res[target - nums[i]], i]
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> hashtable = new HashMap<>();
for(int i = 0; i < nums.length; i++){
if(hashtable.containsKey(target - nums[i])){
return new int[] {hashtable.get(target - nums[i]), i};
}else{
hashtable.put(nums[i], i);
}
}
return new int[0];
}
}
方法三:相向双指针解法
双指针(L,R)法的思路很简单, L指针用来指向第一个值, R指针用来从第L指针的后面查找数组中是否含有和L指针指向值和为目标值的数。
两个指针分别从左从右开始扫描,每次判断这两个数相加是不是等于target,如果小了,那就把左边的指针向右移,同理如果大了,就把右指针往左移。(前提:数组是有序的)
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
# 复制原数组,为后续找原数组中的位置做准备
array = nums.copy()
# 首先对数组进行从小到大排序
nums.sort()
# 初始化左右指针
left, right = 0, len(nums) - 1
# 扫描,找到满足和为target的两个数的位置
while left < right:
if nums[left] + nums[right] < target:
left += 1
elif nums[left] + nums[right] > target:
right -= 1
else:
break
res = []
# 找出该位置对应原数组中的索引
for i in range(len(array)):
if array[i] == nums[left] or array[i] == nums[right]:
res.append(i)
return res
修改一:
如果假设输入一个数组 nums 和一个目标和 target, 请你返回 nums 中能够凑出 target 的两个元素的值,比如输入 nums = [5,3,1,6], target = 9,那么算法返回两个元素 [3,6]。可以假设只有且仅有一对元素可以凑出 target。
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
# 对数组进行排序
nums.sort()
# 初始化双指针
left, right = 0, len(nums) - 1
# 开始遍历
while left < right:
sums = nums[left] + nums[right]
if sums < target:
left += 1
elif sums > target:
right -= 1
else:
# 本修改需要返回的是两个元素的值
return [nums[left], nums[right]]
class Solution {
public int[] twoSum(int[] nums, int target) {
//方法三:排序+双指针
int[] temp = Arrays.copyOf(nums, nums.length);
Arrays.sort(nums); //升序排序
int begin = 0;
int end = nums.length - 1;
while(begin < end){
if(nums[begin] + nums[end] == target){
break;
}else if(nums[begin] + nums[end] > target){
end--;
}else{
begin++;
}
}
int a = -1;
int b = -1;
for(int i=0; i<temp.length; i++){
if(temp[i] == nums[begin] && a == -1){
a = i;
continue;
}
if(temp[i] == nums[end]){
b = i;
}
}
return new int[]{a, b};
}
}
修改二:
nums 中可能有多对元素之和都等于 target,请你的算法返回所有和为 target 的元素对,其中不能出现重复,比如说输入为 nums = [1,3,1,2,2,3], target = 4,那么算法返回的结果就是:[[1,3],[2,2]]。
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
# 先进行排序
nums.sort()
# 初始化双指针
left, right = 0, len(nums) - 1
res = []
# 开始遍历
while left < right:
sums = nums[left] + nums[right]
if sums < target:
left += 1
elif sums > terget:
right -= 1
else:
res.append([nums[left], nums[right]])
# 去重,注意数组是有序的,所以可以这样
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
return res
2.1.2 两数之和II - 输入有序数组(167)
具体步骤如下:
借助数组的有序性:
- 使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。
- 如果两个指针指向元素的和 sum == target,那么得到要求的结果;
- 如果 sum > target,移动较大的元素,使 sum 变小一些;如果 sum < target,移动较小的元素,使 sum 变大一些。
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
# 初始化双指针
left, right = 0, len(numbers) - 1
# 开始遍历
while left < right:
sums = numbers[left] + numbers[right]
if sums < target:
left += 1
elif sums > target:
right -= 1
else:
return [left + 1, right + 1]
# 若没有找到满足的元素对,返回[-1, -1]
return [-1, -1]
class Solution {
public int[] twoSum(int[] numbers, int target) {
int left = 0;
int right = numbers.length - 1;
while(left < right){
int s = numbers[left] + numbers[right];
if(s == target){
return new int[] {left+1, right+1};
}else if(s > target){
right--;
}else{
left++;
}
}
return new int[0];
}
}
2.1.3 三数之和(15)
例如,给定数组 nums = [-1, 0, 1, 2, -1, -4],满足要求的三元组集合为:[[-1, 0, 1], [-1, -1, 2]]。
采取固定一个数,同时用双指针来查找另外两个数的方式。
具体步骤如下:
- 我们可以先对数组进行排序,然后我们选择一个数字做 C 位,然后我们在这个 C 位数字的右边进行双指针搜索:
- 从最左边 i+1(最小值)和最右边 len(nums)-1(最大值)两个数字开始,加上 C 位,计算总和是否等于 0。
如果大于 0,说明实力太强了,就把右侧的数字左移一位。
如果小于 0,说明实力太弱了,就把左边的数字右移一位。 - 当双指针碰到的时候,这轮循环结束,以该数字为 C 位的所有可能都已经尝试完毕了。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
# 先进行排序
nums.sort()
res = []
# 固定一个数,然后用双指针找另外两个数
for i in range(len(nums) - 2):
# 剪枝
if nums[i] > 0:
break
# 去重,如果当前固定的数和上一次循环相等,直接跳过
if i > 0 and nums[i] == nums[i - 1]:
continue
# 初始化双指针
left, right = i + 1, len(nums) - 1
# 开始遍历双指针
while left < right:
sums = nums[i] + nums[left] + nums[right]
if sums < 0:
left += 1
elif sums > 0:
right -= 1
else:
res.append([nums[i], nums[left], nums[right]])
# 去重
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
return res
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
int length = nums.length;
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for(int i=0; i<length-2; i++){
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
if(nums[i] + nums[i+1] + nums[i+2] > 0){
break;
}
if(nums[i] + nums[length-2] + nums[length-1] < 0){
continue;
}
int left = i + 1;
int right = length - 1;
while(left < right){
int sum = nums[i] + nums[left] + nums[right];
if(sum == 0){
List<Integer> temp = new ArrayList<>();
temp.add(nums[i]);
temp.add(nums[left]);
temp.add(nums[right]);
res.add(temp);
left++;
right--;
while(left < right && nums[left] == nums[left-1]){
left++;
}
while(left < right && nums[right] == nums[right+1]){
right--;
}
}else if(sum < 0){
left++;
}else{
right--;
}
}
}
return res;
}
}
2.1.4 最接近的三数之和(16)
例如,给定数组 nums = [-1,2,1,-4], 和 target = 1。与 target 最接近的三个数的和为 2。(-1 + 2 + 1 = 2)。
具体步骤如下:
- 在数组 nums 中,进行遍历,每遍历一个值利用其下标 i,形成一个固定值nums[i]。
- 使用前指针指向 left = i + 1 处,后指针指向 right = len(nums) - 1 处,也就是结尾处,根据 sums = nums[i] + nums[left] + nums[right] 的结果,判断 sums 与目标 target 的距离,如果更近则更新结果 distance。
- 因为数组有序,如果 sums > target 则 right -= 1,如果 sums < target 则 left+= 1,如果 sums == target 则说明距离为 0,直接返回结果。
class Solution:
def threeSumClosest(self, nums: List[int], target: int) -> int:
# 先进行排序
nums.sort()
# 初始化最近距离和对应的三数之和
distance_closest = abs(nums[0] + nums[1] + nums[2] - target)
sums_closest = nums[0] + nums[1] + nums[2]
# 固定一个数,然后用双指针遍历另外两个数
for i in range(len(nums) - 2):
# 初始化双指针
left, right = i + 1, len(nums) - 1
# 双指针遍历
while left < right:
sums = nums[i] + nums[left] + nums[right]
distance = abs(sums - target)
# 判断新距离是否小于最小距离,若是,则更新
if distance < distance_closest:
distance_closest = distance
sums_closest = sums
# 移动双指针
if sums < target:
left += 1
elif sums > target:
right -= 1
# 如果等于target,则距离为0,直接返回
else:
return target
return sums_closest
class Solution {
public int threeSumClosest(int[] nums, int target) {
int dis = 50000;
int res = 0;
int n = nums.length;
Arrays.sort(nums);
for(int i=0; i<n-2; i++){
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
int left = i + 1;
int right = n - 1;
while(left < right){
int s = nums[i] + nums[left] + nums[right];
if(Math.abs(s - target) < dis){
dis = Math.abs(s - target);
res = s;
}
if(s == target){
return target;
}else if(s > target){
right--;
while(left < right && nums[right] == nums[right+1]){
right--;
}
}else{
left++;
while(left < right && nums[left] == nums[left-1]){
left++;
}
}
}
}
return res;
}
}
2.1.5 四数之和(18)
示例:
给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。满足要求的四元组集合为:[[-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2]]。
具体步骤如下:
- 使用两重循环分别枚举前两个数,然后在两重循环枚举到的数之后使用双指针枚举剩下的两个数。假设两重循环枚举到的前两个数分别位于下标 i 和 j,其中 i<j。
- 初始时,左右指针分别指向下标 j+1和下标 n-1。
- 每次计算四个数的和,并进行如下操作:
如果和等于 target,则将枚举到的四个数加到答案中,然后将左指针右移直到遇到不同的数,将右指针左移直到遇到不同的数;
如果和小于target,则将左指针右移一位;
如果和大于target,则将右指针左移一位。
具体实现时,还可以进行一些剪枝操作:
- 在确定第一个数之后,如果nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target,说明此时剩下的三个数无论取什么值,四数之和一定大于 target, 因此退出第一重循环;
- 在确定第一个数之后,如果nums[i]+nums[n-3]+nums[n-2]+nums[n-1]<target, 说明此时剩下的三个数无论取什么值,四数之和一定小于 target, 因此第一重循环直接进入下一轮,枚举nums[i+1];
- 在确定前两个数之后,如果 nums[i]+nums[j]+nums[j+1]+nums[j+2]>target, 说明此时剩下的两个数无论取什么值,四数之和一定大于 target, 因此退出第二重循环;
- 在确定前两个数之后,如果 nums[i]+nums[j]+nums[n-2]+nums[n-1]<target,说明此时剩下的两个数无论取什么值,四数之和一定小于 target, 因此第二重循环直接进入下一轮,枚举 nums[j+1]。
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
# 先进行排序
nums.sort()
res = []
# 固定两个数,双指针找另外两个数
for i in range(len(nums) - 3):
# 去重
if i > 0 and nums[i] == nums[i - 1]:
continue
# 剪枝
if nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target:
break
if nums[i] + nums[-3] + nums[-2] + nums[-1] < target:
continue
for j in range(i + 1, len(nums) - 2):
# 去重,注意是 j > i + 1
if j > i + 1 and nums[j] == nums[j - 1]:
continue
# 剪枝
if nums[i] + nums[j] + nums[j+1] + nums[j+2] > target:
break
if nums[i] + nums[j] + nums[-2] + nums[-1] < target:
continue
# 初始化双指针
left, right = j + 1, len(nums) - 1
# 遍历双指针
while left < right:
sums = nums[i] + nums[j] + nums[left] + nums[right]
if sums < target:
left += 1
elif sums > target:
right -= 1
else:
res.append([nums[i], nums[j], nums[left], nums[right]])
# 去重
while left < right and nums[left] == nums[left + 1]:
left += 1
while left > right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
return res
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
//1、先排序
Arrays.sort(nums);
//2、二重遍历
List<List<Integer>> res = new ArrayList<>();
int n = nums.length;
for(int i=0; i<n-3; i++){
//去重
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
//剪枝,java中四数之和可能超出int的范围,故转为long类型
if((long)nums[i]+nums[i+1]+nums[i+2]+nums[i+3] > target){
break;
}
if((long)nums[i]+nums[n-3]+nums[n-2]+nums[n-1] < target){
continue;
}
for(int j=i+1; j<n-2; j++){
//3、双指针寻找
//去重
if(j > i+1 && nums[j] == nums[j-1]){
continue;
}
//剪枝
if((long)nums[i]+nums[j]+nums[j+1]+nums[j+2] > target){
break;
}
if((long)nums[i]+nums[j]+nums[n-2]+nums[n-1] < target){
continue;
}
int left = j + 1;
int right = n - 1;
while(left < right){
int s = nums[i] + nums[j] + nums[left] + nums[right];
if(s == target){
List<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[j]);
list.add(nums[left]);
list.add(nums[right]);
res.add(list);
left++;
right--;
while(left < right && nums[left] == nums[left-1]){
left++;
}
while(left < right && nums[right] == nums[right+1]){
right--;
}
}else if(s < target){
left++;
}else{
right--;
}
}
}
}
return res;
}
}
2.1.6 盛最多水的容器(11)
解决该问题的关键思想:木桶原理,即短的一端决定了水的多少。
设每一状态下水槽面积为 S(i, j),(0 <= i < j < n), 由于水槽的实际高度由两板中的短板决定,则可得面积公式 S(i,j)=min(h[i],h[j])×(j-i)。
在每一个状态下,无论长板或短板收窄 1 格,都会导致水槽的底边宽度 -1:
若向内移动短板,水槽的短板 min(h[i],h[j]) 可能变大,因此水槽面积 S(i,j) 可能增大。
若向内移动长板,水槽的短板 min(h[i],h[j]) 不变或变小,下个水槽的面积一定小于当前水槽面积。
因此,向内收窄短板可能可以获取面积最大值。
换个角度理解:
若不指定移动规则,所有移动出现的 S(i,j) 的状态数为 C(n,2), 即暴力枚举出所有状态。
在状态 S(i,j) 下向内移动短板至 S(i+1,j)(假设 h[i]<h[j] ), 则相当于消去了
S(i,j-1),S(i,j-2),…,S(i,i+1) 状态集合。而所有消去状态的面积一定 <=S(i,j):
短板高度:相比 S(i,j) 相同或更短(<=h[i]);
底边宽度:相比 S(i,j) 更短。
因此所有消去的状态的面积都<S(i,j)。 通俗的讲,我们每次向内移动短板,所有的消去状态都不会
导致丢失面积最大值 。
class Solution:
def maxArea(self, height: List[int]) -> int:
# 这个问题不需要也不能将数组进行排序
# 初始化双指针和最大水量
left, right, max_water = 0, len(height) - 1, 0
# 开始遍历
while left < right:
if height[left] < height[right]:
water = height[left] * (right - left)
left += 1
else:
water = height[right] * (right - left)
right -= 1
# 若此次水量比最大值更大,则更新最大水量
if water > max_water:
max_water = water
return max_water
class Solution {
public int maxArea(int[] height) {
int n = height.length;
int left = 0;
int right = n - 1;
int max_water = 0;
while(left < right){
int shorter_index = height[left] > height[right] ? right : left;
int area = (right - left) * height[shorter_index];
max_water = max_water >= area ? max_water : area;
if(shorter_index == right){
right--;
}else{
left++;
}
}
return max_water;
}
}
2.1.7 ⭐⭐有效三角形的个数 (611)
思路:
数组排序,便于后序的处理。
- 固定最长的边 c, 然后采用双指针在其左侧寻找合适的 a、 b:a 从最左侧开始(nums[0])、 b 从最右侧开始(nums[i-1])
- 如果 nums[left] + nums[right] > nums[i], 说明 [left,right]、[left+1,right]…[right-1,right] 均满足条件,以 nums[right] 为中间边的情况已全部考虑过,然后 right -= 1
- 如果 nums[left] + nums[right] <= nums[i], 两边之和太小,需要增大, left += 1
class Solution:
def triangleNumber(self, nums: List[int]) -> int:
# 先进行排序
nums.sort()
# 初始化满足条件的三角形个数
count = 0
# 固定最长边,用相向双指针寻找另外两边
for i in range(len(nums) - 1, 1, -1):
# 初始化左右指针
left, right = 0, i - 1
# 相向双指针开始遍历
while left < right:
sums = nums[left] + nums[right]
if sums > nums[i]:
count += right - left
right -= 1
else:
left += 1
return count
class Solution {
public int triangleNumber(int[] nums) {
Arrays.sort(nums);
int res = 0;
int n = nums.length;
//1、固定最长的边
for(int i=n-1; i>=0; i--){
int left = 0;
int right = i - 1;
while(left < right){
if(nums[left]+nums[right] > nums[i]){
res += right - left;
right--;
}else{
left++;
}
}
}
return res;
}
}
2.2 同向双指针 - 快慢指针
快慢指针也是双指针,但是两个指针从同一侧开始遍历数组,将这两个指针分别定义为快指针(fast)和慢指针(slow),两个指针以不同的策略移动,直到两个指针的值相等(或其他特殊条件)为止,如 fast 每次增长两个, slow 每次增长一个。
2.2.1 删除排序数组中的重复项 (26)
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
# 初始化快慢指针
slow, fast = 0, 1
# 开始遍历
while fast < len(nums):
# 若出现不同的数字,则慢指针加入该数值
if nums[slow] != nums[fast]:
slow += 1
nums[slow] == nums[fast]
fast += 1
# 若出现相同的数字,则跳过,快指针指向下一个
else:
fast += 1
返回新数组的长度
return slow + 1
class Solution {
public int removeDuplicates(int[] nums) {
int slow = 0;
int fast = 0;
int n = nums.length;
while(fast < n){
if(nums[fast] == nums[slow]){
fast++;
}else{
slow++;
nums[slow] = nums[fast];
fast++;
}
}
return slow + 1;
}
}
2.2.2 移除元素 (27)
法一思路:
我们可以保留两个指针 i 和 j,其中 i 是慢指针, j 是快指针。当nums[j] 与给定的值相等时,递增 j 以跳过该元素。只要 nums[j] != val,我们就复制 nums[j] 到 nums[i] 并同时递增两个索引。重复这一过程,直到 j 到达数组的末尾,该数组的新长度为 i。
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
# 先进行排序
nums.sort()
# 初始化快慢指针
slow, fast = 0, 0
# 开始遍历
while fast < len(nums):
if nums[fast] == val:
fast += 1
else:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
法二思路:
当我们遇到 nums[i] = val 时,我们可以将当前元素与最后一个元素进行交换,并释放最后一个元素。这实际上使数组的大小减少了 1。 然后再对这个位置的元素进行判断是否等于val,如此循环,如果不等于val,则保留该元素,即指针指向下一个位置。
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
# 初始化两个指针位置
j, last = 0, len(nums) - 1
开始遍历
while j <= last:
if nums[j] == val:
nums[j] == nums[last]
last -= 1
else:
j += 1
return last + 1
class Solution {
public int removeElement(int[] nums, int val) {
int n = nums.length;
int slow = 0;
int fast = 0;
while(fast < n){
if(nums[fast] != val){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
2.2.3 删除排序数组中的重复项 (80)
思路:
遍历整个表:
把当前的元素与它前面的对比,如果二者元素相同(为重复元素):此时统计重复的计数器 count += 1。题目要求只保留 2 个重复的元素,这里需要加入重复元素个数的判断:
这个元素正好重复了 2 次 => 则进行保留。列表长度 i+=1,然后 nums[i]=nums[j];
这个元素重复多于 2 次 => 不进行任何操作。体现在程序上不做处理。
把当前的元素与它前面的对比,如果二者元素不同(为新元素):此时把当前这个结点 (nums[j]) 添加到新表里面去, nums[i] = nums[j], 表长 i+1。
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
# 初始化同向双指针和计数值
slow, fast, count = 0, 1, 1
# 开始遍历
while fast < len(nums):
# 遇到重复元素
if nums[slow] == nums[fast]:
# 首先技术次数加一
count += 1
# 判断次数是否超过两次,若是,则跳过该元素
if count > 2:
fast += 1
# 若不是,则将该元素加入,快慢指针分别加一
else:
slow += 1
nums[slow] = nums[fast]
fast += 1
# 遇到新元素
else:
# 初始化计数次数为1次,且将该元素加入
count = 1
slow += 1
nums[slow] = nums[fast]
fast += 1
return slow + 1
class Solution {
public int removeDuplicates(int[] nums) {
int n = nums.length;
int count = 1;
int slow = 1;
int fast = 1;
while(fast < n){
if(nums[fast] == nums[fast - 1]){
if(count == 1){
count += 1;
nums[slow] = nums[fast];
slow++;
}
}else{
count = 1;
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
2.2.4 移动零 (283)
思路:
nums 中, i 指针用于存放非零元素,j 指针用于遍历寻找非零元素(注: j 指针找到一个非零元素后,方法nums[i] 的位置 i++,用于下一个 j 指针找到的非零元素),j 指针遍历完后,最后 nums 数组还有空位置,存放 0 即可。
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
# 初始化快慢双指针
slow, fast = 0, 0
# 开始遍历
while fast < len(nums):
if nums[fast] == 0:
fast += 1
else:
nums[slow] = nums[fast]
slow += 1
fast += 1
# 补零
for i in range(slow, len(nums)):
nums[i] = 0
class Solution {
public void moveZeroes(int[] nums) {
int slow = 0;
int fast = 0;
while(fast < nums.length) {
if(nums[fast] != 0) {
nums[slow++] = nums[fast];
}
fast += 1;
}
while(slow < nums.length) {
nums[slow++] = 0;
}
}
}
2.2.5 ⭐⭐数组中的最长山脉(845)
思路:
首先固定山峰值,然后分别寻找左、右半边山脉的长度。A[left] < A[left+1],继续向左寻找A[right] < A[right-1],继续向右寻找。如果以当前山峰的山脉长度比最长山脉长,更新最长山脉。
注意:我们可以在只有当前点为山峰的情况(即 A[i-1] < A[i] and A[i+1] < A[i]),才在左右寻找最长山峰,这样可以大大降低搜索的次数。
class Solution:
def longestMountain(self, arr: List[int]) -> int:
# 剪枝
if len(arr) < 3:
return 0
# 初始化最长山脉长度
length = 0
# 先固定山峰
for i in range(1, len(arr) - 1):
# 剪枝,确保是山峰再去计算长度
if arr[i - 1] >= arr[i] or arr[i] <= arr[i + 1]:
continue
# 寻找左右半边山脉的长度
left, right = i - 1, i + 1
while left >= 0 and arr[left + 1] > arr[left]:
left -= 1
while right < len(arr) and arr[right - 1] > arr[right]:
right += 1
length = max(right - left - 1, length)
return length
class Solution {
public int longestMountain(int[] arr) {
int n = arr.length;
int max_length = 0;
for(int i=1; i < n-1; i++){
if(arr[i] > arr[i-1] && arr[i] > arr[i+1]){
int left = i-1;
int right = i+1;
while(left > 0 && arr[left] > arr[left - 1]){
left--;
}
while(right < n - 1 && arr[right] > arr[right + 1]){
right++;
}
max_length = max_length > right - left + 1 ? max_length : right - left + 1;
}
}
return max_length;
}
}
2.3 同向双指针 - 滑动窗口
有些时候,我们需要获得数组或者字符串的连续子部分,这时候我们就可以考虑使用滑动窗口。 nums[left,right] 为滑动窗口,根据具体的要求,通过遍历的时候,来改变 left 和 right 的位置,从而完成任务。
滑动窗口主要用来处理连续问题,从类型上说主要有:
(1)固定窗口大小
(2)窗口大小不固定,求解最大的满足条件的窗口
(3)窗口大小不固定,求解最小的满足条件的窗口
(1)固定窗口大小
对于固定窗口,我们只需要固定初始化左右指针 l 和 r,分别表示窗口的左右顶点,并且保证:
l 初始化为 0,初始化 r,使得 r - l + 1 等于窗口大小,同时移动 l 和 r,判断窗口内的连续元素是否满足题目限定的条件。
如果满足,再判断是否需要更新最优解,如果需要则更新最优解
如果不满足,则继续。
2.3.1 滑动窗口最大值(239)
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值所构成的数组。
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# 初始化双指针
left, right = 0, k - 1
# 初始化输出数组
res = []
while right < len(nums):
res.append(max(nums[left : right + 1]))
left += 1
right += 1
return res
ps:这道题是Hard,这种方法会超出时间限制,应该后面有更好的方法。
⭐⭐补充:优先队列和单调队列
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
// //优先队列
// int n = nums.length;
// if(n == 0){
// return new int[0];
// }
// PriorityQueue<int[]> queue = new PriorityQueue<>(new Comparator<int[]>(){
// public int compare(int[] point1, int[] point2){
// return point1[0] != point2[0] ? point2[0] - point1[0] : point2[1] - point1[1];
// }
// });
// for(int i=0; i<k; i++){
// queue.offer(new int[]{nums[i], i});
// }
// int[] res = new int[n-k+1];
// res[0] = queue.peek()[0];
// for(int i=k; i<n; i++){
// queue.offer(new int[]{nums[i], i});
// while(queue.peek()[1] <= i-k){
// queue.poll();
// }
// res[i-k+1] = queue.peek()[0];
// }
// return res;
//单调队列
int n = nums.length;
if(n == 0 || k == 0){
return new int[0];
}
Deque<Integer> queue = new LinkedList<>();
int[] res = new int[n-k+1];
for(int right=0, left=1-k; right<n; left++, right++){
if(left > 0 && queue.peekFirst() == nums[left-1]){
queue.pollFirst();
}
while(!queue.isEmpty() && nums[right] > queue.peekLast()){
queue.pollLast();
}
queue.offerLast(nums[right]);
if(left >= 0){
res[left] = queue.peekFirst();
}
}
return res;
}
}
(2)可变窗口大小
对于可变窗口,我们同样固定初始化左右指针 l 和 r,分别表示的窗口的左右顶点。后面有所不同,我们需要保证:
1、 l 和 r 都初始化为 0
2、 r 指针移动一步
3、判断窗口内的连续元素是否满足题目限定的条件
3.1 如果满足,再判断是否需要更新最优解,如果需要则更新最优解。
并尝试通过移动 l 指针缩小窗口大小。循环执行 3.1
3.2 如果不满足,则继续。
2.3.2 长度最小的子数组(209)
思路:
- 开始 right 向右滑动,使和变大。
- 当恰好 >=s 时,记录滑动窗口所包括的子数组长度 res,若 res 已有数值,需判断新值是否小于旧值,若是,更新 res; left 向右滑动。
- 判断是否仍 >=s,若是,重复步骤 2, 3。若否,转步骤 1。直到右边框到达最右边 。
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
# 初始化
left, length = 0, len(nums) + 1
# 右指针右移
for right in range(len(nums)):
while left <= right:
# 求和
sums = sum(nums[left:right+1])
if sums >= target:
length = min(length, right - left + 1)
left += 1
else:
break
# 通过length是否改变来判断是否存在符合的数组
if length == len(nums) + 1:
return 0
else:
return length
执行时间太长了,应该是算了很多次sums = sum(nums[left:right+1])的原因,现在把求和改一下:
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
# 初始化
left, length, sums = 0, len(nums) + 1, 0
# 右指针右移
for right in range(len(nums)):
# 和加上右指针新指向的元素
sums += nums[right]
# sums大于等于target, 更新length,并将左指针右移
while sums >= target:
length = min(length, right - left + 1)
sums -= nums[left]
left += 1
# 根据length是否改变来判断是否存在符合的数组
if length == len(nums) + 1:
return 0
else:
return length
class Solution {
public int minSubArrayLen(int target, int[] nums) {
if(nums[0] >= target){
return 1;
}
int n = nums.length;
int left = 0;
int right = 0;
int res = 100001;
int sum = nums[left];
while(right < n){
if(sum >= target){
res = res < right - left + 1 ? res : right - left + 1;
sum -= nums[left];
left++;
}else{
right++;
if(right < n){
sum += nums[right];
}
}
}
if(res == 100001){
return 0;
}
return res;
}
}
2.3.3 ⭐⭐乘积小于 K 的子数组(713)
思路:
- 当 left <= right 且滑动窗口内的乘积小于 k 时,我们可以知道 [left,right]、[left+1,right]…[right-1,right] 均满足条件,因此,计数加 right-left+1,然后移动右边界(滑动区间加大),看剩下的区间是否满足乘积小于 k,如果小于 k,重复步骤 1,否则进行步骤 2。
- 当滑动窗口内的乘积大于等于 k 时,右移左边界(滑动区间减小),如果这个区间内乘积小于 k,进入步骤 1,否则重复步骤 2。
class Solution:
def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
# 初始化
left, count, multiply = 0, 0, 1
# 右指针右移
for right in range(len(nums)):
multiply *= nums[right]
while left <= right and multiply >= k:
multiply /= nums[left]
left += 1
count += right - left + 1
return count
class Solution {
public int numSubarrayProductLessThanK(int[] nums, int k) {
int n = nums.length;
int left = 0;
int count = 0;
int mul = 1;
for(int right = 0; right < n; right++){
mul *= nums[right];
while(mul >= k && left <= right){
mul /= nums[left];
left++;
}
count += right - left + 1;
}
return count;
}
}
2.4 分离双指针
输入是两个数组 / 链表,两个指针分别在两个容器中移动;根据问题的不同,初始位置可能都在头部,或者都在尾部,或一头一尾。
2.4.1 两个数组的交集(350)
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
# 先进行排序
nums1.sort()
nums2.sort()
# 初始化
one, two = 0, 0
res = []
# 开始遍历
while one < len(nums1) and two < len(nums2):
if nums1[one] == nums2[two]:
res.append(nums1[one])
one += 1
two += 1
elif nums1[one] > nums2[two]:
two += 1
else:
one += 1
return res
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
int length1 = nums1.length;
int length2 = nums2.length;
int[] res = new int[Math.min(length1, length2)];
int head1 = 0;
int head2 = 0;
int index = 0;
while(head1 < length1 && head2 < length2){
if(nums1[head1] == nums2[head2]){
res[index] = nums1[head1];
index++;
head1++;
head2++;
}else if(nums1[head1] > nums2[head2]){
head2++;
}else{
head1++;
}
}
return Arrays.copyOfRange(res, 0, index);
}
}
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
int length1 = nums1.length;
int length2 = nums2.length;
Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
int[] res = new int[Math.min(length1, length2)];
int index = 0;
int count = 0;
for(int i=0; i<length1; i++){
count = hashtable.getOrDefault(nums1[i], 0) + 1;
hashtable.put(nums1[i], count);
}
for(int i=0; i<length2; i++){
count = hashtable.getOrDefault(nums2[i], 0);
if(count > 0){
res[index++] = nums2[i];
count--;
}
if(count > 0){
hashtable.put(nums2[i], count);
}else{
hashtable.remove(nums2[i]);
}
}
return Arrays.copyOfRange(res, 0, index);
}
}
2.4.2 两个数组的交集(349)
思路:
和上一题一样,只需要把存储的容器从列表改为集合即可。
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
# 对数组进行排序
nums1.sort()
nums2.sort()
# 初始化双指针
i, j = 0, 0
res = set()
while i < len(nums1) and j < len(nums2):
if nums1[i] == nums2[j]:
res.add(nums1[i])
i += 1
j += 1
elif nums1[i] < nums2[j]:
i += 1
else:
j += 1
return list(res)
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
int length1 = nums1.length;
int length2 = nums2.length;
int h1 = 0;
int h2 = 0;
int[] res = new int[Math.min(length1, length2)];
int index = 0;
while(h1 < length1 && h2 < length2){
if(nums1[h1] == nums2[h2]){
res[index++] = nums1[h1];
h1++;
h2++;
//去重
while(h1 < length1 && nums1[h1] == nums1[h1-1]){
h1++;
}
while(h2 < length2 && nums2[h2] == nums2[h2-1]){
h2++;
}
}else if(nums1[h1] > nums2[h2]){
h2++;
}else{
h1++;
}
}
return Arrays.copyOfRange(res, 0, index);
}
}