两根指针,英文为two pointers ,所以又被称为双P算法。
同向双指针问题
1.window sum问题
例给定数组[1, 2, 7, 8, 5],给定一个长度为k的窗口,求此窗口内的数据元素的和。比如1 + 2 + 7 = 10, 2 + 7 + 8 = 17, 7 + 8 + 5 = 20.
对于这样的题目则是同向双指针问题,设定两个指针,一个指针指向window的前端(如上式中的7),一个指针指向window的尾端(如上式中的1),当求完此窗口内的数据元素的和后,window前后端的指针同时加一,就是同时向前移动一个位置,然后求区间内数据元素的和,这样就实现了window sum的问题。
2.移动数组中特定元素的问题
给定一个数组,将数组中为某个值的数据元素在保持数据元素相对位置保持不变的前提下移动到数组尾部。如,给定一个整型数组,将数组中的0移动到数组尾端并保持其它元素的相对位置不变。
例:nums = [0, 1, 0, 3, 12 ],经过移动后的数组为[1, 3, 12, 0, 0].
这是一个同向双指针的问题,首先定义两个指针i和j,两个指针都被初始化指向数组的第一个元素。然后分别定义两个指针的功能:指针i就指向第一个为零的数,指针j就从左向右的巡视。
1.若指针j所指的位置的元素为0,这保持指针i不变,指针j继续想前移动。
2.若指针j当前所指位置的元非零,则交换当前两个指针指向位置的元素的值。由于指针i指向的是第一个为0的元素,指针j通过移动指向了第一个为非零的元素。交换两个位置的元素(这里的交换也可以直接将指针j指向的元素对指针i指向的元素进行覆盖后将指针j指向的元素置为0,交换完成后需要将指针i向后移动一个位置),便实现了数组中零元素的向后移动。当指针j移动到数组的最后一个位置并完成交换时就实现了任务。
3.从一个整数数组中删除重复的元素,并且返回数组中数组中不重复的元素的个数。
如数组[1, 3, 1, 4, 4, 2],删除之后就成为[1, 3, 4, 2, ?, ?],删除之后不重复的元素个数为4。
解法:使用函数对数组元素进行排序,然后使用两根同向指针i和j,指针i指向某个数据元素,指针j则向后移动以便巡视后面的元素是否与指针i指向的元素重复。
4.快慢指针的方法。
相向双指针。
1.两个指针同时开始相向移动,并且移动的步长相同,可以找出中点。
2.反转字符串。定义两个指针i和j,指针i指向字符串的第一个元素,指针j指向字符串的最后一个元素,分别交换两个指针指向的元素,然后再将两个指针相向移动一个位置,进行后序的交换,指导最后完成字符串的反转。
Two Sum
1.给定一个排序数组,在数组中寻找两个数值元素使得这两个数值元素相加等于一个特定的数,twosunm函数应该返回这两个数在数组中的位置。
如数组[2. 7, 11, 15],target = 9,返回[1, 2]
解法一:使用哈希表来解决。从做到右依次进行for循环,将数组中每个元素都添加到一个哈希表中,在c++11中就是unordered_set对象中去。但是在把每个元素(这里记为num1)丢到哈希表中之前先看一下(target - num1)在不在已经添加的哈希表中。若在,则说明两个数相加能够形成target,则记录下num1在数组中的位置,然后查找另一个元素在数组中的位置。
解法二:使用两根指针。
这种算法能够实现加速的手段就是跳过了一些不可能是结果的值。
举例说明。一个排序数组[2, 3, 7, 11, 19, 21],target = 14。
1.首先定义两个指针L和R,其中L被初始化指向数组中第一个元素,指针R指向数组中最后一个元素。在本例中L和R分别指向2和21,将两个数据元素相加得到23,大于14。21是数组中最大的数,最大的数加上最小的数比目标值要大,那么就判定21不可能成为结果中的两个数之一。排除21,得到可能包含结果的数组[2, 3, 7, 11, 19]。
2.那么就向前移动指针R,减小其指向的元素值,指向19,得到2 + 19 = 21仍然大于14,将元素19也排除;得到可能包含结果的的数组[2, 3, 7, 11]。
3.继续想前移动指针R指向元素11,则得到2 + 11 = 13,小于14,那么目前成为结果的两个数的值只能是2,3,7,11中的两个,但是2 + 11 = 13小于14,那么最小的数即使加上最大的数也仍然小于target,那么此时最小值2就成为了不可能的元素值,排除2,此时得到可能包含结果的数组[3, 7, 11]。
4.向后移动指针L,L指向元素3,此时R指针指向11,此时L和R指向的元素相加得到target,返回位置。
而对于一个未排序的数组,若不要求两个数在数组中的位置,则可以先对这个数组进行排序,得到排序数组,然后进行两根指针的算法。此时使用哈希表的实现复杂度是O(n),空间复杂度也为O(n);若使用排序+两根指针的方法,时间复杂度为O(nlogn),但是空间复杂度为O(1)。但是若是要求对于无序数组也要返回相应元素的下标或者位置,那么就需要先建立一个数组,对原来的无需数组进行一个备份,这样就能在查找到相应的数num1和num2之后在备份的无序数组中找到相应元素的下标,此时空间复杂度就也成为了O(n)。
Two Sum ------ Unique pairs
找出所有的组合
上面的题目是在数组中找出一个组合数num1和num2,使得这两个数的和为target,那么现在的题目要求是找出所有的相加等于target的num1和num2。
那么现在的算法就是在原来算法的基础上,继续执行算法,并且记录下数量。
我们仍然以上述数组为例,但是在数组中添加了三个元素:4, 10成为[2, 3, 4,7,10,11,19,21].target仍设置为14.
前面的步骤继续执行,排除数据剩下[3, 4, 7, 10,11]时,我们的L指针指向3, R指针指向11,此时,两个指针指向的数据元素之和为14,此时为了能够求出所有的相加能够等于target的数据组合num1和num2,则同时移动L和R指针,就是同时排除数据元素3和11,剩下[4, 7, 10]. 4 + 10 = 14,所以继续同时移动两个指针,剩下一个7,跳出循环。
但是数组中往往存在重复值,比如[1, 1, 20, 26, 45, 45],target为46。开始时我们将L和R指针指向两端,得到46,移动指针排除这两个值,实际有意义的数组成为[1, 20, 26, 45],此时得到的结果仍然是1和45,与前面重复,但是前面要求的时unique pairs,那么怎么处理这个问题呢?
方法一:当L指针指在数据元素1的位置得到相应的结果时,将L指针一直向后移动,一直移动到数据元素不是1的位置,跳过所有的1, 比如20处,然后再进行下一步的循环。这个步骤对于R指针也一样。
在这个方法中值得注意的是要时时刻刻注意L、R指针是否交错,所以一定要时时刻刻进行判断
int left = 0; int right = nums.size() - 1; while(left < right) { if(nums[left] + nums[right] == target) { count ++; left ++; right --; while(left < right && nums[left] == nums[left - 1]) { left ++; } while(left < right &7 nums[right] == nums[right +1]) { right --; } } else if(nums[left] + nums[right] > target) { right --; } else{ left ++; } }
方法二,在得到target时,将本次循环得到的两个数num1和num2记录下来,当再次遇到这两个数的组合时直接continue。
那么能不能先在原始数组中去除重复元素再进行查找呢?可以,但是这里的去重不是单纯的去重,比如上述数组中若有两个数据元素7, 7。target仍为14,此时,若进行去重,则会造成结果错误。对于这种这种的方案就是,无论有多少个7,7 +7 = 14只有这么一种组合,我们先进行一次查找,看看数组中有没有多于等于两个7,若是多于等于两个7,则记一个1,然后进行去重,最后得到在得到的组合数上加1即可。
题目三:求三数之和等于target
假设:我们要求出三个数a, b, c使得a + b + c = target;那么相当于求出两个数a + b = target - c.
那么解决此题目的方法是先在数组中挑出一个数a,那么就相当于对剩下的数组元素求两个数之和等于target - a(这个过程和使用两根指针解决两数之和等于一个target的过程相似)。然后对这个数组进行for循环,依次取出数组中为a的值即可。由于a是最小的值,要先对数组进行排序,a的取值范围用下标来表示是[0, nums.size() - 2]。
对于这样的方法,哈希表当然能够来实现,就是先挑出一个数a,再挑出一个数b,最后进行求c,这样要进行两次for循环,那么时间复杂度就是O(n * n),但是若使用两根指针来实现的话,我们可以先对数组进行排序,这样操作就简单的多。
这里同样要考虑a值重复的情况,所以再更新a的过程中,要一直挪动指针到值不一样的地方。
在设置重复值跳过的时候,只要设置一端遇到重复值跳过即可。不需要两端都设置
class Solution { public: vector<vector<int> > threeSum(vector<int> &nums) { vector<vector<int> > result; //先进行排序,便于操作 sort(nums.begin(), nums.end()); for (int i = 0; i < nums.size(); i++) { if (i > 0 && nums[i] == nums[i - 1]) { continue; } // two sum;在跳过重复值的时候只要设置一端遇到重复值跳过即可
int start = i + 1, end = nums.size() - 1; int target = -nums[i]; while (start < end) { if (start > i + 1 && nums[start - 1] == nums[start]) { start++; continue; } if (nums[start] + nums[end] < target) { start++; } else if (nums[start] + nums[end] > target) { end--; } else { vector<int> triple; triple.push_back(nums[i]); triple.push_back(nums[start]); triple.push_back(nums[end]); result.push_back(triple); start++; } } } return result; } };
题目:给定一个数组,在数组中找出三个数使得这三个数能够构成一个合法的三角形,并求出组数。
三角形的重要条件就是两边之和大于第三边
假设a, b, c是三角形的三条边,并且有a <= b <= c,那么我们只要保证 a + b > c即可
这就仍然是一个两根指针的解法,和两数之和的解法很相似。首先对数组进行排序,便于进行操作。然后对c进行for循环。但在这里c是最大的数,所以要先对数组进行排序得到nums数1组,然后c的取值范围用排序后的数组下标来表示是[2, nums.size() - 1]。
但是与前面不同的是,这可以一次性移动很多个指针。