寻找包含给定字符集合的最小子串
来源:互联网 发布:编辑杂志的软件 编辑:程序博客网 时间:2024/05/19 23:01
奉献几篇很早前写给朋友的稿子,后来由于其它原因无法出版就压了箱底。
今天拿出来晒晒太阳,看官觉得能入眼的话,就看看吧~
寻找包含给定字符集合的最小子串
现代的信息处理中,计算机发挥着极其重要的作用。而信息主要以字符串的形式显示在我们面前,所以对字符串的处理在程序领域中有很多的研究,我们在程序中也常会用到字符串和它的相关算法。
想想小学的时候,老师布置的词组造句的作业,我们能否写个程序自动帮老师去判断呢?
我们将这个问题抽象出来:给定一个字符串和一个字符集合,判断字符集合是否都在字符串中出现过;同时再求该字符串的最小子串(子串的长度最小,长度一样时取字典序最小),使得这个子串同样包含字符集合中的所有元素,字符均为小写英文字母。最小子串可能有多个,找到任意一个即可。
例如:
字符串S="abcdefg",字符集合D={'c','f'},那这个最小子串为S'="cdef"。
字符串S="cfcf",字符集合D={'c','f'},那这个最小子串为S'="cf"。
解法一
首先我们先规定下最小子串的判断标准:首先判断长度是否最小,其次如果长度一样,则根据字典序大小进行判断。需要注意的是,我们使用C++ STL中的std::string类的关系运算符默认是按字典序进行字符串大小比较的。所以我们需要定义minstr函数来比较之前定义的最小子串。minstr函数见代码清单1。
string minstr(string obj1, string obj2)
{
if (obj1.length() == obj2.length())
return min(obj1, obj2);
return obj1.length() > obj2.length() ? obj2 : obj1;
}
代码清单1 最小子串比较函数
因为只有当字符集合D里所有字符都在字符串S中出现过,最小包含字串才有可能存在,所以我们先来实现如何判断D中字符是否都在S中出现的问题。对于这个问题,我们只需要先遍历字符串,得到它的字符集合Ds,然后判断D是否是Ds的子集就可以了。理论上很简单,但在实际编码时有个问题,就是如何实现这个字符集合及其相关操作。如果你熟悉STL的话,可以使用std::set模板类及其相关函数,但是这些函数的内部实现都太重太复杂了,我们更倾向于找到轻量级的解决方案。由于我们的集合是针对英文字符而言的,英文字母总共只有26个,所以我们可以直接开大小为26的数组来模拟集合(当然也可以用位来进行,虽然操作会复杂点,但是用位运算来实现集合比较会很简单)。即使要考虑中文字符,开大小为65536的数组也足够了(MBCS字符集的中文字符占2字节,2^16=65536)。我们用is_subset函数来判断之前提到的子集问题,见代码清单2。
bool is_subset(bool subSet[26], bool set[26])
{
for (int i = 0; i < 26; i ++) {
if (subSet[i] && !set[i])
return false;
}
return true;
}
代码清单2 子集判断函数
有了代码清单2后,我们就可以用两个for循环来实现求最小完全包含子串的函数(见代码清单3)。其中最外面的for循环枚举最小子串长度,第二个for循环枚举子串起始位置,不停调用is_subset函数去判断该子串是否满足条件。当然了,还需要一个for循环对bool数组进行赋值。
string min_substr(string S, string D) // 字符串S,字符集合D
{
string ret;
bool Sset[26], Dset[26]; // 数组模拟集合
memset(Dset, 0, sizeof(Dset));
for (int i = 0; i < D.length(); i ++)
Dset[D[i]-'a'] = true; // 字符集合D初始化
for (int i = D.length(); i <= S.length(); i ++) {
for (int j = 0; j <= S.length() - i; j ++) {
memset(Sset, 0, sizeof(Sset));
for (int k = 0; k < i; k ++)
Sset[S[j+k]-'a'] = true; // 字符集合初始化
if (is_subset(Dset, Sset))
ret = ret.empty() ? S.substr(j, i) : minstr(ret, S.substr(j, i)); // substr(offest,length)是取子串函数
}
}
return ret;
}
代码清单3 基于is_subset函数的算法
代码清单3的时间复杂度很容易计算,设N=Len(S),则时间复杂度为O(N^3)。这是高效的算法吗?当然不是,让我们来优化它吧。
在代码清单3中,因为我们是按照子串的长度来进行枚举的,所以在处理子串的过程中,存在着相当多的重复计算,比如字符串”abcdefg”,计算长度为3起始位置为0时子串为”abc”,偏移为0和1的两个字符’a’和’b’,在长度为2起始位置为0的子串”ab”时早已经被计算过,只不过那些状态在每次计算前被memset函数清空了。这样,假如我们能利用之前计算得出的结果,就能够避免这些重复计算,从而提高计算的效率,我们用left 和right指针来标记最小子串的左端和右端,left要始终小于等于right;right指针不停地往右移动,当S[left,right]包含D集合中所有字符时,它就是一个可能的答案。记录完答案后left就该右移一个字符,因为加入left不移动,之后得到的S[left,right]子串,即使满足条件,但它的长度肯定大于当前,就不可能是最小子串了。总结下left右移的条件:当S[left]在子串的其它位置出现过或者S[left]不在D集合中时,left就可以往右移动了,因为S[left]是一个多余的值。最后,对所有可能的答案进行minstr函数的比较,就能得出最小子串了。
例如,字符串S="aaba",字符集合D={'a','b'},程序运行流程为:
“ab”和”ba”两个可能答案经过比较后,最小子串为”ab”。瞧,这例子中N=4,总共也就运行了7步而已,比起代码3可省了不少计算,只要left和right指针各自扫描字符串一遍就可以了,这个算法能够做到时间复杂为O(N)!我们还需要一个更有效方法判断集合中的字符是否都出现了,我们用int记录D集合中的不同字符出现的个数,并且用int数组代替原来的bool数组来实现字符计数。详细算法见代码清单4。
string min_substr2(string S, string D) // 字符串S,字符集合D
{
string ret;
int Sset[26], Dset[26]; // int数组模拟集合,还有引用计数
int Ds; // 字符集合D在字符串S中出现的不同个数
memset(Dset, 0, sizeof(Dset));
memset(Sset, 0, sizeof(Sset));
Ds = 0;
for (int i = 0; i < D.length(); i ++)
Dset[D[i]-'a'] = 1; // 字符集合D初始化
int l = 0;
for (int r = 0; r < S.length(); r ++) {
if (Dset[S[r]-'a'] == 1 && Sset[S[r]-'a'] == 0)
Ds ++; // S中出现新的D集合中字符
Sset[S[r]-'a'] ++;
for (; l <= r; l ++) {
if (Dset[S[l]-'a'] == 1 && Sset[S[l]-'a'] == 1) {
if (Ds == D.length()) { // D集合中字符全部出现
ret = ret.empty() ? S.substr(l, r-l+1) : minstr(ret, S.substr(l, r-l+1));
Sset[S[l++]-'a'] --; // left右移
Ds --;
}
break;
}
Sset[S[l]-'a'] --;
}
}
return ret;
}
代码清单4 基于left和right扫描的算法
总结
我们从朴素的方法开始摸索,寻找重复计算的数据,分析为何会出现重复,这个过程往往能让你发现问题的本质,再提出能避免重复计算的方案来降低时间复杂度。这样逐步优化的好处是因为朴素算法的正确性是不言而喻的,而新算法只是解决重复计算的问题,所以新算法的正确性也很容易被证明。
扩展问题
还不知您发现一个有趣的现象没,最小子串的第一个字符和最后一个字符在该子串中只会出现一次,即子串除去头尾后那两个字符不会再出现,这是为什么呢?这个问题的完整证明就留给读者您自己来完成吧。
- 寻找包含给定字符集合的最小子串
- 寻找包含给定字符集合的最小子串
- 包含给定字符集的最小子串
- 给定一个子串,寻找没有重复字符的最长子串
- 给定一个字符串,包含中文字符和英文字符,取给定大小字节的子串。
- 寻找最长子串,该子串只包含两种字符
- [LeetCode] minimum window 包含所有字符的最小子字符串
- 字符串第一次包含目标字符集合元素的最小范围
- 经典算法学习——求包含某两个字符的最小子串的长度
- 包含所有指定字符的最小子串(shortest substring containing all given characters)
- 源字符串中包含目标字符串所有字符的最小子串
- 求包含字符集的最小子串
- 求字符串内不包含重复字符的最长子串的集合
- 给定字符串str1和str2,求str1中子串含有str2所有字符的最小子串长度
- [经典面试题][搜狗]在一个字符串中寻找包含全部出现字符的最小字串
- leetcode题3 寻找字符串不包含重复字符的最长子字符串
- 40.给字符串s1、s2,在s1中找包含s2里所有字符的最小子串
- 给字符串s1、s2,在s1中找包含s2里所有字符的最小子串
- java人发展
- CUDA入门
- 第九章 设计数据库的方法
- NSScanner类参考
- 编程语言的进化
- 寻找包含给定字符集合的最小子串
- cocoa2D 基础知识教程
- 利用VS 2010快速建立一個WCF程式
- android init.rc语法标准
- Windows phone 8 屏幕适配总结
- 从劫匪的行动来看同步、异步、阻塞、非阻塞
- Spring.Net+NHibenate+Asp.Net mvc +ExtJs 系列 1---准备
- 对计算机研究生的看法
- Linux下链接相互依赖的.a文件遇到undefined reference问题总结