《编程珠玑》阅读笔记1
第一章:
磁盘排序:对于一个提出的问题,不要未经思考就直接给出答案。要先深入研究问题,搞清楚这个问题的特点,根据这个特点,可能有更好的解决方案。
比如:文中:最初的需求只是“我如何对磁盘文件排序”。
我们首先想到了经典的归并排序。
但进一步了解到排序的内容是10000000个记录,每条记录都是一个7位整数,且只有1M可用的内存。每条记录不相同。
第二章:
三个问题:
1、给定一个包含32位整数的顺序文件,它至多只能包含40亿个这样的整数,并且整数的次序是随机的。请查找一个此文件中不存在的32位整数。在有足够内存的情况下,你会如何解决这个问题?如果你可以使用若干外部临时文件但可用主存却只有上百字节,你会如何解决这个问题?
若内存足够用则可用位图方式。
若内存不够用,可用二分查找的方式。
2、将一个具有n个元素的一维向量向左旋转i个位置。例如,假设n=8,i=3,那么向量abcdefgh旋转之后得到向量defghabc。你只能使用1字节的辅助变量。
先将向量abcdefgh逆序,得到hgfedcba,再以后i个位置为分割,hgfed和cba分别逆序,得到defghabc
3、给定一本英语单词词典,请找出所有的变位词集。例如,因为”pots” “stop” “tops”相互之间都是由另一个词的各个字母改变序列而构成的,因此这些词相互之间就是变位词。
将词典中的每个单词都进行签名,这样同一变位词类中的单词会具有相同的签名,然后将具有相同签名的单词归拢到一起。
签名:通过计数表示字母重复次数的方式给出。(例如:”mississippi”的签名可能是”i4m1p2s4”)。
然后以签名为键进行排序,把具有相同签名的单词挤压到一行。
第三章:数据结构程序
表单字母编程:
例如:
当你登录到一个购物网站,其会弹出如下页面:
作为一个程序员,你应该意识到计算机从数据库中查询你的姓名并取回了相关数据。
但是程序该如何精确地从你的数据库记录中构建那个定制的Web页面呢?草率的程序员可能很想像下面那样开始编写程序:
更好的方法就是编写一个依赖于下面这样的表单字母模式的表单字母生成器:
表示法$i表示记录中的第i个字段,所以$0表示姓,等等。下面的伪码将解释该模式。
(这段伪码假定字母$字符在输入模式中写为$$)
read field from database
loop from start to end of schema
c= next character in shema
if c != '$'
printchar c
else
c = next character in schema
case c of
'$': printchar '$'
'0'-'9': printstring field[c]
default: error("bad schema")
与编写明显的程序相比,编写生成器和模式或许更加简单些。将数据从控件中分离开来可以使你大大受益:如果字母重新设计,那么可以在文本编辑器中操作该模式。
构造更多更好的数据结构,把数据从代码中分离出来。 你的程序会更短小、精悍、易于维护、易于扩展。
面向过程编程:把数据从代码中抽离出来
面向对象编程:把数据从代码中抽离出来且把处理这个数据结构的专门代码和此数据结构 绑定到一起,组成一个类。
数据结构对软件的一个贡献:将大程序缩减为小程序。数据结构还有其他许多正面的影响,包括时间和空间的缩减。增加可移植性和可维护性。
程序员在对空间缺乏无能为力时,往往会脱离代码的纠缠,回过头去凝神考虑他的数据,这样会找到更好的方法。表示法是编程的精华。 ——Fred Brook 《Mythical Man Month》
几个原则:
1、将重复性代码改写到数组中。
使用最简单的数据结构——数组——来表示一段冗长的相类似的代码往往能达到最佳效果。
(例如各种if 整合到数组中)
2、封装复杂的结构
当你需要一个复杂的数据结构时,使用抽象的术语对它进行定义,并将那些操作表示成一个类。
3、让数据去构造程序。
使用适当的数据结构去替换复杂的代码,这可以使数据起到构造某个程序的效果。
(在编写代码之前,好的程序员通常都会通篇理解构建程序时所围绕的输入数据结构、输出数据结构以及中间数据结构。)
第四章:编写正确的程序
断言在程序维护期间很关键。
保持代码的简单性通常是正确性的关键。
关于循环不变式:(还可参照《算法导论》插入排序部分)
例如二分查找:
二分查找的关键概念在于我们总是知道如果t在数组x[0…n-1]中的某处,那么它必定在x的某个范围中(我们致力于缩减这个范围)。我们使用简写形式mustbe(range)表示如果t在数组中,那么它必定在range中。
初始化range to 0…..n-1
Loop
{invariant: must(range) }
If range is empty,
Break and report that t is not in the array
计算m (range的中间位置)
用m值来缩减range的范围
若在缩减的过程中发现了t目标值,则break,打印出它的位置。
本程序的关键部分就是loop invariant,即用{}括起来的部分。有关程序状态的这个断言被称为不变式(invariant),因为每一次循环迭代的开始和结尾它都是真值(true)。
l = 0; u = n-1
loop
{mustbe(l, u) }
if l > u
p = -1; break
m= (l + u)/2
case
x[m] < t : l = m+1
x[m] == t : p = m; break
x[m] > t : u = m-1
程序验证的基本技术是先精确指定该不变式,并在我们编写每一行代码时密切关注以保持该不变式。在我们将算法草图转成伪码时,这种技术对我们帮助极大。
(当循环涉及到影响不变式的语句时,要检测此不变式是否更改。)
二叉查找的优化:返回目标数第一次出现的位置。
l = -1; u = n
while l+1 != u
//invariant: x[l] < t && x[u] >= t && l < u
m= (l + u) / 2
if x[m] < t
l = m
else
u = m
//assert l+1 = u && x[l] < t&& x[u] >= t
p = u
if p >= n || x[p] != t
p= -1
第五章:构建脚手架
当不得不调试一个深深嵌入到一个大型程序中的小算法时,我有时会使用诸如单步调试那样的调试工具来调试该大型程序。但是,当我像上面那样使用脚手架调试一个算法时,使用printf语句通常实现要更快一些,比起复杂调试工具来说也要更加有效一些。
我们使用断言来陈述我们相信某个逻辑表达式是正确的。