编程珠玑第2章:排序、二分查找、签名、向量平移

探讨在有限内存条件下处理大量数据的策略,包括缺失数检测、重复整数查找及向量旋转等算法,旨在提高数据处理效率并减少内存占用。
仅有几百字节的内存,又该如何解决问题?


1.在文件中至少缺失一个这样的数?为什么呢?这是因为32为表示能表示N=232-1个数,(42 9496 7296)这个数>4*10的九次方=40亿大的数,因此肯定在文件中缺失一个这样的数。

2.在具有足够内存的情况下如何解决这个问题?根据前一篇介绍的位向量表示法,可以判断该数是否存在。不过要一次性将所有数都映射到内存中,大约所需要40 0000 0000/8Byte=500MB的内存。

3.‍‍0-1二分查找:探测每个整数的每个bit是0还是1,读取n=40亿个整数,第1个bit为0或为1的放到不同的文件中(每个至多为n/2亿),少于N/2个数的那组 必定缺少某个数,接着探测第2个bit是0还是1,输入至多n/2亿,输出至多n/4亿,少于N/4个数的那组 必定缺少某个数,以此类推,总的运行时间和n成正比。通过对某组排序扫描可以得到缺失的数,这样运行时间变为o(nlogn)。


Q2:给定一个包含4300000000个32位整数的顺序文件,请问如何找到一个至少出现两次的整数?

解答:二分查找。由于4.3G>32位的整数空间,根据鸽笼原理,肯定会有重复的整数。搜索范围从所有的32位正整数开始(全部当成unsigned int,简化问题),即[0, 2^32),中间值即为2^31。然后遍历文件,如果小于2^31的整数个数大于N/2=2^31,则调整搜索范围为[0, 2^31],反之亦然;然后再对整个文件再遍历一遍,直到得到最后的结果。这样一共会有logn次的搜索,每次过n个整数(每次都是完全遍历),总体的复杂度为o(nlogn)。

例子:数组[4,2,5,1,3,6,3,7,0,7],假定从3位的整数空间内搜索。第一次的范围为[0,8),遍历过后发现[0,4)范围内的整数个数为5,于是调整为搜索[0,4)范围内的整数。第二次发现[2, 4)范围内的证书为3,大于2,于是调整为[2, 4)。再经过第三次的遍历,找出3为重复出现的整数。

改进:上面的办法有很多的冗余。于是提出了一个办法:建立一个新的文件(是顺序文件就可以)。在一次遍历过后,确定搜索的范围后,把原有文件里这个范围内的整数写到新的文件里去,下次搜索就只要搜索这个新文件了。这样可以得到近似线性的复杂度(但是常数项应该很大)。

若整数是排好序的,一个线性查找的解法

public static void main(String[] args) 

{

        int[] arr = {2,3,4,5,7,11,12,12,13,14,15};

        int i=0;

        int increase=arr[0];                         //suppose = arr[0];                                         

        for(;i<arr.length;i++)

        {                                            //suppose++;

            if(arr[i] > i + increase)                //if(arr[i] > suppose)     

            {

                increase += (arr[i]-i-increase);     //suppose += arr[i];

                continue;                            

            }

            if(arr[i] < i+increase)                  //if(arr[i] < suppose)  

            {

                System.out.println("重复的数字是:"+arr[i]);

                break;

            }

        }

   }

B.将一个n元一维向量X向左旋转i=rotdist个位置。例如,当n=8且i=3时,向量abcdefgh旋转为defghabc。简单的代码使用一个n元的中间向量在n步内完成该工作。你能否仅适用数十位额外字节的存储空间,在正比于n的时间内完成向量的旋转?

方法一:额外的空间,将X中的前i个元素复制到一个临时数组中,接着将余下n-i个元素左移i个位置,然后再将前i个元素从临时数组中复制回X中后面的位置,缺点:i个额外的空间。

方法二:定义一个将X中元素左移一位的函数,然后调用该函数i次,缺点:太费时间

方法三:先将X[0]移动到临时空间t中,然后将X[i]移动到X0中,X2i移动到Xi,下标对n取模依次类推,知道我们有回到X0中提取数据(改从t中取)。如果该过程不能移动所有元素,我们再从X1中开始移动(循环何时终止?)

i=rotdist 和 n的最大公约数 是循环的次数:循环替代元素,每一步前进rotdist=i长度,相当于把所有元素分成了gcd(n,rotdist)个组,每次只有组内的位置可以移动,类此CUDA里面放置bank-conflict也要gcd(i,j) = 1,这样多个i就不会重复访问一个j。

http://hi.baidu.com/banyantree/blog/item/2563b5cc9eb00d1700e9284b.html

运行swap(x, 10, 2),观察每一趟的输出。这个需要2趟。
j:0, k:2
2, 1, 2, 3, 4, 5, 6, 7, 8, 9
j:2, k:4
2, 1, 4, 3, 4, 5, 6, 7, 8, 9
j:4, k:6
2, 1, 4, 3, 6, 5, 6, 7, 8, 9
j:6, k:8
2, 1, 4, 3, 6, 5, 8, 7, 8, 9
2, 1, 4, 3, 6, 5, 8, 7, 0, 9
*****************
j:1, k:3
2, 3, 4, 3, 6, 5, 8, 7, 0, 9
j:3, k:5
2, 3, 4, 5, 6, 5, 8, 7, 0, 9
j:5, k:7
2, 3, 4, 5, 6, 7, 8, 7, 0, 9
j:7, k:9
2, 3, 4, 5, 6, 7, 8, 9, 0, 9
2, 3, 4, 5, 6, 7, 8, 9, 0, 1
*****************

这个方法的好处是每一个元素一步到位,大大减少了交换次数。

//求最大公约数

int gcd(int i, int j){                                i > j
        while(i != j){                                while(i%j){
                if(i > j)                                    i =j;
                        i -= j;                              j = i%j;
                else                                  }
                        j -= i;                       return j; 
        }
        return i;
}

void bitswap(int *x, int n, int rotdist){

        int i, t, j, k, ii;

        if(rotdist == 0 || rotdist == n)
                return;

        for(i = 0; i < gcd(rotdist, n); i++){
                t = x[i];
                j = i;
                while(1){
                        k = j + rotdist;
                        if(k >= n)
                                k -= n;
                        if(k == i)
                                break;
                        x[j] = x[k];
                        j = k;
                }
                x[j] = t;
        }
}

方法四:方向交换,手摇法原地递归旋转向量或字符串。这种算法省内存,但比直接开一块内存然后交换的方法要慢。

void reverse(char* s, int start, int end){
        int i, j, tmp;
        for(i = start, j = end; i < j; i++, j--){
                tmp = s[i];
                s[i] = s[j];
                s[j] = tmp;
        }
}

int main(int argc, char* argv[]){
        int len = strlen(argv[1]);
        int i = atoi(argv[2]);

        reverse(argv[1], 0, i - 1);//转A得  AB --> ArB
        reverse(argv[1], i, len - 1);//转B  ArB-->ArBr

        reverse(argv[1], 0, len - 1);//转ArBr)r 得 BA. ArBr --> BA

        printf("%s\n", argv[1]);

}


方法五:Gries的迭代解决方法,块交换:假设a比b短,将b分为b1和br(和a一样长) 两部分,a和br交换,即ab1br交换为brb1a,递归交换brb1.不过这个每个元素不能一步到位,不如上一个效率高。

//x[a...a+m-1]和x[b...b+m-1]交换

void swap(int *x, int a, int b, int m){
        int tmp;
        int i;

        for(i = 0; i < m; i ++){
                tmp = x[a + i];
                x[a + i] = x[b + i];
                x[b + i] = tmp;
        }
}

void swapm(int* x, int n, int rotdist){
        int i, j, p, ii;

        if(rotdist == 0 || rotdist == n)
                return;

        i = p = rotdist;
        j = n - p;

        while(i != j){
                if(i > j){
                        swap(x, p - i, p, j);
                        i -= j;
                }else{
                        swap(x, p - i, p + j - i, i);
                        j -= i;
                }
        }
        swap(x, p - i, p, i);
}

C.给定一本英语单词词典,请找出所有的变位词集。例如,因为“pots”,“stop”,“tops”相互之间都是由另一个词的各个字母改变序列而构成的,因此这些词相互之间就是变位词。

错误1:考虑如何求解某个单词的各个字母的所有置换,n!个可能。

错误2:比较所有的单词对

正确方法:给每个单词签名,同一变位词就具有相同的签名,然后将相同签名的单词归拢在一起。考虑两个子问题:签名(签名按字母顺序排序单词中的字母)+收集相同签名的词(按签名排序)。

//签名sign

int charcmp(char *a,char *b)

{

    return *a - *b;

}

#define WORDMAX 100

int main(void)

{   char word[WORDMAX],sign[WORDMAX];

    while(scanf(%s,word) != EOF)

    {    strcpy(sign,word);

         qsort(sign,strlen(sign),sizeof(char),charcmp);

         printf("%s %s\n",word,sign);

    }

    return 0;

}

//输出相同签名的 变位词 到单行squash

int main()

{   char word[WORDMAX],sign[WORDMAX],oldsign[WORDMAX];

    int linenum = 0;

    while(scanf("%s %s",word,sign) != EOF)

    {    oldsign = sign;

         if(strcmp(sign,oldsign) != 0 && linenum > 0)

                        printf("\n");

         strcpy(oldsign,sign);  

         linenum++;

         printf("%s ",word);

     } 

     printf("\n");

     return 0;

}

sign < dictionary  | sort | squash > gramlist 

Q7:为了转置以行为主的矩阵,预先考虑每条记录的列和行,然后调用系统磁带排序按列排序,再按行排序,然后使用另一个程序删除行列号。

Q8:定一个具有n个元素的实数集,一个实数t以及一个整数k,请问如何快速确定该实数集是否存在两个元素,它们的总和为t?

寻找k次第k小?

 

本文出自:http://hi.baidu.com/renzherl/item/25c9504eef7062efa4c066a4

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值