LeetCode 3.无重复字符的最长字串(C++)

本文介绍了三种求解字符串中最长无重复子串的算法,分别是两遍遍历、滑动窗口法和时间优化的滑动窗口法。通过实例展示了如何使用ASCII码映射和unordered_set来高效查找最长子串,以及如何利用map存储字符出现位置进行优化。

题目地址:力扣

解法1:两遍遍历

思路:因为字符仅限于英文字母、数字、空格和符号,那么可以用ASCII码来表示,因此开辟一个128大小的bool类型的map用于存放每个字符的情况,初始设置为false,若遍历过了则设为true。从字符串头开始,找从当前头往后的最长无重复子串,并且更新全局最大的子串长度,一直找到末尾(可以设置终止条件提前终止)。

class Solution {
public:
    // 用于初始化map,每次遍历完都重置状态
    void initmap(map<int, bool> &cmap)
    {
        for (int i = 0; i < 128; ++i)
            cmap[i] = false;
    }

    int lengthOfLongestSubstring(string s) {
        // sz用于保存不重复子串的全局最大长度,cmap用于存储每个字符状态
        int sz = 0;
        map<int, bool> cmap;
        
        // 遍历以字符串的每个字符开头的最长不重复子串
        for (int i = 0; i < s.size(); ++i)
        {
            initmap(cmap);
            // 用于存储以当前字符开头的最长子串长度
            int cur_sz = 0;
            // 当前字符开头向后找
            for (int j = i; j < s.size(); ++j)
            {   
                // 若字符未出现过,就把状态置为出现,并且递增当前长度,更新全局长度
                if (!cmap[s[j]])
                {
                    cmap[s[j]] = true;
                    ++cur_sz;
                    sz = cur_sz > sz ? cur_sz : sz;
                // 字符出现过,那么就已经找到了以当前字符开头的最长不重复子串
                } else {
                    break;
                }
            }
            // 若最长子串的长度加上i大于字符串总长度,后面的就不要找了,长度一定更小
            if (sz + i >= s.size())
                break; 
        }
        return sz;
    }
};

解法2:滑动窗口法

思路:我们注意到,在上一种方法里,找到以某个字符为开头的最长不重复子串的循环终止条件就是当前子串的中某处和子串尾出现了相同的字符。因此这就告诉我们了,如果把当前子串的头移动到子串中某处的后一位,那么当前子串一定是一个不重复的串,所以这个信息也就意味着我们可以重复利用这一个串一直往后找,因此就没必要每轮都从起始的字符的头开始找。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // sz为全局不重复子串最长长度,cset用于存储出现过的字符
        int sz = 0;
        unordered_set<char> cset;
        // end用于存储尾部指针
        int end = 0;
        // 头没到字符串尾循环继续
        for (int i = 0; i < s.size(); ++i)
        {
            // end没到字符串尾循环继续
            for (; end < s.size(); ++end)
            {
                // 如果当前的字符未出现过,就插入当前字符并且更新全局最长长度
                // 这里每找一次更新一次主要是为了应对字符串长度为1的情况
                if (cset.count(s[end]) == 0)
                {
                    cset.insert(s[end]);
                    sz = (end - i + 1) > sz ? (end - i + 1) : sz;
                }
                // 若当前字符出现过
                else
                {
                    // 头一直移动到当前字符的位置,并且把经历过的每个字符都从set中清除
                    // 由于发现重复字符时并未重复添加,因此这里也不需要清除,只需要把end往后
                    // 移动,然后跳出循环即可
                    while (s[i] != s[end])
                        cset.erase(s[i++]);
                    ++end;
                    break;
                }
            }
        }

        return sz;
    }
};

解法3:滑动窗口法(时间再优化)

