@Leetcode无重复字符的最长子串--Longest Substring Without Repeating Characters[C++]

问题描述

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

示例 1:

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

示例 2

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

示例 3

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

解题方法及复杂度分析

暴力法

思路

逐个检查所有的子字符串,看它是否不含有重复的字符。

算法

封装一个判断函数 bool allUnique(string s, int start, int end),该函数会判断子字符串中的字符都是唯一的,否则返回false。算法首先遍历给定字符串 s 的所有可能的子字符串并调用函数 allUnique。如果判断函数返回值为 true,那么我们将会更新无重复字符子串的最大长度的答案。

  • 为了枚举给定字符串的所有子字符串,需要枚举它们开始和结束的索引。假设开始和结束的索引分别为 i i j j 。那么我们有 0 i < j n 0 \le i < j \le n (这里的结束索引 j j 是按惯例排除的)。因此,使用 i i 从 0 到 n 1 n-1 以及 j j i + 1 i+1 n n 这两个嵌套的循环,我们可以枚举出 s 的所有字符串。
  • 要检查一个字符串中是否有重复字符,我们可以使用集合。遍历字符串中的所有字符,并将它们逐个放入 set 中。在放置一个字符之前,算法检查该集合是否已经包含它。如果包含,算法返回 false。循环结束后,算法返回 true

复杂度分析

  • 时间复杂度: O ( n 3 ) O(n^3)
    – 要验证索引范围在 [ i , j ) [i,j) 内的字符是否都是唯一的,我们需要检查该范围中的所有字符。因此,它将花费 O ( j i ) O(j-i) 的时间。
    – 对于给定的 i,对于所有的 j [ i + 1 , n ] j \in [i+1,n] 所耗费的时间总和为:
    i + 1 n O ( j i ) \sum_{i+1}^{n}O(j-i)
    因此,执行所有步骤耗去的时间总和为:
    O ( i = 0 n 1 ( j = i + 1 n ) ) = O ( i = 0 n 1 ( 1 + n i ) ( n i ) 2 ) = O ( n 3 ) O(\sum_{i=0}^{n-1}(\sum_{j=i+1}^{n}))=O(\sum_{i=0}^{n-1}\frac{(1+n-i)(n-i)}{2})=O(n^3)

  • 空间复杂度: O ( m i n ( n , m ) ) O(min(n,m)) ,我们需要 O ( k ) O(k) 的空间来检查子字符串中是否有重复字符,其中 k k 表示 set 的大小。而 set 的大小取决于字符串 n n 的大小以及字符集/字母 m m 的大小。

滑动窗口

算法

暴力法时间复杂度过高,运行时间太慢。暴力法会反复检查一个子字符串是否含有有重复的字符,但这没有必要。如果从索引 i i j 1 j-1 之间的字符串 s i j s_{ij} 已经被检查为没有重复字符。我们只需要检查 s [ j ] s[j] 对应的字符是否已经存在于子字符串 s i j s_{ij} 中。

通过使用滑动窗口,可以用 O ( 1 ) O(1) 的时间来完成对字符是否在当前的子字符串中的检查。

滑动窗口是数组/字符串问题中常用的抽象概念。窗口通常是在数组/字符串中由开始和结束索引定义的一系列元素的集合,即 [ i , j ) [i,j) (左闭,右开)。而滑动窗口是可以将两个边界向某一方向“滑动”的窗口。例如,我们将 [ i , j ) [i,j) 向右滑动1个元素,则它将变为 [ i + 1 , j + 1 ) [i+1,j+1) (左闭,右开)。

在此问题中,使用 set 将字符存储在当前窗口 [ i , j ) [i,j) (最初 j = i j=i )中。然后向右滑动索引 j j ,如果它不在 set 中,继续滑动 j j 。直到 s [ j ] s[j] 已经存在于 set 中。此时,我们找到的没有重复字符的最长字符串将会以索引 i i 开头。如果对所有的 i i 这样做,就可以得到答案。

复杂度分析

  • 时间复杂度: O ( 2 n ) = O ( n ) O(2n)=O(n) ,在最糟糕的情况下,每个字符将被 i i j j 访问两次。
  • 空间复杂度: O ( m i n ( m , n ) ) O(min(m,n)) ,与之前的方法相同。滑动窗口法需要 O ( k ) O(k) 的空间,其中 k k 表示 set 的大小。而 set 的大小取决于字符串 n n 的大小以及字符集/字母 m m 的大小。

优化的滑动窗口

滑动窗口最多需要执行 2n 个步骤。事实上,可以被进一步优化为仅需要 n 个步骤。可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。当我们找到重复的字符时,可以立即跳过该窗口。

也就是说,如果 s [ j ] s[j] [ i , j ) [i,j) 范围内有与 j j\prime 重复的字符,我们不需要逐渐增加 i i 。可以直接跳过 [ i , j ] [i,j\prime] 范围内的所有元素,并将 i i 变为 j + 1 j\prime+1

复杂度分析

  • 时间复杂度: O ( n ) O(n) ,索引 j j 将会迭代 n n 次。
  • 空间复杂度: O ( m i n ( m , n ) ) O(min(m,n)) ,与滑动窗口方法相同。

程序实现

暴力法

	class Solution {
	public:
    	int lengthOfLongestSubstring(string s) {
        	int n = s.size();
        	int ans = 0;
        	for (int i = 0; i < n; i++) {
        	    for (int j = i + 1; j <= n; j++) {
        	        if (allUnique(s, i, j)) ans = max(ans, j - i);
        	    }
        	}
        	return ans;
    	}
    	bool allUnique(string s, int start, int end) {
    	    set<char> set;
    	    for (int i = start; i < end; i++) {
    	        char ch = s[i];
    	        if (set.count(ch)) return false;
    	        set.insert(ch);
    	    }
    	    return true;
    	}
	};

滑动窗口

	class Solution {
	public:
	    int lengthOfLongestSubstring(string s) {
	        int n = s.size();
	        int ans = 0, i = 0, j = 0;
	        set<char> set;
	        while (i < n && j < n) {
	            if (!set.count(s[j])) {
	                set.insert(s[j++]);
	                ans = max(ans, j - i);
	            }
	            else {
	                set.erase(s[i++]);
	            }
	        }
	        return ans;
	    }
	};

优化的滑动窗口

	class Solution {
	public:
	    int lengthOfLongestSubstring(string s) {
	        int n = s.length(), ans = 0;
	        map<char, int> map;
	        for (int j = 0, i = 0; j < n; j++) {
	            if (map.count(s[j])) {
	                i = max(map.find(s[j])->second, i);
	                map.erase(map.find(s[j]));
	            }
	            ans = max(ans, j - i + 1);
	            map.insert(pair<char, int>(s[j], j + 1));
	        }
	        return ans;
	    }
	};

数据结构说明

set

1、代码

	template < class T,					// set::key_type/value_type
			   class Compare = less<T>, //set::key_compare/value_compare
			   class Alloc = allocator<T>, //set::allocator_type
			 > class set;

2 、定义及说明

set 是按照特定顺序存储不同元素的容器。

在 set 中,元素的实值也可以用来区分不同的元素,即元素相互之间实值不相同。一旦元素被放入容器中就不允许修改,只允许被插入或删除。

set 内部所有元素都会根据元素的键值自动排序。

set 容器在通过键值直接查询特定的元素方面比unordered_set慢,但是它们可以在基于顺序的 subsets 上直接迭代。

set 的典型应用是二叉搜索树。

3、参数说明

