无重复字符的最长子串之滑动窗口求解

今天刷了一道力扣题:

3. 无重复字符的最长子串

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

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 104

  • s 由英文字母、数字、符号和空格组成

错误解法

看起来没什么难度,可是,当我用三重循环暴力求解的时候,被提示:

好吧 时间复杂度都O(n^3)了,确实容易超时。。。

小技巧:如果提示中给的输入范围很大,那么该题大概率不能暴力求解TAT
解题思路

通过看题解得知,该题有一个很优的解法:滑动窗口。

无重复字符的最长子串 - 力扣题解

滑动窗口 - 无重复字符的最长子串 - 力扣(LeetCode)

定义两个指针,一个记录起始位置,一个记录结束位置。

**什么是滑动窗口?**其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列。

**如何移动?**我们只要把队列的左边的元素移出就行了,直到满足题目要求。

一直维持这样的队列,找出队列出现最长的长度时候,求出解。

可是这个题有一个点把我困住了:怎么判断起始位置和结束位置中间有无重复字符呢?

可以用集合!
常用的数据结构为 哈希集合(即C++ 中的std:: unordered_set,Java中的HashSet,Python中的set, JavaScript 中的 Set)。在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。
set基本函数

set的一些操作见《算法笔记》P197,下面只简单介绍一下用到的函数(需要《算法笔记》pdf 版的可以私聊我~):

  1. set内的元素自动递增排序,且自动去除了重复元素。

  1. insert(x):向集合中插入元素x,并自动去重。

  1. find(value):返回set中对应值为value的迭代器。

  1. erase():

①删除单个元素

  • st.erase(it),it是所需要删除元素的迭代器

st.erase(st.find(100));
  • st.erase(value),value为所需要删除元素的值

st.erase(100);

②删除一个区间里的所有元素

  • st.erase(first, last):first为所需要删除区间的起始迭代器,last为所需要删除区间的末尾迭代器的下一个地址,即删除[first, last)。

  1. count():返回元素在集合中出现的次数。

该函数返回1或0,因为该集合仅包含唯一元素。如果设置的容器中存在该值,则返回1。如果容器中不存在它,则返回0。

set最主要的作用是自动去重并按升序排序,因此碰到需要去重但是不方便直接开数组的情况,可以尝试用set解决。
map基本函数

set的一些操作见《算法笔记》P213,后面一些题可能会用到map,所以写在这里:

  1. 定义map:

unordered_map<typename1, typename2> mp;

<key, value>映射

  1. map可以直接通过对应下标访问

例:unordered_map<char, int> mp; mp['c']; 即为'c'对应的整数值。

代码

题解代码:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // 哈希集合,记录每个字符是否出现过
        unordered_set<char> occ;
        int n = s.size();
        // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
        int rk = -1, ans = 0;
        // 枚举左指针的位置,初始值隐性地表示为 -1
        for (int i = 0; i < n; ++i) {
            if (i != 0) {
                // 左指针向右移动一格,移除一个字符
                occ.erase(s[i - 1]);
            }
            while (rk + 1 < n && !occ.count(s[rk + 1])) {
                // 不断地移动右指针
                occ.insert(s[rk + 1]);
                ++rk;
            }
            // 第 i 到 rk 个字符是一个极长的无重复字符子串
            ans = max(ans, rk - i + 1);
        }
        return ans;
    }
};

时间复杂度:O(n)

哭了。。原来我暴力用双重循环解决的问题只需要一个集合的count函数。。

我的代码:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n = s.size();
        int rk = 0;//右指针
        int mx=0;
        unordered_set<char> st;
        for(int i=0; i<n; i++){
            while(rk<n && !st.count(s[rk])){
                st.insert(s[rk++]);
            }
            if(st.size()>mx)
                mx = st.size();
            st.erase(s[i]);
        }
        return mx;
    }
};
类似题目练手

30. 串联所有单词的子串 - 力扣(LeetCode)

根据题意,写下了如下代码:

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        /*
        错误情况:
        输入:"wordgoodgoodgoodbestword" ["word","good","best","good"]
        输出:[]
        预期结果:[8]

        解决:原来是用集合存string,现在用集合存它们对应的index即可。
        */
        int w_size = words[0].size();
        int size = words.size();
        int n = s.size();
        unordered_set<int> occ;
        vector<int> a;
        int now;
        int rk = -1;
        for(int i=0;i<n-w_size*size+1;i++){ //滑动窗口
            occ.clear();
            rk=i;
            for(int j=0;j<size;j++){ //取一个字符串
                string temp = s.substr(rk,w_size);
                vector<int> t;
                for(int k=0;k<size;k++){ //在字符串数组中找到与之匹配的下标
                    if(temp.compare(words[k])==0){
                        t.push_back(k);
                        // cout<<temp<<" ";
                    }
                }
                for(int h=0;h<t.size();h++){ //如果下标不在集合里面,就把它加入
                    if(!occ.count(t[h])){
                        rk=rk+w_size;
                        occ.insert(t[h]);
                        break;
                    } 
                }                
            }
            if(occ.size()==size) //如果集合中正好包括了字符串数组中的所有下标,就符合条件
                a.push_back(i);
        }
        return a;
    }
};

又超时了。。。

解法参照:详细通俗的思路分析,多解法 - 串联所有单词的子串 - 力扣(LeetCode)解法二

为了方便讨论,我们每次移动一个单词的长度,也就是w_size个字符,这样所有的移动被分成了w_size类。就不用一步一步移动i了
class Solution {
    /*
    基本思路:滑动窗口。
    使用一个无序map,对应的int是该单词的频度。
    初始:把初始滑动窗口中的字符串按单词长度划分后加入map中(频度+1),并把字符串数组中的字符串在map中的频度-1;
    每当map中有频度=0的单词,就把该单词从map中删掉。
    如果map为空,则记录下滑动窗口左边界;
    如果不为空,滑动窗口右移,加入的单词在map中频度+1,减掉的单词在map中频度-1.
    */
public:
    vector<int> findSubstring(string &s, vector<string> &words) {
        vector<int> res;
        int size = words.size(), w_size = words[0].size(), ls = s.size();
        //为了方便讨论,我们每次移动一个单词的长度,也就是w_size个字符,这样所有的移动被分成了w_size类。
        for (int i = 0; i < w_size && i + size * w_size <= ls; ++i) {
            unordered_map<string, int> differ;
            //先把滑动窗口内的单词存一下(单词在map中的频度+1)
            int rk = i;
            for (int j = 0; j < size; ++j) {
                ++differ[s.substr(rk, w_size)];
                rk=rk+w_size;
            }
            //把单词在map中的频度-1,如果单词在map中的频度=0,就把它删掉
            for (string &word: words) {
                if (--differ[word] == 0) {
                    differ.erase(word);
                }
            }
            //向后遍历,每次移动一个单词的长度
            for (int start = i; start < ls - size * w_size + 1; start += w_size) {
                //start相当于左边界,start!=i即滑动窗口需要后移了,此时需要多加入一个单词
                if (start != i) {
                    // 新加入一个单词
                    string word = s.substr(start + (size - 1) * w_size, w_size);
                    if (++differ[word] == 0) {
                        //如果这个单词是之前不在map里,但是单词表中有,那么这个单词在map中的频度<0
                        //当它=0时,把它从map中删掉
                        differ.erase(word);
                    }
                    word = s.substr(start - w_size, w_size);
                    if (--differ[word] == 0) {
                        differ.erase(word);
                    }
                }
                if (differ.empty()) {
                    res.push_back(start);
                }
            }
        }
        return res;
    }
};
什么时候用map?
其实用set的情况下都可以用map。set在去重及判断是否有重复时好用,其它情况可以尝试一下map。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值