第一題:如果不缺內存,如何使用一個具有庫的語言來實現一種排序算法?
直接使用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美元研發一種特殊鋼筆在外太空的環境下書寫,那麼前蘇聯怎麼做?
答案就是鉛筆。不過我看了幾篇介紹的鏈接,發現這個故事是改變的,鉛筆因爲有粉塵,所以可能不太好。