作者在文中阐述了从该章实例引出来的一般原理:
正确的问题。明确问题,这场战役就成功了90%。
位图数据结构。该数据结构描述了一个有限定义域内的稠密集合,其中的每一个元素最多出现一次并且没有其他任何数据与该元素相关联。即使这些条件没有完全满足(例如,存在重复元素或额外的数据),也可以用有限定义域内的键作为一个表项更复杂的表格的索引。
多趟算法。这些算法多趟读入其输入数据,每次完成一步。
时间——空间折中与双赢。编程文献和理论中充斥着时间——空间的折中:通过使用更多的时间,可以减少程序所需的空间。但我的经验常常是这样的:减少程序的空间需求也会减少其运行时间。空间上高效的位图结构显著地减少了排序的运行时间。空间需求的减少之所以会导致运行时间的减少,有两个原因:需要处理的数据变少了,意味着处理这些数据的时间也变少了;同时将这些数据保存在内存中而不是磁盘上,进一步避免了磁盘访问的时间。当然了,只有在原始的设计远非最佳方案时,才有可能时空双赢。
简单的设计。Antoine de Saint-Exupery是法国作家兼飞机设计师,他曾经说过:“设计者确定其设计已经达到了完美的标准不是不能再增加任何东西,而是不能再减少任何东西。”更多的程序员应该使用该标准来检验自己完成的程序。简单的程序通常比具有相同功能的复杂的程序更可靠、更安全、更健壮、更高效,而且易于实现和维护。
Chuck Yeager将军(第一个超音速飞行的人)赞扬一架飞机的机械系统时用的词是“结构简单、部件很少、易于维护、非常坚固”。
在《编程珠玑》(第2版)的第1章(P4),作者对一个问题描述如下:
1. 问题描述
输入:一个最多包含n个正整数的文件,每个数都小于n,其中n=10^7。如果在输入文件中有任何整数重复出现就是致命错误。没有其他数据与该整数相关联。
输出:按升序排列的输入整数的列表。
约束:最多有(大约)1MB的内存空间可用,有充足的磁盘存储空间可用。运行时间最多几分钟,运行时间为10秒就不需要进一步优化了。
2. 问题分析
1MB大约有800万个位,如果需要排序的数据量小于或刚好等于1MB(单个数据的最大值不能超过1MB,而且任意两个数据不相等),那我们是否可以直接用这1MB个位来表示这个需要排序的数呢?问题当然是可以的。具体做法跟上一篇博文判断质数的几种方法的法五埃拉托斯特尼筛选法是一样的。这样做的结果,虽然占用空间大,但非常有时间优势。
3. 解决方法
作者在书中提出的做法是若给定文件中整数集合的位图数据结构,则可以分三阶段来编写程序:
1)将所有的位都置为0,从而将集合初始化为空;
2)通过读入文件中的每个整数来建立集合,将每个对应的位都置为1;
3)检验每一位,如果该位为1,就输出对应的整数,由此产生有序的输出文件。
4. 代码实现
本人实现的C++代码如下(如果已经知道输入的上限,就没有必要判断输入向量的最大值了,见下代码):
1 vector<int> SortNumbers::sort(vector<int> nums) 2 { 3 int sz = nums.size(); 4 int max = 0; 5 for (int i = 0; i < sz; i++) 6 { 7 if (nums[i] > max) 8 max = nums[i]; 9 } 10 11 vector<bool> isexist; 12 for (int i = 0; i < max + 1; i++) 13 isexist.push_back(false); 14 15 for (int i = 0; i < sz; i++) 16 isexist[nums[i]] = true; 17 18 vector<int> sorted; 19 for (int i = 0; i < max + 1; i++) 20 { 21 if (isexist[i]) 22 sorted.push_back(i); 23 } 24 25 return sorted; 26 }
注:bool类型在内存中是占一个字节的,而不是一位,这点得特别注意!
完整的代码及单元测试代码如下:
源代码:
1 #ifndef SORTNUMBERS_H 2 #define SORTNUMBERS_H 3 4 #include <iostream> 5 #include <vector> 6 using namespace std; 7 8 class SortNumbers 9 { 10 public: 11 SortNumbers(); 12 virtual ~SortNumbers(); 13 vector<int> sort(vector<int> nums); 14 15 }; 16 17 SortNumbers::SortNumbers() 18 { 19 20 } 21 22 SortNumbers::~SortNumbers() 23 { 24 25 } 26 27 vector<int> SortNumbers::sort(vector<int> nums) 28 { 29 int sz = nums.size(); 30 int max = 0; 31 for (int i = 0; i < sz; i++) 32 { 33 if (nums[i] > max) 34 max = nums[i]; 35 } 36 37 vector<bool> isexist; 38 for (int i = 0; i < max + 1; i++) 39 isexist.push_back(false); 40 41 for (int i = 0; i < sz; i++) 42 isexist[nums[i]] = true; 43 44 vector<int> sorted; 45 for (int i = 0; i < max + 1; i++) 46 { 47 if (isexist[i]) 48 sorted.push_back(i); 49 } 50 51 return sorted; 52 } 53 54 55 #endif
Boost单元测试代码(只证明其正确性):
1 #define BOOST_TEST_MODULE SortNumbers_Test_Module 2 3 #include "stdafx.h" 4 #include "../SortNumbers/SortNumbers.hpp" 5 6 struct SortNumbers_Fixture 7 { 8 SortNumbers_Fixture() 9 { 10 test = new SortNumbers; 11 } 12 ~SortNumbers_Fixture() 13 { 14 delete test; 15 } 16 17 SortNumbers * test; 18 }; 19 20 BOOST_FIXTURE_TEST_SUITE(SortNumbers_Test_Suite, SortNumbers_Fixture) 21 22 BOOST_AUTO_TEST_CASE( Sort ) 23 { 24 int in_array[15] = { 22, 11, 17, 4, 5, 33, 13, 1, 20, 2, 30, 300, 222, 55, 43 }; 25 int out_array[15] = { 1, 2, 3, 4, 5, 11, 13, 17, 20, 22, 30, 43, 55, 222, 300 }; 26 vector<int> in, out; 27 for (int i = 0; i < 15; i++) 28 in.push_back(in_array[i]); 29 30 out = test->sort(in); 31 for (int i = 0; i < 15; i++) 32 BOOST_REQUIRE(out[i] = out_array[i]); 33 34 } 35 36 37 BOOST_AUTO_TEST_SUITE_END()
6. 利用位向量来实现同样功能
这个也是第一章课后题目的第2题,解答参考自该章参考答案及一博文的解释。只想说,实在太精彩了!
先贴出针对位向量的处理程序:
1 #define BITSPERWORD 32 2 #define SHIFT 5 3 #define MASK 0x1F 4 #define N 10000000 5 int a[1 + N / BITSPERWORD]; 6 7 void set(int i) 8 { 9 a[i >> SHIFT] |= (1 << (i & MASK)); 10 } 11 12 void clr(int i) 13 { 14 a[i >> SHIFT] &= ~(1 << (i & MASK)); 15 } 16 17 int test(int i) 18 { 19 return a[i >> SHIFT] & (1 << (i & MASK)); 20 }
位向量顾名思义就是用位来存储一个数,文中说存储N=10000000个数,每一位代表一个数。我们可以定义一个int类型的数组int a[N],那么如果a[9]的值为1,则表明文件中存在一个值为9。这样的话,我们就可以用一个数组来表示这么多数。我们又知道,一个int型的数有4个字节,也就是32位,那么我们可以用N/32个int型数来表示这N个数:
a[0]表示第1~32个数(0~31)
a[1]表示第33~64个数(32~63)
…
这样,每当输入一个数字i,我们应该先找到该数字在数组的第几个元素中,也就是a[?],然后再确定在这个元素的第几位中。举个例子来说,比如输入35,那么35/32为1余3,则应该将a[1]的第4位置为1。
好,有了上面的概念,可以先来看看题中set是怎么实现的:
void set(int i) { a[i >> SHIFT] |= (1 << (i & MASK)); }
根据题目的要求,我们不可以用/运算符来设计程序,那除的话我们可以用右移来替代:
m>>n,表示m往右移动n位。输入i,除以32相当于往右移动5位,则i>>SHIFT代表i/32得到应该放在数组的第几个元素中,然后要置相应的位置位1了:
先来看看1<<(i&MASK)是什么意思。i&MASK相当于取i右移掉的部分,说白了就是取余数。比如,35的二进制表示是:… 0010 0011,MASK的二进制是0001 1111,两个相与操作得到0 0011。
而右移5位,移掉的数是0 0011,换算成10进制是3,正是余数,与上面的操作值相等,都是0 0011。
因此,1<<(i&MASK)就变成了1<<3,也就是将1右移3位,变成了1000。
然后再做一个|操作就将a[1]的第4位置1了。
对于clr函数,就是找到位置,然后清零
对于test函数,就是找到位置,做一个与操作,如果存在这个数,则返回1,不存在的话,因为是&操作,所以返回0。
该博文的作者还提供了一个测试函数:
1 #include <iostream> 2 using namespace std; 3 4 int main() 5 { 6 int i = 35; 7 //设置i,也就是置相应位置位1 8 set(i); 9 //测试是否置1了 10 if (test(i)) 11 cout << "ok" << endl; 12 13 return 0; 14 }
参考自另一博文,上边对位的操作函数改下以下形式更易于理解:
1 void set(int i) 2 { 3 a[i / 32] |= (1 << (i % 32)); 4 } 5 void clr(int i) 6 { 7 a[i / 32] &= ~(1 << (i % 32)); 8 } 9 int test(int i) 10 { 11 return a[i / 32] & (1 << (i % 32)); 12 }
接下来的测试就跟以前做的测试一样就行了。