C++双指针技巧

来源:https://leetcode-cn.com/explore/learn/card/array-and-string/201/two-pointer-technique/782/
通常,我们只使用从第一个元素开始并在最后一个元素结束的一个指针来进行迭代。 但是,有时候,我们可能需要同时使用两个指针来进行迭代。

场景一

一、同步指针:从两端向中间迭代数组。

典型例子 反转数组中的元素

其思想是将第一个元素与末尾进行交换,再向前移动到下一个元素,并不断地交换,直到它到达中间位置。

我们可以同时使用两个指针来完成迭代:一个从第一个元素开始,另一个从最后一个元素开始。持续交换它们所指向的元素,直到这两个指针相遇。

void reverse(int *v, int N) {
    int i = 0;
    int j = N - 1;
    while (i < j) {
        swap(v[i], v[j]);
        i++;
        j--;
    }
}

场景一总结

这时你可以使用双指针技巧:
一个指针从始端开始,而另一个指针从末端开始。
值得注意的是,这种技巧经常在排序数组中使用。

场景二

有时,我们可以使用两个不同步的指针来解决问题。

示例

让我们从另一个经典问题开始:

给定一个数组和一个值,原地删除该值的所有实例并返回新的长度。

如果我们没有空间复杂度上的限制,那就更容易了。我们可以初始化一个新的数组来存储答案。如果元素不等于给定的目标值,则迭代原始数组并将元素添加到新的数组中。

实际上,它相当于使用了两个指针,一个用于原始数组的迭代,另一个总是指向新数组的最后一个位置。

重新考虑空间限制

现在让我们重新考虑空间受到限制的情况。

我们可以采用类似的策略,我们继续使用两个指针:一个仍然用于迭代,而第二个指针总是指向下一次添加的位置。

以下代码可以供你参考:

int removeElement(vector<int>& nums, int val) {
    int k = 0;
    for (int i = 0; i < nums.size(); ++i) {
        if (nums[i] != val) {
            nums[k] = nums[i];
            ++k;
        }
    }
    return k;
}

在上面的例子中,我们使用两个指针,一个快指针 i 和一个慢指针 k 。i 每次移动一步,而 k 只在添加新的被需要的值时才移动一步。

场景二总结

这是你需要使用双指针技巧的一种非常常见的情况:

同时有一个慢指针和一个快指针。
解决这类问题的关键是

确定两个指针的移动策略
与前一个场景类似,你有时可能需要在使用双指针技巧之前对数组进行排序,也可能需要运用贪心想法来决定你的运动策略。

场景一例题

反转字符串

Write a function that reverses a string. The input string is given as an array of characters char[].

Do not allocate extra space for another array, you must do this by modifying the input array in-place with O(1) extra memory.

You may assume all the characters consist of printable ascii characters.
Example 1:

Input: [“h”,“e”,“l”,“l”,“o”]
Output: [“o”,“l”,“l”,“e”,“h”]
Example 2:

Input: [“H”,“a”,“n”,“n”,“a”,“h”]
Output: [“h”,“a”,“n”,“n”,“a”,“H”]

思路一 内置函数法

class Solution {
public:
	void reverseString(vector<char>& s) {
		reverse(s.begin(), s.end());
	}
};

思路二

定义 swap函数,加快运算速度 和省内存。
class Solution {
public:
	void reverseString(vector<char>& s) {
		int M = 0;
		int N = s.size()-1;
		char tmp;
		while (M < N)
		{
			swap(s[M], s[N]);
			M++;
			N--;
		}
	}
	void swap(char& a, char& b) { char tmp = a; a = b; b = tmp; }
};

数组拆分 I
Given an array of 2n integers, your task is to group these integers into n pairs of integer, say (a1, b1), (a2, b2), …, (an, bn) which makes sum of min(ai, bi) for all i from 1 to n as large as possible.

Example 1:
Input: [1,4,3,2]

Output: 4
Explanation: n is 2, and the maximum sum of pairs is 4 = min(1, 2) + min(3, 4).
Note:
n is a positive integer, which is in the range of [1, 10000].
All the integers in the array will be in the range of [-10000, 10000].

排序后,相邻的两数之间组合。

class Solution {
public:
	int arrayPairSum(vector<int>& nums) {
		sort(nums.begin(), nums.end());
		for (int i = 1; i < nums.size(); i += 2)
			nums[i] = 0;
		return accumulate(nums.begin(),nums.end(),0);
	}
};

场景二例题

移除元素

Given an array nums and a value val, remove all instances of that value in-place and return the new length.

Do not allocate extra space for another array, you must do this by modifying the input array in-place with O(1) extra memory.

The order of elements can be changed. It doesn’t matter what you leave beyond the new length.

Example 1:

Given nums = [3,2,2,3], val = 3,

Your function should return length = 2, with the first two elements of nums being 2.

It doesn't matter what you leave beyond the returned length.

Example 2:

Given nums = [0,1,2,2,3,0,4,2], val = 2,

Your function should return length = 5, with the first five elements of nums containing 0, 1, 3, 0, and 4.

