编程珠玑第二版 ---- 第一章个人习题集(Java)

第一题:如果不缺内存,如何使用一个具有库的语言来实现一种排序算法?

直接使用Collections.sort(list)排序

第二题:如何使用位逻辑运算来实现位向量?

package com.xck.chapter01.util;

import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.function.Consumer;

/**
 * 位向量
 *
 * 这里使用字节数组来实现位向量,主要目的是给文件中所保存的(无重复的)整数排序 --- 位排序。
 *
 * 原理:
 * 一个字节有8bit,相当于一个字节可以表示8个非负整数。
 * 在每个bit上用0/1来表示是否有这个整数存在,0-没有,1-有。
 * 存放完成之后,只需要从头开始遍历,跳过0的位,将有1的位所表示的整数放入文件中。
 *
 * 好处:在内存中只需要处理0/1即可,所有的实现均采用位运算
 */
public class BitVector implements Iterable{
    private byte[] bitVector; //位向量
    private int size; //位向量可以存放整数的数量,最多Integer.MAX_VALUE

    public BitVector(int size) {
        //size>>3=size/8
        this.bitVector = new byte[(size>>3)+1];
        this.size = size;
    }

    /**
     * 置位:1
     * index&7=index%8
     * bitVector[index>>3] = 00000000
     * 00000000 | (1<<5) = 00000000 | 00010000 = 00010000
     * 异或| 只有不相同才会为1,所以需要有重复判断
     * @param index 需要存储的数字
     */
    public boolean set(int index){
        if(isExist(index)){
            return false;
        }
        bitVector[index>>3] |= 1<<(index&7);
        return true;
    }

    /**
     * & 只有都是1才会返回1
     * 判断index是否存在
     * 11110111 & 00001000 = 0
     * 11111111 & 10000000 = 10000000
     * @param index
     * @return
     */
    public boolean isExist(int index){
        return (bitVector[index>>3] & (1<<(index&7))) != 0;
    }

    /**
     * 清0
     * @param index
     */
    public void clear(int index){
        if(!isExist(index)){
            return;
        }
        bitVector[index>>3] &= ~(1<<(index&7));
    }

    public int getSize(){
        return size;
    }


    @Override
    public Iterator<Integer> iterator() {
        return new Itr();
    }

    /**
     * 方便后面遍历,没有进行同步
     */
    private class Itr implements Iterator<Integer>{
        int cursor = 0; //游标表示下一个要返回的索引

        Itr() {}

        @Override
        public void remove() {
            throw new UnsupportedClassVersionError();
        }

        @Override
        public void forEachRemaining(Consumer action) {
            throw new UnsupportedClassVersionError();
        }

        @Override
        public boolean hasNext() {
            int i = cursor;
            if(i >= size){
                return false;
            }
            while (!isExist(i)){
                i++;
                if(i >= size)
                    return false;
            }
            cursor = i;
            return true;
        }

        @Override
        public Integer next() {
            int i = cursor;
            if(i >= size)
                throw new NoSuchElementException();
            cursor = i+1;
            return i;
        }
    }
}

第三题:实现一个位图排序。给不重复的,最大为1000w的,100w个整数排序。

这里面会涉及到如何生成这样的一个输入集合。(第四题),利用第四题生成的txt文件来排序。

package com.xck.chapter01.sortNoRepeatNum;

import com.xck.chapter01.util.BitVector;

import java.io.*;
import java.util.Iterator;

public class SortNoRepeatNum {

