双指针算法:同向双指针模板以及相关面试题

Posted BudingCode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了双指针算法:同向双指针模板以及相关面试题相关的知识,希望对你有一定的参考价值。

双指针算法之同向双指针

基础概念以及模板

同向双指针
两根指针都从头出发,朝着同一个方向移动

先来一道例题说明同向双指针的应用场景和优点:

例题:两数之差问题

给定一个排序后的整数数组,找到两个数的 差 等于目标值。
你需要返回一个包含两个数字的列表[num1, num2], 使得num1与num2的差为target,同时num1必须小于num2。
注意:要求用O(1)空间复杂度完成

因为要求O(1)空间复杂度完成,所以我们不能使用哈希表

注意:当不能使用哈希表时,可以在排序数据集上进行二分来替代

不能使用哈希表的情况比如数据集很大或者题目要求不适用额外空间

使用二分的做法:

public class Solution 
    /**
     * @param nums: an array of Integer
     * @param target: an integer
     * @return: [num1, num2] (num1 < num2)
     */
    public int[] twoSum7(int[] nums, int target) 
        // write your code here
        if(nums == null || nums.length < 2)
            return new int[] -1,-1;
        
         
        target = Math.abs(target);
        for(int i = 0; i < nums.length; i++)
            int j = binaraySearch(nums, i + 1, nums.length -1, target + nums[i]);
            if(j != -1)
                return new int[]nums[i], nums[j];
            
        
        return new int[] -1,-1;
    
    
    private int binaraySearch(int[]nums, int left, int right, int target)
        while(left  <= right)
            int mid = left + (right - left)/2;
            if(nums[mid] == target)
                return mid;
            else if(nums[mid] > target)
                right = mid - 1;
            else
                left = mid + 1;
            
        
        return -1;
    

那我们分析这个方法的时间复杂度是多少呢?
二分查找 时间复杂度 O(log n)
for循环 时间复杂度 O(n)
因此本方法的时间复杂度为 --> O(n log n),是否还有优化的方法?

使用同向双指针的算法

public class Solution 
    /**
     * @param nums: an array of Integer
     * @param target: an integer
     * @return: [num1, num2] (num1 < num2)
     */
    public int[] twoSum7(int[] nums, int target) 
        // write your code here
        if(nums == null || nums.length < 2)
            return new int[]-1,-1;
        
        
        target = Math.abs(target);
        int j = 1;
        for(int i = 0; i < nums.length - 1; i++)
        	//确保 j 在 i 的 前面
            j = Math.max(j, i+1);
            while(j < nums.length && nums[j] - nums[i] < target)
                j++;
            
            if(j > nums.length)
                break;
            
            if(nums[j] - nums[i] == target)
                return new int[]nums[i], nums[j];
            
        
        return new int[]-1,-1;
    

因为数组是已经有序的,因此我们只需要O(n)的时间即可解决这个问题。

为何需要target = Math.abs(target) ?

如果不将target置为其绝对值,会出现无法获得target的情况。因为我们的双指针移动过程中判断的都nums[j] - nums[i] 是否等于target。而如果j > i,那么nums[j] >= nums[i],nums[j] - nums[i] 一定不与target相等,因此会找不到解。

双指针模板

//双指针模板
int j = 0 or int j = 1;
for(int i = 0; i < nums.length - 1; i++)
	while(j < nums.length)
		j ++;
	
	if(i, j 的搭配满足条件)
		处理 i, j的这次搭配;
	

相关例题

全零子串的个数

全零子串的个数
给出一个只包含0或1的字符串str,请返回这个字符串中全为0的子字符串的个数

例:
输入:"00010011"
输出:9
解释:
"0"子字符串有5,
"00"子字符串有3,
"000"子字符串有1个。
所以返回9

思路:
我们考虑第一个0位起点,然后后面有几个连续的0,就可以有几个终点。比如"00000",以第一个0开始,就有5个子串"00000",“0000”,“000”,“00”,“0”

Code:

public class Solution 
    /**
     * @param str: the string
     * @return: the number of substrings 
     */
    public int stringCount(String str) 
        // Write your code here.
        if(str == null)
            return -1;
        
        int j = 1, ans = 0;
        for(int i = 0; i < str.length(); i++)
            if(str.charAt(i) != '0')
                continue;
            

            j = Math.max(j, i+1);
            while(j < str.length() && str.charAt(j) == '0')
                j ++;
            
            ans += j - i;
        
        return ans;
    

去除重复元素

去除重复元素
描述
给一个整数数组,去除重复的元素。

例子:

输入:
nums = [1,3,1,4,4,2]
输出:
[1,3,4,2,?,?]
4

解释:
1. 将重复的整数移动到 nums 的尾部 => nums = [1,3,4,2,?,?].
2. 返回 nums 中唯一整数的数量  => 4.
事实上我们并不关心你把什么放在了 ?, 只关心没有重复整数的部分.

注意:
1.在原数组上操作
2.将去除重复之后的元素放在数组的开头
3.返回去除重复元素之后的元素个数

Code:

