神奇的算法:双指针

神奇的算法:双指针

1、算法解释

​ 双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。

​ 若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的 区域即为当前的窗口),经常用于区间搜索。

​ 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。 如:翻转数组、二分搜索。

2、快慢指针

​ 快慢指针是双指针中的一种情况,利用两个指针移动速度的差异进行结题。

​ 快慢指针的前进方向相同,且它们步伐的「差」是恒定的,根据这种确定性去解决链表中的一些问题。使用这种思想还可以解决链表的以下问题:

  • 第19题:倒数第k个结点

  • 第141题:环形链表

  • 第876题:链表的中间结点

LeetCode19:倒数第k个结点

题目描述:

​ 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
在这里插入图片描述

题解:

  1. 创建指针fast和slow均指向head,首先要先找到快指针fast的位置,fast与slow之间间隔n-1个节点。
  2. 同时使用fast和slow对链表进行遍历,直到fast或者fast.next为空。
  3. 当fast==null时,找了倒数第n+1个节点,此时slow的下一个节点就是要删除的节点。
  4. 当fast.next==null时,找了倒数第n个节点(也就是头节点),将head向下即可。

代码展示:

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode fast = head, slow = head;
        //找出快指针的位置
    for(int i = 0; i < n; i++) fast = fast.next;
        
    while(true){
        if(fast == null)return head = head.next;
        else if(fast.next == null){
            slow.next = slow.next.next;
            return head;
        }
        //同时移动两个指针
        slow = slow.next;
        fast = fast.next;
    }
}

LeetCode141:环形链表

题目描述:

​ 给你一个链表的头节点 head ,判断链表中是否有环。

在这里插入图片描述

题解:

​ 定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。

代码展示:

public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null){
        return false;
    }
    ListNode fast = head;
    ListNode slow = head;
    while (fast != null && fast.next != null){
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow){
            return true;
        }
    }
    return false;
}

LeetCode876:链表的中间结点

题目描述:

​ 给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
在这里插入图片描述

题解:

​ 定义两个指针,一个快指针每次走两步,一个慢指针每次走一步,当快指针走到链表末尾时,慢指针刚好指向中间结点。

代码展示:

//快慢指针
public ListNode middleNode2(ListNode head) {
    // 当q走到末尾时p正好走到中间
    ListNode p = head, q = head;
    while (q != null && q.next != null) {
        q = q.next.next;  // 走两步
        p = p.next; // 走一步
    }
    return p;
}

3、方向相反的双指针

​ 在同一个数组中,一个指针指向头部向右移动,一个指向尾部左移动,当两个指针相遇后结束循环。常见有以下例题:

  • 第167题:两数之和 II - 输入有序数组

  • 第344题:反转字符串

  • 第977题:有序数组的平方

LeetCode167:两数之和 II - 输入有序数组

题目描述:

​ 给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。

​ 以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。

​ 你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。

​ 你所设计的解决方案必须只使用常量级的额外空间。
在这里插入图片描述

题解:

​ 因为给定的数组是有序的,我们将一个指针left从左开始(最小值)向右移动,另一个指针right从右开始(最大值)向左移动,当两个指针之和大于target时,则right–,小于target时,则left++,等于target,则返回结果。

func twoSum(numbers []int, target int) []int {
    left, right := 0, len(numbers)-1
	ret := make([]int, 2)
	for left <= right {
		if numbers[left]+numbers[right] > target {
			right--
		} else if numbers[left]+numbers[right] < target {
			left++
		} else {
			ret[0] = left + 1
			ret[1] = right + 1
			return ret
		}
	}
	return nil
}

LeetCode977:有序数组的平方

题目描述:

​ 给你一个按非递减顺序排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vuAspXgc-1669795507360)(玩转双指针.assets/1669733911226.png)]

题解:

​ 数组是有序排列的。因此,数组平方的最大值只能在数组的两端,不是最左边就是最右边,不可能是中间,因为负数平方之后也可能成为最大值。

​ 我们可以使用双指针的方法。left指向数组左端,right指向数组的右端。idx指向结果数组的末尾。

代码展示:

func sortedSquares(nums []int) []int {
    ret := make([]int, len(nums))
    //idx是指向结果数组ret的下标
	left, right,idx := 0, len(nums)-1,len(nums)-1
	//ret数组从后往前遍历
	for left <= right {
		if nums[left]*nums[left] < nums[right]*nums[right] {
			ret[idx] = nums[right]*nums[right]
			right--
		}else {
			ret[idx] = nums[left] *nums[left]
			left++
		}
		idx--
	}
	return ret
}

4、滑动窗口

滑动窗口就是有一个大小可变的窗口, 这个窗口可以是固定长度,也可以是可变长度,左右两端方向一致的向前滑动(右端固定,左端滑动;左端固定,右端滑动)。

​ 滑动窗口算法就是用以解决数组/字符串的子元素问题。可以将嵌套的for循环问题,转换为单循环问题,降低时间复杂度。

LeetCode3:无重复字符的最长子串

题目描述:

​ 给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。

在这里插入图片描述

题解:

​ 本题使用滑动窗口求解,[left, right]表示滑动窗口范围,使用Set集合存储不重复的字符。当set集合中不存在当前字符,则将其加入set中,窗口右端移动,窗口大小加1。如果存在该元素,则从set集合中移除,窗口左端移动,窗口大小减1。最终使用maxLen存储最长子串长度。

代码展示:

public class _3_无重复字符的最长子串 {
    public static void main(String[] args) {
        System.out.println(lengthOfLongestSubstring("abcabcbb"));
    }

    public static int lengthOfLongestSubstring(String s) {

        //存储不重复子串
        Set<Character> set = new HashSet<>();

        int len = 0, maxLen = 0;
        //[left,right]滑动窗口  len为窗口大小
        int left = 0, right = 0;
        while (right < s.length()) {

            if (!set.contains(s.charAt(right))) { //set中不存在char[right],添加到set中,right右移,len++
                set.add(s.charAt(right));
                right++;
                len++;
            }else { //set中存在char[right],移除char[left],left右移,len--
                set.remove(s.charAt(left));
                left++;
                len--;
            }
            maxLen = Math.max(len,maxLen);
        }
        return maxLen;
    }
}

LeetCode209:长度最小的子数组

题目描述:

​ 给定一个含有 n 个正整数的数组和一个正整数 target 。

​ 找出该数组中满足其和 ≥ target 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0 。

在这里插入图片描述

题解:

​ 本题可以使用滑动窗口求解。[left, right]表示滑动窗口范围,minLen记录最小子数组长度,sum记录子数组的总和。通过移动窗口右端right,增加sum,当sum >= target时,记录当前窗口大小,并更新最优解, 然后将left尝试向右边移动,直到sum<target为止, 这样就能得到滑动窗口中最小长度。

代码展示:

public class _209_长度最小的子数组 {
    public static void main(String[] args) {
        int[] nums = {2, 3, 1, 2, 4, 3};
        System.out.println(minSubArrayLen(7,nums));
    }

    public static int minSubArrayLen(int target, int[] nums) {
        //窗口区间[left,right]
        int left = 0;
        int right = 0;

        int minLen = Integer.MAX_VALUE; //最小滑动窗口的长度
        int sum = 0;   //滑动窗口数值之和
        while (right < nums.length) {
            sum += nums[right]; //窗口扩大,加入right对应元素,更新当前结果
            while (sum >= target) { //结果满足的条件
                //更新最优解
                minLen = Math.min(minLen,right - left + 1)
                //窗口缩小,移除left对应元素,left右移
                sum -= nums[left];
                left++;
            }
            right++;
        }
        return minLen;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值