leetcode:567. 字符串的排列

题目来源

题目描述

在这里插入图片描述

class Solution {
public:
    bool checkInclusion(string s1, string s2) {

    }
};

题目解析

分析数据量

1 <= s1.length, s2.length <= 10^4,因为两个10^4相乘为10^6,所以两个字符最多只能看一遍,就要得出答案,时间复杂度为O(N)

分析题意

str1,str2每种种类必须一样,个数必须一样,顺序可以不一样。这叫做编写次

滑动窗口 + 欠账表

思路

依次尝试固定以s2中的每一个位置l作为左端点开始的len1长度的子串s2[l…l+len1]是否是s2的排列。即可

如果固定l做左断点,尝试失败,继续尝试l+1位置位置,【左右边界都不需要回退】,这是滑动窗口的前提。

  • 通过一个记账本,charCount作为【总账表】维护s1的词频表;
  • 滑动窗口内每一个右边界字符进入窗口后,【还账】,charCount[str[r] - ‘a’]–
    • 如果某个字符多还了(变成赋值),即尝试失败,开始尝试下一个左端点(l++);
  • 左边界出窗口后,表示【重新赊账】:charCount[str2[l] - ‘a’]++
  • 最终如果欠账还足了(窗口长度达到len1),则尝试成功,直接返回true。

class Solution {
    int process( string str1, string str2){
        std::vector<int> count (256);
        int M = str1.size();
        for (int i = 0; i < M; ++i) {
            count[str1[i]]++;
        }


        int all = str1.size();  //一开始欠所有
        int R = 0;
        // 0~M-1
        for (; R < M; R++) { // 最早的M个字符,让其窗口初步形成
            if (count[str2[R]] > 0) {  //--之前 >0才是有效还款
                all--;
            }
            count[str2[R]]--;
        }
        // 窗口初步形成了,并没有判断有效无效,决定下一个位置一上来判断
        // 接下来的过程,窗口右进一个,左吐一个
        for (; R < str2.size(); R++) {
            if (all == 0) { // R-1 
                return R - M;  
            }

        
            if (count[str2[R]] > 0) {  //--之前大于0才是有效还款
                all--;
            }
            count[str2[R]]--;

            if (count[str2[R - M]] >= 0) {  //只有++之前>=0时才是有效吐出
                all++;
            }
            count[str2[R - M]]++;
        }
        return all == 0 ? R - M : -1;
    }
public:
    bool checkInclusion(string s1, string s2) {
        if(s1.size() > s2.size()){
            return false;
        }

        return process(s1, s2) != -1;
    }
};
class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        if(s1 > s2){
            return false;
        }

        int len1 = s1.size(); //借款人借出去了多少款项
        int len2 = s2.size(); //还款人手里有多少可还款项

        //账本,题目规定只包含小写字母
        std::vector<int> charCountArr(26, 0);
        //将借出去的东西先计入账本
        for (char c : s1) {
            charCountArr[c - 'a']++;
        }

        //开始尝试对每个款项分别还款
        //首先定义还款窗口的左右边界指针
        int left = 0, right = 0;
        //从可还款项的第0位开始,如果移动次数已经超出len2 - len1了,无法从后面新增款数了,必然就还不完,所以外层循环直接跳出
        while (left <= len2 - len1){
            //开始尝试还款,每进入还款窗口的款项要尝试减一,而且,减掉后不能小于0,否则就还多了
            while (right <= left + len1 && right < len2 && charCountArr[s2[right] - 'a'] > 0){
                charCountArr[s2[right] - 'a']--;  //对应的还款项代还次数减一
                right++; //继续进入下一个还款项
            }

            //跳出上面循环会有如下情况:
            //1、最后一次尝试了,right马上就越界了
            //2、还没还完,出现某一个款项还多了
            //3、本身就不需要还款(0),但是尝试还款
            //4、刚好每个款项还完了,又尝试还一个本不该还的款项
            //上面四种情况中,只有最后一种情况才算还款成功,这时还款窗口宽度刚好等于还款项数,才能算还款成功
            if (right - left == len1) {
                return true;
            }
            //如果是其他三种情况,则left在当前位置是不可能还款成功的,左边界的款项在进入还款窗口的时候,已经做过还账了,但是此时这种情况是恒定还款失败的,所以需要将这一次错误的或者是无效的还账取消,将这个款项重新赊账
            //特别注意出现一种特殊情况的时候,如何理解下面这行代码
            //如果第一次就尝试还一个本不该还的(0)款项,为什么还要赊账?这一步可以不理解为赊账了,left移动后right还没有移动,也就是right在left的左边,如果这个款项恒定为0,则right永远都不会移动走了,为了避免这样,我们可以假装先借着,下一次循环,必定能够还给他,循环才得以继续运行
            charCountArr[s2[left] - 'a']++; // 重新【"赊账"】
            //往后移动
            left++;
        }
        return false; // 所有的左端点均尝试还账失败,不可能再有答案了
    }
};

举个例子(例子待更改)

假设str1=ccabastr2=cccbabacbac....

(1)先使用str1做出一张欠账表
在这里插入图片描述
(2)遍历str2,开始花钱
(2.1) 先做出一个窗口来
在这里插入图片描述

当前窗口是不是str1的变形词呢?不是,因为all != 0

(2.2)开始移动窗口

  • 每次移动一个单词,然后判断all 是否为0,如果发现中途有all = 0,那么就返回true

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值