啊哈!算法:三个算法问题(左旋转,大数据,变位词集)

无意中找到一本书《编程珠玑》,刚看到第二章,感觉作者讲解方式比较独特并且很有意思,在这里记录第二章的三个问题,以下是这三个问题。

三个问题

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个单词的词典进行处理的情形:三部曲 签名->排序->压缩

啊哈!算法:三个算法问题(左旋转,大数据,变位词集)_第1张图片

#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;
	}
}
参考资料:
  • 编程珠玑
  • 编程珠玑源码(免积分下载)




你可能感兴趣的:(面试题)