leetcode 03:无重复字符的最长子串的三种解法

题面如下:

给定一个字符串 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 由英文字母、数字、符号和空格组成

我当时的第一反应很简单:用哈希表,一直向后扫描,记录当前的无重复字符长度,然后每当遇到一个重复的字符就将哈希表清空,长度与最大长度比较后归零,然后从当前位置重新开始扫描。但运行了一次发现这样会存在遗漏数据的情况:之前扫描过的、位于重复字符后面的那些字符也应当计入下一次扫描的总长度中。但是c++提供的unordered_map与unorder_set两个容器似乎都没有提供返回某一字符是第几个存储的这样的函数,我自己也懒得手搓一个哈希表出来,于是只能采取下下策:遍历每一个字符,并从那个字符开始向后扫描, 遇到重复字符就break。代码如下:

//1.哈希表法1:双循环
int lengthOfLongestSubstring(string s) 
{
	unordered_set<int> hash;
	int length = 0;
	int maxlength = 0;

	for (int i = 0; i < s.size(); i++)
	{
		for (int j = i; j < s.size(); j++)
		{
			if (hash.find(s[j]) == hash.end())
			{
				length++;
				hash.insert(s[j]);
				maxlength = max(length, maxlength);
			}
			else
			{
				hash.clear();
				length = 0;
				break;
			}

		}
	}
	return maxlength;
}

一个时间复杂度接近o(n^2)的双循环,虽然勉强能过……

后来去看官方题解,学到一个新方法:滑动窗口。

大概思路为:定义一左一右两个指针,分别代表滑动窗口的左右两边。左右指针初始都在最左边。之后不断向右移动右指针,直到窗口右边遇到字符串边界或是重复字符,计算当前长度并与最大长度比较,然后窗口左边界也开始向右移动,之前删除位于窗口内的字符元素,并再次判断右边界上的是否仍是重复字符。若是则继续移动左指针直到不是了为止,反之则继续移动右指针,之后步骤依然如上,如此循环。在此方法中,左右指针各自遍历了一次整个字符串,时间复杂度为o(n)。

代码如下:

//1.哈希表法2:滑动窗口
int lengthOfLongestSubstring(string s) {
	// 哈希集合,记录每个字符是否出现过
	unordered_set<char> occ;
	int n = s.size();
	int rk = -1, ans = 0;
	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;
}

这个方法巧妙地运用了各个变量在本题中的实际意义,最大化了每个变量的利用效率,进而避免了我之前的无脑双循环。

受之启发,加上前几周刚好学了二分法,又有了一个新思路:

我们将不同的可能的最大无重复字符串长度作为二分查找的对象,然后对每一个查找所得的长度做判断:判断整个字符串中是否存在这个长度的无重复字符串,如果是,则让二分法中的head,也就是最小值等于这个值(不等于这个值加一的原因是这个值有可能就是我们要找的最大值),其他部分就和普通二分查找一样。最后返回head的值即可。

关于判断的函数,同样也是使用了滑动窗口的思想:定义一个数组用于存储每种字符在窗口内出现的数量,再定义一个存储当前窗口中不同元素个数的数量的变量。维护一个长度等于我们所要求的长度的滑动窗口。窗口不断滑动的过程中,若这个变量等于我们传入的需要判断的那个长度,则可以返回true,反之若循环结束都没有返回过true,则返回false。时间复杂度为o(n)。

还有一些具体细节见代码:

//2.二分算法(二分算法的01泛型)
//二分法在这里用来搜索可能的最大的无重复长度
bool check(int length, string s)
{
	int count[256] = { 0 };//用于判断每一种字符的数量(ascII码一共256个)
	int k = 0;//判断当前的窗口中的不同的字符的数量
	for (int i = 0; i < s.size(); i++)
	{
		count[s[i]]++;
		if (count[s[i]] == 1)//若当前字符在窗口中的个数只有一个
		{
			k++;
		}
		if (i > length - 1)//窗口右边界开始移动
		{
			count[s[i - length]]--;
			if (count[s[i - length]] == 0;//若一个无重复字符离开窗口
			{
				k--;
			}
		}
		if (k == length)
			return true;
	}
	return false;
}
int lengthOfLongestSubstring(string s) 
{
	if (s.empty())
		return 0;
	int head, tail, mid;
	head = 1, tail = s.size();
	while (tail > head)
	{
		mid = (head + tail + 1) / 2;//+1可以使二分算法的mid值为非整数时会偏向与之相邻的两个整数中的较大的那个,这是为了防止前面mid=head,后面head又=mid的死循环
		if (check(mid, s))//用于判断当前的mid代表的长度在整个序列中是否存在无重复的序列
		{
			head = mid;
		}
		else
		{
			tail = mid - 1;
		}
	}
	return head;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值