【编程珠玑】第二章:啊哈,算法

博客包含十个算法相关问题及解答。如找单词变位词、在大量整数中找重复数、探讨向量旋转算法、确定电话簿错误匹配等。涉及扫描、二分查找、Bit - map位向量等方法,部分问题待解,还包含一个利用物理方法算体积的小插曲。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一题

问题:请思考一下找到给定输入单词的所有变位词这个问题。如果提供了单词和词典,你会如何解决这个问题呢?在回答和查询之前,如果你可以花些时间和空间处理一下该词典,结果又怎样呢?

解答:

(1)如果提供了单词和词典,那么可以先遍历整个词典,计算出每个单词的标签,然后计算提供的单词的标签,用扫描的方法把该单词标签和词典中标签进行对比。

(2)如果是要花些时间来预处理,可以先将词典标签进行排序,然后用二分法查找的方法求出该单词标签所在范围。

第二题

问题:给定一个包含4300000000个32位整数的顺序文件,请问你如何可以找到一个至少出现两次的整数。

解答:首先,很明显,32为整数的范围为0-2^32-1,这样,4.3G个数据里面肯定有重复的数据,这个叫鸽笼原理。

(1)可以采用Bit-map位向量的方法。只是出现两次,那么两次也算了。方法和前面的查看没出现的数类似,test(i)即可,这里不再介绍。

(2)可以采用二分查找法。既然数据都在0-2^32-1之间,那么我们可以看一下在[0,2^31-1]和[2^31,2^32-1]之间哪个区间的数据比较多,其实也可以不用这样,如果可以得知数据的数量大于该范围的结论也就可以了。但是,如果挑一个数据比较多的,那么理论上讲可以早点找那个重复的数。具体执行,从高位开始为第一位,0和1分开,其实就是二分法了,然后挑选数据量多的,从第二位开始,也是0和1分开,最终找到那个数,复杂度为O(logn)。


第三题

问题:我们浏览了两个向量的旋转算法,它们都要求代码精细,并且都实现为程序了。请问i和n的最大公约数是怎么出现在程序中的?

解答:给出求解最大公约数的代码。我们用更相减损术或者辗转相除法。

/***********************************************************/
// 程序目的:求解最大公约数(更相减损术))
// 日期:    2014-09-01 
// 作者:    spencer_chong
// 邮箱:    zhuangxb91@qq.com
/***********************************************************/
#include <iostream>
using namespace std;

int gcd(int a, int b)
{
    while(a!=b)
    {
               if(a>b)
               a-=b;
               else 
               b-=a;
    }
    return a;
}
int main()
{
	int m,n;
	cout<<"请输入两个数:"<<endl;
	cin>>m>>n;
	cout<<gcd(m,n)<<endl;
	return 0;

}
请输入两个数:
5 3
1
请按任意键继续. . .

       杂技算法思路:先解释为什么叫杂技算法。看过杂技表演抛橙子的可能知道,左右两只手各有一个橙子,在左右手之上方有一条抛物线的轨迹,可能有两个三个或者更多的橙子,这个看表演者的技术了。然后看我们的算法,我们算法和这个过程有可以类比的地方。“移动x[0]到临时变量t,然后移动x[i]至x[0],x[2i]至x[i],依此类推(将x中的所有下标对n取模),直至返回到取x[0]中的元素,此时x[0]取值为t,然后终止过程。如果该过程存在没有移动过的元素,就从x[1]开始再次进行移动,同样t=x[1],直到所有的元素都已经移动为止。

       这个过程最大公约数起到什么作用呢?”最大公约数为1的能够填满数据,否则不能填满。最大公约数为2,则需要分两步, x[0]<->x[i]<->x[2i], x[1]<->x[i+1]<->x[2i+1],以此类推,总之需最大公约数步移动。两个相差1的数的最大公约数不可能是2。若两个数a,b的最大公约数为1,则将其中的一个数(假设为a)增加任意数k并对b求余,则结果为0...b-1。基于这个道理若数组的长度和旋转的长度的最大公约数为1,则从数组的某位开始以任意长度k(此处为旋转长度)为步长遍历数组,则每个元素都可以遍历到。通过移位操作若第k+1个元素到达指定的位置,则通过一次遍历所有元素都将到达指定位置。若最大公约数不为1则需要遍历最大公约数次。

/***********************************************************/
// 程序目的:向量旋转(杂技算法)
// 日期:    2014-09-01 
// 作者:    spencer_chong
// 邮箱:    zhuangxb91@qq.com
/***********************************************************/
#include <iostream>
#include <string.h>
using namespace std;

int gcd(int a, int b)
{
    while(a!=b)
    {
               if(a>b)
               a-=b;
               else 
               b-=a;
    }
    return a;
}

void jugglerot(char x[],int n, int rotdist)
{
     int i, j, m,r, k=0;
	 char temp;
     m=gcd(n, rotdist);  //求最大公约数
     while(m)
     {
                     i=rotdist+k;
                     j=k;
                     temp=x[j];
                     r=j;
                     //while(i!=j)  i和j都是变化的,故不能用j做标签,需要用另外一个值r
                     while(i!=r)
                     {
                            x[j]=x[i];
							cout<<x<<endl;;
                            j=i;
                            i=(i+rotdist)%n;
  
                     }
                     x[j]=temp;
                     k++;
                     m--;
     }
} 

int main()
{
	char s[]="abcdefgh";
	jugglerot(s,8,4);
	cout<<s<<endl;

}
ebcdefgh
efcdafgh
efgdabgh
efghabch
result:
efghabcd
请按任意键继续. . .


第四题

问题:有些读者指出,尽管三个旋转算法所需要的时间全都和n成比例,但是杂耍算法的速度显然是转置算法的两倍:它存储和检索数组元素的次数只有一次,而转置算法则要两次。请在实际的机器上体验一下这些函数,比较它们的速度;尤其要注意内存引用的局部性问题。

解答:块移动法。旋转向量x实际上就是交换向量ab的两段,得到向量ba,这里a代表x的前i个元素。假设a比b短。将b分割成bl和br,使br的长度和a的长度一样。交换a和br,将ablbr转换成brbla。因为序列a已在它的最终位置了,所以我们可以集中精力交换b的两个部分了。由于这个新问题和原先的问题是一样的,所以我们以递归的方式进行解决。这种方法可以得到优雅的程序,但是需要巧妙的代码,并且要进行一些思考才能看出它的效率足够高。

/***********************************************************/
// 程序目的:向量旋转(块移动法)
// 日期:    2014-09-01 
// 作者:    spencer_chong
// 邮箱:    zhuangxb91@qq.com
/***********************************************************/
#include <iostream>
#include <string.h>
using namespace std;
//这里只是交换数据而已
void swap(char s[],int start, int final,int length)
{
	char temp;
	for(int i=0;i<length;i++)
	{
		temp = s[start+i];
		s[start+i] = s[final-length+i];
		 s[final-length+i] = temp;
	}
}

void blockrot(char s[],int start, int mid, int final )
{
	int length;
	cout<<s<<endl;
	//递归停止条件,就是只剩下一个元素,无法分成两个块进行交换
	if(start+1 == final)
		return;
	//比较两个块的长度,取比较短的长度作为交换的长度
	if (mid-start>final-mid)//? final-mid:mid-start;
	{
		length = final-mid;
		swap(s,start,final,length);
		//如果交换位置比较偏后,那么中间没有被交换到的数据应该属于前面的,那么下一步就得和后面的数据作交换
		//所以起点就要往后移动,终点保持不变。建议画个示意图看看
		blockrot(s,start+length,mid,final);

	}
	else
	{
		length = mid-start;
		swap(s,start,final,length);
		//如果交换位置比较靠前,那么中间没有被交换到的数据应该属于后面的,那么下一步就得和前面的数据作交换
		//所以终点就要往前移动,起点保持不变。建议画个示意图看看
		blockrot(s,start,mid,final-length);
	}
	
} 

int main()
{
	char s[]="abcdefgh";
	//输入s的起始位置和开始交换的位置
	blockrot(s,0,5,8);
	cout<<"result:"<<endl;
	cout<<s<<endl;
	return 0;


}
abcdefgh
fghdeabc
fghbcade
fghacbde
fghabcde
result:
fghabcde
请按任意键继续. . .

求逆算法。"我们将问题看做是把数组ab转换成ba,同时假定我们拥有一个函数可以将数组中特定部分的元素求逆。从ab开始,首先对a求逆,得到arb,然后对b求逆,得到arbr。最后整体求逆,得到(arbr)r。此时恰好是ba。"

/***********************************************************/
// 程序目的:向量旋转(求逆算法)
// 日期:    2014-09-02 
// 作者:    spencer_chong
// 邮箱:    zhuangxb91@qq.com
/***********************************************************/
#include <iostream>
#include <string.h>
using namespace std;
//这里只是求逆而已
void reverse(char s[],int start, int final)
{
	char temp;
	while(final-start>=1)
	{
		temp = s[start];
		s[start] = s[final];
		s[final] = temp;
		start++;
		final--;
	}
}
//ba=(aTbT)T,思路很简单
void reverserot(char s[],int n, int i )
{
	reverse(s,0,i-1);
	reverse(s,i,n-1);
	reverse(s,0,n-1);
	
} 

