编程珠玑总结笔记.docx
《编程珠玑总结笔记.docx》由会员分享,可在线阅读,更多相关《编程珠玑总结笔记.docx(41页珍藏版)》请在冰豆网上搜索。
编程珠玑总结笔记
1.打印出小于10000的素数(如何优化程序)
原始:
从2开始到n-1都不能整除则为素数
优化1:
从2到sqrt(n)不能整除就可以
优化2:
通过对被2、3和5整除的特殊检验,避免了近3/4的开方运算,
其次,只考虑奇数作为可能的因子,在剩余的数中避免了大约一半的整除检验
(注意一点,2,3,5本身也是素数)
(If(n%2==0)return(n==2);//能被2整除且不是2本身的不是素数)
优化3:
用乘法运算代替开方运算
intprime(intn)
{
if(n%2==0)return(n==2);
if(n%3==0)return(n==3);
if(n%5==0)return(n==5);
for(inti=7;i*i<=n;i+=2)
if(n%i==0)return0;
return1;
}
实现简单的埃氏筛法(SieveofEratosthenes)来计算所有小于n的素数。
这个程序的主要数据结构是一个n比特的数组,初始值都为真。
每发现一个素数时,数组中所有这个素数的倍数就设置为假。
下一个素数就是数组中下一个为真的比特。
答案:
下面的C程序实现了埃氏筛法来计算所有小于n的素数。
其基本数据结构是n比特数组x,初始值全部为1。
每发现一个素数,数组中所有它的倍数都设为0。
下一个素数就是数组中的下一个取值为1的比特位。
(可以考虑用位图存储)
1.#include
2.using namespace std;
3.int main()
4.{
5. int i, p, n;
6. char x[100001]; n = 100000;
7. for (i = 1; i <= n; i++)
8. x[i] = 1;
9. x[1] = 0; //1不是素数 p = 2; //第一个素数
10. while (p <= n)
11. { cout<
12. for (i = 2*p; i <= n; i = i+p)
13. x[i] = 0;
14. do
15. p++;
16. while (x[p] == 0); //得到第一个标记为1的数,为素数
17. }
18.}
2.如何在1MB的空间里面对一千万个整数(非重复)进行排序?
并且每个数都小于1千万
位图法,采用一个1千万的字符串来表示所有的1千万的数,如果该位置的数存在则标为1,否则标为0。
1.#include
2.#include
3.#define SHIFT 5
4.#define MASK 0x1F
5.#define N 10000000
6.int a[1 + N/32]; //0-N
7.void set(int i) { a[i>>SHIFT] |= (1<<(i & MASK)); }
8.void clr(int i) { a[i>>SHIFT] &= ~(1<<(i & MASK)); }
9.int test(int i){ return a[i>>SHIFT] & (1<<(i & MASK)); }
10.int main()
11.{ int i = 0;
12. while (scanf("%d", &i))
13. set(i);
14. for (i = 0; i < N; i++)
15. if (test(i))
16. printf("%d\n", i);
17. return 0;
18.}
NOTE:
A.i>>SHIFT相当于i/32(用来确定在哪一个int中)
B.i&MASK相当于imod32(用来确定在位中的哪一位)
C.按照本题的算法,大概需要1.25M
D.如果1MB是严格控制的空间,那么应该是需要读取2次。
两次遍历:
对1---4999999之间的数排序,对5000000-10000000之间的数排序。
时间开销nk,空间开销n/k
E.如果每个数据出现最多10次,那么需要4个bit位来刻录一个数。
这时存储空间减小至原来的1/4。
如果一定要按照bitmap的方式来进行处理,需要遍历4次
3.给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中的32位整数
A.方法1:
采用位图存储,然后判断每个int是否等于-1。
因为-1的二进制表示是全1的。
如果不等于-1。
那么说明某一位没有置位(这种比每个位判断的效率高)。
需要内存2^32/8B=512MB,
B.方法2:
如果内存空间有限的情况下,如何实现?
考虑二分的方法,采用已知包含一个出现至少两次的整数的一个连续整数序列作为范围,统计范围中间点之上和之下的元素个数,其中较多那部分必然仍然包含一个出现至少两次的整数。
1)将数据分成两组,最高位为1的一组(记为组1)和最高位为0的一组(记为组0)(如果包含32位数的所有的数,则最高位为1与最高位为0的数的数目应该一样)
2)如果组0中含有的元素数目大于组1中含有的元素的数目,则说明组1中一定包含不在文件中的数在中。
反之,在组0中包含。
3)按上述方法处理每一位
NOTE:
1)如果不写中间文件,即每次都读所有数据(因为文件中数据是),那么Read的数据量大约为N*logN。
2)如果允许在二分搜索的过程中将二分所得的两部分数据写入两个临时文件中,下次搜索时只在其中之一的文件中读取,那么Read的数据量约为(N+N/2+N/4+...)<2N,Write的数据量同样<2N。
a为需要测试的数组,alen为数组中所含的元素的数目,bit为数组中所存的数的位数
1.int get_lost(int *a, int *b, int *c, int alen, int bit)
2.{
3. int re = 0, v = 0, biter = 0, *t, citer, i = 0;
4.//如果数组的长度为2^bit说明所有数据都含有了
5.//b,c可以看作临时数组,其实在函数内部分配也可以,没必要作为参数
6.//a,b,c数组大小相同
7. if (!
a || alen ==(unsigned long)( (1<< bit))) return -1;
8. while (bit--)
9. { v = (1 << bit); //实际上这个就是中间点
10.//通过最高位进行划分
11. for (biter = citer = i = 0; i < alen; ++i)
12. {
13. if (a[i] & (1 << bit)) b[biter++] = a[i];
14. else c[citer++] = a[i];
15. }
16. if (biter <= citer) //确定所在区间
17. { re += v;
18. t = a; a = b; b = t;
19. alen = biter;
20. }
21. else
22. { t = a; a = c; c = t;
23. alen = citer;
24. }
25. }
26. return re;
27.}
扩展:
1)给定一个包含43亿个随机排列的整数的顺序文件,找出一个出现至少两次的32位整数。
KEY:
实际上这个问题本身具有其特殊性。
因为既然包含43亿个整数,大于32位整数所能表示的不同整数的个数,所以至少会有一个整数出现两次。
同样考虑二分的方法,采用已知包含一个出现至少两次的整数的一个连续整数序列作为范围,统计范围中间点之上和之下的元素个数,其中较多那部分必然仍然包含一个出现至少两次的整数。
2)给定一个包含40亿个随机排列的整数的顺序文件,找出一个出现至少两次的32位整数。
4.字符串循环移位比如abcdef右移两位,则变成cdefab(字符串右旋n位)
方法1:
循环左移2位:
abcdef=>cdefab相当于交换两部分AB=>BA
循环右移2位:
abcdef=>efabcd相当于交换两部分AB=>BA
(arbr)r =ba
方法2:
杂技算法(用1个char作为临时变量,然后以固定步长移位),如果将a[n]左移i次,则循环的次数为gcd(n,i),即n与i的最大公约数
将数组中a[0]到a[n-1]逆转
1.static void _res(char *a, int n)
2.{
3. int i = 0, j = n - 1;
4. char t;
5. while (i < j)
6. {
7. t = a[i]; a[i] = a[j]; a[j] = t; //交换
8. ++i; --j;
9. }
10.}
扩展:
1)向量旋转函数将向量ab变为ba。
如何将向量abc变为bca?
(向量内的元素顺序不变,这对交换相邻内存块的问题进行了建模)
KEY:
(arbrcr)r =cba
2)输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。
句子中单词以空格符隔开。
为简单起见,标点符号和普通字母一样处理
KEY:
比较接受的做法是先分别对各个单词进行逆转,然后对整个句子进行逆转。
其实上就是用了上面的公式
3)最大公约数的求法gcd(a,b)
1.intgcd(inta,intb)
2.{
3.if(a>b)
4.gcd(a-b,b);
5.elseif(a
6.gcd(a,b-a);
7.else
8.returna;
9.}
10.intgcd2(inta,intb)
11.{
12.while(a!
=b)
13.{
14.if(a>b)
15.a-=b;
16.else
17.b-=a;
18.}
19.returna;
20.}
5.给定一个单词集合,找出可以相互转换的集合。
如abcbcacba都可以相互转换(变位词)
解析:
将每个单词按照字母顺序排序(使用两个数组记录原单词及排序后的单词的对应关系,或者使用map进行映射),然后将字典中所有排序后的单词进行排序,相邻的单词进行比较看是否相同,相同则为变位词
NOTE:
使用map(关联数组)大大的简化了算法,不需要排序单词然后再查看相邻单词
1.void gen_label(vector &dict, map >&rec)
2.{ for (int i = 0; i < dict.size(); ++i)
3. {
4. string line = dict[i]; sort(line.begin(), line.end());
5. rec[line].push_back(dict[i]);
6. }
7. for (map >:
:
iterator iter = rec.begin();iter !
= rec.end(); ++iter)
8. { copy((iter->second).begin(), (iter->second).end(), ostream_iterator(cout , " "));
9. cout << endl;
10. }
11.}
6.二分查找
1.普通的二分查找
2.int binarySearch(int *a, int n, int t)
3.{
4. intl=0,u=n-1,mid;
5.//invariant:
iftispresent,itisina[l..u]
6. while (l <=u)
7. {
8. mid = l + ((u - l) >> 1);
9. if (a[mid] < t) l = mid+1;
10. else if (a[mid] > t) u = mid - 1;
11. else return mid;
12. }
13. return -1;
14.}
在数组a中查找t元素首次出现的位置
#include"assert.h"
#include
usingnamespacestd;
intbinarySearch(int*a,intn,intt)
{
intl,u;l=-1;u=n;
//invariant:
a[l]=t&&lwhile(l+1!
=u)
{
intm=l+((u-l)>>1);
if(a[m]l=m;
else
u=m;
}
assert(l+1==u);
intp=u;
if(p>=n||a[p]!
=t)
p=-1;
returnp;
}
7.最大子向量问题(通过累加数组求解)
问题:
求一维数组中连续子向量的最大和。
(设数组中所有数都为负数时,最大和为0)
例如:
a[6]={3,4,-2,-9,10,8};则最大连续子向量的和为10+8=18
1.#include
2.#define max(a, b) ((a)>(b)?
(a):
(b))
3.int main()
4.{
5. int a[6]={3,4,-2,-9,10,8};
6. int cumarr[6], cumarr[-1]=0;
7. int i,j;
8. int sum=0,maxsofar=0;
9.//cumarr[i]为x[0..i]的和
10. for(i=0;i<6;++i)
11. cumarr[i] = cumarr[i-1] + a[i];
12. for(i=0;i<6;++i)
13. {
14. sum=0;
15. for(j=i;j<6;++j)
16. { //sum为x[i..j]的和
17. sum = cumarr[j] - cumarr [i-1];
18. maxsofar=max(maxsofar,sum);
19. }
20. }
21. printf("max=%d\n",maxsofar);
22. return 0;
23.}
该算法的时间复杂度为O(n^2)
此外还可以通过分治算法,扫描算法实现
扩展:
如果要查找总和最接近0的子向量和,如何实现?
解析:
初始化累加数组sum[i]=a[0]+a[1]+a[2]+….+a[i]
如果cum[l-1]=cum[u],那么则说明a[l..u]为0.
所以可以通过在累积数组中找到两个接近的元素的元素来找到最接近0的子向量
具体做法:
先求累加数组,然后对累加数组进行排序,最后比较相邻数组找出最小子向量
8.排序
插入排序(保证a[0..i-1]有序,然后插入a[i]以后还是有序)
1.for(i=1;i2.{
3.intt=a[i];
4.for(intj=i;j>0&&a[j-1]>t;j--)//a[0..i-1]已经有序,需要将a[i]插入到有序序列中
5.a[j]=a[j-1];//后移,此时j-1的位置空出来了
6.a[j]=t;
7.}
快速排序(采用分治法,要使得数组有序就要保证左右两侧的数组都有序)
1.
2.
3.
4.如果a[i]大于a[l]那么一切正常
5.如果a[i]小于a[l],则先让m加1,然后交换a[i]和a[m]
6.i属于[l+1,u]
7.voidqsort(int*a,intl,intu)
8.{
9.if(l>=u)return;
10.intm=l;//划分
11.for(inti=l+1;i<=u;i++)//[l+1,u]
12.if(a[i]13.swap(a[++m],a[i]);
14.swap(a[m],a[l]);
15.qsort(a,l,m-1);qsort(a,m+1,u);//qsort
16.}
优化:
1)随机选择划分元素:
swap(L,randint(L,u))=>[L,u]中随机选择一个元素作为划分点
2)使用插入排序等简单方法来排序划分得到的小数组
if(u-l程序结束时,数组并不是有序的,而是小块内无序,块间有序
qsort(a,0,n-1);
isort();//调用插入排序,因为数组整体有序
3)上面的划分算法当数组是有序的时候,需要n-1次划分,并且每次划分需要o(n)的时间
1.#include
2.#include
3.using namespace std;
4.void quictSort(int a[],int l,int u)
5.{
6. if(u-l < cutoff)
7. return;
8. swap(a[L],a[rand()%(u-L+1) + L]);
9. int t = a[l];
10.//i与j初始化为要排序的数组的两端[i+1,u]
11. int i = l;
12. int j = u+1;
13.//划分
14. for(;;)
15. {
16. do i++;while(i <= u && a[i] < t);
17. do j--;while(a[j] > t);
18. if(i > j)
19. break;
20. swap(a[i],a[j]);
21. }
22. swap(a[l],a[j]);
23.//qsort
24. quictSort(a,l,j-1);
25. quictSort(a,j+1,u);
26.}
扩展:
编写程序,在O(n)时间内从数组x[0..n-1]中找出第k个最小的元素
KEY:
可采用类似于快速排序的方法进行查找(在数组a[l..u]中查找第k个最小的数)
if(jelseif(j>k)select(a,l,j-1,k);
elseprintf(a[k]);
由于每次排除了一半,只在另一半中进行查找操作,所以需要的是kn,也就是O(n)
9.其他的排序
选择排序:
首先将最小的值放到x[0],然后将剩下的最小值放到x[1],依次类推。
类似冒泡,但在比较过程中,不进行交换操作,只记录元素位置。
一次遍历只进行一次交换操作
选择:
简单选择,堆排序;插入:
简单插入,希尔;交换:
冒泡,快速
加上归并,计数,桶排序
///归并排序算法时间复杂度为:
O(nlgn),但是要消耗空间
#include
//将两个已排序的子数组合并
//A[p..q]与A[q+1..r]合并成一个新的A
voidmerge(intA[],intp,intq,intr)
{
intn1=q+1-p;//数组a[p..q]的长度
intn2=r-q;//数组a[q+1..r]的长度
inti,j;
//长度多了,是为了用于存储哨兵牌
int*L=(int*)malloc(n1+1);
int*R=(int*)malloc(n2+1);
for(i=0;iL[i]=A[p+i];
L[n1]=INT_MAX;
for(j=0;jR[j]=A[q+1+j];
R[n2]=INT_MAX;
i=0;
j=0;
for(intk=p;k<=r;k++)
{
if(L[i]{
A[k]=L[i];
i++;
}
else
{
A[k]=R[j];
j++;
}
}
}
//递归实质是按照树形进行归并,左右根的顺序
voidmerge_sort(intA[],intp,i