无意中找到一本书《编程珠玑》,刚看到第二章,感觉作者讲解方式比较独特并且很有意思,在这里记录第二章的三个问题,以下是这三个问题。
三个问题
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
!细节:需要记住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
解法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
#include
#include
#include
#include
参考资料: