算法技巧总结(一)双指针

22 篇文章 0 订阅
1 篇文章 0 订阅
  • 序言
    • 最近刷算法题时发现有的题目要求是:不要使用额外数组空间,必须在原地修改输入数组,并在使用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;
    }
};


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值