编程珠玑 Chap1

来源:互联网 发布:开发人员从事数据分析 编辑:程序博客网 时间:2024/05/21 11:29
第一章中讨论的问题:给定最多1000万个无重复的7位整数,如何利用1MB(左右)的内存空间(磁盘空间充足)完成排序?

三种解决方案:

1.利用基于磁盘的归并排序。这种方法最容易想到,缺陷也很明显,归并排序通常需要原始文本两倍的内存空间,原始文本大小为10000000*32bits(int型数据)=40MB,内存肯定是不够用的,需要外部工作文件(磁盘)的支持。即整个过程是:读取数据->多次读取磁盘归并排序->读取磁盘输出文件,O(nlogn)的复杂度加上反复的磁盘读取,排序速度必定很慢。

2.采用多趟排序算法。将原始40M文件分为40个段落(按由大到小),读取数据说40趟,每次对其中某个段落的数据排序(使用快排)、输出,最后组合。

3.利用位图的数据结构(很巧的方法,简单又有效)。假设当前有数据1、7、3、8,那么可以使用10100011的比特串来表示(从低位读取可以直接得到排序后的结果),按照这个原理,1000万个7位整数需要长度为1000万的bit串,大约需要1.2M的空间。过程变的很简单,读取数据流,将比特串相应的位置置为1,然后从低位遍历整个比特串,将位置为1所对应的整数输出,得到排序结果。答案给出的实现代码:

/* * Organized by jfk, 2012/10/17 * Ref: Programming Pearls */#include <stdio.h>#define BITSPERWORD 32#define SHIFT 5#define MASK 0x1F#define N 10000000int a[1 + N/BITSPERWORD];/* “i>>SHIFT”用来判断i属于哪一个int数,一个int占用32 bits空间(通常来说); * “(i & MASK)”取i与32的余数,相当于确定offset后的value。 * 注意,"i<<(i & MASK)"是将1向右移(i & MASK)位! */void set(int i){ a[i>>SHIFT] |= (1<<(i & MASK));}void clr(int i){ a[i>>SHIFT] &= ~(1<<(i & MASK));}int test(int i){ return a[i>>SHIFT] & (1<<{i & MASK});}void main(){//将所有位置0for(int i = 0; i < N; i++)clr(i);//将位图对应的i位置为1while(scanf("%d", &i) != EOF)set(i);//若位图的i位为1的话,则输出i的值for(int i = 0; i < N; i++)if(test(i)) printf("%d\n", i);}

Step further:若严格限制使用1M内存,怎么办?

1.找稀疏位,比如问题中的整数时电话号码;

2.使用两趟排序,在2n的时间开销和n/2的空间开销完成排序。

Step further&further:上述位图排序的方法很取巧,但限定了前提,输入数据必须没有重复。如果每个整数最多出现10次,如何重写代码?我的实现方法如下:

/* * Copywrite @ jfk, 2012/10/17 * Statement when Ref. */#include <stdio.h>/* * 每个整数最多出现10次,需要使用4bit表示, * 因此,一个int型数据可以表示8个整数。 */#define WORDS_PER_INT 8#define SHIFT 3#define MASK 0x07#define CHECK_0 0x0F#define N 10000000int a[1 + N/WORDS_PER_INT];/* * set()函数将整数i的计数加1。 * 实现思路: * 1.确定offset。判断整数i所在a[]数组下标,用i除以8(即右移3位,i>>SHIFT)得到; * 2.确定value。取i除以8的余数"(i & MASK)",判断i位于当前a[]元素记录的哪一个数字, *   由于每一个数字包含4 bits,因此乘以4得到当前位置,即((i & MASK) * 4); * 3.将当前位置的数值增1。即,a[i]的值加上(1<<((i & MASK) * 4));    */void set(int i) { a[i>>SHIFT] += (1<<((i & MASK) * 4));}/* * clr()函数将整数i出现的次数减times次,默认减1次; * 思路:类比set()的实现思路,CHECK_0用于查看当前i的次数 * 边界处理:当a[]中记录该整数的次数小于times次时,直接清0,防止影响到其他位 */void clr(int i, int times = 1){int tmp;tmp = a[i>>SHIFT] & (CHECK_0<<((i & MASK) * 4));tmp>>((i & MASK) * 4);if(tmp < times) a[i>>SHIFT] -= (tmp<<((i & MASK) * 4));else a[i>>SHIFT] -= (times<<((i & MASK) * 4));}/* * test()函数打印整数i的值和a[]中记录i出现的次数 */void test(int i){int tmp;tmp = a[i>>SHIFT] & (CHECK_0<<((i & MASK) * 4));tmp = tmp>>((i & MASK) * 4);if(tmp != 0)printf("Integer: %d, Times: %d\n", i, tmp);}void main(){// User Define}

Back now:回到问题开始,如何生成[0,n-1]之间的k个不同的随机顺序的随机整数(无重复)?代码:

void random(int *arr, int k, int n){for(int i = 0; i < k; i++)swap(arr[i], arr[i + rand() % (n - i)]);}


习题9:One problem with trading more space to use less time is that initializing the space can itself take a great deal of time. Show how to circumvent this problem by designing a technique to initialize an entry of a vector to zero the first time it is accessed. Your scheme should use constant time for initialization and for each vector access, and use extra space proportional to the size of the vector. Because this method reduces initialization time by using even more space, it should be considered only when space is cheap, time is dear and the vector is sparse.

理解题意:这一步很关键,反复读了几次才大致弄明白作者要表达的意思,后来还是参考了别人的分析(点击这里):

给定一个稀疏数组,对所有元素初始化要耗费大量精力,如何设计一种方式:开始时不进行全局初始化,当访问某个元素时,若判断该元素之前未被初始化过(即第一次访问),则将该元素初始化为0(此时禁止进行读取操作);若该元素已被初始化,则可以进行正常的读取操作,防止多次初始化而覆盖数据?弄明白题意,答案也就看懂了:借助两个额外的n元向量from、to和一个整数top,如果from[i]<top并且to[from[i]]==i,则元素data[i]已被初始化;否则,data[i]未被初始化,执行以下操作

from[i]=top;to[top]=i;data[i]=0;top++;

注:top初始值为0。


“程序员的主要问题与其说是技术问题,还不如说是心理问题,他不能解决问题,是因为他企图解决错误的问题。问题的最终解决,是通过打破概念壁垒,进而去解决一个交简单的问题而实现的,Conceptual Blockbusting.”(翻译水平太差劲了)