上面的滑动窗口法在发现字符重复之后,需要一个个移动头来找到下一个头应该在的位置,这个步骤可以使用map存对应的下标来进行时间的优化。而且上面用的是头指针来控制循环的,但我们知道,实际上尾指针走到了字符串末尾,最长不重复的子字符串长度就可以确定下来,结束循环,因此这里我们采用尾指针来控制循环。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // sz保存全局最长不重复子串长度,cmap保存字符和出现的下标位置
        int sz = 0;
        unordered_map<char, int> cmap;
        // 头指针和尾指针都置为0
        int end = 0, start = 0;
        // 只要尾指针不到尾就继续循环
        while (end < s.size())
        {   
            // 如果当前字符出现过
            if (cmap.find(s[end]) != cmap.end())
            {
                // 若当前字符出现的位置在头指针之后,那么说明子串出现了重复字符
                if (cmap[s[end]] >= start)
                {
                    // 更新长度,并且更新头指针位置
                    sz = max(sz, end - start);
                    start = cmap[s[end]] + 1;
                }
            } 
            // 无论字符是否出现过,都更新当前元素的位置,并且尾指针向后移动
            cmap[s[end]] = end;
            ++end;
        }
        // 当尾指针走到末尾的时候再更新一次长度
        sz = max(sz, end - start);
        return sz;
    }
};

Accepted

  • 987/987 cases passed (16 ms)
  • Your runtime beats 70.01 % of cpp submissions
  • Your memory usage beats 50.85 % of cpp submissions (8.8 MB)
