算法练习 - 字符串包含

题目描述

假设字符串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<<stringB[i]<<" is not exit"<<endl;	
			return;
		}
	}
	cout<<"All characters in "<<stringB<<" exist in "<<stringA<<endl;
}

 

读完上面的代码,会发现简单易懂,构思巧妙。代码巧妙的利用了位操作的“|”运算,清楚地标记了字符串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<<stringB[i]<<" is not exit"<<endl;	
			return;
		}
	}
	cout<<"All characters in "<<stringB<<" exist in "<<stringA<<endl;
}

如果要支持更多的字符,恐怕还是需要使用真正的hash表,这个已经超出了本文的讨论范围。

 

另一个相对比较费空间的做法

今天做练习,找到了另一个占用更多空间,但是可以支持全部字符串的方法

void CharactersExisting(/*const string &stringA, const string &stringB*/)
{
	string stringA;
	cout<<"please input original string(A~Z,a~z): "<<std::endl;
	cin>>stringA;

	string stringB;
	cout<<"please input characters need to find: "<<std::endl;
	cin>>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<<stringB[i]<<" is not exit"<<endl;	
			return;
		}
	}
	cout<<"All characters in "<<stringB<<" exist in "<<stringA<<endl;
}

其实还有其它的方法,比如结合上面的两种处理方式,使用long long hash[4]。实现方式自己想吧。

 

 

题外话

其实这个题目是在回文字符串Manancher算法之前看的,题目同样来自《程序员编程艺术:面试和算法心得》,本来以为很简单,事实上也真的很简单。但是昨天心血来潮用vc2012实现了一下,发现仍然问题多多。看来每一个小算法都不容小觑,必须要踏踏实实一步一步来。

 

    原文作者:eva_qu
    原文地址: https://blog.csdn.net/beggar200/article/details/50215035
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