4.双指针和滑动窗口

操作链表--快慢指针 反转数组,有序数组--左右指针 子串--滑动窗口

快慢指针

141--给定一个链表判断链表中是否含有环

public boolean hasCycle(ListNode head) {
        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;
    }

142--有环的话就指出链表的环头

 public ListNode detectCycle(ListNode head) {
        if(head==null)return null;
        ListNode fast=head,slow=head;
        boolean hasLoop=false;
        while (fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            if(slow==fast){
                hasLoop=true;
                break;
            }
        }
        //假设slow走了k步,fast走了2k步,这多出的k步其实就是环的整数倍
        //我们将所以现在我们将任意一个指针放到头部,然后两个指针以相同的速度往前走,再次相遇就说明到了环的起始点
        if(hasLoop){
            slow=head;
            while(slow!=fast){
                fast=fast.next;
                slow=slow.next;
            }
            return slow;
        }
        return null;
    }

876--寻找链表的中点,偶数则返回第二个

 public ListNode middleNode(ListNode head) {
        ListNode fast=head,slow=head;
        while(fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        return slow;
    }

19--删除链表的倒数第n个元素

具体思路是让快指针先走n步,当快指针的next为null时,这时的slow指针指向的就是要删除节点的前一个节点

public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode fast=head,slow=head;
        while(n-->0){
            fast=fast.next;
        }
        //指针走到头了说明指向了第一个节点,我们删除head就可以了
        if(fast==null)return head.next;
        while (fast.next!=null){
            fast=fast.next;
            slow=slow.next;
        }
        slow.next=slow.next.next;
        return head;
    }

左右指针

167--两数之和二

 只要是有序数组就应该想到双指针技巧

public int[] twoSum(int[] numbers, int target) {
        int left=0,right=numbers.length-1;
        while (left<=right){
            //题目要求最后结果+1
            if(numbers[left]+numbers[right]==target)return new int[]{left+1,right+1};
            else if(numbers[left]+numbers[right]<target){
                left++;//让sum大一点
            }else if(numbers[left]+numbers[right]>target){
                right--;//让sum小一点
            }
        }
        return null;
    }

344-反转字符串

 public void reverseString(char[] s) {
        int left=0;
        int right=s.length-1;
        while(left<=right){
            char temp=s[left];
            s[left]=s[right];
            s[right]=temp;
            left++;
            right--;
        }
    }

滑动窗口

滑动窗口的框架如下,它的基本时间复杂度为O(N)

  1. 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。
  2. 先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)
  3. 此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
  4. 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。

/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
    int[] need=new int[128];
    int[] window=new int[128];
    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++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

76--最小覆盖子串

    public String minWindow(String s, String t) {
        if (s == null || s == "" || t == null || t == "" || s.length() < t.length()) {
            return "";
        }
        //维护两个数组,记录已有字符串指定字符的出现次数,和目标字符串指定字符的出现次数
        //ASCII表总长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;
        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);
    }

3--最大不重复子串

public int lengthOfLongestSubstring(String s) {
        int max=0;
        int[] window=new int[128];
        int left=0;
        int right=0;
        while (right<s.length()){
            char c=s.charAt(right);
            right++;
            window[c]++;
            while (window[c]>1){
                char d=s.charAt(left);
                window[d]--;
                left++;
            }
            max=Math.max(max,right-left);
        }
        return max;
    }

438--找到字符串中所有字母异位词

使用哈希表来解决

    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> res = new ArrayList<>();
        int left = 0, right = 0;
        HashMap<Character, Integer> need = new HashMap<>();
        HashMap<Character, Integer> have = new HashMap<>();
        int count=0;
        for (int i = 0; i < p.length(); i++) {
            need.put(p.charAt(i),need.getOrDefault(p.charAt(i),0)+1);
        }
        while (right<s.length()) {
            char c=s.charAt(right);
            right++;
            if(need.containsKey(c)){
                have.put(c,have.getOrDefault(c,0)+1);
                if(have.get(c).equals(need.get(c)))count++;
            }
            //此时说明需要收缩窗口
            while (right-left>=p.length()) {
                if(count==need.size()){
                    res.add(left);
                }
                char d=s.charAt(left);
                left++;
                //进行窗口数据更新
                if(need.containsKey(d)){
                    if(need.get(d).equals(have.get(d)))count--;
                    have.put(d,have.getOrDefault(d,0)-1);
                }
            }
        }
        return res;
    }

 使用数组解决

 public List<Integer> findAnagrams(String s, String p) {
        List<Integer> res = new ArrayList<>();
        
        HashSet<Object> set = new HashSet<>();
        int left = 0, right = 0;
        int[] need = new int[128];
        int[] have = new int[128];
        int count=0;
        for (int i = 0; i < p.length(); i++) {
            need[p.charAt(i)]++;
            //去重操作
            set.add(p.charAt(i));
        }
        while (right<s.length()) {
            char c=s.charAt(right);
            right++;
            if(need[c]>0){
                have[c]++;
//有一个字符已经完全匹配成功了
                if(have[c]==need[c])count++;
            }
            //此时说明需要收缩窗口
            while (right-left>=p.length()) {
                if(count==set.size()){
                    res.add(left);
                }
                char d=s.charAt(left);
                left++;
                //进行窗口数据更新
                if(need[d]>0){
                    if(need[d]==have[d])count--;
                    have[d]--;
                }
            }
        }
        return res;
    }

567--字符串的排列

public boolean checkInclusion(String s1, String s2) {
        int left=0,right=0,valid=0;
        int[] need=new int[128];
        HashSet<Character> set = new HashSet<>();
        for (int i = 0; i < s1.length(); i++) {
            need[s1.charAt(i)]++;
            set.add(s1.charAt(i));
        }
        int[] have=new int[128];
        while (right<s2.length()){
            char c=s2.charAt(right);
            right++;
            if(need[c]>0){
                have[c]++;
                if(need[c]==have[c])valid++;
            }
            //判断是否要收缩
            while (right-left>=s1.length()){
                if(valid==set.size())return true;
                char d=s2.charAt(left);
                left++;
                if(need[d]>0){
                    if(have[d]==need[d])valid--;
                    have[d]--;
                }
            }
           
        }
        return false;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值