第一章
1.外部排序的一个经典方法:多路归并排序
解法1:把n个元素分为k路,先读取[1,k]个元素进行排序,然后写入临时文件1中,再读取[k+1,k+k]元素进行排序,写入文件2中,依次类推,直到所有元素排序完。然后从所有临时文件中按顺序读取选择最大/小的元素写入输出文件。
解法2:如果内存空间允许,且元素都不相同,可以把元素映射到位图上,不存在的元素对应位图的位就不设置,n个元素所在的范围为1-S,需要S/8个字节的位图空间。
第二章
1.在最多40亿个32位整数的文件中,查找一个不存在的数:
解法:可以使用二分查找,第一趟遍历40亿个数,分为A,B两组,A组数都小于32位整数的中位数,B组都大于。选择A,B中较少的一组,并设置中位数为那组数值范围的中位数,继续二分查找,如果没用临时文件,最多比较N*logN次。如果使用临时文件分别保存A,B,则比较(N+N/2+N/4。。。)<2N次。
2.n元素向量的左旋转i位,如1234567旋转4位变为5671234,:
解法1:前i位为x,后n-i位为y,则可表示为xy旋转为yx,可以翻转x->x',再翻转y->y',最后翻转(x'y')',就变为yx。总的需要移动所有元素2次。
解法2:杂技算法:先将x[0]一到临时变量t中,然后将x[(0+i)%n]移到x[0],将x[(0+2*i)%n]移到x[(0+i)%n],将x[(0+3*i)%n]移到x[(0+2*i)%n],依次类
推,直到(0+k*i)%n==0.然后由x[1]开始,将x[(1+i)%n]移到x[1],将x[(1+2*i)%n]移到x[(1+i)%n],依次类推。需要重复的次数为n和i的最大公约数次,而每一次重复移动的个数为n和i的最小公倍数/i的商次。总的只需要移动所有元素依次就可以。
特别的,当n非常大时,解法1的平均效率反而比解法2高,因为解法1可以利用机器的局部性原理,减少访存操作,而解法2由于跨度大,cache未命中带来较
大性能开销。
3.找出词典中的所有变位词:(变位词:包含字母相同,但是位置不同的单词,例如deposit,dopiest,posited)
解法:给每一个单词打上标签,这里的“标签”可以设计为每一个单词的所有字母按26字母顺序排序,例如deposit,dopiest,posited的标签都是deiopst。
把所有单词都排序,分标签后,同一标签的就是变位词。
第六章
1..程序加速的几个方式:
1.算法和数据结构,主要是对问题的抽象和表示方式。不同数据结构(树/链表/堆/哈希表)有不同特性,要根据问题特点选择合适的数据结构。
2.算法优化,主要是在选定了数据结构后,对问题中一些特别的情况给予优化,即利用数据结构的特性来改善算法。
3.数据结构重组,数据结构的重组织(按一定规则调整),虽然耗费一点时间,但是可能可以减少其他更耗时的操作。
4.代码优化,使用汇编重写主要耗时的代码。
5.硬件,更换速度更快的机器,或者添加专用处理器(浮点加速器)
第七章
1.封底计算:估计系统整体性能,使其应该几乎符合要求。
1.“72法则”:假设投入一笔钱,时间是y,利率每年x%,且y*x=72,那么大致来说投入的钱会翻番。
“72法则”利于估计指数过程的增长:假设一个程序花了10秒解决一个规模为n=40的问题,并且n增加1就增加12%的运行时间,那么n每增加6,运行
时间翻倍。
2.安全系数:当你在估算系统性能时,需要按2,4或者6的系数降低性能,以补偿我们未考虑到的因素对系统性能的影响。
在估算健壮性/延迟需求时,也同样需要一个安全系数来保证系统在一定的偏差范围内仍然可以正确运行。
3.利特尔法则:系统中物体的平均数量就是系统中物体离开系统的平均比率和每个物体在系统中所花费的平均时间的乘积。
这个可用于多用户系统的响应时间准则:假设平均思考时间为z的n个用户连接到一个系统中,系统响应时间为r。吞吐量为x,则利特尔法则表明
n=x*(z+r),则r=n/x-z。
4.在多远的距离下,骑自行车的邮差传送可移动媒介的速度可以比高速数据线传递信息更快?
解答:老式硬盘100MB一个,ISDN速度112kbit/s,一小时50MB,那么就是让邮差带这一个硬盘用2小时绕半径为15英里的圆一周。如果让他带100张
DVD,他的带宽增加17000倍。
第八章
1. 宏在预处理时被展开,使用时注意:如果传递给宏的是函数调用,那么宏将以此调用每个函数,例如
#define max(a, b) a > b ? a : b
max(f(), g());
那么将展开为f() > g() ? f() : g(); 实际计算量将加倍。
使用内联函数是一个不错的选择。
2.找出n元素的数组中连续和最大的子数组,O(n)的算法:
maxsofar = 0;
maxendinghere = 0;
for i = [1, n)
maxendinghere = max(maxendinghere + x[i], 0);
maxsofar = max(maxendinghere, maxsofar);
思想:只要某个子数组累计和比0大,那么对整体就是有贡献的,所以maxendinghere从1到n-1累加所有的元素,保留累加后比0大的值。
maxsofar则保存目前的最大值,即如果x[i]是负的,maxendinghere减少,那么肯定不会更新maxsofar的值。
遍历完成后,maxsofar为找到的最大和。
第九章
1.运算符%的时间花费比基本算术运算多得多。
2.当需要通过大量计算f(x),然后对结果进行比较,找出最大/小值时,可以考虑找到另一种简单计算g(x),使得g(x)的单调性和f(x)相同,那么就可以通过g(x)来计
算并找最值。
例如找出球面上最接近的2点,需要计算球面上两点距离,即计算两点与圆心的连线的夹角f(p1, p2),需要10个三角函数。
可以将这种计算简化为计算两点的欧几里得距离g(p1, p2),因为欧几里得距离近的两点必然接近(单调性与f(p1, p2)相同),只需要几个平方及开方函数。
3.代码优化流程:
首先,找到代码中花费时间最多的地方(热点);然后针对热点进行优化。
4.顺序查找的优化:
原始代码为:
for(i = 1; i < n-1; i++)
if x[i] == t;
return i;
return -1;
每次for循环中,做一次加法,一次与n-1的比较,一次访存与t的比较,可以通过在数组末端设立岗哨,优化为一次加法和一次访存比较:
hold = x[n]
x[n] = t
for (i = 0;;i++)
if (x[i] == t)
break;
x[n] = hold;
if (i == n)
return -1;
else
return i;
如果手动把循环展开一部分,例如展开8次,下标递增8(i+=8),速度可以更快。
5.二分查找的优化:
原始代码:
l = 0; u = n - 1;
while(1){
if (l > u)
p = -1;
break;
m = (l + u) / 2;
if (x[m] < t)
l = m + 1;
else if (x[m] == t) {
p = m;
break;
} else
u = m - 1;
}
原始代码在while中需要做一次除法,有时需要三次的比较,有时需要2次加法;
可以优化为一次除法,两次比较和一次加法(设元素有n=1000个):
i = 512;//(恰好比n小的2的幂值)
l = -1;
if (x[511] < t) //第一次二分,选择小于n的2的幂值作为查找范围
l = 1000 - 512;
while( i != 1) {
i = i / 2; //利用2的幂值的特点,直接除以二来取中点,且最终值肯定是1.
if (x[i + l] <t)
l += i;
}
p = l + 1;
if (p > 1000 || x[p] != t)
p = -1;
6.cache的使用:如果常常需要为某个特定大小的结构调用malloc分配内存,可以使用如下定制的pmalloc
void *pmalloc(int size)
{
void *p;
if (size != nodesize) //一般分配,用不到cache
return malloc(size);
if (nodesleft == 0) { //如果缓存用完了,再分配一个
freenode = malloc(nodegroup * nodesize);
nodesleft = nodegroup;
}
nodesleft--;
p = (void *)freenode; //从缓存中分配
freenode += nodesize;
return p;
}
7.统计n位二进制数中出现“1”的次数:编程之美介绍了5种方法,这里有第六种:
思路:利用汉明权重计算,下列为计算16bit中出现1的个数,计算n个可以计算n/8次然后把次数累加起来。
Expression Binary Decimal Comment
A 01 10 11 00 10 11 10 10 The original number
B = A & 01 01 01 01 01 01 01 01 01 00 01 00 00 01 00 00 1,0,1,0,0,1,0,0 every other bit from A
C = (A >> 1) & 01 01 01 01 01 01 01 01 00 01 01 00 01 01 01 01 0,1,1,0,1,1,1,1 the remaining bits from A
D = B + C 01 01 10 00 01 10 01 01 1,1,2,0,1,2,1,1 list giving # of 1s in each 2-bit piece
of A
E = D & 0011 0011 0011 0011 0001 0000 0010 0001 1,0,2,1 every other count from D
F = (D >> 2) & 0011 0011 0011 0011 0001 0010 0001 0001 1,2,1,1 the remaining counts from D
G = E + F 0010 0010 0011 0010 2,2,3,2 list giving # of 1s in each 4-bit piece
of A
H = G & 00001111 00001111 00000010 00000010 2,2 every other count from G
I = (G >> 4) & 00001111 00001111 00000010 00000011 2,3 the remaining counts from G
J = H + I 00000100 00000101 4,5 list giving # of 1s in each 8-bit piece
of A
K = J & 0000000011111111 0000000000000101 5 every other count from J
L = (J >> 8) & 0000000011111111 0000000000000100 4 the remaining counts from J
M = K + L 0000000000001001 9 the final answer
第十章
1.稀疏矩阵的储存:需要压缩空间,因此可以把矩阵变为链表:
例如一个200x200=40000的矩阵,矩阵中只有2000个元素有值,可以压缩为a[行号]->{列号,值}->{列号,值}的方式:
a[0]->{3, 10}->...
a[1]->{5, 43}->...
...
a[200]->{41, 32}->...
2.数据压缩:c = 10*a+b,可以把两个十进制数字a,b编码在一个字节c中,解码时使用a = c/10; b = c % 10;
可以进一步优化速度,使用c = a<<4 + b, a = c >> 4. b = c &0x0f.
第十一章
1.快排的优化:在剩下少量元素需要排序时,可以不进行快排而直接返回,最后整个快排完成后,序列已经基本有序,再使用插入排序,速度会加快一些。
第十二章
1.已知m和n,m<n,要从n个元素中取出m个,并且所有m的组合概率相等:
int select = m
int remaining = n
for i = 1 to n do
if (random() % m) < select
print i;
select--;
remaining--;
假设n=5,m=2,那么选中最后两个元素的概率是3/5 * 2/4 * 1/3 * 2/2 * 1/1 = 1/10,刚好等于5个挑2个的组合数C(5,2)分之一。
2.产生大量排序好的随机的整数:
可以用集合来实现:随机一个数,如果集合里没有就加入集合,否则再随机一个,知道满足需求。
当需求的个数多时,可以反过来,产生不要的数,只留下要的。
第十三章:查找,二分查找
第十四章:堆排序
第十五章
1.表示单词集合的方式有两种:
1).平衡查找树:将字符串作为不可分割的部分处理,多数STL集合和映射的实现使用了它。总保持元素的有序性,能高效地执行查找前序和按序输出元素
2).散列表:速度快,但不保证平衡树的最差性能,也不支持涉及到顺序的问题。
附录4:代码优化规则
1.空间换时间:扩展数据结构,高速缓存,懒惰计算
2.时间换空间:压缩,解释程序
3.循环规则:尽量把代码移除循环,合并测试条件,循环解开,循环合并
4.逻辑规则:
1.简化单调函数:测试几个变量的单调非递减函数是不是超过了特点的阀值,如果是就不要计算。
2.重新排序测试:逻辑测试时,将廉价的经常成功的测试放在昂贵的很少成功的测试之前。
3.消除布尔变量:用if-else消除布尔变量。
5.过程规则:
1.压缩函数层次,减少调用开销(使用宏,内联函数)
2.消除递归:使用迭代消除递归。
6.表示规则:
1.初始化编译时间:程序执行前,尽可能初始化变量