数组,最基础的数据结构
。其最强大的用处就是:快速查询,能够在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);
}