<think>好的,我需要解决用户的问题:如何用C++实现一个算法,找到不含重复数字的最长长度,其中星号(*)可以匹配任意数字。首先,我得理解这个问题的具体要求。 用户提到“不含重复数字的最长”,也就是说,子中的每个数字只能出现一次,但星号可以替代任何数字,这可能允许重复的数字被星号覆盖。例如,假设有字符"1*2",这里的星号可以代表1,那么实际子可能是"112",但由于星号的存在,是否算作不含重复呢?需要明确规则:星号是否算作一个独立字符,还是可以灵活替换以帮助避免重复? 根据用户的问题描述,星号可匹配任意数字,这意味着在判断重复时,星号本身不被视为一个固定数字。例如,子"1*2"中的星号可以代表任何数字,包括已经存在的1或2,但如果我们允许星号代替不同的数字,可能可以避免重复。或者,可能星号每次出现时被视为同一数字?这点需要进一步澄清,但用户没有给出具体说明,可能需要假设星号可以匹配不同的数字,从而帮助避免重复。 例如,假设输入是"1*2",那么最长可能是3,因为星号可以代表一个不在1或2中的数字,比如0,这样整个子就是102,没有重复。但如果输入是"11*",那么星号可以代表另一个数字,比如2,所以最长3,即112中的星号作为2,避免重复。但如果是"1*1",这里星号可能被用作非1的数字,从而允许子长度为3,比如121,所以没有重复。 但另一种情况是,如果星号被允许多次替换为不同的数字,那么可能出现更灵活的情况。例如,子中如果有多个星号,每个可以代表不同的数字,从而避免重复。例如,"*1*"可以变成A1B,其中A和B都是不同的数字,且不等于1,这样长度为3,没有重复。 但问题可能要求星号在子中每次出现时只能代表同一数字,这会影响结果。例如,子"*1*"如果要求两个星号必须代表相同的数字,那么可能无法避免重复。因此,用户的问题可能存在歧义,需要明确规则。但用户并未详细说明,可能需要根据常规题目的处理方式假设:星号每次可以代表不同的数字,以大化子长度。这种情况下,星号可以作为通配符,每次使用时可以替换为不同的数字,以确保不重复。 接下来,我需要设计一个算法来解决这个问题。通常,处理最长无重复字符的问题可以使用滑动窗口(双指针)的方法,例如LeetCode3题。但这里加入了星号作为通配符,需要调整条件。 常规的无重复字符最长问题中,滑动窗口维护一个字符到其新位置的映射。当遇到重复字符时,移动左指针到重复字符的下一个位置。但在此问题中,星号可以代表任何数字,这为窗口的扩展提供了灵活性。 可能的思路是:在滑动窗口内,统计当前窗口中的数字出现次数,同时记录星号的数量。当添加一个字符到窗口时: - 如果是星号,可以暂时不计入重复,但需要考虑如何利用星号来填补可能的重复。 - 如果是数字,必须确保该数字在窗口中出现的次数不超过一次(除非有星号可以替换掉之前的出现)。 这似乎比较复杂。另一种方法是:允许窗口内包含星号,并且当遇到数字重复时,使用星号来替代其中一个重复的数字。例如,如果当前窗口中有数字x,此时又遇到x,但窗口中有星号,那么可以将其中一个星号视为另一个数字,从而避免重复。但如何统计这种情况可能需要更复杂的数据结构。 或者,可以将问题转化为允许窗口内的数字多出现一次,而星号的数量可以用来抵消重复的次数。例如,每当有一个重复的数字出现时,必须有一个星号可以用来替代其中一个,从而消除重复。这种情况下,窗口的有效性条件变为:窗口内所有非星号数字的出现次数不超过1,并且重复的次数不超过星号的数量。 这似乎比较困难,可能需要动态维护窗口中的数字计数以及可用的星号数量,以判断当前窗口是否有效。 举个例子:字符是 "1*1"。当窗口包含所有三个字符时,非星号数字是1和1,出现两次。此时需要至少一个星号来替代其中一个1。但星号本身在这里是中间的字符,所以可以将中间的星号视为另一个数字,比如0,从而使得整个窗口变为"101",没有重复。因此,这种情况下,窗口长度为3是有效的。所以,星号可以用来替换重复的数字。 因此,可能的策略是:在滑动窗口内,统计每个数字的出现次数,同时记录星号的数量。当添加一个数字到窗口时: - 如果该数字已经出现过,并且当前窗口中没有足够的星号来替换重复的数字,则需要移动左指针,直到重复被解决。 - 如果该数字未出现过,或者有足够的星号可以替换之前的出现,则可以扩展窗口。 但具体如何计算需要详细分析。例如,假设窗口中有k个星号,那么多可以允许k次重复的数字出现,每个星号可以替换一个重复的数字。或者,每个星号可以用来填补一个重复,因此如果有m个重复的数字,需要至少m个星号。 例如,窗口中有两个1,并且有一个星号,那么可以用这个星号替换其中一个1,从而消除重复。因此,允许窗口中有两个1和一个星号,总的有效长度为3,但实际上星号被用来替换其中一个1,所以实际不重复。 因此,判断窗口是否有效的方法是:对于所有非星号数字,它们的出现次数减去可用的星号数量是否小于等于1。或者更准确地说,统计每个数字的出现次数,然后计算所有重复次数的总和,是否小于等于可用的星号数量。 例如,窗口中的数字统计为:1出现了2次,其他数字出现1次,星号有3个。那么重复次数总和是1(因为只有1重复了一次),因此需要至少1个星号来替换其中一个1。此时,可用星号数量是3,足够,因此窗口有效。 因此,算法的大致步骤可能是: 1. 使用滑动窗口[left, right],初始时left=0。 2. 维护一个哈希表count,记录每个数字在窗口中的出现次数。 3. 维护变量star_count记录窗口中星号的数量。 4. 遍历每个right,将当前字符加入窗口: a. 如果是星号,增加star_count。 b. 否则,增加该数字的计数。 5. 检查当前窗口是否有效: a. 计算所有数字的重复次数总和(即每个数字出现次数减1的总和,如果出现次数超过1的话)。 b. 如果这个总和 <= star_count,则窗口有效,可以更新大长度。 c. 否则,需要移动左指针,直到窗口有效。 但如何高效计算重复次数总和?例如,每次移动right后,需要遍历所有数字的计数,计算sum(max(0, count[d] - 1))。这可能带来较高的时间复杂度,尤其是在每个步骤中都遍历整个哈希表。假设字符集是数字0-9,那么每次遍历10次,这应该是可以接受的。 因此,具体实现步骤: - 初始化left=0, max_len=0, star_count=0, count数组(大小为10,初始为0)。 - 遍历right从0到n-1: - 当前字符c = s[right] - 如果c是&#39;*&#39;,则star_count++ - 否则,digit = c - &#39;0&#39;,count[digit]++ - 计算当前重复次数总和required = sum_{d=0到9} max(0, count[d]-1) - 如果required <= star_count: - 当前窗口有效,计算窗口长度right-left+1,更新max_len - 否则: - 移动左指针,直到required <= star_count - 每次移动left时: - 如果s[left]是&#39;*&#39;,star_count-- - 否则,digit = s[left] - &#39;0&#39;,count[digit]-- - left++ - 重新计算required - 需要注意的是,当移动left时,可能导致required减少,直到满足条件。 这样,每个字符多被访问两次(left和right各一次),时间复杂度为O(n*10) = O(n),对于较大的n来说是可接受的。 例如,对于输入字符"1*1",当right=2时,字符是&#39;1&#39;,此时count[1]变为2,star_count=1。计算required=1(因为1出现了两次,sum是1)。此时,star_count=1,required=1,满足1<=1,因此窗口长度3是有效的,max_len=3。 另一个例子,输入"*11",当right=2时,字符是&#39;1&#39;,此时count[1]=2,star_count=1。required=1<=1,窗口有效,长度3。同样正确。 但是如果输入是"111***",当处理到第三个1时,count[1]=3,star_count=0。required=2(3-1=2),此时需要star_count>=2,但当前star_count=0,不满足。因此需要移动left,直到count[1]减少到1,这时required=0,此时窗口可能变为从left=3到right=2,但此时right < left,可能需要调整。或者,当left超过right时,重置窗口。 但根据算法逻辑,当right移动时,left只能向右移动,因此当遇到无法满足条件时,会持续移动left直到条件满足,或者left超过right。例如,当处理到第三个1时,left会一直移动到right+1的位置,此时窗口长度为0,继续处理下一个字符。 现在,考虑如何用C++实现这个逻辑。 首先,将字符中的每个字符处理为数字或星号。维护一个count数组,大小为10,初始化为0。维护star_count变量。维护max_len和left指针。 例如: class Solution { public: int longestSubarray(string s) { int max_len = 0; int left = 0; int star_count = 0; vector<int> count(10, 0); for (int right = 0; right < s.size(); right++) { char c = s[right]; if (c == &#39;*&#39;) { star_count++; } else { int digit = c - &#39;0&#39;; count[digit]++; } // 计算当前required int required = 0; for (int d = 0; d < 10; d++) { required += max(0, count[d] - 1); } // 调整左指针直到required <= star_count while (required > star_count && left <= right) { char left_c = s[left]; if (left_c == &#39;*&#39;) { star_count--; } else { int d_left = left_c - &#39;0&#39;; count[d_left]--; // 需要重新计算required,因为count发生了变化 // 但是这里可能效率较低,因为每次移动左指针都要重新计算required // 或者可以在移动左指针时,动态调整required // 例如,当移除一个数字d_left时,原来的count[d_left]可能是超过1的,现在减少后可能变化 required -= max(0, count[d_left] + 1 - 1) - max(0, count[d_left] - 1); // 因为原来在required的计算中,该数字贡献的是max(0, count-1) // 当count减少1后,新的贡献是max(0, (count-1)-1) // 所以变化量为 [max(0, (count-1)-1)] - [max(0, count-1)] // 例如,假设原来的count是3,贡献是2。减少1后,count=2,贡献1。变化量是-1 // 如果原来的count是2,贡献1。减少后是1,贡献0。变化量是-1 // 如果原来的count是1,贡献0。减少后是0,贡献0。变化量是0 } left++; // 重新计算required可能更简单,但效率较低 // 所以,可能需要每次调整后重新计算required,或者在移动左指针时动态调整 // 这里可能需要重新计算required,但会导致O(10)的时间复杂度每次循环 // 但是,因为每个字符多被处理两次,总的时间复杂度还是O(n*10) } // 更新max_len int current_len = right - left + 1; if (current_len > max_len) { max_len = current_len; } } return max_len; } }; 然而,在移动左指针时,计算required的调整可能比较复杂。例如,当移除一个数字d_left时,原来的count[d_left]是old_count,现在变为old_count - 1。原来的贡献是max(0, old_count -1),新的贡献是max(0, (old_count -1) -1)。因此,required的变化量是 [max(0, old_count -2)] - [max(0, old_count -1)]。这可能为-1(如果old_count >=2)或者 0(如果old_count ==1)。 例如,如果old_count是3,原贡献是2,新贡献是1,变化量-1。如果old_count是2,原贡献1,新贡献0,变化量-1。如果old_count是1,原贡献0,新贡献0,变化量0。 因此,在移动左指针时,可以动态调整required: 当移除一个数字d_left时: int old_contribution = max(0, count[d_left] -1); count[d_left]--; int new_contribution = max(0, count[d_left] -1); required += (new_contribution - old_contribution); 这样,就可以在O(1)的时间内调整required,而不需要重新遍历所有数字。 同样,当移除星号时,star_count减少,但星号不影响required,所以required不变。 因此,在代码中,当处理左指针时: 当字符是星号,减少star_count。否则,处理该数字的count,并调整required。 修改后的循环: while (required > star_count && left <= right) { char left_c = s[left]; if (left_c == &#39;*&#39;) { star_count--; } else { int d_left = left_c - &#39;0&#39;; int old_contribution = max(0, count[d_left] -1); count[d_left]--; int new_contribution = max(0, count[d_left] -1); required += (new_contribution - old_contribution); } left++; } 这样可以避免每次重新计算required,提高效率。 因此,完整的代码如下: class Solution { public: int longestSubarray(string s) { int max_len = 0; int left = 0; int star_count = 0; vector<int> count(10, 0); int required = 0; for (int right = 0; right < s.size(); ++right) { char c = s[right]; if (c == &#39;*&#39;) { ++star_count; } else { int digit = c - &#39;0&#39;; int old = count[digit]; count[digit]++; if (old >= 1) { // 原来已经至少出现一次,现在增加,可能增加required required += (max(0, count[digit] -1) - max(0, old -1)); } else { // 原来没有重复,现在出现一次,不会增加required // 只有当count[digit]变为2时才会增加required if (count[digit] >=2) { required += 1; } } } // 调整左指针直到required <= star_count while (required > star_count && left <= right) { char left_c = s[left]; if (left_c == &#39;*&#39;) { --star_count; } else { int d_left = left_c - &#39;0&#39;; int old = count[d_left]; count[d_left]--; if (old >= 1) { required += (max(0, count[d_left] -1) - max(0, old -1)); } } ++left; } // 计算当前窗口长度 max_len = max(max_len, right - left + 1); } return max_len; } }; 这样,当添加或移除一个数字时,动态调整required的值,而不需要每次遍历整个count数组。这样时间复杂度为O(n),因为每个字符被处理两次(进入窗口和离开窗口),而每次处理只需常数时间。 测试案例: 例如输入"1*1",处理到第三个字符时: right=0: &#39;1&#39;,count[1]=1,required=0。窗口长度1,max=1. right=1: &#39;*&#39;, star_count=1. required=0 <=1.窗口长度2,max=2. right=2: &#39;1&#39;,count[1]=2. required的变化:old=1-1=0,new=2-1=1. required +=1-0=1.此时required=1,star_count=1,条件满足。窗口长度3,max=3. 另一个测试案例:"*11" right=0: &#39;*&#39;, star_count=1. required=0.窗口长度1. right=1: &#39;1&#39;,count[1]=1. required=0.窗口长度2. right=2: &#39;1&#39;,count[1]=2. required=1. star_count=1,条件满足。窗口长度3. 如果输入是"111***",则处理到第三个&#39;1&#39;时: right=2时,count[1]=3,required=2. star_count=0. required>star_count,需要移动左指针。left=0,移除第一个&#39;1&#39;,count[1]=2,required变成1. star_count=0仍不满足。继续移动left到1,移除第二个&#39;1&#39;,count[1]=1,required变成0.此时star_count=0,条件满足。窗口长度为right-left+1=2-2+1=1. 然后继续处理后面的星号,可能得到更长的窗口。 现在,问题中的示例代码可能类似于LeetCode问题中的某个解法,但需要调整以处理星号的情况。用户提供的引用中有类似的问题,例如引用[1]中的代码,但处理的是最长全1子数组,允许删除一个0。而这里的问题更复杂,因为星号可以作为通配符,可以替换多个不同的数字,而不仅仅是删除一个字符。 因此,终的C++代码应如上所述,使用滑动窗口,动态维护required和star_count,确保窗口内所需的星号数量足够覆盖所有重复的数字。</think>要解决寻找不含重复数字的最长长度(其中星号可匹配任意数字)的问题,可以采用滑动窗口(双指针)算法,动态维护窗口内数字的重复次数与星号的匹配能力。以下是具体实现方法: ### 算法思路 1. **滑动窗口**:使用双指针$left$和$right$定义窗口范围,逐步扩展右边界。 2. **统计计数**:记录窗口中每个数字的出现次数和星号数量。 3. **动态调整**:当窗口内重复数字的总需求超过星号可替换的能力时,收缩左边界。 4. **计算最长**:每次扩展右边界后,更新大窗口长度。 ### C++实现代码 ```cpp #include <vector> #include <algorithm> using namespace std; class Solution { public: int longestSubarray(string s) { int max_len = 0; int left = 0; int star_count = 0; vector<int> count(10, 0); int required = 0; // 需要星号替换的重复次数总和 for (int right = 0; right < s.size(); ++right) { char c = s[right]; if (c == &#39;*&#39;) { star_count++; } else { int digit = c - &#39;0&#39;; int old = count[digit]; count[digit]++; // 更新required:新增的重复次数 if (old >= 1) { required += (max(0, count[digit] - 1) - max(0, old - 1)); } else if (count[digit] == 2) { required += 1; } } // 收缩左边界直到条件满足 while (required > star_count && left <= right) { char left_c = s[left]; if (left_c == &#39;*&#39;) { star_count--; } else { int d_left = left_c - &#39;0&#39;; int old = count[d_left]; count[d_left]--; // 更新required:减少的重复次数 if (old >= 1) { required += (max(0, count[d_left] - 1) - max(0, old - 1)); } } left++; } max_len = max(max_len, right - left + 1); } return max_len; } }; ``` ### 算法解析 1. **初始化变量**:`count`数组记录数字出现次数,`star_count`记录星号数量,`required`表示需要星号替换的重复次数总和。 2. **扩展右边界**: - 遇到星号时,增加`star_count`。 - 遇到数字时,更新其计数并计算新增的重复需求。 3. **收缩左边界**:当`required > star_count`时,逐步左移`left`指针,减少窗口内的重复需求。 4. **更新大长度**:每次调整窗口后,计算当前窗口长度并更新大值。 ### 示例说明 - 输入`"1*1"`,星号可替换为任意数字,最长有效子长度为3(如`1*1`替换为`101`)。 - 输入`"*11"`,星号替换为不同数字,最长长度为3(如`*11`替换为`012`)。 ### 复杂度分析 - **时间复杂度**:$O(n)$,每个字符多被访问两次(进入和离开窗口)。 - **空间复杂度**:$O(1)$,仅使用固定大小的计数数组。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值