public class Solution 
    /**
     * @param nums: an array of integers
     * @return: the number of unique integers
     */
    public int deduplication(int[] nums) 
        // write your code here
        if(nums == null || nums.length == 0)
            return 0;
        
        Arrays.sort(nums);
        
        int j = 1, i = 0;
        for(i = 0; i < nums.length; i++)
            j = Math.max(i + 1, j);
            while(j < nums.length && nums[j] == nums[i])
                j++;
            
            if(j >= nums.length)
                break;
            
            nums[i+1] = nums[j];
        
        return i + 1;
    

最多有k个不同字符的最长子字符串

最多有k个不同字符的最长子字符串

描述
给定字符串S,找到最多有k个不同字符的最长子串T。

样例

样例 1:
输入: S = "eceba" 并且 k = 3
输出: 4
解释: T = "eceb"

样例 2:
输入: S = "WORLD" 并且 k = 4
输出: 4
解释: T = "WORL""ORLD"
public class Solution 
    /**
     * @param s: A string
     * @param k: An integer
     * @return: An integer
     */
    public int lengthOfLongestSubstringKDistinct(String s, int k) 
        
        if (s.length() == 0 || k == 0) 
            return 0;
        
        
        int left = 0, right = 0, cnt = 0;
        int charSet[] = new int[256];
        int ans = 0;
        
        while (right < s.length()) 
            // 统计right指向的字符
            // 当字符在窗口内第一次出现时,字符种类数+1,该字符出现次数+1
            if (charSet[s.charAt(right)] == 0) 
                cnt++;
            
            charSet[s.charAt(right)]++;
            right++;
            
            // 向右移动left,保持窗口内只有k种不同的字符
            while (cnt > k) 
                charSet[s.charAt(left)]--;
                // 当该字符在本窗口不再出现时,字符种类数-1
                if (charSet[s.charAt(left)] == 0) 
                    cnt--;
                
                left++;
            
            
            // 更新答案
            ans = Math.max(ans, right - left);
        
        return ans;
    


滑动窗口求和

描述:
给你一个大小为n的整型数组和一个大小为k的滑动窗口,将滑动窗口从头移到尾,输出从开始到结束每一个时刻滑动窗口内的数的和。

样例 

输入:array = [1,2,7,8,5], k = 3
输出:[10,17,20]
解析:
1 + 2 + 7 = 10
2 + 7 + 8 = 17
7 + 8 + 5 = 20

Code:

public class Solution 
    /**
     * @param nums: a list of integers.
     * @param k: length of window.
     * @return: the sum of the element inside the window at each moving.
     */
    public int[] winSum(int[] nums, int k) 
        // write your code here
        if(nums == null || nums.length == 0)
            return new int[];
        
        if(k == 0)
            return new int[nums.length];
        
        
        int[] results = new int[nums.length - k + 1];
        int j = 0, sum = 0;
        for(int i = 0; i < nums.length; i++)
            while(j - i < k && j < nums.length)
                sum += nums[j];
                j ++;
                
            
            if(j - i == k)
                results[i] = sum;
            
            sum -= nums[i];
        
        return results;
        
    

可见,万变不离其中,只要我们用好模版,同向双指针的问题就会变得非常简单。在字符串中要注意的时候就是要好好考虑双指针代表的区间,以及有效的部分的子串数量、长度等边界条件。
接下来我们来看一下另一个会运用到同向双指针的数据结构 —— 链表。

链表类问题

链表的中点

问题描述
求一个链表的中点

问题分析
你可能的想法是:

先遍历一下整个链表,求出长度 L,然后再遍历一下链表找到第 L/2 的那个位置的节点。

但是在你抛出这个想法之后,面试官会追问你:如果只允许遍历链表一次怎么办?

可以看到这种 Follow up 并不是让你优化算法的时间复杂度,而是严格的限制了你遍历整个链表的次数。你可能会认为,这种优化有意义么?事实上是很有意义的。因为遍历一次这种场景,在真实的工程环境中会经常遇到,也就是我们常说的数据流问题(Data Stream Problem)。

数据流问题 Data Stream Problem
所谓的数据流问题,就是说,你需要设计一个在线系统,这个系统不断的接受一些数据,并维护这些数据的一些信息。比如这个问题就是在数据流中维护中点在哪儿。

Code:

ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) 
    slow = slow.next;
    fast = fast.next.next;


return slow;

环形链表

环形链表
描述
给定一个链表,判断它是否有环

Code:

public class Solution 
    /**
     * @param head: The first node of linked list.
     * @return: True if it has a cycle, or false
     */
    public boolean hasCycle(ListNode head) 
        // write your code here
        if(head == null)
            return false;
        
        ListNode slow = head;
        ListNode fast = head.next;
        
        while(fast != null && fast.next != null)
            if(slow == fast)
                return true;
            
            fast = fast.next.next;
            slow = slow.next;
        
        return false;
    

相交链表

相交链表
描述
请写一个程序,找到两个单链表最开始的交叉节点。

public class Solution 
    /**
     * @param headA: the first list
     * @param headB: the second list
     * @return: a ListNode
     */
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) 
        // write your code here
     
        ListNode curA = headA;
        ListNode curB = headB;

        while (curA != curB) 
            curA = curA != null ? curA.next : headB;
            curB = curB != null ? curB.next : headA;
        
        return curA;
    

以上是关于双指针算法:同向双指针模板以及相关面试题的主要内容,如果未能解决你的问题,请参考以下文章

算法模板-双指针

基础算法一:同向双指针

JS 双指针解决三数四数之和

双指针

两根指针

双指针算法总结