数组—滑动窗口/字母哈希表简单实现 实践leetcode438, leetcode76

数组,最基础的数据结构。其最强大的用处就是:快速查询,能够在O(1)的实践复杂度下完成数据的查询
这里简单介绍两个常见操作滑动窗口, 字母表的计数索引,并以两个经典面试题来实践此算法思路。

1. 滑动窗口

滑动窗口,通常在数组中的特点就是有左右两个边界,标识着这个区间[l, r]为一个窗口。通常此窗口在数组的合法区间内进行滑动,并且动态地记录一些有用的数据,很多情况下,能够极大地提高算法地效率。

2. 字母表的哈希表实现

这里先引入三个重要的数据区间,ASCII码的十进制表示:

a~z [97~122]
[91~96] 6个字符我们暂时不关心
A~Z [65~90]
1~9 [48~57]

当一个字符串只由小写或者大写字母组成的时候,我们可以使用
int freq[26]这个数据记录此字符串中的每个字母的个数
下面是部分代码片段,此代码可以用来记录s中每个小写字母的个数

/*C/C++*/
string s = "abcccabcd";
int freq[26] = {0}
for(int i = 0 ; i < s.size() ; i++) 
	freq[s[i]-'a']++;

于是,考虑一个字符串中全是字母,既包含大写又包含小写
此时可以考虑建立64大小的数组,当然准确的说是26+6+26=58.

此时上述的代码可以改为

/*C/C++*/
string s = "AbCccAbcD";
int freq[64] = {0}
for(int i = 0 ; i < s.size() ; i++) 
	freq[s[i]-'A']++;

3. leetcode 438. 找到字符串中所有字母异位词(leetcode 438. Find All Anagrams in a String)

题意为:

给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。 字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。
示例 1:
输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。
示例 2:
输入:
s: "abab" p: "ab"
输出:
[0, 1, 2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的字母异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的字母异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的字母异位词。

/* 算是简洁的了。时间复杂度O(N), 空间复杂度O(1)。
稍微难一点的同样思路的题目可以查看leetcode76号题。

1. 利用题目给出的信息:字符串全是a-z, ASCII码范围为[97, 122]。
2. 所以用长度为26的数组来统计每个字母的频率(Freqency)。
3. 同时,维护一个长度为p.size()大小的"滑动窗口", 窗口的左右边界分别为l, r。
下面是详细的代码实现,
*/
class Solution {
private:
	const int GAP = 97; // 一个偏移量,也可以设置为const char ch = 'a';
public:
	vector<int> findAnagrams(string s, string p) {

		if (s.size() < p.size()) return vector<int>();  // 排除不可能的情况

		int pFreq[26] = { 0 };
		for (int i = 0; i < p.size(); i++) pFreq[p[i] - GAP]++; 

		vector<int> ret;
		int sFreq[26] = { 0 };
		int l = 0, r = 0;   // [l, r]区间是我们需要判定的区间,但是只需要扫描频率数组即可。
		while (l <= s.size() - p.size()) {
                        //窗口大小<p.size(),扩大窗口
			if (r-l+1 <= p.size()) { sFreq[s[r++] - GAP]++; continue; }
			int i;
			for (i = 0; i < 26 && sFreq[i] == pFreq[i]; i++);
			if (i == 26) ret.push_back(l);  // 符合条件
			sFreq[s[l++] - GAP]--; // 缩小窗口
		}
		return ret;
	}
};

4. leetcode 76. 最小覆盖子串(leetcode 76.Minimum Window Substring)

给定一个字符串 S 和一个字符串 T,请在 S 中找出包含 T 所有字母的最小子串。
示例:
输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"
说明:
如果 S 中不存这样的子串,则返回空字符串 ""。
如果 S 中存在这样的子串,我们保证它是唯一的答案。

/**时间复杂度O(N),空间复杂度O(1);  技术:滑动窗口+计数索引(不知道是不是这样叫,可以理解为简单的Hash表实现)
这道题有一定难度,leetcode438 号题也可使用类似的思路,不过稍微简单一些。

1. 注意到题目的关键:"所有字母的最小子串",也就是说两个串都只能是字母。
2. 于是,可以开辟一个大小为64的数组,来存放数组中字母的频率(Frequency)。准确的说,
   通过字母的ASCII码作为数组的索引,开辟空间的大小为26+6+26=58:26个大写字母,26个小写字母,
   还有中间的6个非字母  A~Z[65~90]  非字母[91~96]  a~z[97~122]
3. 滑动窗口的使用:分三种情况来移动窗口:(这里令当前窗口的左右边界分别为l,r,窗口的大小为winSize=r-l+1)
   1) 当winSize < t.size()  r++;  也就是窗口右边界向右移动
   2) 当winSize == t.size() :
	   2.1) 当窗口中的字符已经符合要求了,直接返回return,已经找到了
	   2.2) 否则r++,窗口右边界向右移动
   3) 当winSize > t.size()
	   2.1) 当窗口中的字符已经符合要求了,l++,窗口左边界向右移动
	   2.2) 否则r++,窗口右边界向右移动

4. 上面是滑动窗口的使用思路,具体实现上有一定的不同,下面是需要考虑到的要点:
   1) 啥叫作窗口中的字符已经符合要求了?
   1) 窗口滑动时的操作是关键
   2) 要考虑到数组越界的问题

下面是Accepted代码:

string minWindow(string s, string t) {
	if (s.size() < t.size()) return "";

	int sFreq[64] = { 0 }, tFreq[64] = { 0 };  // frequency数组
	for (int i = 0; i < t.size(); i++) tFreq[t[i] - 'A']++;

	int l = 0, r = -1, edge[2] = { -1, s.size() + 1 };  //edge数组表示要求的串的左右边界
	while (l <= s.size() - t.size()) {

		// < t.size()  直接窗口右边界右移,循环continue
		if (r - l + 1 < t.size()) {
			if (r + 1 < s.size()) {      // 这里注意到数组越界
				sFreq[s[++r] - 'A']++; continue;
			}
			else break;
		}

		// >= t.size() 先判断当前窗口中的字符是否满足“题目要求”
		int i = 0;
		while (i < 64) {
			if (sFreq[i] < tFreq[i]) break;
			i++;
		}
		if (i < 64) {
                        // 这里注意到数组越界
			if (r + 1 < s.size()) sFreq[s[++r] - 'A']++;
			else sFreq[s[l++] - 'A']--;
		}
		else {
			if (r - l + 1 == t.size()) return string(s.begin() + l, s.begin() + r + 1); 
			else {
				if (r - l < edge[1] - edge[0]) {
					edge[0] = l;
					edge[1] = r;
				}
				sFreq[s[l++] - 'A']--;
			}
		}
	}
	return edge[0] == -1 ? "" : string(s.begin() + edge[0], s.begin() + edge[1] + 1);
}
  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值