LeetCode题解:双指针专栏

LeetCode题解:双指针专栏

自大学开始,我便陆陆续续的学习一些 算法和数据结构 方面的内容,同时也开始在一些平台刷题,也会参加一些大大小小的算法竞赛。但是平时刷题缺少目的性、系统性,最终导致算法方面进步缓慢。最终,为了自己的未来,我决定开始在LeetCode上进行系统的学习和练习,同时将刷题的轨迹整理记录,分享出来与大家共勉。


参考教材: LeetCode 101 - A LeetCode Grinding Guide

参考资料: LeetCode社区官方提供的思路/题解 以及 评论区/题解区各路大神提供的思路/答案



167. 两数之和 II - 输入有序数组

难度: 简单

给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target

函数应该以长度为 2 的整数数组的形式返回这两个数的下标值*。*numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length

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

示例:

输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。

题解:

因为数组已经排好序,我们可以采用方向相反的双指针来寻找这两个数字,一个初始指向最 小的元素,即数组最左边,向右遍历;一个初始指向最大的元素,即数组最右边,向左遍历。 如果两个指针指向元素的和等于给定值,那么它们就是我们要的结果。如果两个指针指向元 素的和小于给定值,我们把左边的指针右移一位,使得当前的和增加一点。如果两个指针指向元 素的和大于给定值,我们把右边的指针左移一位,使得当前的和减少一点。

class Solution {
    public int[] twoSum(int[] numbers, int target) {

        int left=0,right=numbers.length-1;

        while (left<right){
            if (numbers[left]+numbers[right]==target){
                return new int[]{left + 1, right + 1};
            }

            if (numbers[left]+numbers[right]<target){
                left++;
            }else {
                right--;
            }
        }

        return new int[]{-1,-1};
    }
}
88. 合并两个有序数组

难度: 简单

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

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

示例:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]

题解:

  • 方法一

一般而言,对于有序数组可以通过 双指针法 达到O(n + m)O(n+m)的时间复杂度。

最直接的算法实现是将指针p1 置为 nums1的开头, p2为 nums2的开头,在每一步将最小值放入输出数组中。

由于 nums1 是用于输出的数组,需要将nums1中的前m个元素放在其他地方,也就需要 O(m) 的空间复杂度。

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int[] nums1_copy=new int[m];
        System.arraycopy(nums1,0,nums1_copy,0,m);

        int p1=0;
        int p2=0;
        int p=0;

        while ( (p1<m) && (p2<n)){
            nums1[p++]=(nums1_copy[p1]<nums2[p2])?nums1_copy[p1++]:nums2[p2++];
        }

        if (p1<m){
            System.arraycopy(nums1_copy,p1,nums1,p1+p2,m+n-p1-p2);
        }

        if (p2<n){
            System.arraycopy(nums2,p2,nums1,p1+p2,m+n-p1-p2);
        }
    }
}
  • 方法二

方法一已经取得了最优的时间复杂度O(n+m),但需要使用额外空间。这是由于在从头改变nums1的值时,需要把nums1中的元素存放在其他位置。

如果我们从结尾开始改写 nums1 的值又会如何呢?这里没有信息,因此不需要额外空间。

这里的指针 p 用于追踪添加元素的位置。

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int p1=m-1;
        int p2=n-1;
        int p=m+n-1;
        
        while (p1>=0 && p2>=0){
            nums1[p--]=(nums1[p1]<nums2[p2])?nums2[p2--]:nums1[p1--];
        }
        
        System.arraycopy(nums2,0,nums1,0,p2+1);
    }
}
141. 环形链表

难度: 简单

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false

进阶:

你能用 O(1)(即,常量)内存解决此问题吗?

示例:

image-20210219093751134

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

题解:

经典解法就是用两个指针,一个每次前进两步,一个每次前进一步。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。

public class Solution {
    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;
    }
}
142. 环形链表 II

难度: 中等

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

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

**说明:**不允许修改给定的链表。

进阶:

  • 你是否可以使用 O(1) 空间解决此题?

示例:

image-20210219093808859

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

题解:

当快慢指针相遇时,让其中任一个指针重新指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。这是为什么呢?

第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。

设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。

巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。

所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后就会相遇,相遇之处就是环的起点了。

class Solution {
    public ListNode detectCycle(ListNode head) {
        boolean flag=false;//标记是否有环
        ListNode fast,slow;
        fast=slow=head;
        while (fast!=null && fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            if (fast==slow){
                flag=true;
                break;
            }

        }

        if (!flag){
            return null;//没有环
        }

        slow=head;
        while (slow!=fast){
            fast=fast.next;
            slow=slow.next;
        }

        return slow;
    }
}
76. 最小覆盖子串

难度: 困难

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

**注意:**如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"

题解:

滑动窗口算法,利用左右指针维护窗口中的数据,窗口区间是左闭右开区间,[left,right)。

滑动窗口算法通用流程:

初始化:初始化左右指针,left, right = 0, 0;初始化窗口window={},最初时window不包含数据;
寻找可行解:不断扩大窗口的右边界,右指针right+=1,将新数据加入窗口,并检测此时窗口中的字串是否满足题目要求;
优化可行解:当窗口包含的字串满足题目要求时,开始收缩窗口的左边界,left+=1,直至不再满足题目要求;
滑动窗口过程中,注意数据的实时更新,找到保存结果的位置。

class Solution {
    public String minWindow(String s, String t) {
        HashMap<Character,Integer> need = new HashMap();
        HashMap<Character,Integer> window = new HashMap();
        //need存放的不重复的字符出现的次数
        for(char c:t.toCharArray())
            need.put(c,need.getOrDefault(c,0)+1);

        //left,right 表示滑动窗口的左右指针
        int left = 0 , right = 0;
        //valid表示是否满足了t中的字符,不算重复的
        int valid = 0;
        //记录最小覆盖子串的起始索引及长度
        int start = 0 , len = Integer.MAX_VALUE;
        while(right < s.length()){
            char c = s.charAt(right);
            right++;
            //判断取出的字符是否在需要的Map中
            if(need.containsKey(c)){
                window.put(c,window.getOrDefault(c,0)+1);
                if(window.get(c).equals(need.get(c)))
                    valid++;
            }

            //判断是否需要收缩(即已经找到了合适的覆盖串)
            while(valid == need.size()){
                //更新最小覆盖子串
                if(right - left < len){
                    start = left;
                    len = right - left;
                }

                char c1 = s.charAt(left);
                //左移窗口
                left++;

                //进行窗口内数据的一系列更新
                //如果当前要移动的字符是包含在need中,我们需要进行讨论,如果该字符的次数刚好与我们需要的次数相等,则valid--,并同时更新window中这个值出现的次数
                if(need.containsKey(c1)){
                    if(window.get(c1).equals(need.get(c1)))
                        valid--;
                    window.put(c1,window.getOrDefault(c1,0)-1);
                }
            }
        }
        return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
    }
}
633. 平方数之和

难度: 中等

给定一个非负整数 c ,你要判断是否存在两个整数 ab,使得 a2 + b2 = c

示例:

输入:c = 5
输出:true
解释:1 * 1 + 2 * 2 = 5

题解:

对于从 left=0 到 right=sqrt© 之间使用双指针,逐渐找到可行解。

class Solution {
    public boolean judgeSquareSum(int c) {

        int l=0;
        int r=(int) Math.sqrt(c);

        while (l<=r){
            if (l*l==c-r*r){
                return true;
            }
            if (l*l<c-r*r){
                l++;
            }else {
                r--;
            }
        }

        return false;
    }
}
680. 验证回文字符串 Ⅱ

难度: 简单

给定一个非空字符串 s最多删除一个字符。判断是否能成为回文字符串。

示例:

输入: "aba"
输出: True

题解:

双指针+简单递归

class Solution {
    public boolean validPalindrome(String s) {
        int left = 0, right = s.length() - 1, count = 0;
        char lc, rc;
        while (left <= right) {
            if (s.charAt(left) != s.charAt(right)) {
                return validPalindrome(s,left+1,right) || validPalindrome(s,left,right-1);
            }
            left++;
            right--;
        }
        return true;
    }

    public boolean validPalindrome(String s,int left,int right) {
        while (left<right){
            if (s.charAt(left) != s.charAt(right)) {
                return false;//再一次出现不等
            }
            left++;
            right--;
        }
        return true;
    }
}

作者:耿鬼不会笑
时间:2021年2月
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值