问题描述:给定两个字符串str1和str2,字符串中只包含字母(区分大小写)和数字,并且字符可以重复。现在让你判断str2中的字符是否都来源于str1中的字符(假设str1的长度为m,str2的长度为n)?限制时间复杂度为O(m+n),空间复杂度为O(1)。
分析:采用暴力法可以很容易地实现要求,但是效率比较低,需要拿str2中的每一位去和str1中的每一位进行比较,因此时间复杂度为O(mn),不符合题目要求,空间复杂度为O(1)。
因此需要寻找新的解法。这里我提供三种解法,第一种解法利用哈希表,第二中解法利用素数,第三种解法利用编码(在穷举子集合问题中我已经详细介绍过了,读者可以查看我的博客中的“穷举子集合”这篇博客,这里不再解释)。
第一种解法(利用哈希表):
可以建立一个长度为62的哈希表,用布尔数组模拟即可,因为布尔类型占用字节少,可以节省空间。
假设这个数组为hash[0]~hash[61],一开始全部初始化为false,表示不存在这个字符,其中下标0~25对用A~Z,26~51对应a~z,52~61对应0~9。
将str1中的每个字符哈希到对应的数组元素中,并将元素赋值为true。
可以先假设str2中的字符全部来自于str1中,可以设置标志变量实现。
将str2中的每个字符也哈希到对应的位置,如果该位置元素为true,那么接着往后比较,如果比较到某一位,哈希表中某元素为false,那么直接返回false即可。
如果执行到最后都没有返回false,那么说明str2中的字符全部来自于str1中,返回true即可。
这种解法就是利用哈希表实现,需要浪费一点空间,但是可以满足题目要求,并且时间复杂度为O(m+n),空间复杂度为O(1)。
第二种解法(利用素数):
我们可以让字符串str1中的每个字符与一个素数对应,从2开始,往后类推,A对应2,B对应3,C对应5,......。遍历第一个字符串str1,把每个字符对应素数相乘。最终会得到一个整数。
利用上面字母和素数的对应关系,对应第二个字符串中的字符,然后轮询,用每个字符对应的素数除前面得到的整数。
如果结果有余数,说明该字符不是来源于str1中,直接返回false。
如果整个过程中没有余数,则说明第二个字符串是第一个的子集了(判断是不是真子集,可以比较两个字符串对应的素数乘积,若相等则不是真子集)。
算法的时间复杂度为O(m+n),最好的情况为O(n)(遍历短的字符串的第一个数,与长字符串素数的乘积相除,即出现余数,便可退出程序,返回false),空间复杂度为O(1),符合题目要求。
但是该方法存在一个致命的问题,就是数据可能会溢出,因为当字符串很长时,所得的成积会很大,极有可能会溢出,所以这种方法慎用。
第三种解法(利用编码):
可以将所有可能出现的字符进行编码,一共可能出现62个字符,可以用62位对其进行编码,1表示对应的位出现,0表示对应的位不出现。
62位的编码,可以使用8个字节实现,大多数编程语言中都有长整型类型,所定义的是占用8个字节,我们可以使用它的低62位。
假设A~Z对应低0~25位, a~z对应低26~51位, 0~9对应低52~61位。
可以扫描str1中的每个字符,进行置位。
可以先假设str2中的字符全部来自于str1中,再去找假设不成立的条件。
再扫描str2中的每个字符,判断标志位是否为0,若为0,说明该字符在str1中不存在,直接返回false。直到最后,如果还没有返回false,那么就返回true。
算法的时间复杂度为O(m+n),空间复杂度为O(1),符合题目要求。
三种解法比较:
解法1占用空间大,但是计算比较简单,编程也简单。
解法2占用空间小,但是计算比较复杂,需要去寻找素数,编程复杂,最大的问题是可能会溢出。
解法3占用空间小,但是计算比较复杂,用到了位运算,编程相对简单。需要去找一个满足编码位数的类型变量。
综上所述:三种方法各有利弊,读者可以根据自己的应用场合以及自己的编程习惯选用适当的方法,当然还可能存在其他的解法。
注意无论是哪种解法,我们要学会每种解法的思想,而不是只会应用到这个题目中,比如在我之前的博客中,有一个问题是穷举子集合,当时我就用到了字符编码,这里我同样用到了字符编码。
总之学算法,一定要学算法思想,而不是某一道题。
鉴于哈希表和素数这两种解法,大家都曾经用过,或者写过类似的程序,这里就不再写详细的代码了。
由于位运算大家用的不多,这里我写出了解法3详细的Java代码实现,都是通用的算法语句,读者可以很容易转换为其他语言。具体的Java代码如下:
1 import java.util.*; 2 class Test { 3 public static boolean StringContain(String str1,String str2){ 4 long code=0; //用来编码的变量,8个字节,共64位,我们只用低62位 5 for(int i=0;i<str1.length();i++) //扫描str1字符串,为对应的字符置位 6 { 7 if(str1.charAt(i)>='A' && str1.charAt(i)<='Z') //A~Z 8 code |= (1 << (str1.charAt(i) - 'A')); 9 if(str1.charAt(i)>='a' && str1.charAt(i)<='z') //a~z 10 code |= (1 << ((str1.charAt(i) - 'a')+26)); 11 if(str1.charAt(i)>='0' && str1.charAt(i)<='9') //0~9 12 code |= (1 << ((str1.charAt(i) - '0')+52)); 13 } 14 15 for(int i=0;i<str2.length();i++) //扫描str2字符串,对应的字符进行判断 16 { 17 if(str2.charAt(i)>='A' && str2.charAt(i)<='Z') 18 if ((code & (1 << (str2.charAt(i) - 'A'))) == 0) 19 return false; 20 if(str2.charAt(i)>='a' && str2.charAt(i)<='z') 21 if ((code & (1 << ((str2.charAt(i) - 'a')+26))) == 0) 22 return false; 23 if(str2.charAt(i)>='0' && str2.charAt(i)<='9') 24 if ((code & (1 << ((str2.charAt(i) - '0')+52))) == 0) 25 return false; 26 } 27 28 return true; 29 } 30 } 31 32 public class Main { 33 public static void main(String[] args) { 34 String str1=new String("a3ABCD"); 35 String str2=new String("BADa3"); 36 System.out.println("str1="+str1); 37 System.out.println("str2="+str2); 38 System.out.println("是否包含(true/false):"+Test.StringContain(str1,str2)); 39 40 } 41 42 }
输出结果为:
str1=a3ABCD
str2=BADa3
是否包含(true/false):true