    /**
     * 读取指定文件中的所有整数到位向量中
     * @param numSize
     * @param srcPathName
     * @param targetPathName
     * @return
     */
    public static void readFileToVector(int numSize, String srcPathName, String targetPathName){
        File srcFile = null;
        FileReader fr = null;
        BufferedReader br = null;
        BitVector bitVector = new BitVector(numSize);
        try {
            srcFile = new File(srcPathName);
            if(srcFile.exists()){
                fr = new FileReader(srcFile);
                br = new BufferedReader(fr);

                int count = 0;
                String result;
                while((result = br.readLine())!=null){
                    boolean setResult = bitVector.set(Integer.parseInt(result));
                    if(!setResult){
                        System.out.println(result);
                    }
                    count++;
                }
                System.out.println("读取整数: " + count);

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
                if(fr != null){
                    fr.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        File targetFile = null;
        FileWriter fw = null;
        BufferedWriter bw = null;
        long start = System.currentTimeMillis();
        try {
            targetFile = new File(targetPathName);
            if(!targetFile.exists()){
                targetFile.createNewFile();
                fw = new FileWriter(targetFile);
                bw = new BufferedWriter(fw);
                Iterator<Integer> it = bitVector.iterator();
                int count = 0;
                while (it.hasNext()){
                    bw.write(it.next()+"\r\n");
                    count++;
                }
                System.out.println("写入总数: " + count);
            }
            System.out.println("写入耗时: " + (System.currentTimeMillis()-start));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bw != null) {
                    bw.close();
                }
                if(fw != null){
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 运行耗时:

读取整数: 1000000
写入总数: 1000000
写入耗时: 386

第四题:如果你看了第三题,你将会面对生成小于n且没有重复的k个整数的问题。那么如何生成位于0至n-1之间的k个不同的随机顺序的随机整数?

看到这题,我的第一个思路就是:随机数一个个生成,然后用Set集合去重,若重复则重新生成随机数,否则直接写入文件中。这里需要注意一个问题,虽然Set是无序的,但是最后用迭代器遍历出来的并不是真正意义上的无序,大致上还是有序的。

package com.xck.chapter01.generateNoRepeat;

import com.xck.chapter01.util.Util;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

public class GenNoRepeatByRandomNum {

    /**
     * 用于测试,生成无重复的,最大为1000w的,100w个非负整数,并写入文件中.
     * 利用Set集合不允许重复的特性
     * @param numSize 生成随机数的数量
     * @param minBound 生成随机数的下限
     * @param maxBound 生成随机数的上限
     * @param pathName
     */
    public static void writeRandomNum_NoRepeate1(int numSize, int minBound, int maxBound, String pathName){
        File file = null;
        FileWriter fw = null;
        BufferedWriter bw = null;
        long repeatCountSum = 0;
        long getNumTimeSum = 0;
        long start = System.currentTimeMillis();
        try {
            file = new File(pathName);
            if(!file.exists()){
                file.createNewFile();
                fw = new FileWriter(file);
                bw = new BufferedWriter(fw);
                Set<Integer> set = new HashSet<>();

                int oldSize = set.size();
                long roudStart = System.nanoTime();
                int count = 0; //计数获取的数量
                int repeatCount = 0; //计数获取一个数字需要随机几次
                while(set.size()<numSize){
                    int random = Util.getRandomNumInRange(minBound, maxBound);
                    set.add(random);
                    int newSize = set.size();
                    if (newSize > oldSize) {
                        bw.write(random + "\r\n");
                        oldSize = newSize;
                        //计数和测试
                        long roudEnd = System.nanoTime();
                        getNumTimeSum+=(roudEnd-roudStart);
                        count++;
                        roudStart = roudEnd;

                        repeatCountSum+=repeatCount;
                        repeatCount = 0;
                    }
                    repeatCount++;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bw != null) {
                    bw.close();
                }
                if(fw != null){
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("总耗时:"+(System.currentTimeMillis()-start)+"ms");
            System.out.println("平均获取数字耗时:"+getNumTimeSum/numSize+"纳秒");
            System.out.println("平均获取数字重复随机次数:"+repeatCountSum/numSize+"次");
        }
    }
}
int bw = 100*10000;
//生成1百万个数字,每个数字都是大于>1百万的整数
GenNoRepeatByRandomNum.writeRandomNum_NoRepeate(bw*10, bw, bw*10-1, "D:\\no_repeat_num.txt");

运行的效率如下,看起来还可以:

总耗时:1013ms
平均获取数字耗时:1008纳秒
平均获取数字重复随机次数:1次

经过网上搜索,我看到这位博主的思路,觉得也很不错。他的思想说起来也很简单,就是创建一个可以存放从1到1000w的数组,然后按序放进去,然后取前面100w的长度遍历,对于每个遍历到的index都随机一个index与之交换,目的是打乱顺序,这样就生成了一组随机数。

    /**
     * 参考地址https://www.cnblogs.com/Zeroinger/p/5502382.html
     * 通过事先初始化一定长度的数组,而后进行随机交换达到随机的目的
     * @param numSize 需要获取随机数的数量
     * @param minBound 需要获取随机数的下限
     * @param maxBound 需要获取随机数的上限
     * @param pathName
     */
    public static void writeRandomNumAndNoRepeateInRandomIndex(int numSize, int minBound, int maxBound, String pathName){
        File file = null;
        FileWriter fw = null;
        BufferedWriter bw = null;
        long start = System.currentTimeMillis();
        try {
            file = new File(pathName);
            if(!file.exists()){
                int diff = maxBound - minBound;
                //要生成多少个数字,list就多长
                List<Integer> list = new ArrayList<>(numSize);
                for(int i=0; i<numSize; i++){
                    //因为要生成指定区间内的顺序数字 100~200 1000
                    list.add((minBound+i)>maxBound?minBound+(minBound+i)%diff:(minBound+i));
                }
                System.out.println("初始化耗时: " + (System.currentTimeMillis()-start));

                start = System.currentTimeMillis();
                int temp;
                for(int i=0; i<numSize; i++){//要生成多少个数字,就随机多少次
                    int randomIndex = Util.getRandomNumInRange(0, numSize);
                    if(randomIndex!=i){
                        temp = list.set(i, list.get(randomIndex));
                        list.set(randomIndex, temp);
                    }
                }
                System.out.println("随机耗时: " + (System.currentTimeMillis()-start));

                start = System.currentTimeMillis();
                file.createNewFile();
                fw = new FileWriter(file);
                bw = new BufferedWriter(fw);
                for(int i=0; i<numSize; i++){
                    bw.write(list.get(i) + "\r\n");
                }
                System.out.println("写入耗时: " + (System.currentTimeMillis()-start));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bw != null) {
                    bw.close();
                }
                if(fw != null){
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
int bw = 100*10000;
//生成1百万个数字,每个数字都是大于>1百万的整数
GenNoRepeatByRandomIndex.writeRandomNumAndNoRepeateInRandomIndex(bw, bw, bw*10-1, "D:\\no_repeat_num.txt");

下面是耗时对比,差异还是有的,耗时主要影响在拆装箱上面:

//=============List<Integer>
初始化耗时: 34
随机耗时: 318
写入耗时: 370
//=============int[]
初始化耗时: 13
随机耗时: 191
写入耗时: 204

第五题:那个程序员说他有1MB的可用存储空间,但是我们概要描述的代码需要1.25MB的空间。他可以不费力气的索取到额外的空间。如果1MB空间是严格的边界,你会推荐如何处理呢?你的算法运行时间又是多少呢?

第一种方案:根据书前面所述,可以采用多趟读取的方式,每次读取一定范围内的数据放入内存中排序,然后输出到文件中,循环往复。一个int是4个字节,1M大约可以存储100w个字节,也就是25w个int类型的整数。

所以实现方式就比较简单了:扫描txt文件,读取小于25w的全部数据,排序然后输出到文件中;再读取小于50w的全部数据,排序追加到文件末尾,以此类推,需要40趟。

第二种方案:采用两趟算法,为了节约内存,可以使用位向量,500w个位,也就是625000B来表示500w个数,第一次读取0~500w放入位向量中排序,然后输出到文件中,第二次全部读完。

第三种方案:1000w个位可以表示1000w个整数,而1000wbit占了125w个字节。这里因为电话号码位数是固定的7位,所以0开头的电话号码可以不予考虑(100w),答案说没有以1开头的(不知道为啥,姑且信了),然后这样空间需求可以降低到105wB<1MB。

但是如果用的是java,假设有1MB内存,那这个内存中肯定还有其他占用,比如说class,字符串常量等。这样就无法精确控制内存的使用。但是其实我们也不需要精确控制,对于1MB的内存来实现上面3种方案,而后对比运行时间。

使用系统的排序,int数组最大也就是131067长度,读取1千万以内的1百万个数字,起码也要70多次。

        int bw = 100*10000;
        int diff = 131067;
        int min = bw;
        int max = bw+diff;
        while (true) {
            try {
                for (int i=0;i<10*bw/diff+1;i++) {
                    SortNoRepeatNum.readIntToArrayInRange(diff, min, max
                            , "D:/no_repeat_num.txt"
                            , "D:/Users/xck/workDir/sort_num.txt");
                    min=max+1;
                    max=min+diff-1;
                }
            } catch (OutOfMemoryError e) {
                e.printStackTrace();
                max-=8;
            }
        }
    /**
     * 读取指定文件中的指定范围的整数到位向量中
     * @param numSize
     * @param srcPathName
     * @param targetPathName
     * @return
     */
    public static void readIntToArrayInRange(int numSize, int minBound, int maxBound
            , String srcPathName, String targetPathName){
        File srcFile = null;
        FileReader fr = null;
        BufferedReader br = null;
        int[] tmpArray = new int[numSize+1];
        try {
            srcFile = new File(srcPathName);
            if(srcFile.exists()){
                fr = new FileReader(srcFile);
                br = new BufferedReader(fr);

                int count = 0;
                String result;
                while((result = br.readLine())!=null){
                    int targetInt = Integer.parseInt(result);
                    if(targetInt>maxBound || targetInt<minBound) continue;
                    tmpArray[count] = targetInt;
                    count++;
                }
                System.out.println("读取总数: " + count);

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
                if(fr != null){
                    fr.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        long start = System.currentTimeMillis();
        Arrays.sort(tmpArray);
        System.out.println("排序耗时:"+(System.currentTimeMillis()-start));

        File targetFile = null;
        FileWriter fw = null;
        BufferedWriter bw = null;
        start = System.currentTimeMillis();
        try {
            targetFile = new File(targetPathName);
            if(!targetFile.exists()){
                targetFile.createNewFile();
            }
            fw = new FileWriter(targetFile, true);
            bw = new BufferedWriter(fw);
            for(int i=0; i<tmpArray.length;i++){
                bw.write(tmpArray[i] + "\n\r");
            }
            System.out.println("写入耗时: " + (System.currentTimeMillis()-start));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bw != null) {
                    bw.close();
                }
                if(fw != null){
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

如果是使用位向量,限制堆最大为1MB,经过实验,最后位数就定在4194000位上面,需要读取三趟。

        int bw = 100*10000;
        int min = bw;
        int max = 4194000;
        int diff = max-min;
        while (true) {
            try {
                for (int i=0;i<bw/diff+1;i++) {
                    SortNoRepeatNum.readIntToVectorInRange(diff, min, max
                            , "D:/no_repeat_num.txt"
                            , "D:/sort_num.txt");
                    min=max+1;
                    max=min+diff;
                }
            } catch (OutOfMemoryError e) {
                e.printStackTrace();
                max-=8;
            }
        }
    /**
     * 读取指定文件中的所有整数到位向量中
     * @param numSize
     * @param srcPathName
     * @param targetPathName
     * @return
     */
    public static void readIntToVectorInRange(int numSize, int minBound, int maxBound
            , String srcPathName, String targetPathName){
        File srcFile = null;
        FileReader fr = null;
        BufferedReader br = null;
        BitVector bitVector = new BitVector(numSize);
        System.out.println("最大位数: " + numSize);
        try {
            srcFile = new File(srcPathName);
            if(srcFile.exists()){
                fr = new FileReader(srcFile);
                br = new BufferedReader(fr);

                int count = 0;
                String result;
                while((result = br.readLine())!=null){
                    int targetInt = Integer.parseInt(result);
                    if(targetInt>maxBound || targetInt<minBound) continue;
                    boolean setResult = bitVector.set(targetInt);
                    if(!setResult){
                        System.out.println(result);
                    }
                    count++;
                }
                System.out.println("读取总数: " + count);

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
                if(fr != null){
                    fr.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        File targetFile = null;
        FileWriter fw = null;
        BufferedWriter bw = null;
        long start = System.currentTimeMillis();
        try {
            targetFile = new File(targetPathName);
            if(!targetFile.exists()){
                targetFile.createNewFile();
            }
            fw = new FileWriter(targetFile, true);
            bw = new BufferedWriter(fw);
            Iterator<Integer> it = bitVector.iterator();
            int count = 0;
            while (it.hasNext()){
                bw.write((minBound+it.next())+"\r\n");
                count++;
            }
            System.out.println("写入总数: " + count);
            System.out.println("写入耗时: " + (System.currentTimeMillis()-start));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bw != null) {
                    bw.close();
                }
                if(fw != null){
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

 所以直观上,还是位向量好。

第六题:如果那个程序员说的不是每个整数最多出现一次,而是每个整数最多出现10次,你又如何建议他呢?你的解决方案如何随着可用存储空间总量的变化而变化呢?

这个可以根据之前第三题不重复的思路进行延伸。之前因为是不重复所以可以用1bit表示是否存在,现在变成10次,10可以用4bit表示,所以这次可以变成4位表示一个整数,若是0则表示不存在,最多到10(这里生成的时候要进行限制,计数的时候也要)。

第七题:本书的第1.4节描述的程序存在一些缺陷。首先是假定在输入中没有出现重复的整数。那么如果某个数出现超过一次的话,会发生什么?在这种情况下,如何修改程序来调用错误处理函数?当输入的整数小于0或大于等于n时,又会发生什么?如果某个输入不是数值又如何?在这些情况下,程序该如何处理?程序还应该包含哪些明智的检查?描述一些用以测试程序的小型数据集合,并说明如何正确处理上述以及其他的不良情况。

第一个问题:某个数出现超过一次,因为1|1=0多关系所以需要进行是否存在的判断。

第二个问题:输入整数小于0或大于等于n,在从文件读取数据的时候,可以从配置文件中读取预先设定好的范围

第三个问题:若输入不是数值会报错,这里考虑可以加个小的try-catch,若Integer转换报错则continue。

第八题:当那个程序员解决该问题的时候,美国所有免费电话的区号都是800.现在免费电话的区号包括800,877和888,而且还在增多。如何在1MB空间内完成对所有这些免费电话号码的排序?如何将免费电话号码储存在一个集合中,要求可以实现非常快速的查找以判定一个给定的免费电话号码是否可用或者已经存在?

免费电话的区号有三个,目前我能想到也就是常规做法,就是按照顺序依次遍历到位向量中然后输出。输出我们可以分三个文件,这样你想要查找,就直接定位文件后读取对应的数据到位向量中判断即可。因为只有1MB,目前相出这方法。

第九题:使用更多的空间来换取更少的运行时间存在一个问题:初始化空间本身需要消耗大量时间。说明如何设计一种技术,在第一次访问向量的项时将其初始化为0。你的方案应该使用常量时间进行初始化和向量访问,使用的额外空间应正比于向量的大小。因为该方法通过进一步增加空间来减少初始化时间,所以仅在空间很廉价,时间很宝贵且向量很稀疏的情形下使用。

首先题目说明了初始化空间是耗时的,所以让我们在第一次访问的时候,先将其初始化为0。当然java中如果int数组未初始化,它会自动将其初始化。但是我没懂什么意思,既然不想提前初始化,那就在访问该索引的时候先将其置为0,感觉我想的有点简单,还是直接去看看已有的解答好了。

找到一篇转载的文章:https://blog.csdn.net/wordwarwordwar/article/details/40517331,看了之后我明白一件事情,那就是你得要知道你是否初始化了。因为在未初始化的时候,访问该索引在内存中的值是一个随机数,也就是你不知道这个数是什么,故而有初始化一说。好吧,现在才明白题目的意思。

上面链接里面有详细分析,简单来说:

就是给你两个和需要操作的数组相同长度的数组,称为from和to。而且这三个数组都未进行初始化,本来就是抢时间你还初始化什么。

下面先给个等式:to[from[i]]是否和i相同,如果是仅有一个还有可能,但是经过两重索引那这个概率虽然有,但是已经很小了;

前面再加上一个判断条件from[i]<top,也就是from[i]<top&&to[from[i]]==i。后面那个上面已经说了,前面那个等式表示已经被初始化过了,如果不符合那后面也不用看了,但是如果符合,也有可能是个随机数,还是要过滤一下。

为了省事,我直接copy了链接里面的图片:

观察上面的图片,to存储了data已经初始化的索引,而from和data是相对应的,里面存储了指向to的索引。例如:to[from[1]]==1。如果单用from,比如说你初始化后就在对应索引上面置0,主要是你不能肯定那个位置原本是不是0。如果单用to,那每次都要遍历一遍,而且万一本来就有个一样的随机数呢。所以二者结合一下。惊叹一下,果然是典型空间换时间,我连一开始题目的含义都没弄明白。

但是注意到使用的条件里面有个是向量很稀疏。那如果向量很稠密会怎么样,我猜想,很稠密可能会导致data数组所有索引都要访问一遍,降低了改方法的高效性?

第十题:在成本低廉的隔日送达时代之前,商店允许顾客通过电话订购商品,并在几天后上门自取。商店数据库使用客户的电话号码作为其检索的主关键字(客户知道他们的电话号码,并且这些关键字几乎都是唯一的)。你如何组织商店的数据库,以允许高效的插入和检索操作?

通过手机的尾号,标识其关键字,就像哈希表一样,冲突就用拉链法顺序检索。为了简便手机尾号后两位,这样需要有99种。取的尾号越多,冲突越少。

第十一题:

题目就不说了,就说这个答案,反正我看提示也想不出完整的答案,只想到一个飞鸽传书。

第十二题:NASA花费100w美元研发一种特殊钢笔在外太空的环境下书写,那么前苏联怎么做?

答案就是铅笔。不过我看了几篇介绍的链接,发现这个故事是改变的,铅笔因为有粉尘,所以可能不太好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章