假设这有两个分别由字母组成的字符串A另外字符串B,字符串B的字母数较字符串A少一些。什么方法能最快地查出字符串B所有字母是不是都在字符串A里?也就是说判断字符串B是不是字符串A的真子集(为了简化,姑且认为两个集合都不是空集,即字符串都不为空。)。为了简单起见,我们规定输入的字符串只包含大写英文字母。
实现函数bool compare(string &A,string &B)
比如,如果是下面两个字符串:
String 1: ABCDEFGHLMNOPQRS
String 2: DCGSRQPO
答案是true,即String2里的字母在String1里也都有,或者说String2是String1的真子集。
如果是下面两个字符串:
String 1: ABCDEFGHLMNOPQRS
String 2: DCGSRQPZ
答案是false,因为字符串String2里的Z字母不在字符串String1里。
点评:
-
题目描述虽长,但题意很明了,就是给定一长一短的两个字符串A,B,假设A长B短,要求判断B是否包含在字符串A中,即B?(-A。
-
似乎简单,但实现起来并不轻松,且如果面试官步步紧逼,一个一个否决你能想到的方法,要你给出更好、最好的方案时,恐怕就要伤不少脑筋了。
在揭晓答案之前,读者最好先想一想,看看自己能想到的最好方案是什么,是否与本文作者所见略同。
判断string2中的字符是否在string1中?:
String 1: ABCDEFGHLMNOPQRS
String 2: DCGSRQPO
最直观也是最简单的思路是,针对string2中每一个字符,一一与string1中每个字符依次轮询比较,看它是否在String1中。
代码可如下编写:
//copyright@caopengcs 2013-01-14
bool compare(string &a,string &b)
{
for (int i = 0; i < b.length(); ++i) {
int j;
for (j = 0; (j < a.length()) && (a[j] != b[i]); ++j)
;
if (j >= a.length())
{
return false;
}
}
return true;
}
假设n是字符串String1的长度,m是字符串String2的长度,那么此算法,需要O(nm)次操作,以上面的例子来说,将会有168 = 128次操作。显然,时间开销太大,应该找到一种更好的办法。
一个稍微好一点的方案是先对这两个字符串的字母进行排序,然后同时对两个字串依次轮询。两个字串的排序需要(常规情况)O(m log m) + O(n log n)次操作,之后的线性扫描需要O(m+n)次操作。
同样拿上面的字串做例子,将会需要164 + 83 = 88,再加上对两个字串线性扫描的16 + 8 = 24的操作。(随着字串长度的增长,你会发现这个算法的效果会越来越好)
关于排序方法,可采用最常用的快速排序,C有库函数qsort,C++有库函数sort,排序不是重点,因此使用库函数好一些。
//copyright@caopengcs 2014-01-14
//注意A B中可能包含重复字符,所以注意A下标不要轻易移动。这种方法改变了字符串。如不想改变请自己复制
bool compare(string &a,string &b)
{
sort(a.begin(),a.end());
sort(b.begin(),b.end());
for (int pa = 0, pb = 0; pb < b.length();)
{
while ((pa < a.length()) && (a[pa] < b[pb]))
{
++pa;
}
if ((pa >= a.length()) || (a[pa] > b[pb]))
{
return false;
}
//a[pa] == b[pb]
++pb;
}
return true;
}
此方案与上述相比,区别在于不用排序。采用线性时间的计数方法,假设需要比较字符串A(n)中是否包含字符串B(m),统计A中出现的字符O(n),比较B中是否有出现的字符O(m),总计时间复杂度为:O(n+m)。
代码如下:
// copyright@caopengcs 2014-01-14
// modified by @古道西风 2014-01-14
bool compare(string &a,string &b)
{
vector<int> have;
have.resize(26,0);
for (int i = 0; i < a.length(); ++i)
{
++have[a[i] - 'A'];
}
for (int i = 0; i < b.length(); ++i)
{
//若A中只需要包含同一个相同的字符即可代表B中重复出现的字符
//即A:CDEFAB B:AABBCC 合法
if (have[b[i] - 'A'] == 0)
{
return false;
}
//若A中需要包含B中所有重复出现字符
//即A:AAACCDBBCCE B:AABBCC 合法
//即A:BCDEFA B:AABBCC 非法
//if (have[b[i] - 'A']-- == 0) {
// return false;
//}
}
return true;
}
上述方案中,较好的方法是先对字符串进行排序,然后再线性扫描,总的时间已经优化到了:O(m+n),貌似到了极限,还有没有更好的办法?
可以对短字符串进行轮询(此思路的叙述可能与网上的一些叙述有出入。最好是把短的先存储,那样,会降低时间复杂度),把其中的每个字母都放入一个Hashtable里(始终设m为短字符串的长度,那么此项操作成本是O(m)或8次操作)。然后轮询长字符串,在Hashtable里查询短字符串的每个字符,看能否找到。如果找不到,说明没有匹配成功,轮询长字符串将消耗掉16次操作,这样两项操作加起来一共只有8+16=24次。
当然,理想情况是如果长字串的前缀就为短字串,只需消耗8次操作,这样总共只需8+8=16次。
或如梦想天窗所说: 我之前用散列表做过一次,算法如下:
- hash[26],先全部清零,然后扫描短的字符串,若有相应的置1,
- 计算hash[26]中1的个数,记为m
- 扫描长字符串的每个字符a;若原来hash[a] == 1 ,则修改hash[a] = 0,并将m减1;若hash[a] == 0,则不做处理
- 若m == 0 or 扫描结束,退出循环。
这种方法其实和1.3节方法类似。
代码实现,也不难,如下:
// copyright@caopengcs 2014-01-14
bool compare(string &a,string &b)
{
vector<int> hash;
hash.resize(26,0);
int m = 0;
for (int i = 0; i < b.length(); ++i)
{
int x = b[i] - 'A';
if (hash[x] == 0)
{
hash[x] = 1;
++m;
}
}
for (int i = 0; i < a.length(); ++i)
{
int x = a[i] - 'A';
if (hash[x] == 1)
{
--m;
hash[x] = 0;
}
}
return m == 0;
}
继续追问,还有更好的方案么?
或许想到:O(n+m)已经是能得到的最好结果了,对每个字母至少访问一次才能完成这项操作,而前述的后两个方案是刚好是对每个字母只访问一次。
ok,下面给出的方案更让人思路大开:
假设有一个仅由字母组成字串,让每个字母与一个素数对应,从2开始,往后类推,A对应2,B对应3,C对应5,......。遍历第一个字串,把每个字母对应素数相乘。最终会得到一个整数。
利用上面字母和素数的对应关系,对应第二个字符串中的字母,然后轮询,用每个字母对应的素数除前面得到的整数。如果结果有余数,说明结果为false。如果整个过程中没有余数,则说明第二个字符串是第一个的子集了(判断是不是真子集,可以比较两个字符串对应的素数乘积,若相等则不是真子集)。
思路总结如下:
- 按照从小到大的顺序,用26个素数分别与字符'A'到'Z'一一对应。
- 遍历长字符串,求得每个字符对应素数的乘积。
- 遍历短字符串,判断乘积能否被短字符串中的字符对应的素数整除。
- 输出结果。
如前所述,算法的时间复杂度为O(m+n)的最好的情况为O(n)(遍历短的字符串的第一个数,与长字符串素数的乘积相除,即出现余数,便可退出程序,返回false),n为长字串的长度,空间复杂度为O(1)。
不过,正如原文中所述:“现在我想告诉你 —— Guy的方案在算法上并不能说就比我的好。而且在实际操作中,你很可能仍会使用我的方案,因为它更通用,无需跟麻烦的大型数字打交道。但从”巧妙水平“上讲,Guy提供的是一种更、更、更有趣的方案。”
ok,如果你有更好的思路,欢迎在本文的评论中给出,非常感谢。
这种方法的缺点是可能整数溢出……
// copyright@caopengcs
//此方法只有理论意义,因为整数乘积很大,有溢出风险
bool compare(string &a,string &b)
{
const int p[26] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59,61, 67, 71, 73, 79, 83, 89, 97, 101};
int f = 1;
for (int i = 0; i < a.length(); ++i)
{
int x = p[a[i] - 'A'];
if (f % x)
{
f *= x;
}
}
for (int i = 0; i < b.length(); ++i)
{
int x = p[b[i] - 'A'];
if (f % x)
{
return false;
}
}
return true;
}
最好的思路是对字符串A,用位运算(26bit整数表示)计算出一个“签名”,再用B中的字符到A里面进行查找。这个方法的实质是用一个整数代替了hashtable,空间复杂度可以降低为O(1)。时间复杂度还是O(n + m)。
// copyright@caopengcs
// “最好的方法”,时间复杂度O(n + m),空间复杂度O(1)
bool compare(string &a,string &b)
{
int hash = 0;
for (int i = 0; i < a.length(); ++i)
{
hash |= (1 << (a[i] - 'A'));
}
for (int i = 0; i < b.length(); ++i)
{
if ((hash & (1 << (b[i] - 'A'))) == 0)
{
return false;
}
}
return true;
}