《编程珠玑》笔记2 算法设计
这一章主要讨论算法设计这个主题。
1.问题
A:给一个最多含有40亿(设为n)个随机排列的32位整数的顺序文件,找出一个不再文件中的数。(若具有足够内存?若只有几百字节的内存可用?)
首先,2^32 = 4294967296 > 40亿,所以一定缺失数。40亿个数字放入内存,每个四字节(unsigned int),需要(4*10^8)/(8*10^6) = 500MB.
B: 将一个n元向量左旋转i个位置,如n=8,i=3,向量abcdefgh旋转为defghabc.
C: 给定一个英语词典,找出所有变位词集合,变位词如pots, stop, tops为一组变位词。其中每个单词通过改变其他单词中的字母顺序得到。
2.解法
简单来想,对这三个问题,
A. 可以通过建立一个大小为 2^32 的bool数组,用来表示相应的整数是否出现,进而,这种数组其实可以用第一章中学过的位图bitmap来实现,先对各位全部初始化为0。遍历输入文件,若某数出现,将该位置为1。最后,遍历一遍这个位图,所有为0的位就表示该位对应的索引为出现过。时间复杂度为 O(n)+ 2*L (L为定值,表示 2^32,乘以2表示一次初始化和一次遍历),空间上, (2^32)/(8*10^6) = 512 MB.
这个方法的最大缺点,就是耗费内存。这时采用多趟算法,假如我们只有 500 B内存,就是说我们一次只能申请到 0~3999 个可用位(实际上加上其他开销,可能申请不到这么多),可以先遍历一趟文件,只测试在 0~3999 之间的整数,第二趟,测试 4000~7999 ,依次类推……。(如果题目要求只找到一个数就停止,那这种方法应该很不错。。)
B. 简单方法就是每次左移一位,然后重复调用这个左移操作i次;
比较好一点的方法是 Reverse(0, i-1); Reverse(i, n-1); Reverse(0, n-1);
C. 第一次想到的是使用STL中的map,采用26位的数组作为键,然后对每一个单词,计算它所对应的数组,属于同一数组的放入一个map中,其值是属于该键的单词的链表(也可以用STL中的set来实现)
书上的解法如下:
A:利用二分搜索的方法,从每个整数都是32位来看,
第一趟,读取40亿个输入,测试每个数的最高位,把起始位为0的写入一个文件,把起始位为1的写入另一个文件;读取n个
第二趟,对第一趟较小的文件(数目一定小于20亿),测试第二位。按照同样的分法; 读取n/2个
……
最后一趟,一定会有一个文件为空,总时间: n+n/2+n/4+……, = 2n,
排序文件并扫描,得到所有缺失的整数。
B:提出了三种方法,最后一种与前面相同。
//杂技算法 //块交换递归算法 若要交换ab得到ba,设a比b短,将b分成bl和br,另br长度等于a,我们的目标是得到bl br a,但可以先交换br和a,得到br bl a,这样只要交换br和bl即可。可以递归实现。 //求逆算法,需要时间为O(n) char *str; void reverse(int st, int ed) //表示从st到ed的位置求逆 { char temp; for(; st < ed; st++; ed--) { temp = str[st]; str[st] = str[ed]; str[ed] = temp; } } void shift(int n, int i) { reverse(0, i-1); reverse(i, n-1); reverse(0, n-1); }
C:肯定不能使用基本的比较方法,所以这里提出了 标识 的思想,我们确定字典中每一个单词的标识,然后把有相同标识的单词集中起来。关于标识的方法,一种就是基于确定排序的,也就是对每个单词的字母,按照字母表排序,把排序过后的字符串作为标识;另一种就是上面的,使用长为26的数组作为标识;
3.原理
排序:产生有序输出;为另一个程序做准备(通常为二分搜索程序)。
二分搜索:在有序查找中很常用。
标识:要进行分类时,选择一个好的标识。
4.习题
4.1给定一个单词,查找该单词在字典中的所有变位词。
首先计算单词的标识, 然后顺序读取整个字典,计算每个单词的标识并比较;
如果允许对字典预处理,可以使用map<标识,单词集>先预处理词典,使用二分搜索或hash的方法,直接找到相应的变位词集。
4.2仍可以使用bitmap,最好使用多趟算法,因为只要求找到一个即可
4.3
4.5使用求逆算法,最后一步加上对b的翻转:
abc def gh —— cba def gh —— cba def hg —— gh fed abc —— gh def abc
4.6这里只是把名字的标识作为 按键编码 来表示。
方法与4.1相同,先对名字文件预处理,然后返回对应标识的所有名字即可。
4.7对在磁带上的矩阵求转置:先按列排序,在按行排序。
4.8是否存在这样的k元子集,只要找到前k个最小元素即可。与TopK算法相同,TopK算法具体见《编程之美》2.5节,有多种方法:
全部数据堆排序:O(nlogn)
选择法:O(K*N),直接选择出前k个最小的数。
维护k个元素的数组,读取n个输入,每次找到k元素数组中最大的数,然后新到来的数与该数比较。这样对k元素的数组,可以遍历找到最大的数,也可以用堆,相应的时间复杂度为O(n*logk)和O(n*k).
4.10往灯泡中灌水。。
5.变位词程序实现
下面是一个用C++实现的简单的变位词程序
1 #include<iostream> 2 #include<fstream> 3 #include<string> 4 #include<cctype> //tolower function 5 #include<vector> 6 #include<map> 7 using namespace std; 8 9 string getfg(const string & str) 10 { 11 string temp; 12 int count[26]; 13 for(int i = 0; i < 26; i++) 14 count[i] = 0; 15 for(int i = 0; i < str.size(); i++) 16 { 17 int n = tolower(str[i]) - 'a'; 18 count[n]++; 19 } 20 21 for(int i = 0; i < 26; i++) 22 { 23 while(count[i] != 0) 24 { 25 char cur = (char)(i+'a'); 26 temp.append(1, cur); 27 count[i]--; 28 } 29 } 30 return temp; 31 } 32 33 int main(int argc, char**argv) 34 { 35 ifstream fin(argv[1]); 36 37 map<string, vector<string> > shuf; 38 string str, flag; 39 while(fin >> str) 40 { 41 flag = getfg(str); 42 if(shuf.find(flag) == shuf.end()) 43 { 44 vector<string> ns; 45 ns.push_back(str); 46 shuf.insert(make_pair(flag, ns)); 47 } 48 else 49 shuf[flag].push_back(str); 50 } 51 52 map<string, vector<string> > ::iterator iter = shuf.begin(); 53 while(iter != shuf.end()) 54 { 55 vector<string>::iterator iv = iter->second.begin(); 56 while(iv != iter->second.end()) 57 { 58 cout << *iv++ << " "; 59 } 60 iter++; 61 62 cout << endl; 63 } 64 }