2、双指针算法学习及leetcode力扣网例题详解

双指针

概述

双指针是一种思想和技巧,并不是什么特别具体的算法。常用于线性的数据结构中,比如链表和数组,有时候也会用在图中。

双指针分为快慢指针和左右指针,其中快慢指针主要用于解决链表问题,而左右指针用于解决数组问题。

快慢指针

顾名思义,快慢指针是指一个指针走的快,一个指针走得慢。两个指针从同一侧开始遍历数组以不同的策略移动。

快慢指针一般都初始化指向链表的头结点 head,前进时快指针 fast 在前,慢指针 slow 在后。

有时候快慢指针也可以用来判定链表中是否含有环,如果链表中不包含环,那么这个指针最终会遇到空指针 null 表示链表到头了,标识链表不包含环;如果链表中含有环,那么快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。

左右指针

左右指针又称为碰撞指针,指双指针中一个指针在数组的最左侧,而另一个在最右侧,然后从两头向中间进行数组遍历。

一般都是排好序的数组或链表,否则无序的话这两个指针的位置也没有什么意义,常用于解决二分查找问题和n数之和的问题。

滑动窗口法

滑动窗口法是两个指针,一前一后组成滑动窗口,并计算滑动窗口中的元素的问题。

这类问题一般有字符串匹配问题和子数组问题。

例题

Two Sum

在一个增序的整数数组里找到两个数,使它们的和为给定值。已知有且只有一对解。

输入输出样例
Input: numbers = [2,7,11,15], target = 9 
Output: [1,2]
题解

因为数组已经排好序,因此已知用左右指针来实现。

初始时两个指针分别指向第一个元素位置和最后一个元素的位置。每次计算两个指针指向的两个元素之和,并和目标值比较。

如果两个元素之和等于目标值,则发现了唯一解。
如果两个元素之和小于目标值,则将左侧指针右移一位。
如果两个元素之和大于目标值,则将右侧指针左移一位。
移动指针之后,重复上述操作,直到找到答案。

代码实现
public class Client {
	public static void main(String[] args) {
		int[] numbers = new int[] {2,7,11,15};
		int target = 9;
		int[] sum = twoSum(numbers, target);
		System.out.println("["+sum[0]+","+sum[1]+"]");
	}
	
	public static int[] twoSum(int[] numbers, int target) {
		int left = 0, right = numbers.length-1, sum;
		while (left < right) {
			sum = numbers[left] + numbers[right]; //两数之和
			if (sum == target) break;
			if (sum < target) 
                //如果两数之和小于目标值,则使左指针向右移
                ++left;
			else 
                //如果两数之和大于目标值,则使右指针向左移
                --right;
		}
		return new int[] {left+1, right+1};
	}
}

归并两个有序数组

给定两个有序数组,把两个数组合并为一个。

输入输出样例

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。

初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。

Input: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 
Output: nums1 = [1,2,2,3,5,6]
题解

因为这两个数组已经排好序,可以用两个指针来定位两个数组。 如果把指针放在数组的前面,那么需要从头改变nums1的值,造成额外的空间开销,因此把指针放在数组后面,然后进行合并。

即 nums1 的 m−1位和nums2的 n−1位。每次将较大的那个数字复制到nums1的后边,然后向前移动一位。 因为我们也要定位nums1的末尾,所以我们还需要第三个指针,以便复制。

代码实现
public class Client {
	public static void main(String[] args) {
		int[] nums1 = new int[] {1,2,3,0,0,0};
		int m = 3;
		int[] nums2 = new int[] {2,5,6};
		int n = 3;
		merge(nums1, m, nums2, n);
		for (int i : nums1) {
			System.out.print(i+",");
		}
	}
	
	public static void merge(int[] nums1, int m, int[] nums2, int n) {
		int pos = m-- + n-- -1; //pos指向的是nums1数组的最后一个值的位置,给pos赋值后再执行m--和n--
		while (m>=0 && n>=0) { //向右遍历,当数组1或数组2的其中一个遍历完成则跳出循环
			//比较数组1和数组2当前指定的值,若数组1的大,则向nums[pos]赋值数组1的值
			nums1[pos--] = nums1[m]>nums2[n] ? nums1[m--] : nums2[n--];
		}
		while (n >= 0) {
			//如果是数组1先被遍历完,则把剩下的数组2的值放入数组1
			nums1[pos--] = nums2[n--];
		}
	}
}

快慢指针

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

输入输出样例

img

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
题解

对于链表找环路的问题,一般使用快慢指针的方法。

给定两个指针, 分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步,slow 前进一步。
如果 fast 可以走到尽头,那么说明没有环路;
如果fast可以无限走下去,那么说明一定有环路,且一定存在一个时刻slow和fast相遇。
当slow和fast第一次相遇时,我们将fast重新移动到链表开头,并让slow和fast每次都前进一步。
当slow和fast第二次相遇时,相遇的节点即为环路的开始点。

代码实现
public class Client {
	public static void main(String[] args) {
		ListNode node1 = new ListNode(3);
		ListNode node2 = new ListNode(2);
		ListNode node3 = new ListNode(0);
		ListNode node4 = new ListNode(-4);
		node1.next = node2;
		node2.next = node3;
		node3.next = node4;
		node4.next = node2;
		System.out.println(detectCycle(node1).val);
	}
	
