1. 程式人生 > >程式設計珠璣——第一章習題解答

程式設計珠璣——第一章習題解答

1、如果不缺記憶體,如何使用一個具有庫的語言來實現以後總排序演算法和排序集合?

答:C++有實現排序的庫函式:sort。該函式的實現是快速排序。另外C++的容器Map和set均可以實現排序。由於Map和set的實現是紅黑樹,所以具有自動排序功能。

快速排序演算法實現:

void QuickSort(int *array,int left,int right){
	
 int i =left;
 int j = right;
 int temp = array[left];
 if(left>=right)return;
 while(i!=j){
	 while(array[j]>=temp&&i<j)
		j--;
	 if(j>i)
	 array[i]=array[j];
	 while(array[i]<=temp&&i<j)
		 i++;
	 if(i<j)
		 array[j] = array[i];
 }
 array[i] = temp;
 QuickSort(array,left,i-1);
 QuickSort(array,i+1,right);
}
快速排序疑問:

 QuickSort(array,left,i-1);
 QuickSort(array,i,right);

如果改成這樣的遞迴形式,為什麼程式會出錯??

標準的快速排序一次排序將陣列分成三段(左邊小於標稱值的陣列,標稱值,右邊大於標稱值的陣列)。如果在遞迴過程中,如上將陣列分成兩段(其中一段陣列包含標稱值),則在演算法具體實現的時候會出現問題。

考慮一種特殊的陣列 {1,5,6,7,8},這個陣列按書上的呼叫模式QuickSort(array,left,i-1);,QuickSort(array,i,right);;在演算法的具體實現中,我們總是將標稱值選擇為陣列的第一個元素。對於這個陣列,一次排序之後的結果不變,陣列還是{1,5,6,7,8},此時看遞迴呼叫的表達形式QuickSort(array,0,-1),QuickSort(array,0,length-1);且不管QuickSort(array,0,-1)這裡是否對輸入做了邊界檢查;注意,QuickSort(array,0,length-1),這步遞迴排序的還是原始的整個陣列,這也就意味著遞迴呼叫不會有結束的時候,執行程式肯定會出現棧溢位。

實測發現, QuickSort(array,left,i);;QuickSort(array,i+1,right);這樣遞迴呼叫,程式是沒有問題的;此時我們發現表達形式變為了QuickSort(array,0,0),QuickSort(array,1,length-1);其中QuickSort(array,0,0)符合遞迴的終止條件;QuickSort(array,1,length-1)實際排序的陣列長度減了1,那也就意味著遞迴肯定有結束的時候,所以這個呼叫在這種快速排序的寫法中是沒有問題的。但是如果我們將標稱值設定為陣列的最後一個元素,那麼這種寫法就會出現棧溢位的情況。

所以,用演算法導論上快速排序的寫法才是最保險的

2、如何使用位邏輯運算(例如與、或、移位)來實現位向量?(指的是實現位向量的置位,清零,探測的三個操作)

答:

class BitVector{
	private:
		const int shift;
		const int mask;
		const int bitPerWord;
		int *a;

	public:
		const int n;

	public:
		BitVector():shift(5),mask(0x1F),bitPerWord(32),n(10000000)
		{
			a = new int[1+n/bitPerWord];
			for(int i=0;i<n;i++){
				clr(i);
			}
		}
		~BitVector(){
		
		}

		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);
		}
	};
總體來說,這個用一個一維陣列標書一個二維陣列的方法。首先看輸入引數i,這個引數是用來指定到底想要操作哪一位bit的。具體來說,i的第五位確定

某一行的具體哪一個元素,i的高位用來確定是具體哪一行(陣列a[]中的哪一個元素,這個int型別的元素有32個bit)。

a[i>>shift]  確定取哪一個陣列元素,i>>shift表示取i中的高位來作為陣列a的下標</span>
(i&mask)   取i的低五位,在置位,清零,探測操作中,都是先取得低5位的陣列(表示具體32bit的哪一位),
置位操作是按位或;清零操作是與非,探測是與

3、執行時效率是設計目標的一個重要組成部分,所得到的程式需要足夠高效。在你自己的系統上實現點陣圖程式並度量其執行時間。該時間與系統排序的執行時間以及習題一種排序的執行時間相比如何??假設n為10 000 000,切輸入檔案包含1 000 000個整數。

答:

點陣圖是一種很特殊的資料結構,可以利用點陣圖來排序,但是這種排序方法對輸入的資料是有比較嚴格的要求(資料不能重複,大致知道資料的範圍)。舉個例子,假如有一個集合{3,5,7,8,2,1},我們可以用一個8位的二進位制向量set[1-8]來表示該集合,如果資料存在,則將set相對應的二進位制位置1,否則置0.根據給出的集合得到的set為{1,1,0,1,0,1,1,1},然後再根據set集合的值輸出對應的下標即可得到集合{3,5,7,8,2,1}的排序結果。

