- 序言:
- 最近刷算法题时发现有的题目要求是:不要使用额外数组空间,必须在原地修改输入数组,并在使用O(1)额外的条件完成。
- 这对于常规的采用循环甚至双循环的方法来说空间开销就很大,而且题设要求不能开辟更多的空间,只能再原来的内存空间进行修改。因此,双指针法(双下标法)的用处就体现出来了。
- 那什么是双指针算法呢???
- 双指针算法其实就是初始化两个指针,一个指向数组的首位置元素,另外一个指向数组的末尾的位置元素,然后根据自定义需求条件进行两指针的移动,最后是找到两个满足条件的数或者不存在这样的两个数字,其中在两个指针相遇之前,指针A只能向右移动,指针B只能向左移动。
- 双指针法主要应用在哪呢???
- 双指针主要分为两大类,一类是快慢指针,另一类时左右指针,前者主要是解决链表问题等,后者主要解决是数组问题或字符串问题等。
双指针技巧与应用
一、快慢指针常见算法
- 快慢指针一般都初始化指向链表的头结点 head,前进时快指针 fast 在前,慢指针 slow 在后,巧妙解决一些链表中的问题。
(1)判定链表中是否含有环
- 单链表的特点是每个节点只知道下一个节点,所以一个指针的话无法判断链表中是否含有环的。
- 如果链表中不含环,那么这个指针最终会遇到空指针 null 表示链表到头了,这还好说,可以判断该链表不含环。
public boolean hasCycle(ListNode head) {
while (head != null){
head = head.next;
}
return false;
}
- 但是如果链表中含有环,那么这个指针就会陷入死循环,因为环形数组中没有 null 指针作为尾部节点。
- 经典解法就是用两个指针,一个每次前进两步,一个每次前进一步。如果不含有环,跑得快的那个指针最终会遇到
null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表中有环。
public boolean hasCycle(ListNode head){
ListNode fast, slow;
fast = slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
return true;
}
}
return false;
}
(2)已知链表中含有环,返回这个环的起始位置
public static ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
ListNode index1 = fast;
ListNode index2 = head;
while (index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
return index2;
}
}
return null;
}
(3)寻找链表的中点
- 类似上面的思路,我们还可以让快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。
public static ListNode findMid(ListNode head) {
ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;// slow 就在中间位置
}
- 当链表的长度是奇数时,slow 恰巧停在中点位置;如果长度是偶数,slow 最终的位置是中间偏右。
- 寻找链表中点的一个重要作用是对链表进行归并排序。
- 回想数组的归并排序:求中点索引递归地把数组二分,最后合并两个有序数组。对于链表,合并两个有序链表是很简单的,难点就在于二分。
- 但是现在你学会了找到链表的中点,就能实现链表的二分了。
(4)寻找链表的倒数第 k 个元素
- 我们的思路还是使用快慢指针,让快指针先走 k 步,然后快慢指针开始同速前进。这样当快指针走到链表末尾 null
时,慢指针所在的位置就是倒数第 k 个链表节点(为了简化,假设 k 不会超过链表长度):
public static ListNode findK(ListNode head, int k) {
ListNode slow, fast;
slow = fast = head;
while (k-- > 0){
fast = fast.next;
}
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
(5)应用:删除链表的倒数第n个元素
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode slow = head;
ListNode fast = head;
for(int i = 0; i < n; i++){
fast = fast.next;
}
if(fast == null){// 如果此时快指针走到头了,说明倒数第 n 个节点就是第一个结点
return head.next;
}
while(fast != null && fast.next != null){// 让慢指针和快指针同步向前
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;// slow.next 就是倒数第 n 个节点,删除它
return head;
}
}
二、左右指针的常用算法
- 左右指针在数组中实际是指两个索引值,一般初始化为 left = 0, right = nums.length - 1 。
(1)二分查找
- 个人觉得二分查找比较重要的一点就是:当移动值大于目标值时要左移,当移动值小于目标值时要右移。
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
return -1;
}
1、寻找一个数(基本的二分搜索)
int binary_search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// 直接返回
return mid;
}
}
// 直接返回
return -1;
}
2、寻找左侧边界的二分搜索
int testLeft1(vector<int>&nums,int target){
int left=0,right=nums.size();
while(left<right){
int mid=right+left>>1;
if(nums[mid]==target){
right=mid;
}else if(nums[mid]<target){
left=mid+1;
}else if(nums[mid]>target){
right=mid;
}
}
if(left==nums.size()) return -1;
return nums[left]==target?left:-1;
}
int testLeft2(vector<int>&nums,int target){
int left=0,right=nums.size()-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==target){
right=mid-1;
}else if(nums[left]<target){
left=mid+1;
}else if(nums[mid]>target){
right=mid-1;
}
}
if(left>=nums.size()+1||nums[left]!=target) return -1;
return left;
}
3、寻找右侧边界的二分查找
/*right*/
int testRight1(vector<int>&nums,int target){
int left=0,right=nums.size();
while(left<right){
int mid=left+right>>1;
if(nums[mid]==target){
left=mid+1;
}else if(nums[mid]>target){
right=mid;
}else if(nums[mid]<target){
left=mid+1;
}
}
if(left==0) return -1;
return nums[left-1]==target?left-1:-1;
}
/*right*/
int testRight2(vector<int>&nums,int target){
int left=0,right=nums.size()-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==target){
left=mid+1;
}else if(nums[mid]>target){
right=mid-1;
}else if(nums[mid]<target){
left=mid+1;
}
}
if(right<0||nums[right]!=target) return -1;
return right;
}
(2)应用:两数之和
- 只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 可以调整 sum 的大小:
int twoSum(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left < right) {
int sum = nums[left] + nums[right];
if(sum == target)
return new int[]{left + 1, right + 1};
else if (sum < target)
left++;
else if (sum > target)
right--;
}
return -1;
}
(3)应用:反转数组
public void reverse(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
// swap(nums[left], nums[right])
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++; right--;
}
}
(4)应用:滑动窗口算法
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
- 其中两处…表示的更新窗口数据的地方,到时候直接往里面填就行了。而且,这两个…处的操作分别是右移和左移窗口更新操作,等会你会发现它们操作是完全对称的。
- 注:把索引左闭右开区间[left, right)称为一个窗口。
(5)应用:三数之和
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int n=nums.size();
sort(nums.begin(),nums.end());
vector<vector<int>>res;
for(int first=0;first<n;first++){
//防止相邻元素相同
if(first>0&&nums[first]==nums[first-1]) continue;
int third =n-1;
int target=-nums[first]; // * + * = - *
for(int second=first+1;second<n;second++){
//防止相邻元素相同
if(second>first+1&&nums[second]==nums[second-1]) continue;
//双指针
while(second<third&&nums[second]+nums[third]>target) third--;
//当双指针位移相同时
if(third==second)break;
//当双指针位移数组值相加等于目标值时
if(nums[second]+nums[third]==target){
res.push_back({nums[first],nums[second],nums[third]});
}
}
}
return res;
}
};
(6)应用:最接近的三数之和
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
/*
int n = nums.size(), minDiff = 100000000, ans, temp;
sort(nums.begin(), nums.end());
for(int i = 0; i < n; i++){
for(int j = i + 1, k = n - 1; j < k;){
temp = nums[i] + nums[j] + nums[k];
if(abs(temp - target) < minDiff){
minDiff = abs(temp - target);
ans = temp;
}
if(temp > target){
k--;
}
else if(temp < target){
j++;
}
else{
return ans;
}
}
}
return ans;*/
sort(nums.begin(), nums.end());
int res = nums[0] + nums[1] + nums[nums.size() - 1];
for (int i = 0; i < nums.size(); ++i)
{
int left = i+1, right = nums.size()-1;
while (left < right)
{
int sum = nums[i] + nums[left] + nums[right];
//相等的话最接近
if (target == sum) return sum;
//比较差的绝对值,取小的,表示更近
if (abs(target-res) > abs(target-sum)) res = sum;
//比目标值小,移动左指针//
if (target > sum) left ++;
else right --;
}
}
return res;
}
};
(7)应用:删除有序数组中的重复项
- 题目:
解题方法:
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if(nums.empty())return 0;
int slow=0,fast=1;
while(fast!=nums.size()){
if(nums[slow]!=nums[fast]){
slow++;
nums[slow]=nums[fast];
}
fast++;
}
return slow+1;
}
};
(8)应用:移除元素
- 题目:
解法方法:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int fast=0,slow=nums.size();
while(fast!=slow){
if(nums[fast]==val){
nums[fast]=nums[slow-1];
slow--;
}else{
fast++;
}
}
return fast;
}
};