	public static ListNode detectCycle(ListNode head){
		ListNode slow = head, fast = head;
		//判断是否存在回路
		do {
			if (fast==null || fast.next==null) {
				//如果fast能够走到尽头,那么说明没有环路
				//设置fast==null这个条件是防止一个空链表进行判断
				//设置fast.next==null这个条件是防止一个只有一个结点的链表进行判断
				return null;
			}
			//fast每次移动两个结点,slow每次移动一个结点
			fast = fast.next.next;
			slow = slow.next;
			//当fast和slow相遇,说明有环,则跳出循环
		} while (fast != slow);
		//如果存在,查找环路节点
		fast = head;
		while (fast != slow) {
			//当slow和fast第二次相遇时,相遇的节点即为环路的开始点
			slow = slow.next;
			fast = fast.next;
		}
		return fast;
	}
}

class ListNode{
	int val;
	ListNode next;
	public ListNode(int x) {
		val = x;
		next = null;
	}
}

滑动窗口

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “”

输入输出样例
Input: S = "ADOBECODEBANC", T = "ABC" 
    Output: "BANC"

在这个样例中,S 中同时包含一个A、一个B、一个C的最短子字符串是“BANC”。

题解

我们可以用双指针的思想解决这个问题。使用两个指针,其中 rrr 用于「延伸」现有窗口,lll 用于「收缩」现有窗口。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 sss 上使用双指针,通过移动 rrr 指针不断扩张窗口,当窗口包含 ttt 全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。

如何判断当前的窗口包含所有 ttt 所需的字符呢?我们可以用一个哈希表表示 ttt 中所有的字符以及它们的个数,用一个哈希表动态维护窗口中所有的字符以及它们的个数,如果这个动态表中包含 ttt 的哈希表中的所有字符,并且对应的个数都不小于 ttt 的哈希表中各个字符的个数,那么当前的窗口是「可行」的。

fig1

来源:力扣(LeetCode)

代码实现

在这里使用了长度为 128 的数组来映射字符,没有使用哈希表,不过也可以用哈希表替代

public class Client {
	public static void main(String[] args) {
		String s = "ADOBECODEBANC";
		String t = "ABC";
		System.out.println(minWindow(s, t));
	}
	
	public static String minWindow(String s, String t) {
		if (s == null || s == "" || t == null || t == "" || s.length() < t.length()) {
            return "";
        }
        //由于ASCII表总长128,因此声明的两个数组长度是128
        int[] need = new int[128]; //记录目标字符串指定字符的出现次数
        int[] have = new int[128]; //记录已有字符串指定字符的出现次数

        //将目标字符串指定字符的出现次数记录
        for (int i = 0; i < t.length(); i++) {
            need[t.charAt(i)]++;
        }

        //分别为左指针,右指针,窗口的长度的最小值(初值需要是更大的值)
        //当前滑动窗口已拥有目标字符串的字符个数,最小覆盖子串的起始位置
        int left = 0, right = 0, min = s.length() + 1, count = 0, start = 0;
        
        //主要思路为:
        //右指针移动到长字符串的最右侧为止
        //右指针一直移动,直到当前滑动窗口能够包含所有小字符串的子串,此时开始移动左指针
        //左指针一直移动,直到当前滑动窗口又不能包含所有小字符串的子串时,开始继续移动右指针
        //重复以上两个步骤。注意:当前滑动窗口能够包含所有小字符串的子串时,需要记录起始位置,即start值
        while (right < s.length()) {
            char r = s.charAt(right);
            if (need[r] == 0) {
                right++;
                continue;
            }
            //如果已有字符串目标字符出现的次数比目标字符串字符的出现次数小,则count+1
            if (have[r] < need[r]) {
                count++;
            }
            //已有字符串中目标字符出现的次数+1
            have[r]++;
            //移动右指针
            right++;
            //如果当前滑动窗口包含所有的小字符串,那么开始移动左指针
            while (count == t.length()) {
                //挡窗口的长度比已有的最短值小时,更改最小值,并记录起始位置
                if (right - left < min) {
                    min = right - left;
                    start = left;
                }
                char l = s.charAt(left);
                //如果左边即将要去掉的字符不被目标字符串需要,那么不需要多余判断,直接可以移动左指针
                if (need[l] == 0) {
                    left++;
                    continue;
                }
                //如果左边即将要去掉的字符被目标字符串需要,且出现的频次正好等于指定频次,那么如果去掉了这个字符,
                //就不满足覆盖子串的条件,此时要破坏循环条件跳出循环,即控制目标字符串指定字符的出现总频次(count)-1
                if (have[l] == need[l]) {
                    count--;
                }
                //已有字符串中目标字符出现的次数-1
                have[l]--;
                //移动左指针
                left++;
            }
        }
        //如果最小长度还为初始值,说明没有符合条件的子串
        if (min == s.length() + 1) {
            return "";
        }
        //返回的为以记录的起始位置为起点,记录的最短长度为距离的指定字符串中截取的子串
        return s.substring(start, start + min);
	}
}

本篇文章参考书籍有:
《LeetCode101:和你一起你轻松刷题(C++)》 高畅


作者:阿涛
CSDN博客主页:https://blog.csdn.net/qq_43313113
如有不对的地方,欢迎在评论区指正
欢迎大家关注我,我将持续更新更多的文章


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值