Note that the order of those five elements can be arbitrary.

It doesn't matter what values are set beyond the returned length.

Clarification:

Confused why the returned value is an integer but your answer is an array?

Note that the input array is passed in by reference, which means modification to the input array will be known to the caller as well.

Internally you can think of this:

// nums is passed in by reference. (i.e., without making a copy)
int len = removeElement(nums, val);

// any modification to nums in your function would be known by the caller.
// using the length returned by your function, it prints the first len elements.
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

思路:双指针,一指针遍历数组,一指针从尾部往前移动与删除数字交换。

class Solution {
public:
	int removeElement(vector<int>& nums, int val) {
		int k=0;
		if (nums.size() < 1)
			return 0;

		int m = nums.size() - 1;
		for (int i = 0; i <=m;)
			if (nums[i] != val)
			{
				k++;
				i++;
			}
			else
			{
				nums[i] = nums[m];
				m--;
			}
		return k;
	}
};

最大连续1的个数

给定一个二进制数组, 计算其中最大连续1的个数。

示例 1:

输入: [1,1,0,1,1,1]
输出: 3
解释: 开头的两位和最后的三位都是连续1,所以最大连续1的个数是 3.
注意:

输入的数组只包含 0 和1。
输入数组的长度是正整数,且不超过 10,000。

class Solution {
public:
	int findMaxConsecutiveOnes(vector<int>& nums) {
		int max = 0;
		int k = 0;
		for (int i = 0; i < nums.size(); i++)
			if (nums[i] == 1)
			{
				k++;
				if (k > max)
					max = k;
			}
			else
				k = 0;		
		return max;
	}
};

长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。

示例:

输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
进阶:

如果你已经完成了O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。

思路一(双指针)
一个指针子数组起始位置循环遍历,一个指针嵌套寻找子数组结尾位置,得到每个位置的最短子数组,记录最短长度实现>s的子数组.首先通过求和判断是否存在符合<s的子数组,复杂度为O(n^2) 408ms。

class Solution
{
public:
	int minSubArrayLen(int s, vector<int>& nums)
	{
		if (accumulate(nums.begin(), nums.end(),0)<s)
			return 0;
			
		int minlen = nums.size();
		for (int i = 0; i < nums.size(); i++)
		{
			int sum = 0;
			for (int j = i; j < nums.size(); j++)
			{	
				sum += nums[j];
				if (sum >= s)
				{
					if (j - i + 1 < minlen)
						minlen = j - i + 1;
					break;
				}
			}
			if (minlen <= 1)
				break;			
		}
		return minlen;
	}
};

改进思路一
对当前minlen来说,后续的任何检索的字串长度都小于minlen,长于minlen的不考虑。提升较小,仍未O(n^2) 420 ms

class Solution
{
public:
	int minSubArrayLen(int s, vector<int>& nums)
	{
		if (accumulate(nums.begin(), nums.end(), 0) < s)
			return 0;

		int minlen = nums.size();
		for (int i = 0; i < nums.size(); i++)
		{
			int sum = 0;
			for (int j = i; j-i < minlen&&j<nums.size(); j++)
			{
				sum += nums[j];
				if (sum >= s)
				{
					if (j - i + 1 < minlen)
						minlen = j - i + 1;
					break;
				}
			}
			if (minlen <= 1)
				break;
		}
		return minlen;
	}
};

改进思路二(滑动窗口)
思想:贪心算法,它可以帮助我们设计指针的移动策略,尽量不进行重复的加法,以一定的准则去移动指针。
实现方法:根据sum<s是否成立决定移动哪个指针。过程中记录minlen.
不移动重复的指针,因此复杂度O(n) 结果 8 ms。

指针移动方法(不包含minlen更新和判断):
while(true)
{
if sum>=s
移动左指针
更新sum
if sum < s
移动右指针,更新sum,至满足sum>s,若不能移了,则break;
}

class Solution
{
public:
	int minSubArrayLen(int s, vector<int>& nums)
	{
		if (accumulate(nums.begin(), nums.end(), 0) < s)
			return 0;

		int minlen = nums.size();
		int b=0, e;
		int sum = 0;
		for (int i = 0; i < nums.size(); i++)
		{
			sum += nums[i];
			if (sum >= s)
			{
				e = i;				
				break;
			}
		}
		minlen = e - b + 1;
		while (b!=nums.size()-1)
		{	
			sum -= nums[b++];
			if (sum >= s && e - b + 1 < minlen)
				minlen = e - b + 1;
			else if (sum < s)
			{
				if (++e > nums.size() - 1)
					break;
				sum += nums[e];
			}
				
		}

		return minlen;
	}
};

改进思路三
O(n log n) 时间复杂度,用二分。因为问题是找到合适的长度的子数组长度,因此只需对子数组的长度是否符合要求使用二分法。使用后若满足则缩小窗口,否则扩大窗口。判断长度是否符合要求的函数的复杂度为O(n),二分法复杂度为O(logn);

  • 4
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值