无意中找到一本书《编程珠玑》,刚看到第二章,感觉作者讲解方式比较独特并且很有意思,在这里记录第二章的三个问题,以下是这三个问题。
三个问题
A.给定一个包含32位整数的顺序文件,它至多包含40亿个这样的整数,并且整数的次序是随机的,请查找一个此文件中不存在的32位整数(至少必有一个遗漏,为什么?)。在有足够内存的情况下,你会如何解决这个问题?如果可以使用若干外部临时文件但可用主存却只有上百字节,你会如何解决这个问题?
解:至少必有一个遗漏,为什么?2^32=4294967296(约43亿),由于文件至多包含了40亿个32位整数,则至少有大约3亿个整数被遗漏(已经不仅仅至少有一个遗漏这么简单了,是至少有2亿个被遗漏)。
如果有足够的内存,则可以建立一个2^32位(4294967296)的位图,需要内存512MB(4294967296bit =512MB,不知道有没有算错),然后依次扫描顺序文件,如果文件中的数据出现,则在位图的相应位标记为1,扫描完整个文件,只需要搜索位图中为0的位就是文件中不存在的数了。
如果可以使用若干外部临时文件但可用主存却只有上百字节,书中的方法是用二分法。一般的二分法要先进行排序,但是如果要排序的话,那内存就不只几百字节了,不如用原来的方法。书中方法如下:
先找到一个包含所有输入数的一个范围,比如0~2^32-1;取这个范围的中点2^31,然后对文件中的数据(大概40亿个)进行统计,在中点以下的数有几个,在中点以上的数有几个,对比它们本来应该有的数量,就可以确定在哪个范围内有缺数(元素重复无影响)比如说,文件数据中在0~2^31这个范围内有22亿(大于2^31)个数,则说明2^31~2^32范围内一定有缺数;在那个范围下继续二分即可。
B.请将一个具有n个元素的一维向量向左旋转i个位置,例如,假设n=8,i=3那么向量abcdefgh旋转之后得到向量defghabc。简单编码使用一个具有n个元素的中间向量分为n步即可完成此作业。你可以仅使用几十字节的微小内存,花费与n成正比例的时间来旋转该向量吗?
解法1:
将x中的前i个元素复制到一个临时的数组中,接着将余下的n-i个元素左移i个位置,然后再将前i个元素从临时数组中复制到x中的后面i个位置上。这样可以实现,但是要使用i个额外的空间,空间消耗太大。
//Solution 1 void LeftRotate6(char* str,int left,int len) { if( left >= len || left <=0 ) return ; char* Temp = new char[left]; for( int i=0 ; i<left ; ++i ) //复制到临时空间 Temp[i] = str[i] ; for( int i=0 ; i< len - left ; ++i) str[i] =str[left+i]; for( int i=0 ; i<left ; ++i ) str[i] = Temp[i] ; delete []Temp ; }!细节:需要记住delete,否则会内存泄露!内存检测可以参考:http://msdn.microsoft.com/zh-cn/library/e5ewb1h3(v=vs.90).aspx
解法2:
定义函数,它的作用是将x向左旋转一个位置(时间上与n成比例),然后调用该函数i次,即可实现i循环。但是总时间是 O(i*n),过于浪费时间。
//Solution2 void subLeftRotate(char* str,int len) { char temp = str[0] ; for(int i=0 ; i< len-1 ;++i) str[i] = str[i+1]; str[len-1]=temp; } void LeftRotate7(char* str,int left,int len) { if( left >= len || left <= 0) return ; for(int i = 0 ; i<left ; ++i ) subLeftRotate(str,len); }
解法3:
可以用一个堪称巧妙的杂技表演的方法,它的前提是要循环的i次必须是总数n的约数,否则如果他们只有约数1时,退化为解法2的复杂度。
先将x[0]移动临时变量t中,然后将x[i]移动到x[0],x[2i]移动到x[i]中,依次类推,直到结尾(可以使用下标对n取模的方法)。将t赋值到复制的最后一个元素中去。
t = A;AB C D E F G HI J K L —>D B C GE F I H A J KL依次循环,即可在正比于n的时间复杂度,以及t大小的空间下实现循环移位。
int gcd(int i, int j) //欧几里德算法:求最大公约数 { int temp; while (i != 0) { if (j >= i) j -= i; else { temp = i; i = j; j = temp; } } return j; } void LeftRotate4(int left, int n) { int cycles, i, j, k, temp; cycles = gcd(left, n); for (i = 0; i < cycles; i++) { /* move i-th values of blocks */ temp = x[i]; j = i; for (;;) { k = j + left; if (k >= n) k -= n; if (k == i) break; x[j] = x[k]; j = k; } x[j] = temp; } } void LeftRotate5(int left, int n) { int cycles, i, j, k, temp; cycles = gcd(left, n); for (i = 0; i < cycles; i++) { /* move i-th values of blocks */ temp = x[i]; j = i; for (;;) { /* Replace with mod below k = j + left; if (k >= n) k -= n; */ k = (j + left) % n; if (k == i) break; x[j] = x[k]; j = k; } x[j] = temp; } }
解法4:
case1:不同的算法源自对问题的不同看法:旋转向量x实际上就是将向量ab的两个部分交换为ba,这里a代表x的前i个元素。假设a比b短,将b分割为bl和br两部分,使得br的长度和a的长度一样,交换a和br,将ablbr换成brbla,因为序列a已经在它的最终位置了,所以,我们可以可以集中精力交换b的两个部分了。由于这个问题与原问题相同,所以我们可以采用递归的方式进行解决。
case2:同时也可以换一个角度思考,假设a比b短,将b分割为bl和br两部分,使得bl的长度和a的长度一样,交换a和bl,将ablbr换成blabr,因为序列bl已经在它的最终位置了,所以,我们可以集中精力交换abr这两个部分了。由于这个问题与原问题相同,所以我们可以采用递归的方式进行解决。
case3:由上面的两个递归程序,我们可以比较简单地从逻辑上将递归转化为迭代,使效率更高。
//Solution 4 //case1: void LeftRotate1(char* str,int left,int len) { if(len <= left || left <= 0) return ; int dist = len - left; if(left < dist ) //如果前半部分短(a blbr),交换成(brbl a),a已经在它最终位置 { for(int i = 0; i < left; i++) swap(str[i],str[dist+i]); LeftRotate1(str,left,len-left);//递归处理子问题brbl } else { //如果前半部分长(alar b),交换成(b aral),b已经在它的最终位置 for(int i = 0; i < dist; i++) swap(str[i],str[left+i]); //递归算理子问题 aral LeftRotate1(str+dist,left - dist, len-dist ); } } //case2: void LeftRotate2(char* str,int left,int len) { if(len <= left || left <= 0) return ; int dist = len - left; if(left <= dist ) //如果前半部分短(a blbr),交换成(bl abr),bl已经在它最终位置 { for(int i = 0; i < left; i++) swap(str[i],str[i+left]); LeftRotate2(str+left,left,len-left); //递归处理子问题(a br) } else { //如果前半部分长(alar b),交换成(b aral),b已经在它的最终位置 for(int i = 0; i < dist; i++) swap(str[i],str[i+left]); LeftRotate2(str+dist,left - dist,len-dist);//递归算理子问题 aral } } //case3: void swap(char* str,int i, int j, int k) /* swap str[i..i+k-1] with str[j..j+k-1] */ { int t; while (k-- > 0) { t = str[i]; str[i] = str[j]; str[j] = t; i++; j++; } } void LeftRotate3(char* str,int left, int len) { int i, j, p; if (left <= 0 || left >= len) return; i = p = left; j = len - p; while (i != j) { /*str[0 ..p-i ] 处于最终位置了 str[p-i..p-1 ] = a (将要与b进行块交换) str[p ..p+j-1] = b (将要与a进行块交换) str[p+j..len-1 ] 处于最终位置了 */ if (i > j) { swap(str,p-i, p, j); i -= j; } else { swap(str,p-i, p+j-i, i); j -= i; } } swap(str,p-i, p, i); }
解法5:也可以参考《剑指offer》
对于要旋转的两部分,我们可以对这两部分单独做一次旋转,再整体做一次旋转,即可达到目标。例如abcdefgh,对两部分单独旋转结果是cbahgfed,再整体旋转
defghabc,十分巧妙地达到了效果,即简单又优雅。
reverse(0, i-1);
reverse(i, n-1);reverse(0, n-1);
//Solution 5 void reverse(char* str,int beg, int end) { int t; for ( ; beg < end ; beg++,end--) { t = str[beg]; str[beg] = str[end]; str[end] = t; } } void LeftRotate(char* str,int left, int len) { reverse(str,0, left-1); reverse(str,left, len-1); reverse(str,0, len-1); }
C.给定一本英语单词词典,请找出所有的变位词集。例如,因为“pots”, “stop”,“tops”相互之间都是由另一个词的各个字母改变序列而构成的, 因此这些词相互之间就是变位词。
这个问题很容易使人想到,可以找出一个单词的所有排列,然后在字典中一一对比,但是,这个办法有一个致命的缺陷,那就是效率奇差,如果单词长度为n,则找单词的排列时间上是O(n!),这迫使我们走别的路子。
这个问题可以分3步来解决:
第一步将每个单词按字典序排序, 做为原单词的签名,这样一来,变位词就会具有相同的签名。
第二步对所有的单词按照其签名进行排序,这样一来,变位词就会聚集到一起。
第三步将变位词压缩,形成变位词集。示意图如下:
下面是具有6个单词的词典进行处理的情形:三部曲 签名->排序->压缩
#include<iostream> #include<string> #include<algorithm> #include<fstream> #include<map> #include<vector> using namespace std; void main() { map<string,vector<string> > mapStr; string str1,str2; ifstream in("F:\\k.txt"); //k.txt内容:pans pots opt snap stop tops while(in >> str1) { str2 = str1 ; sort(str2.begin(),str2.end()); mapStr[str2].push_back(str1); } for(map<string,vector<string> >::iterator it=mapStr.begin() ; it != mapStr.end() ;++it) { for(vector<string>::iterator i=it->second.begin() ; i != it->second.end() ;++i ) cout<<*i<<' '; cout<<endl; } }参考资料: