假设字符串A和字符串B都是由字母组成,不含有特殊字符。给定两个分别由字母组成的字符串A和字符串B,如何最快地判断字符串B中所有字母是否都在字符串A里?比如下面4个字符串,字符串B和字符串C中所有的字符都在字符串A中,但是一字符串D中并非所有字符都在字符串A中(字符‘E’不在字符串A中)
字符串A:ABCD
字符串B:AB
字符串C:BCCBA
字符串D:CE
这个题目不难,July和他伙伴们在《程序员编程艺术:面试和算法心得》中也分析的非常透彻,从最容易想但是效率最低的循环遍历,到逐渐优化先排序再轮询,再到更为优化的素数乘除法,直到最后的位操作。逐渐进阶,算法越来越巧妙,时间复杂度和空间复杂度也越来越低。书中全部4中实现都清晰简单,一目了然。之所以还要写下来,主要是针对最后也是最有技巧的位操作算法,举出真实应用中可能会遇到的一个小陷阱。
解决方法非常简单,我们可以对字符串A,用位运算(26bit整数表示)计算出一个“签名”,再用B中的字符到A里面进行查找。
void CharactersExisting(const string &stringA, const string &stringB)
{
int hash = 0; //save character existing or not (each character one bit)
// Traverse the string A to record each character
for(int i = 0; i < stringA.length(); ++i)
{
hash |= (1<<(stringA[i] - 'A'));
}
// Traverse the string B to find each character
for(int i = 0; i < stringB.length(); ++i)
{
if(!(hash & (1<<(stringB[i] - 'A'))))
{
cout<
读完上面的代码,会发现简单易懂,构思巧妙。代码巧妙的利用了位操作的“|”运算,清楚地标记了字符串A中包含的字符,然后利用位操作的“&”运算查找字符串B中的字符是否在A中存在。
但是,看到这里,不知道看官您是否发现了问题?如果字符串A或者字符串B中含有小写字母肿么办?
先来看一下英文字母在ASIIC码表中的情况
根据上图,很容易发现,如果stringA[i]是小写字母,那么stringA[i] - 'A'的值将大于等于32,此时(1<<(stringA[i] - 'A'))的结果将超出int的取值范围。也许你会觉得这有什么难的,把int hash =0; 重新定义为long long hash =0; 不就解决问题了吗?那么,恭喜你,犯了与我同样的错误。最初想到这个问题的时候,我也是这么解决的,但是很快程序一运行,我就发现了问题。
我输入的字符串A是“abc”,当执行第一次循环标记字符串A中所有的字符的时候,可以清楚地看到,此时stringA[0]的字符是'a',hash是64位整型。根据ASIIC表,'a' - 'A' = 97-65 = 32,因此理论上hash应该等于1左移32位,也就是0x0000000010000000。但是,实际上hash的值与我们想象的并不一样,从图中我们可以看到此时hash的值为1。
那么为什么会发生这种情况呢?这就要从位运算“<<”说起了。“<<”是根据它的左值得类型来判断位移后的值是否在有效取值范围内的。因为我们在代码中使用了(1<<(stringA[i] - 'A')),所以移位操作是根据1的类型(即int)进行判断的,从而左移32位就超出了取值范围。
根据上面的分析,我们将代码简单修改一下,就可以完成对全部26个字母(大小写)的支持。
void CharactersExisting(const string &stringA, const string &stringB)
{
long long hash = 0; //save character existing or not (each character one bit)
long long one = 1;
// Traverse the string A to record each character
for(int i = 0; i < stringA.length(); ++i)
{
hash |= (one<<(stringA[i] -'A'));
}
// Traverse the string B to find each character
for(int i = 0; i < stringB.length(); ++i)
{
if(!(hash & (one<<(stringB[i] -'A'))))
{
cout<
如果要支持更多的字符,恐怕还是需要使用真正的hash表,这个已经超出了本文的讨论范围。
今天做练习,找到了另一个占用更多空间,但是可以支持全部字符串的方法
void CharactersExisting(/*const string &stringA, const string &stringB*/)
{
string stringA;
cout<<"please input original string(A~Z,a~z): "<>stringA;
string stringB;
cout<<"please input characters need to find: "<>stringB;
int hash[256] = {0};
// Traverse the string A to record each character
for(int i = 0; i < int(stringA.size()); ++i)
{
hash[stringA[i]] = 1;
}
// Traverse the string B to find each character
for(int i = 0; i < int(stringB.size()); ++i)
{
if(hash[stringB[i]] == 0)
{
cout<
其实还有其它的方法,比如结合上面的两种处理方式,使用long long hash[4]。实现方式自己想吧。
其实这个题目是在回文字符串Manancher算法之前看的,题目同样来自《程序员编程艺术:面试和算法心得》,本来以为很简单,事实上也真的很简单。但是昨天心血来潮用vc2012实现了一下,发现仍然问题多多。看来每一个小算法都不容小觑,必须要踏踏实实一步一步来。