作者:whisper
链接:http://proprogrammar.com:443/article/225
声明:请尊重原作者的劳动,如需转载请注明出处
完成前面的章节后,我们现在已经熟悉了数组和字符串的概念,并且能够在数组或字符串中执行基本操作。
通过执行这些操作,我们可以解决一些基本问题。但这显然是不够的。
本章节中,我们将探讨双指针技巧,它可以帮助我们解决许多与数组/字符串相关的问题。
在前一章中,我们通过迭代数组来解决一些问题。通常,我们只使用从第一个元素开始并在最后一个元素结束的一个指针来进行迭代。 但是,有时候,我们可能需要同时使用两个指针来进行迭代。
示例
让我们从一个经典问题开始:
反转数组中的元素。
其思想是将第一个元素与末尾进行交换,再向前移动到下一个元素,并不断地交换,直到它到达中间位置。
我们可以同时使用两个指针来完成迭代:一个从第一个元素开始,另一个从最后一个元素开始。持续交换它们所指向的元素,直到这两个指针相遇。
以下代码可以供你参考:
public static void reverse(int[] v, int N) {
int i = 0;
int j = N - 1;
while (i < j) {
swap(v, i, j); // this is a self-defined function
i++;
j--;
}
}
总结
总之,使用双指针技巧的典型场景之一是你想要
从两端向中间迭代数组。
这时你可以使用双指针技巧:
一个指针从始端开始,而另一个指针从末端开始。
值得注意的是,这种技巧经常在排序数组中使用。
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
示例 1:
输入:["h","e","l","l","o"] 输出:["o","l","l","e","h"]
示例 2:
输入:["H","a","n","n","a","h"] 输出:["h","a","n","n","a","H"]
// 想法:获取两端并交换,然后逐渐向中间逼近
public void reverseString(char[] s) {
int i = 0, j = s.length - 1;
char temp;
while(i < j){
temp = s[i];
s[i] = s[j];
s[j] = temp;
i++;
j--;
}
}
很简单了,但还有优化的空间,最后的两句自增自减可以放到前面操作中
给定长度为 2n 的数组, 你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), ..., (an, bn) ,使得从1 到 n 的 min(ai, bi) 总和最大。
示例 1:
输入: [1,4,3,2] 输出: 4 解释: n 等于 2, 最大总和为 4 = min(1, 2) + min(3, 4).
提示:
- n 是正整数,范围在 [1, 10000].
- 数组中的元素范围在 [-10000, 10000].
// 想法:先排序,再按顺序两两分组取元素即可
public int arrayPairSum(int[] nums) {
int result = 0;
Arrays.sort(nums);
for(int i = 0; i < nums.length; i+=2){
result += nums[i];
}
return result;
}
再看一种比较有趣的算法
public int arrayPairSum(int[] nums) {
int[] temp=new int[20001];
for(int i=0;i<nums.length;i++){
temp[nums[i]+10000]++;
}
int change=0;
int sum=0;
for(int i=0;i<temp.length;i++){
if(temp[i]!=0){
if(change==0){
sum+=i-10000;
change=1;
}else{
change=0;
}
temp[i]--;
i--; //处理重复
}
}
return sum;
}
定义了一个很大的数组,然后就是一个变相的排序,给原数组中的数在新数组中定义一个标记1,数越大标记越靠后(变相排序),然后与我的方法类似相邻的两个数取一个数,定义很大的数组的目的是存放原数组中的所有数(原数组中的数小于10000),通过元素不为0把索引转化为原数组中的数字,比较巧妙
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
- 返回的下标值(index1 和 index2)不是从零开始的。
- 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:
输入: numbers = [2, 7, 11, 15], target = 9 输出: [1,2] 解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
// 想法:因为有序,所以从两端开始,如果两端和大于目标,大端减小,如果两端和小于目标,小端增加,直到找到结果
public int[] twoSum(int[] numbers, int target) {
int little = 1;
int large = numbers.length;
while(numbers[little-1] + numbers[large-1] != target)
{
if(numbers[little-1] + numbers[large-1] > target)
{
large--;
}
else
{
little++;
}
}
return new int[]{little, large};
}
双端向中间逼近,算法已经很简洁了
有时,我们可以使用两个不同步的指针来解决问题。
示例
让我们从另一个经典问题开始:
给定一个数组和一个值,原地删除该值的所有实例并返回新的长度。
如果我们没有空间复杂度上的限制,那就更容易了。我们可以初始化一个新的数组来存储答案。如果元素不等于给定的目标值,则迭代原始数组并将元素添加到新的数组中。
实际上,它相当于使用了两个指针,一个用于原始数组的迭代,另一个总是指向新数组的最后一个位置。
重新考虑空间限制
现在让我们重新考虑空间受到限制的情况。
我们可以采用类似的策略,我们继续使用两个指针:一个仍然用于迭代,而第二个指针总是指向下一次添加的位置。
以下代码可以供你参考:
public int removeElement(int[] nums, int val) {
int k = 0;
for (int i = 0; i < nums.length; ++i) {
if (nums[i] != val) {
nums[k] = nums[i];
k++;
}
}
return k;
}
在上面的例子中,我们使用两个指针,一个快指针 i 和一个慢指针 k 。i 每次移动一步,而 k 只在添加新的被需要的值时才移动一步。
总结
这是你需要使用双指针技巧的一种非常常见的情况:
同时有一个慢指针和一个快指针。
解决这类问题的关键是
确定两个指针的移动策略。
与前一个场景类似,你有时可能需要在使用双指针技巧之前对数组进行排序,也可能需要运用贪心想法来决定你的运动策略。
给定一个数组 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。 注意这五个元素可为任意顺序。 你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝 int len = removeElement(nums, val); // 在函数里修改输入数组对于调用者是可见的。 // 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。 for (int i = 0; i < len; i++) { print(nums[i]); }
// 想法:用移动零的算法,不同的是现在是移动val
public int removeElement(int[] nums, int val) {
int curIndex = 0;
int actIndex = 0;
for(actIndex = 0; actIndex < nums.length; actIndex++)
{
if(nums[actIndex] != val)
{
nums[curIndex++] = nums[actIndex];
}
}
return curIndex;
}
上面说的情况,快慢指针删除元素
给定一个二进制数组, 计算其中最大连续1的个数。
示例 1:
输入: [1,1,0,1,1,1] 输出: 3 解释: 开头的两位和最后的三位都是连续1,所以最大连续1的个数是 3.
注意:
- 输入的数组只包含 0 和1。
- 输入数组的长度是正整数,且不超过 10,000。
// 两个指针,一个指向第一次出现1,一个指向最后一个1,两者之差即为长度
public int findMaxConsecutiveOnes(int[] nums) {
int slow = -1, fast = -2, max = 0, temp;
for(int i = 0; i < nums.length; i++){
if(nums[i] == 1){
if(slow == -1){
slow = i;
fast = i;
}else{
fast = i;
}
}else{
temp = fast - slow + 1;
if(max < temp){
max = temp;
}
slow = -1;
fast = -2;
}
}
temp = fast - slow + 1;
max = temp > max ? temp : max;
return max;
}
slow初始-1和fast初始-2有点技巧,所有操作统一了起来
看另一种算法
public int findMaxConsecutiveOnes(int[] nums) {
int res = 0;
int tmpLen = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
tmpLen++;
} else {
if (tmpLen > res) {
res = tmpLen;
}
tmpLen = 0;
}
}
if (tmpLen > res) {
res = tmpLen;
}
return res;
}
这种算法更直接易想到,tmpLen每出现1自加,出现0重置为0,返回最大的tmpLen,没有用到双指针,更加简单
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。
示例:
输入: s = 7, nums = [2,3,1,2,4,3] 输出: 2 解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
进阶:
如果你已经完成了O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
这个算法在以前的文章中说过,文章地址:leetcode探索之数组类算法学习 双索引技巧 - 滑动窗口
亲爱的读者:有时间可以点赞评论一下
全部评论