序号 名称 说明
1 T 元素的类型。在 set 容器中每个元素可通过它们的实值去区别(每个实值也就是元素的键值)
2 Compare 一个二元谓词,可以接受与元素相同类型的两个参数并返回一个bool值。表达式 c o m p ( a , b ) comp(a,b) ,其中 c o m p comp 是这种类型的对象, a a b b 是键值,如果 a a 被判断在函数定义的严格弱序中位于 b b 之前,则返回 true。set 对象使用这个表达式来确定元素在容器中遵循的循序以及两个元素键值是否相等(通过reflexivelv比较它们:如果 !comp(a,b) && !comp(b,a),它们是等价的)。set 容器中没有两个元素可以是相等的。这可以是函数指针或函数对象。默认为,返回与应用小于运算符 ( a &lt; b ) (a&lt;b) 相同的值。
3 Alloc 用于定义存储分配模型的分配器对象的类型。默认情况下,使用allocator类模板,该模板定义最简单的内存分配模型,并且与实值无关。

4、函数说明

  • pair<iterator,bool> insert (const value_type& val);
    函数功能:插入元素。
    使用方法:std::set::insert

  • size_type erase (const value_type& val);
    函数功能:删除元素。
    使用方法:std::set::erase

  • size_type count (const value_type& val) const;
    函数功能:依照 val 搜索容器中每个元素并返回匹配数量。
    使用方法:std::set::count

map

1、代码

	template < class Key,               //map::key_type
          	   class T,                 //map::mapped_type
               class Compare = less<Key>, //map::key_compare
           	   class Alloc = allocator<pair<const Key,T> > //map::allocator_type
             > class map;

2、定义及说明

map 是以键值和映射值为组合的形式存储元素的关联式容器,元素的存储自动排序。

在 map 中,通常使用键值去排序和区别元素,而映射值用来存储实际内容,二者一一对应。键值和映射值的种类可以不同,并在成员类型 value_type 中组合在一起,value_type 是组合两者的 pair 类型:typedef pair<const Key, T> value_type;

map 容器通过键值查找单个元素比 unordered_map 慢,但是它们可以直接在基于它们顺序的子集上进行迭代。

在 map 中映射值可以直接通过对应的键值查询,方式是使用换位运算符(operator[])

map 的典型应用是二叉搜索树。

3、参数说明

序号 名称 说明
1 Key 键值类型。在 map 中每个元素都可以通过键值区别。
2 T 映射值的类型。在 map 中每个元素以映射值形式存储数据。
3 Compare 一个二元谓词,可以接受与元素相同类型的两个参数并返回一个bool值。表达式 c o m p ( a , b ) comp(a,b) ,其中 c o m p comp 是这种类型的对象, a a b b 是键值,如果 a a 被判断在函数定义的严格弱序中位于 b b 之前,则返回 true。map 对象使用这个表达式来确定元素在容器中遵循的循序以及两个元素键值是否相等(通过reflexivelv比较它们:如果 !comp(a,b) && !comp(b,a),它们是等价的)。map 容器中没有两个元素可以是相等的。这可以是函数指针或函数对象。默认为,返回与应用小于运算符 ( a &lt; b ) (a&lt;b) 相同的值。
4 Alloc 用于定义存储分配模型的分配器对象的类型。默认情况下,使用allocator类模板,该模板定义最简单的内存分配模型,并且与实值无关。

4、函数说明

  • pair<iterator,bool> insert (const value_type& val);
    函数功能:插入元素。
    使用方法:std::map::insert
  • void erase (iterator position);
    函数功能:删除元素。
    使用方法:std::map::erase
  • iterator find (const key_type& k);
    函数功能:根据键值查找容器中是否有相等键值的元素,如果找到返回一个迭代器,反之返回一个迭代器指向map::end
    使用方法:std::map::find
  • size_type count (const key_type& k) const;
    函数功能:根据键值查找容器中是否有相等键值的元素并返回匹配数量。

@ 山东·威海 2019.01.20

猜你喜欢

转载自blog.csdn.net/weixin_43340943/article/details/85794332