int main()
{
	char s[]="abcdefgh";
	//输入s的起始位置和开始交换的位置
	int n=8;
	int i=5;
	reverserot(s,8,5);
	cout<<"result:"<<endl;
	cout<<s<<endl;
	return 0;
	
}
result:
fghabcde
请按任意键继续. . .

下面附图说明效率的比较


第五题

问题:向量旋转函数将向量ab更改成ba;那你应如何将向量abc转换成cba呢?(这个问题建模了如何交换非邻接内存块的问题。)

解答:可以这么认为,既然ab我们知道变成ba了,那么abc就可以变成bca,把后面那块当成一个整体就行了。然后再把前面的bc变成cb,那结果就是cba了。一句话总结就是,分别对每一块求逆,再对整体求逆。

第六题

问题:在20世纪70年代后期,贝尔实验室部署了一个“用户操作的电话簿辅助”程序,允许员工使用标准的按钮电话查找公司电话簿中的号码。要查找设计者LESK的号码,你只需按下“LESK*M*”(也即“5375*6*”),系统即会说出他的号码。这一设备现在随处可见。这一系统有一个问题,那就是不同的名字可能具有相同的按钮编码;这种现象在LESK系统中发生时,它会要求用户提供更多的信息。给定一个大的名字文件,比如大城市的电话簿,你会如何确定这些“错误的匹配”呢?(但LESK在这样的电话簿上做这个试验时,他发现错误匹配发生率只有0.2%)你如何实现给定一个名字按钮编码,然后返回可能需要匹配名字的集合这一功能呢?



解答:把名字对应的按键形成一个唯一的标识符,可以先对名字进行预处理。 可以用二分法进行查找,但是更多的答案说是用hash, hash_map<int, hash_set > rec;

第七题

问题:在20世纪60年代早期,vic vyssotsky和某个程序员一起共事,该程序员需要转置一个存储在磁带上4000*4000矩阵(每一条记录都有相同的格式,并且有几十个字节大小)。这位同事最初提出了一个程序可能需要花费50个小时的运行时间;Vyssotsky是如何将运行时间减少到半小时的呢?

解答:(待解)这问题主要是找出一种效率高一点的转置方法。在网上找到有人这么回答的,我不理解,也不确定这是否就效率高了,特向网友求助~~。网上是这么说的:列作为标识,先按列排序,那么第一列的所有元素就排在最前面,接下来是第二列,第三列……也就是说把整个矩阵拉成一行,从左到右分别是第一列,第二列......然后再各列内按行排序,转置后,同一列的中第一行的元素在第二行的前面,接下来是第三行第四行,所以按列内按行号排序即可。


第八题

问题:给定一个具有n个元素的实数集、一个实数t以及一个整数k,请问你如何快速地确定该实数集是否存在一具有k个元素的子集,其中各元素的总和至多只能为t。

解答:这题比较好解。其实就是找集合中最小的k个数得和是小于等于t就行了。如果用快速排序的方法,复杂度为o(nlogn)。其实也有另一种方法,就是后面的我根本不需要用到,所以我可以不对其排序。先遍历找出排第k的数。怎么找?统计每个数比它小的数量a和比它大的数量b,如果a<k&&n-k>b就可以。怎么理解?打个比方,1 2 3 4 4 4 5 5 6 7。共有10个数,要找出第五大的。这里为了分析方便,先排了序,实际上是不需要的。那么如果要把中间那个4找出来,那么就肯定要a<4,b<5。a<4容易理解,这样保证那个数比较靠前,b<5实际上就是说比它大的不能太多,那样就达到了一种准确的位置。这里,中间3个4任何一个都符合要求,结果都一样。找出来之后就取k个不大于这个数的加起来就可以了,判断是否小于等于t。


第九题

问题:顺序查找和二分查找代表了查找事件和预处理时间之间的一种权衡。在具有n个元素的表中,需要执行多少次二分查找才能弥补对表进行排序时所需要的与处理时间?

解答:(待解)现在对于算法复杂度的东西还没有完全弄懂,以后再写。看到网上有说搜索次数C > nlgn/ (n - lgn)的,仅供参考。

第十题

问题:在一位新研究员报到为托马斯爱迪生做事的那一天,爱迪生要他计算一个空电灯泡壳的体积。这位新雇员使用测径器和微积分算了几个小时之后,得到的答案是150立方厘米,没过几秒钟,爱迪生就算出了“接近155立方厘米”的答案,他是怎么做到的呢?

解答:放到水里。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值