一.點陣圖的應用:

      1.給40億個不重複的unsigned int的整數,沒有排過序,然後再給一個數,如果快速判斷這個數是否在那40億個數當中。

      因為unsigned int資料的最大範圍在在40億左右,40*10^8/1024*1024*8=476,因此只需申請512M的記憶體空間,每個bit位表示一個unsigned int。讀入40億個數,並設定相應的bit位為1.然後讀取要查詢的數,檢視該bit是否為1,是1則存在,否則不存在。

      2.給40億個unsigned int的整數,如何判斷這40億個數中哪些數重複?

      同理,可以申請512M的記憶體空間,然後讀取40億個整數,並且將相應的bit位置1。如果是第一次讀取某個資料,則在將該bit位置1之前,此bit位必定是0;如果是第二次讀取該資料,則可根據相應的bit位是否為1判斷該資料是否重複。

二.點陣圖的實現

     由於在C語言中沒有bit這種資料型別,因此必須通過位操作來實現。

     假如有若干個不重複的正整數,範圍在[1-100]之間,因此可以申請一個int陣列,int陣列大小為100/32+1。

假如有資料32,則應該將邏輯下標為32的二進位制位置1,這個邏輯位置在A[1]的最低位(第0位)。

因此要進行置1位操作,必須先確定邏輯位置:位元組位置(陣列下標)和位位置。

位元組位置=資料/32;(採用位運算即右移5位)

位位置=資料%32;(採用位運算即跟0X1F進行與操作)。

其他操作如清0和判斷兩個操作類似。

用bitset集合實現點陣圖排序:

#include<iostream>
#include<bitset>

#define MAX 1000000
using namespace std;

bitset<MAX+1> bit;	//宣告一個有(MAX+1)個二進位制位的bitset集合,初始預設所有二進位制為0

int main(int argc,char *argv[]){
	int n,i;
	while(scanf("%d",&n)!=EOF)
	{
		bit.set(n,1);	//將n位置1
	}
	for(i = 0;i<MAX+1;i++){
		if(bit[i]==1)
			printf("%d  ",i);
	}
	system("pause");
}

自己實現bitset的點陣圖程式:

#include<iostream>
#include<bitset>

#define MAX 1000000
using namespace std;
class BitVector{
	private:
		const int shift;
		const int mask;
		const int bitPerWord;
		int *a;

	public:
		const int n;

	public:
		BitVector():shift(5),mask(0x1F),bitPerWord(32),n(10000000)
		{
			a = new int[1+n/bitPerWord];
			for(int i=0;i<n;i++){
				clr(i);
			}
		}
		~BitVector(){
		
		}

		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);
		}
	};



int main(int argc,char *argv[]){
	int n,i;
	BitVector bit;

	
	while(scanf("%d",&n)!=EOF)
	{
		bit.set(n);	//將n位置1
	}
	for(i = 0;i<MAX+1;i++){
		if(bit.test(i)==1)
			printf("%d  ",i);
	}
	
	system("pause");

}

程式有一點小問題,,繼續除錯

進過除錯之後發現,test函式的編寫有問題。

return a[i>>shift]&(1<<i&mask);<span style="white-space:pre">	</span>//這裡返回的是2^n,n=i&mask
<span style="font-family:Arial, Helvetica, sans-serif;">應該修改為:return (a[i>>shift]&(1<<(i&mask)))>>(i&mask);<span style="white-space:pre">	</span>//應該還有一個右移n位的操作</span>
#include<iostream>
#include<bitset>

#define MAX 1000000
using namespace std;
class BitVector{
	private:
		const int shift;
		const int mask;
		const int bitPerWord;
		int *a;

	public:
		const int n;

	public:
		BitVector():shift(5),mask(0x1F),bitPerWord(32),n(10000000)
		{
			a = new int[1+n/bitPerWord];
			for(int i=0;i<n;i++){
				clr(i);
			}
		}
		~BitVector(){
		
		}

		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)))>>(i&mask);
		}
	};



int main(int argc,char *argv[]){
	int n,i;
	BitVector bit;

	
	while(scanf("%d",&n)!=EOF)
	{
		bit.set(n);	//將n位置1

	}
	for(i = 0;i<MAX+1;i++){
		if(bit.test(i)==1)
			printf("%d  ",i);
	}
	
	system("pause");

}

4、如果認真考慮了習題3,你將會面對生成小於n且沒有重複的k個整數的問題。最簡單的方法就是使用前k個正整數。這個極端的資料集合將不會明顯地改變點陣圖方法的執行時間,但是可能會歪曲系統排序的執行時間。如何生成位於0至n-1之間的k個不同的隨機順序的隨機整數?儘量使你的程式簡短且有效。

答:

解決這個問題可以使用以空間換時間的方式,基本的思想是 利用洗牌的原理,將n個數(0至n-1)按次序排好,依次讓每個數和一個隨機挑選出的位子進行互換,這樣肯定不會重複,而且次序被打亂,具有隨性。 只用交換k次,就可以取出k個小於n的互不相同的隨機數。(洗牌演算法)

Java程式碼實現:

public static void main(String[] agrs) {
		long start = System.currentTimeMillis();
		int n = 10000000;
		int k = 1000000;
		boolean[] appear = new boolean[n];
		Random rand = new Random();
		int[] jj = new int[k];
		for (int i = 0; i < k; i++) {
			int j = -1;
			while (appear[j = rand.nextInt(n)])
				;
			jj[i] = j;
			appear[j] = true;	//標記法
		}

		System.out.println("jj'length:" + jj.length);
		System.out.println(System.currentTimeMillis() - start);
	}

5、那個程式設計師說他有1MB的可用儲存空間,但是我們概要描述的程式碼需要1.25MB的空間,他可以不費力氣地索取到額外的空間。

答: 用位向量表示10 000 000個整數需要10 000 000個位,10000000 bits = 1.192MB.所以程式設計師可利用的1MB空間是顯然不夠的,可以採用兩趟演算法,第一趟利用5000000個位來排序0~4999999之間的整數,然後在第二趟中排序排序5000000~9999999之間的整數

  k趟演算法可以在kn的時間開銷和n/k的空間開銷內完成對最多n個小於n的無重複正整數的排序。

#include<iostream>
#include<stdio.h>
#include<bitset>

#define MAX 5000000	//分兩次獲取
using namespace std;

bitset<MAX+1> bit;	//宣告一個有(MAX+1)個二進位制位的bitset集合,初始預設所有二進位制為0

int main(int argc,char *argv[]){
	int n,i;
	FILE *fp;
	fp = freopen("C:\\Users\\zj\\Desktop\\DATA.txt","r",stdin);
	while(scanf("%d",&n)!=EOF)
	{
		if(n<=MAX)
		bit.set(n,1);	//將n位置1
		else if(n>2*MAX){
			printf("輸入資料有誤,超出範圍");
			return 0;
		}
	}
	for(i = 0;i<MAX+1;i++){
		if(bit[i]==1)
		{
			printf("%d  ",i);
			bit.reset(i);
		}
	}
	
	fseek(fp ,0,SEEK_SET);
	
	while(scanf("%d",&n)!=EOF)
	{
		if(n>MAX)
		{
			bit.set(n-MAX,1);
		}
	}

	for(i = 0;i<MAX+1;i++){
		if(bit[i]==1)
			printf("%d  ",i+MAX);
		
	}
	freopen( "CON", "r", stdin );	//轉化為標準輸入
	system("pause");
}

6、如果那個程式設計師說的不是每個整數最多出現一次,而是每個整數最多出現10次,你又如何建議他?你的解決方案如何隨著可用儲存空間總量的變化而變化?

答:首先使用4個bit來統計每個整數出現的次數。

7、本書1.4節中存在一些缺陷。首先是假定在輸入中沒有出現兩次的整數。如果某個數出現超過一次的話,會發生什麼?在這種情況下,如何修改程式來呼叫錯誤處理函式?

當輸入整數小於零或大於等於n時,又會發生什麼?如果某個輸入不是數值又如何?在這些情況下,程式應該如何處理?程式還應該包含哪些明智的檢查?描述一些用於測試程式的小型資料集合。並說明如何正確處理上述以及其他的不良情況。

答:首先應該做輸入型別檢查,輸入邊界檢查

8、當那個程式設計師解決該問題的時候,美國所有免費電話的區號都是800.現在免費電話的區號包括800、877和888,而且還在增多。如何在1MB空間內完成對所有這些免費電話

號碼的排序?如何將免費號碼儲存在一個集合中,要求可以實現非常快速的查詢以判定一個給定的免費電話號碼是否可用或者已經存在?

答:

9、使用更多的空間來換取更少的執行時間存在一個問題;初始化空間本身需要消耗大量的時間。說明如何設計一種技術,在第一次訪問向量的項時將其初始化為0.你的方案應該使用常量時間進行初始化和向量訪問,使用的額外空間應正比於向量的大小。因為該方法通過進一步增加空間來減少初始化的時間,所以僅在空間很廉價,時間很寶貴且向量

很稀疏的情況下才考慮使用。

答:這個以空間換時間的資料結構是很Amusing的。首先應考慮初始化陣列導致的直接結果是什麼? 就是第一次訪問某個元素的時候,這個元素的值是初始化後的值。由此就想到了,我們提出的演算法應該能夠實現:當我們第一次訪問某個元素的時候,這個元素的值是初始化後的值。這個實現的關鍵點是:如何判斷是第一次訪問,即如何判斷陣列中的元素是初始化過還是沒有初始化過。思考如下幾點:

1)當時我看這道題的時候第一個反應就是:再建立一個數組,然後第一次訪問時候將已初始化元素的陣列下標儲存在裡面,每次訪問的時候查詢這個陣列,要是陣列下標在裡面,就說明這個陣列下標的元素已經被初始化了。相信很多人都有同樣的想法,但是如果這樣做的話,雖然節省了初始化的時間,但是每次訪問都要在存已訪問元素下標的數組裡進行線性搜尋,這樣的時間也是浪費不少,如何不用線性時間而用常數時間就能判斷是否一個數組元素是否初始化過呢

2)然後我就又想到,還不如建立一個和需要訪問陣列等長的陣列,在訪問某個元素的時候先看這個陣列,要是這個陣列的對應元素標記為1則表示已經初始化過了,沒有就把相應元素置為1。不過同樣 ,這個等長的陣列沒有被初始化過,那麼它裡面儲存的隨機數有很大的概率就是1,這樣很容易誤判。

3)然後我看了習題答案後,恍然大悟。作者通過巧妙地利用from和to陣列以及整數top保障了方法的可靠性。

       分析:首先,我們應該對題目進行徹底分析---我們需要訪問的是一個長度(假設為n)非常大的陣列,一般而言對陣列中某個元素訪問前我們必須要進行初始化,但是當n值非常大而程式對time要求較嚴格時,對所有的陣列元素都進行統一的初始化是不可取的。為了達到程式對time的要求,我們應該對需要訪問的元素(它的個數相對於n來說很小)進行初始化。其次,對元素初始化的判斷---為了提高判斷的準確性,答案引入了兩個陣列from和to以及整數top,且對於元素data[i]已初始化的條件是from[i]<top && to[from[i]]==i。現在讓我們來具體分析這些規則是如何被應用的:假設當我們第一次訪問的陣列元素下標為1時,先判斷初始化條件(此時陣列from和to都沒有初始化,當然time也不允許讓我們多管閒事),一般而言from[1]中的隨機數是大於top(現在為0)的,但我們不能保證,於是我們加入了第二個判斷條件--to[from[1]]==1,對於這個表示式我們兩次取隨機值且讓後者等於1,這樣的概率有但幾乎為0!因此,data[1]未被初始化,於是執行from[1]=top; to[top]=1; data[1]=0; top++;這樣做的目的就是保證以後再次訪問data[1]時不需要再初始化(條件滿足了直接讀取即可)。最後,對於該方法的可靠性分析---讓我們先來分析一下整數top的作用,不難發現,top記錄了當前data中已初始化元素的個數,但主要是保證了from中已初始化的元素都小於top(通過from[i]=top; top++),這給我們的判斷條件(from[i]<top)提供了一定的可靠性,當然再加上第二到保險(to[from[i]]==i)使得此方法可靠性值得信賴!

       答案:藉助於兩個額外的n元向量from、to和一個整數top,我們就可以使用標識來初始化向量data[0....n-1]。如果元素data[i]已始化,那麼from[i] < top並且 to[from[i]] =i.因此,from是一個簡單的標識,to和top一起確保了from中不會被寫入記憶體裡的隨機內容,變數top初始為0。下圖中data的空白項未被始化:

  1. from[i] =top;    
  2. to[top] = i;    
  3. data[i] = 0;    
  4. top ++;   

       假如我現在要判斷data[5]=8是否被初始化過,那麼我先看to[from[5]]是否等於5,同理data[3]就判斷to[from[3]]是否等於3就可以了。

10、在成本低廉的隔日送達時代之前,商店允許顧客通過電話訂購商品,並在幾天後上門自取。商店的資料庫使用客戶的電話號碼作為其檢索的主關鍵字(客戶知道他們自己的電話號碼,而且這些關鍵字集合都是唯一的)。你如何組織商店的資料庫,以允許高效的插入和檢索操作?

答:

11、在20世紀80年代早期,工程師們每天都要將許多由計算機輔助設計(CAD)系統生成的圖紙從工廠送到位於XXX的測試站。雖然僅有40公里遠,但使用汽車快遞服務每天都需要一個小時的時間(由於交通阻塞和山路崎嶇),花費100美元。請給出通道資料傳輸方案並估計每一種方案的費用。

答:個人覺得時間與金錢的關係類似於空間與時間的關係。最優的方法一定是在兩者之間折中,如果實時性要求沒有那麼高,可以找便宜的方式運。

12、載人航天的先驅意識到需要在外太空的極端環境下實現順利書寫。民間盛傳美國花費100萬美元研發出了一種特殊的鋼筆來解決這個問題。那麼,前蘇聯又會如何解決相同的問題?

答:為什麼要用鋼筆??用鉛筆不就行了。。省錢省力