330 likes | 484 Views
書名 Java 於資料結構與演算法之實習應用 書號 PG20098 原著 河西朝雄 著 譯者 周明憲 / 徐堯譯. 台北總公司/台北縣汐止市新台五路一段 112 號 10 樓 A 棟 Building A, 10F, 112, Shin-Tai-Wu Road Sec. 1, Shijr, Taipei, Taiwan. 電話/ 02-26962869 傳真/ 02-26962867 台中辦事處/台中市文心路一段 540 號 4 樓 -1 4-1F, 540, Wen Shing Road, Sec. 1, Taichung, Taiwan.
E N D
書名 Java於資料結構與演算法之實習應用 書號 PG20098 原著 河西朝雄著 譯者 周明憲/徐堯譯 台北總公司/台北縣汐止市新台五路一段112號10樓A棟 Building A, 10F, 112, Shin-Tai-Wu Road Sec. 1, Shijr, Taipei, Taiwan. 電話/02-26962869 傳真/02-26962867 台中辦事處/台中市文心路一段540號4樓-1 4-1F, 540, Wen Shing Road, Sec. 1, Taichung, Taiwan. 電話/04-23287870 傳真/04-23108168 博碩網址:http://www.drmaster.com.tw
第三章 學習重點 • 數值分析計算與大量的資料處理都是電腦的重要工作。資料處理的基本作業有排序(sort)與搜尋(search)。 • 排序的方法有基本型及其改良型,前者包括,直接選擇法、氣泡排序與基本插入法等,後者則有Shell排序。本章將會說明這些排序方法。至於更高速的排序方法,如快速排序與堆積排序則會在第4章與第6章說明。 • 在搜尋的方法方面,本章將會說明逐筆搜尋與2元搜尋,同時也會說明較特殊且高速的搜尋方法——Hash。而2元搜尋樹搜尋法則將會在第6章說明。 • 本章也會說明將2組資料列合而為一的合併(merge)、字串的取代(replace)與字串的核對(pattern matching)。
3-0何謂排序與搜尋 • 排序(sort)排序為將資料按照一定的規則予以排列之意。1、5、11、20…之類由小排到大的順序稱為遞增順序,…20、11、5、1之類由大排到小的順序則稱為遞減順序。 • 搜尋(search)從大量的資料中找出必要資料的作業稱為搜尋(search)。搜尋的方法可大致分為逐筆搜尋法與2元搜尋法。
3-1 基本排序 • 從數列中找出最小項次,再與a1交換,如此從開始反覆操作直至數列成為a1為止的作業稱為直接選擇法排序。 • 將過程製作成演算法後,其內容如下: (1)將對象項i從0移至,並反覆操作系列步驟。 (2)將對象項設為初始值(最小值)。 (3)關於對象項項,則反覆操作下列步驟。 (4)找出最小項,在s中求出其項次編號。 (5)將i項與s項交換。
public void paint(Graphics g) { final int N=6; // 資料數 int[] a={80,41,35,90,40,20}; int min,s,t,i,j; for (i=0;i<N-1;i++) { min=a[i]; s=i; for (j=i+1;j<N;j++) { if (a[j]<min) { min=a[j]; s=j; } } t=a[i];a[i]=a[s];a[s]=t; } for (i=0;i<N;i++) g.drawString(""+a[i],i*20,20); } }
3-1 氣泡排序 • 比較相鄰的2項資料,下面(後面)的資料項若比上面(前面)的資料項小,則反覆進行這2個資料項更換作業。由於較小的資料項恰似氣泡由下往上升的樣子,因此稱為氣泡排序。
public void paint(Graphics g) { final int N=6; // 資料數 int[] a={80,41,35,90,40,20}; int t,i,j; for (i=0;i<N-1;i++) { for (j=N-1;j>i;j--) { if (a[j]<a[j-1]) { t=a[j];a[j]=a[j-1];a[j-1]=t; } } } for (i=0;i<N;i++) g.drawString(""+a[i],i*20,20); } }
3-1 shaker排序 • 氣泡排序由於不論述列是否正確排列皆會無條件的反覆進行比較,因此效率較差。如果由數列的前方(左側)開始掃瞄至交換為止的位置為i與i+1,則i+1~n-1的元素由於應已正確排列,因此無須再次掃瞄。為此,我們可將最後交換的位置儲存至shift,並將此位置設為下次掃瞄時的範圍。 • 不過要是有小的元素位於右端,則最後的交換均會在數列的後方進行,上面的shift的效果就少得很多。因此掃瞄的方向就要改為由左至右及由右至左雙向交互進行。這就是shaker排序。
public void paint(Graphics g) { final int N=9; // 資料數 int[] a={3,5,6,9,2,7,8,10,4}; int left,right,i,shift=0,t; left=0;right=N-1; while (left<right) { for (i=left;i<right;i++) { if (a[i]>a[i+1]) { t=a[i];a[i]=a[i+1];a[i+1]=t; shift=i; } } right=shift; for (i=right;i>left;i--) { if (a[i]<a[i-1]) { t=a[i];a[i]=a[i-1];a[i-1]=t; shift=i; } } left=shift; } for (i=0;i<N;i++) g.drawString(""+a[i],i*20,20); } }
3-2 Shell排序 • 基本插入法的原理為將數列的視為已排列整齊的數列,再查出應放入此數列的哪個位置,然後再將其插入至此位置。這個步驟只要將i在1至反覆操作即可。 • 至於要放在數列的哪個位置,則只要由數列的右端開始,在比數列記錄項較小的空間中反覆執行交換作業。
public void paint(Graphics g) { final int N=100; // 資料數 int i,j,t; int[] a=new int[N]; for (i=0;i<N;i++) // N個亂數 a[i]=(int)(1000*Math.random()+1); for (i=1;i<N;i++) { for (j=i-1;j>=0;j--) { if (a[j]>a[j+1]) { t=a[j];a[j]=a[j+1];a[j+1]=t; } else break; } } int y=0; for (i=0;i<N;i++) { if (i%14==0) y++; g.drawString(""+a[i],(i%14)*30,y*20); } } }
3-2 shell排序 • 不以基本插入法一口氣將數列a0, a1, a2, ..., an-1做排序,而按一定的間隔(gap)將數列分成數個數列,再逐一以基本插入法做排序。像這樣一面將一部份的數列大略做排序,一面將一部份的數列收斂至全部數列(將gap設為1)後,最後的排序作業即告結束。也就是說,在適用gap=1的基本插入法以前,先將較小的元素大略的排列至前面,較大的元素則排列至後面,以減低比較與交換的次數,這種方式稱為shell排序。這個名稱是取自提出此方法的Shell。 • gap的決定方式中有最佳的方式,不過這裡僅單純的說明將gap劃分一半的方法。
3-2 shell排序 • 我們以圖3.8為例。假設資料有8個,則最初的gap為8/2=4,然後將4個間隔的數列(51,45)、(60,70)、...逐一以基本插入法進行排序。 • 接下來將gap設為4/2=2,在將2個間隔的數列(45,55,51,80)、(60,21,70,30)逐一以基本插入法進行排序。 • 然後接下來將gap設為2/2=2,在將1個間隔的數列(45,21,...)以基本插入法進行排序。由於gap=1的數列為全部數列,因此排序作業就此結束。 上圖中gap=2時的數列有以下2組: 01234567 ○□○□○□○□
3-2 shell排序 • 如製作成演算法,則其大致內容如下: (1)設定gap的初始值(N/2) (2)反覆操作以下步驟直至gap的值為1。 (3)gap間隔的數列共有gap個,因此反覆操 作的次數為此gap個數以下 (4)以基本插入法將gap間隔的數列 (aj, aj+gap, aj+2gap,...)進行排序
3-2 Shell排序的改良 • gap的選定方法一般來說,將gap系統選為可互為基礎的方法是最有效率的方法,而更簡單的方法有:使用..., 121, 40, 13, 4, 1這類數列的方法。gap的初始值為從資料數N以內的前面之數列中選出的最大值,而像練習問題19中以..., 8, 4, 2, 1之類的2的2次方來取出gap後,效率就變得較差。 • 資料的區域參考性將數列○□○□○□○□分成○○○○與□□□□這2個數列,再以基本插入法分別進行排序時,由於陣列元素的參考會分散,因此記憶存取的效率就會很差。如要查詢大量資料時,最好查詢連續的資料區塊,這樣存取的效率會比較好。這種情形稱為區域參考(local reference)。
3-3 逐筆搜尋與衛兵 • 逐筆搜尋是指從儲存於陣列等的資料之開頭起,依序逐一搜尋所要找的資料,當找到所要的資料後,便停止搜尋作業,是一種單純的搜尋法。 • 設衛兵以設衛兵的方式來改進結束判斷的設定。
逐筆搜尋--- public void actionPerformed(ActionEvent e) { final int N=10; // 資料數 int i; Girl[] a={new Girl("Ann",18),new Girl("Rolla",19), new Girl("Nancy",16),new Girl("Eluza",17), new Girl("Juliet",18),new Girl("Machilda",20), new Girl("Emy",15),new Girl("Candy",16), new Girl("Ema",17),new Girl("Mari",18)}; String key=tf.getText(); i=0; while (i<N && !key.equals(a[i].name)) i++; if (i<N) ta.setText(ta.getText()+a[i].name+","+a[i].age+"\n"); else ta.setText(ta.getText()+key+"的資料找不到\n"); }
設衛兵--- public void actionPerformed(ActionEvent e) { final int N=10; // 資料數 int i; Girl[] a={new Girl("Ann",18),new Girl("Rolla",19), new Girl("Nancy",16),new Girl("Eluza",17), new Girl("Juliet",18),new Girl("Machilda",20), new Girl("Emy",15),new Girl("Candy",16), new Girl("Ema",17),new Girl("Mari",18), new Girl("",0)}; // 衛兵 String key=tf.getText(); a[N]=new Girl(key,0); i=0; while (!key.equals(a[i].name)) i++; if (i<N) ta.setText(ta.getText()+a[i].name+","+a[i].age+"\n"); else ta.setText(ta.getText()+key+"的資料找不到\n"); }
3-4 2元搜尋 • 2元搜尋法為在資料按遞增順序或遞減順序排列時有效的搜尋法。 • 我們將搜尋範圍的上下限分別設為upper與low,然後將位於x=(low+upper)/2位置的資料與鍵值(欲搜尋的鍵值)做一比較。若鍵值較大,則鍵值的位置應位於x的上面,因此low便為x+1。反之,若鍵值較小,則鍵值的位置應位於x的下面,因此low便為x-1。如此在low≦ upper之間反覆操作這個步驟。
3-4 2元搜尋 實際搜尋範例: • (1) 起先由於low=0,upper=9,因此x=(0+9)/2=4,然後將第4個的資料31與欲搜尋的資料50做一比較,由於50比31大,因此low=4+1=5。 • (2) 當low=5,upper=9時,x=(5+9)/2=7,然後將第7個的資料70與欲搜尋的資料50做一比較,由於50比70小,因此upper=7-1=6。 • (3) 當low=5,upper=6時,x=(5+6)/2=5,然後將第5個的資料50與欲搜尋的資料50做一比較,而找出欲搜尋的資料。
public void actionPerformed(ActionEvent e) { final int N=10; // 資料數 int[] a={2,3,7,11,31,50,55,70,77,80}; int key,low,high,mid,flag=0; key=Integer.parseInt(tf.getText()); low=0;high=N-1; while (low<=high) { mid=(low+high)/2; if (a[mid]==key) { ta.setText(ta.getText()+a[mid]+"在第"+mid+"個位置\n"); flag=1; break; } if (a[mid]<key) low=mid+1; else high=mid-1; } if (flag!=1) ta.setText(ta.getText()+key+"沒找到\n"); }
3-5 合併 • 合併(merge)是指將經排序過的2組(或3組以上)的資料組合成1個經過同樣排序的資料。 • 設a、b為經過排序的2組資料,c為新的資料,各資料的開頭編號各為i、j、p,合併演算法的大致內容如下: (1)反覆操作下列步驟至資料a或b的尾端。 (2)比較ai與bj,將較小的一方複製至cp,然後將較小方的資料的編號向前推進1位。 (3)將尾端為止的資料複製至c。
public void paint(Graphics g) { final int M=10, // a[]的資料數 N=5; // b[]的資料數 int i=0,j=0,p=0; int[] a={2,4,5,7,8,10,15,20,30,40}, b={6,11,25,33,35}, c=new int[M+N]; while (i<M && j<N) { // 當還未達到a[],b[]的尾端時 if (a[i]<=b[j]) c[p++]=a[i++]; else c[p++]=b[j++]; } while (i<M) // 直到a[]的尾端 c[p++]=a[i++]; while (j<N) // 直到b[]的尾端 c[p++]=b[j++]; for (i=0;i<M+N;i++) g.drawString(""+c[i],i*20,20); } }
3-6 字串的核對(pattern matching) • 設文字text內含有欲核對的字串key。 如欲從text之中找出key,可先從text的開頭起逐字與key做比較。 如發現到key,方法search會傳回key在text中的位置,如果搜尋到text的結尾(正確來說應是text的結尾至key的長度前的位置)卻未找到key,方法search便會傳回-1的值。
…… int search(int p,String text,String key) { int i,m,n; m=text.length(); n=key.length(); for (i=p;i<=m-n;i++) { if ((text.substring(i,i+n)).equals(key)) return i; } return -1; } ……… bt.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { String text="This is a pen. That is a pencil."; String key=tf.getText(); int m,n,p; m=text.length(); n=key.length(); p=search(0,text,key); while (p!=-1){ ta.setText(ta.getText()+text.substring(p,m)+"\n"); p=search(p+n,text,key); }
3-6 鍵值字串與區分字元 • 如要讓程式瞭解pen與pencil、sharpen的pen之不同,只要在字串pen被找到時,讓pen的前後字串為區分字元(空格或.)即可。pen的位置若在文字的開頭或結尾時,(1)部分的程式寫法如下: if ((text.substring(i,i+n)).equals(key)) { if (i==p || i==m-n) return i; String f=text.substring(i-1,i); String b=text.substring(i+n,i+n+1); if ((f.equals(" ") || f.equals(".")) && (b.equals(" ") || b.equals("."))) return i; }
3-6 Boyer-Moore法 • Boyer-Moore法則以字串與鍵值的右端為基準進行核對作業,當這2者不相符時,並不向前推進1個字元,而是向欲核對字元的右端的字元之值前進。 • Boyer-Moore法演算法的大致內容如下: (1) 製作「前進值」表。 (2)核對完text以前,反覆操作以下步驟。 (3)如text中的p位置的字元與key右邊的字元(以key[n-1]表示)一致的話,就將以p-n+1為首位的n字元的字串與key比較,若一致的話,即為p-n+1所求的位置。 (4)將位置p向後移動「前進值」所定的字元。
……… int search(int p,String text,String key) { int i,m,n; m=text.length(); n=key.length(); while (p<m) { if ((text.substring(p-n+1,p+1)).equals(key)) return p-n+1; p=p+skip[text.charAt(p)]; // 將搜尋位置向前推進 } return -1; // 若沒找到 } void table(String key) { // 製作跳躍表 int k,n; n=key.length(); for (k=0;k<=255;k++) skip[k]=n; for (k=0;k<n-1;k++) skip[key.charAt(k)]=n-k-1; } ………
3-7 字串的取代 • 從text中找出key字串的內容,再以字串rep的內容取代key字串的內容。
……… int search(int p,String text,String key) { int i,m,n; m=text.length(); n=key.length(); for (i=p;i<=m-n;i++) { if ((text.substring(i,i+n)).equals(key)) return i; } return -1; } String replace(String text,String key,String rep) { int p; String d1,d2; p=search(0,text,key); while (p!=-1) { d1=text.substring(0,p); d2=text.substring(p+key.length(),text.length()); text=d1+rep+d2; p=search(p+rep.length(),text,key); } return text; } ……
3-8 Hash(雜湊) • 假設有一個班級的學生資料,如要以學號管理這些資料,只要將這班學生的學號設為記錄編號並存入陣列(檔案),便可立即以記錄編號基礎來參考這些資料。 • 不過,有些資料無法以編號來管理,這時一般都將姓名等非數值資料設為鍵值,再進行搜尋,否則無法將資料參考起來。
3-8 Hash函數 • 我們將由英文大寫字母組成的姓名設為鍵值,將這些鍵值的名稱設為A1、A2、A3...、An,共有n個字元。若將鍵值的長度設為n個字元,則所能取得的姓名組合便有26n+1個,這是1個很大的數值範圍。因此我們使用鍵值開頭的A1、鍵值中間的An/2與鍵值倒數第2位的An-1這3個字元,來設定下列的函數。 • 假設鍵值為“SUZUKI”,則 • 因此只要將數值428視為陣列的元素或檔案的紀錄編號,再對應至SUZUKI即可。
……… int hash(String s) { // Hash函數 final int ModSize=1000; int n; n=s.length(); return (s.charAt(0)-'A'+(s.charAt(n/2-1)-'A')*26 +(s.charAt(n-2)-'A')*26*26)%ModSize; } …………