java 數據結構與算法

學習目標  
  1.    衡量一個算法是否好壞的標準  
  2.    各種常用查找算法、排序算法的掌握  
  3.    遞歸的原理及實現  
  4.    遞歸的各種應用  
  5.    快速排序算法的實現  
  6.    
  7. 算法(algorithm):  
  8. 對一個現有的問題我們採取的解決過程及方法,可簡單可複雜,可高效可低效。一個用算法實現的程序會耗費兩種資源:處理時間和內存。很顯然,一個好的算法應該是耗費時間少、所用內存低,但是,在實際中,我們往往不能兩方面顧全!  
  9. 算法的效率分析標準:  
  10. 衡量算法是否高效主要是從以下幾個方面來分析:  
  11. •   簡單性和清晰度  
  12. 一般我們都希望算法越簡單越清晰就越好,但是要保證效率爲前提。可是,往往我們在複雜的項目開發中所遇見的問題比較複雜,對時間和空間效率的要求也較高,因此,算法一般都會比較複雜。  
  13. •   空間效率  
  14. 注意:這裏的空間效率並不是指算法代碼佔用的內存指令空間,而是指代碼中的數據分配(變量與變量所引用值的分配)以及方法調用所使用的內存(調用棧的空間分配)。比如,我們常用的遞歸,雖然會使代碼清晰簡單,但是內存的使用也會大大提高。理想的,程序所使用的內存應該和數據及方法調用 所佔用內存相等。但事實總是會有些額外的開銷!因此,空間效率也是我們衡量算法的方面之一。  
  15. •   時間效率  
  16. 針對同一任務所使用的不同算法所執行的時間都會不同。  
  17. 比如:在一個數據集合中查找數據,我們會從第一個數據開始查找,一直找到需要的數據爲止,如果查找數據存在,則這種查找方式(稱之爲線性查找)一般要查找半個列表!然而,如果數據的排放是有序的,則通過另一種查找方法會更有效,即二分查找法,首先從集合的中間開始,如果查找值在中間值的前面,則從集合的前一半重複查找,否則從後一半查找,每執行一次則將查找的集合減少爲前一次的一半。  
  18. 算法的類型:  
  19. 所有的算法可以大概分爲以下三種類型:  
  20. 1.貪婪算法(greedy algorithm)  
  21. 該算法每一步所做的都是當前最緊急、最有利或者最滿意的,不會考慮所做的後果,直到完成任務。這種算法的穩定性很差,很容易帶來嚴重後果,但是,如果方向正確,那該算法也是高效的。  
  22. 2.分治算法(divide-and-conquer algorithm)  
  23. 該算法就是將一個大問題分解成許多小問題,然後單獨處理這些小問題,最終將結果結合起來形成對整個問題的解決方案。當子問題和總問題類型類似時,該算法很有效,遞歸就屬於該算法。  
  24. 3.回溯算法(backtracking algorithm)  
  25. 也可以稱之排除算法,一種組織好的試錯法。某一點,如果有多個選擇,則任意選擇一個,如果不能解決問題則退回選擇另一個,直到找到正確的選擇。這種算法的效率很低,除非運氣好。比如迷宮就可以使用這種算法來實現。  
  26. 實際上,我們對算法的效率高低評價,主要是在時間和內存之間權衡。根據實際情況來決定,比如有的客戶不在乎耗用的內存是多少,他在乎的是執行的速度,那麼一個用內存來換取更高執行時間的算法可能是更好的。同樣,有的客戶可能不想耗用過多內存同時對速度也不是特別要求。不管怎樣,效率是算法的主要特性,因此關注算法的性能尤其重要!標準的測量方法就是找出一個函數(增長率),將執行時間表示爲輸入大小的函數。選擇處理的輸入大小來說增長率比較低的算法!  
  27. 計算增長率的方式:  
  28. 1.測量執行時間  
  29. 通過System.currentTimeMillis()方法來測試  
  30. 部分代碼:  
  31.     // 測量執行時間  
  32.     static void calculate_time()  
  33.     {  
  34.     long test_data = 1000000;  
  35.     long start_time = 0;  
  36.     long end_time = 0;  
  37.     int testVar = 0;  
  38.                   
  39.                   
  40.     for (int i = 1; i <= 5; i++)  
  41.     {  
  42.     // 算法執行前的當前時間  
  43.     start_time = System.currentTimeMillis();  
  44.     for(int j = 1; j <= test_data; j++)  
  45.     {  
  46.         testVar++;  
  47.         testVar--;  
  48.     }  
  49.     // 算法執行後的當前時間  
  50.     end_time = System.currentTimeMillis();  
  51.     // 打印總共執行時間  
  52.     System.out.println("test_data = " + test_data + "\n" +  
  53.     "Time in msec = " + (end_time - start_time) + "ms");  
  54.      環後將循環次數加倍  
  55.     test_data = test_data * 2;  
  56.                 }  
  57.     }  
  58. 以上代碼將分別計算出100000020000004000000...次的循環時間。  
  59. 缺點:  
  60.    不同的平臺執行的時間不同  
  61.    有些算法隨着輸入數據的加大,測試時間會變得不切實際!  
  62. 2.指令計數  
  63. 指令---指編寫算法的代碼.對一個算法的實現代碼計算執行指令次數。兩種類型指令:不管輸入大小,執行次數永遠不變;執行次數隨着輸入大小改變而改變。一般,我們主要測試後一種指令。  
  64. 例:計算指令執行次數  
  65. static void calculate_instruction()  
  66. {  
  67.     long test_data = 1000;  
  68.     int work = 0;  
  69.       
  70.     for (int i = 1; i <= 5; i++)  
  71.     {  
  72.         int count = 0;   
  73.         for (int k = 1; k <= test_data; k++)  
  74.         {  
  75.             for(int j = 1; j <= test_data; j++)  
  76.             {  
  77.                 // 指令執行次數計數  
  78.                 count++;  
  79.                 work++;  
  80.                 work--;  
  81.             }  
  82.         }  
  83.           
  84.         System.out.println("test_data = " + test_data + "\n" +  
  85.                             "Instr. count = " + count );  
  86.           
  87.         test_data = test_data * 2;  
  88.     }  
  89. }  
  90. 3.代數計算  
  91. 代碼1:  
  92.     long end_time = 0;              t1  
  93.     int testVar = 0;                t2  
  94.     for (int i = 1; i <= test_data; i++)            t3  
  95.     {  
  96.         testVar++;              t4  
  97.         testVar--;              t4  
  98.     }  
  99. 假設t1 --- t4分別代表每條語句的執行時間,那麼,以上代碼的總執行時間爲:t1 + t2 + n(t3 + 2t4).其中n = test_data,當test_data增大時,t1和t2可以忽略不計,也就是說,對於很大的n,執行時間可以近似於:n(t3 + 2t4)  
  100. 4.測量內存使用率  
  101. 一個算法中包含的對象和引用的數目,越多則內存使用越高,反之越低。  
  102. 比較增長率:  
  103. 1.代數比較法  
  104. 條件1:c≦ f(n)/g(n) ≦ d (其中c和d爲正常數,n代表輸入大小)  
  105. 當滿足以上條件1時,則f(n)和g(n)具備相同的增長率,或者兩函數複雜度的階相同!  
  106. 如:f(n) = n + 100  和  g(n) = 0.1n + 10兩函數就具備相同的增長率。  
  107. 條件2: 當n增大時,f(n)/g(n)趨向於0  
  108. 當滿足此條件2時,則該兩個增長函數有不同的增長率。  
  109. 比如:f(n) = 10000n + 20000  和  g(n) = n?2 + n + 1 。請大家比較以上兩函數增長率是否一樣,如果不一樣,誰的增長率小?  
  110. 2.大O表示法  
  111.     如果f的增長率小於或者等於g的增長率,則我們可以用如下的大O表示法:  
  112.     f = O(g)  
  113.     O表示on the order of  
  114. 將代碼1的代數增長率函數用大O表達式如下:  
  115.     f(n) = t1 + t2 + n(t3 + 2t4)  
  116.     = a1*n + a  
  117.     = O(n)  
  118.     其中a1 = t3 + 2t4; a = t1 + t2  
  119. 3.最佳、最差、平均性能  
  120. 對每一個算法不能只考慮單一的增長率,而應該給出最佳、最差、平均的增長率函數  
  121.   
  122. 查找算法:  
  123. 1.線性查找  
  124.     從數組的第一個元素開始查找,並將其與查找值比較,如果相等則停止,否則繼續下一個元素查找,直到找到匹配值。  
  125. 注意:要求被查找的數組中的元素是無序的、隨機的。  
  126. 比如,對一個整型數組的線性查找代碼:  
  127.     static boolean linearSearch(int target, int[] array)  
  128.     {  
  129.         // 遍歷整個數組,並分別將每個遍歷元素與查找值對比  
  130.         for (int i = 0; i < array.length; i++)  
  131.         {  
  132.             if (array[i] == target)  
  133.             {  
  134.                 return true;  
  135.             }  
  136.         }  
  137.           
  138.         return false;  
  139.     }  
  140. 分析該算法的三種情況:  
  141. a.最佳情況  
  142. 要查找的值在數組的第一個位置。也就是說只需比較一次就可達到目的,因此最佳情況的大O表達式爲:O(1)  
  143. b.最差情況  
  144. 要查找的值在數組的末尾或者不存在,則對大小爲n的數組必須比較n次,大O表達式爲:O(n)  
  145. c.平均情況  
  146. 估計會執行:(n + (n - 1) + (n - 2) + ….. + 1)/n = (n + 1) / 2次比較,複雜度爲O(n)  
  147.   
  148. 2.二分查找  
  149. 假設被查找數組中的元素是升序排列的,那我們查找值時,首先會直接到數組的中間位置(數組長度/2),並將中間值和查找值比較,如果相等則返回,否則,如果當前元素值小於查找值,則繼續在數組的後面一半查找(重複上面過程);如果當前元素值大於查找值,則繼續在數組的前面一半查找,直到找到目標值或者無法再二分數組時停止。  
  150. 注意:二分查找只是針對有序排列的各種數組或集合  
  151. 代碼:  
  152. static boolean binarySearch(int target, int[] array)  
  153. {  
  154.     int front = 0;  
  155.     int tail = array.length - 1;  
  156.       
  157.     // 判斷子數組是否能再次二分  
  158.     while (front <= tail)  
  159.     {  
  160.         // 獲取子數組的中間位置,並依據此中間位置進行二分  
  161.         int middle = (front + tail) / 2;  
  162.           
  163.         if (array[middle] == target)  
  164.         {  
  165.             return true;  
  166.         }  
  167.         else if (array[middle] > target)  
  168.         {  
  169.             tail = middle - 1;  
  170.         }  
  171.         else  
  172.         {  
  173.             front = middle + 1;  
  174.         }  
  175.     }  
  176.       
  177.     return false;  
  178. }  
  179. 最佳情況:  
  180. 中間值爲查找值,只需比較一次,複雜度爲O(1)  
  181. 最差、平均:  
  182. 當我們對一個數組執行二分查找時,最多的查找次數是滿足n < 2^k的最小整數k,比如:當數組長度爲20時,那麼使用二分法的查找次數最多爲5次,即:2^5 > 20因此可以得出二分法的最差及平均情況的複雜度爲O(logn)。  
  183. 分析:123456789  
  184. 在上面數組中查找7需要比較多少次?  
  185. 查找2.5需要比較多少次?(假設存儲的數值都是雙精度數據類型)  
  186. 顯然,對於一個有序數組或集合,使用二分查找會比線性查找更加有效!但是注意,雖然二分法效率更高,但使用的同時系統也會增加額外的開銷,爲什麼?  
  187.   
  188.    
  189. 排序算法:  
  190. 1.選擇排序  
  191. 首先在數組中查找最小值,如果該值不在第一個位置,那麼將其和處在第一個位置的元素交換,然後從第二個位置重複此過程,將剩下元素中最小值交換到第二個位置。當到最後一位時,數組排序結束。  
  192. 代碼:  
  193. static void selectionSort(int[] array)  
  194.     {  
  195.         for (int i = 0; i < array.length - 1; i++)  
  196.         {  
  197.             int min_idx = i;  
  198.               
  199.             for (int j = i + 1; j < array.length; j++)  
  200.             {  
  201.                 if (array[j] < array[min_idx])  
  202.                 {  
  203.                     min_idx = j;  
  204.                 }  
  205.             }  
  206.               
  207.             if (min_idx != i)  
  208.             {  
  209.                 swap(array,min_idx,i);  
  210.             }  
  211.               
  212.         }  
  213.     }  
  214. 從上面代碼我們可以看出,假設數組大小爲n,外循環共執行n-1次;那麼第一次執行外循環時,內循環將執行n-1次;第二次執行外循環時內循環將執行n-2次;最後一次執行外循環時內循環將執行1次,因此我們可以通過代數計算方法得出增長函數爲:(n - 1) + (n - 2) + (n - 3) + ….. + 1 = n(n - 1) / 2 = 1/2 * n^2 + 1/2 * n,即可得出複雜度爲:O(n^2)。我們可以分析得知,當數組非常大時,用於元素交換的開銷也相當大。這都屬於額外開銷,是呈線性增長的。注意:如果是對存儲對象的集合進行排序,則存儲對象必須實現Comparable接口,並通過compareTo()方法來比較大小。  
  215.   
  216. 2.冒泡排序  
  217. 冒泡排序法是運用數據值比較後,依判斷規則對數據位置進行交換,以達到排序的目的。具體算法是將相鄰的兩個數據加以比較,若左邊的值大於右邊的值,則將此兩個值互相交換;若左邊的值小於等於右邊的值,則此兩個值的位置不變。右邊的值繼續和下一個值做比較,重複此操作,直到比較到最後一個值。此方法在每比較一趟就會以交換位置的方式將該趟的最大者移向數組的尾端,就像氣泡從水底浮向水面一樣,到水面時氣泡最大,故稱爲冒泡排序法。  
  218. 冒泡和選擇的複雜度很相似,對於大小爲n的數組,對於最佳、最差還是平均,冒泡的複雜度都是O(n^2)。注意:冒泡的最差情況是高於線性的  
  219. 大家可以發現,冒泡的效率是比較低的,因爲它不論是那種情況複雜度都是O(n^2),但是我們可以改進一下,來實現當冒泡處於最佳情況時只會執行一次外循環,即實現線性。我們可以推斷,如果執行一次外循環,結果並沒有發生元素交換(調用swap()),那麼我們就能判定該數組是已經排序好的,而通過上面的冒泡程序得知,不管是否已經排序,外循環會執行n-1次,而最佳情況就是發生在第一次外循環,因此,我們可以改良以上程序,通過使用一個布爾型的值來記錄是否有元素交換的狀態,是就爲true,否就爲false,如果內循環沒有交換元素(沒有改變布爾值),那麼直接返回。  
  220. 改後代碼:  
  221.     public void bubbleSort(int[] array)// 冒泡排序算法  
  222.     {  
  223.         int out, in;  
  224.         // 外循環記錄冒泡次數  
  225.         for (out = nElems - 1; out > 1; out--)  
  226.         {  
  227.             boolean flag = false;  
  228.             // 進行冒泡  
  229.             for (in = 0; in < out; in++)  
  230.             {  
  231.                 // 交換數據  
  232.                 if (array[in] > array[in + 1]) {  
  233.                     swap(in, in + 1);  
  234.                     flag=true;  
  235.                 }  
  236.             }  
  237.             if(!flag){break;}  
  238.                   
  239.         }  
  240.     } // end bubbleSort()  
  241.   
  242.     private void swap(int one, int two)// 交換數據  
  243.     {  
  244.         int temp = array[one];  
  245.         array[one] = array[two];  
  246.         array[two] = temp;  
  247.     }  
  248. 注意:以上改良程序只會提高最佳情況的性能,而對於平均和最差來說,複雜度還是O(n^2)。該改良程序適合於對已經排序好的數組或者只需稍微重排的數組,比選擇排序性能更好。  
  249.   
  250. 3.插入排序  
  251. 插入排序是對於欲排序的元素以插入的方式尋找該元素的適當位置,以達到排序的目的。插入排序法的優點是利用一個一個元素的插入比較,將元素放入適當的位置,所以是一種很簡單排序方式。但因每次元素插入都必須與之前已排序好的元素做比較,故需花費較長的排序時間。  
  252. 步驟如下:(假設數組長度爲n)  
  253. a.對數組的每次(第i次)循環,下標值爲i的元素應該插入到數組的前i個元素的正確位置(如果是升序,則i元素應插入到小於它的元素之後,大於它的元素之前,降序則反之)  
  254. b.每次循環(第i次)結束時,應保證前i個元素排序是正確的!  
  255. c.包含兩個循環,外循環(循環變量爲i)遍歷下標從1到n-1的元素,保存每次循環的所遍歷的元素的值,內循環(循環變量爲k)從i -1開始,即遍歷前將k賦值爲i-1,每次k--,直到k < 0。在內循環中,將第i個元素和該元素之前的所有元素一一對比,並將元素插入到合適的位置,如果第i個元素的位置是正確的,那麼就跳出內循環,重新開始外循環。  
  256. 代碼:  
  257.     public void insertSort()// 插入排序算法  
  258.     {  
  259.         int in, out;  
  260.         for (out = 1; out < nElems; out++)// 外循環是給每個數據循環  
  261.         {  
  262.             int temp = array[out]; // 先取出來保存到臨時變量裏  
  263.             in = out; // in是記錄插入數據之前的每個數據下標  
  264.             // while內循環是找插入數據的位置,並且把該位置之後的數據(包括該位置)  
  265.             // 依次往後順移。  
  266.             while (in > 0 && array[in - 1] >= temp) {  
  267.                 array[in] = array[in - 1]; // 往後順移  
  268.                 --in; // 繼續往前搜索  
  269.             }  
  270.             array[in] = temp; // 該數據要插入的位置  
  271.         } // end for  
  272.     } // end insertionSort()  
  273. 分析:內循環在第一次外循環時執行1次,第二次外循環時執行2次,。。。。第n - 1次外循環時執行n - 1次,因此,插入排序的最差和平均情況的性能是O(n^2)。但是,在最佳情況下(即數組中的元素順序是完全正確的),插入排序的性能是線性的。注意:插入排序適合針對於已排序元素多的數組,即數組中已排序的元素越多,插入排序的性能就越好。  
  274.   
  275.    
  276. 遞歸(recursive):  
  277. 定義函數1:sum(1) = 1  
  278. 定義函數2:sum(n) = n + sum(n - 1)  
  279. 假設n = 5,那麼sum(5)  = 5 + sum(4)  
  280. =5 + 4 + sum(3)  
  281. =5 + 4 + 3 + sum(2)  
  282. =5 + 4 + 3 + 2 + sum(1)  
  283. =5 + 4 + 3 + 2 + 1  
  284. 以上這種在自身中使用自身定義函數的過程稱之遞歸。  
  285. 階乘遞歸(factorial recursive):  
  286. 階乘!4 = 4 * 3 * 2 * 1  
  287. 可以用遞歸來表示爲:  
  288. factorial(1) = 1  
  289. factorial(n) = n * factorial(n - 1)  
  290. 其中n>1。  
  291. 斐波納契遞歸(fibonacci recursive)  
  292.     1,1,2,3,5,8,13,21,34,55,89,144…………  
  293. 斐波納契數列的第一個和第二個數字都定義爲1,後面的每個數字都爲前兩個數之和。  
  294. 用遞歸表示爲:  
  295. fibonacci(1) = fibonacci(2) = 1  
  296. fibonacci(n) = fibonacci(n-1) + fibonacci(n-2)  
  297. 其中n>2。  
  298. 實現遞歸必須滿足兩個條件:  
  299. 1.基本條件(base case)的成立  
  300. 實際上就是定義遞歸應該什麼時候終止,比如在上面兩個例子中,factorial(1) = 1和fibonacci(1) = fibonacci(2) = 1就是遞歸的基本條件,一旦當遞歸執行到滿足基本條件時就是結束遞歸。  
  301. 2.遞歸步驟  
  302. 對於所有的n值,函數都是以其自身通過n值較小的函數來定義,也就是說,所有n值函數通過許多步驟來達到最終用n值較小的函數(基本條件)來定義。可以得知,遞歸函數就是反覆執行遞歸步,直到n達到基本條件爲止。  
  303. factorial的遞歸代碼實現:  
  304. factorial:  
  305. static int factorial(int n)  
  306.     {  
  307.         // 基本條件  
  308. 1       if (n <= 1)  
  309.         {  
  310. 2           return 1;  
  311.         }  
  312.         else  
  313.         {  
  314.             // 遞歸步,執行n-1次  
  315. 3           return n * factorial(n - 1);  
  316.         }  
  317.     }  
  318. 分析:  
  319. 語句3將被執行n-1次,一直到n<=1時結束遞歸。  
  320. 假設n=4,那麼我們可以得知:  
  321. 第一步:調用factorial(4)  
  322. 第二步:調用4 * factorial(3)    n = 4  
  323. 第三步:調用3* factorial(2)     n = 3  
  324. 第四步:調用2 * factorial(1)    n = 2  
  325. 第五步:返回1給factorial(2)  
  326. 第六步:返回2給factorial(3)  
  327. 第七步:返回6給factorial(4)  
  328. 第八步:返回值24。  
  329.   
  330. fibonacci:  
  331. static int fibonacci(int n)  
  332.     {  
  333.         // 基本條件  
  334.         if (n <= 2)  
  335.         {  
  336.             return 1;  
  337.         }  
  338.         else  
  339.         {  
  340.             // 遞歸步,執行n-2次  
  341.             return fibonacci(n-1) + fibonacci(n-2);  
  342.         }  
  343.     }  
  344. 分析:可以用遞歸調用樹來描述。每次都會兩次調用到自己。  
  345. 編寫遞歸應注意事項:  
  346. ----避免死循環,一般都是由於沒有基本條件而造成,也可能是遞歸步的邏輯錯誤導致。  
  347. 遞歸方法的運行實現原理:  
  348. 我們發現,遞歸就是一個不停調用方法自身的一個過程,即方法的反覆調用!  
  349. 計算機是通過棧的機制來實現方法調用的。首先,我們來了解下計算機的方法調用機制:  
  350.     1.程序執行前,計算機會在內存中創建一個調用棧 ,一般會比較大  
  351.     2.當調用某個方法時,會有一個和該被調用方法相關的記錄信息被推入到棧中  
  352.     3.被推入到棧中的記錄信息包括內容:傳遞到被調用方法中的參數值、該方法的局部變量、該方法的返回值。  
  353.     4.當返回某個方法或者方法結束時,會從棧中取出對應的方法記錄信息  
  354. 棧的使用機制:後進先出(LIFO)。注意:最然遞歸方法簡潔,但是效率不是完全就比迭代高,有時甚至低。因爲我們考慮算法不僅要從時間、增長率來考慮,還要考慮空間(一般指內存)問題,遞歸的棧空間是我們必須考慮的,因爲每次方法的調用都需額外的空間來存儲相關信息。  
  355. 遞歸和查找:  
  356. 在前面,我們已使用迭代來實現了線性和二分查找,同樣,這兩種算法也能用遞歸來實現,但是線性查找用遞歸來實現並沒任何優勢,因此一般線性不採用遞歸。  
  357. 線性查找的遞歸實現:  
  358. static boolean linearSearch(int[] array, int target, int pos)  
  359. {  
  360.     if (pos >= array.length)  
  361.     {  
  362.         return false;  
  363.     }  
  364.     else if (array[pos] == target)  
  365.     {  
  366.         return true;  
  367.     }  
  368.     else  
  369.     {  
  370.         return linearSearch(array,target,pos + 1);  
  371.     }  
  372. }  
  373. 注意:pos = 0  
  374. 二分查找的遞歸實現:  
  375. static boolean binarySearch(int[] array, int target, int front, int tail)  
  376.     {  
  377.         if (front > tail)  
  378.         {  
  379.             return false;  
  380.         }  
  381.         else  
  382.         {  
  383.             int middle = (front + tail) / 2;  
  384.             if (array[middle] == target)  
  385.             {  
  386.                 return true;  
  387.             }  
  388.             else if (array[middle] < target)  
  389.             {  
  390.                 return binarySearch(array,target,middle + 1,tail);  
  391.             }  
  392.             else  
  393.             {  
  394.                 return binarySearch(array,target,front,middle - 1);  
  395.             }  
  396.         }  
  397.     }  
  398. 二分查找的遞歸方法在平均和最差情況下的複雜度和用迭代實現二分的一樣,都是O(logn)。其空間複雜度也爲O(nlogn)。所以,用遞歸實現的二分查找也是可行的。  
  399.   
  400. 遞歸和迭代的選擇:  
  401. 在我們選擇一個算法來實現一個方法時,我們應該對多個理由進行對比,高效+簡潔+易維護就是最好的。一般,遞歸由於方法調用的時間和空間的開銷,往往比相應的非遞歸方法效率低,但有時遞歸的精確和簡潔有時稱爲用戶首選。  
  402. 尾遞歸:  
  403. 遞歸調用之後算法不用再做任何工作,那這個算法就是尾遞歸。尾遞歸的主要特點就是編譯器可將尾遞歸代碼轉換成機器語言形式的循環,從而不會產生與遞歸有關的開銷。  
  404. 例:階乘的尾遞歸實現  
  405. static int factIter(int n, int result)  
  406. {  
  407.     if (n <= 1)  
  408.     {  
  409.         return result;  
  410.     }  
  411.     else  
  412.     {  
  413.         return factIter(n-1, n*result);  
  414.     }  
  415. }  
  416. 遞歸和排序:  
  417. 前面的幾種排序的複雜度都是O(n^2),效率不高,這裏我們要學習兩種通過分治算法(遞歸)來實現的比較高效的排序方法,分別是快速排序和歸併排序(擴展內容)。注意:效率越高其算法的編寫複雜性自然也會提高  
  418. 快速排序:  
  419. 快速排序的思想其實就是,先找個參照物(一般以最後一個數據作爲參照物)然後用兩個變量遍歷該數據,左變量從左邊找第一個比參照物大的數據,右變量從右邊開始找第一個比參照物小的數據,然後交換這兩個數據。然後左變量繼續往右邊找一個比參照物大的數據,右變量繼續往左找比參照物小的數據,然後再交換這兩個數據。依次直到左變量和右變量出現交差爲止。然後把左變量所指的值和參照物進行交換。交換完之後,從左變量的位置開始分成兩半,然後先從前半部分按照上面的方法進行排序,排完後在從後半部分進行同樣的排序,直到排完爲止,整個思想就是個遞歸調用!  
  420.   
  421. 原理:  
  422. 1.首先,尋找數組的中間值,然後將該中間值看成一個樞軸(pivot)  
  423. 2.接着開始劃分數據,將所有小於樞軸的數據項都排放在它的位置之前,所有大於樞軸的數據項都排放在它的位置之後。樞軸(中間值)最終所在位置是由其自身的實際值大小來決定的,如果它是數組最大值,那麼它就處於數組的最右側,否則處於最左側。  
  424. 3.上面的劃分過程遞歸應用於所有的根據樞軸劃分出的子數組中。每一個子數組必須擁有自己的樞軸。每個子數組由樞軸左、右的數據元素組成。  
  425. 4.當子數組小到不能在劃分的時候(比如數組元素個數小於2),快速排序結束。  
  426. 分析:通過以上原理可以理解,快速排序是基於二分法的基礎上實現的一個複雜而又高效的排序算法,在排序算法中,最爲關鍵的步驟就是樞軸的位置控制、數組的劃分。如果我們把劃分的原理理解了,那也就基本掌握了快速排序算法。下面我們來重點分析一下劃分是怎樣實現的:  
  427. 劃分:  
  428. 第一步:將樞軸與子數組中的最後一個數據項交換。  
  429. 第二步:建立一個邊界(boundary),最初該邊界在數組的第一個元素之前。該邊界主要是用於區分大於樞軸的數據元素和小於樞軸的數據元素。  
  430. 第三步:從劃分出的子數組的第一個元素開始,逐步掃描整個數組,在掃描過程中,如果遇見小於樞軸的元素,那麼就將該元素與邊界後的第一個元素交換,同時邊界得往前移動一個位置。  
  431. 第四步:當掃描完整個數組之後,得將樞軸與邊界後的第一個元素交換,這時,劃分過程完成了。  
  432. 代碼如下:  
  433. static int partition(int[] array, int front, int tail)  
  434. {  
  435.     // 用於保存中間位置  
  436.     int middle;  
  437.     // 保存邊界位置  
  438.     int boundary;  
  439.     // 保存樞軸位置  
  440.     int pivot;  
  441.     // 保存臨時值,用於值交換  
  442.     int temp;  
  443.       
  444.     // 獲取中間值的位置  
  445.     middle = (front + tail) / 2;  
  446.     // 得到樞軸  
  447.     pivot = array[middle];  
  448.       
  449.     // 執行第一步,將樞軸與子數組中的最後一個數據項交換  
  450.     array[middle] = array[tail];  
  451.     array[tail] = pivot;  
  452.       
  453.     // 執行第二步,建立一個邊界(boundary),最初該邊界在數組的第一個元素之前  
  454.     boundary = front;  
  455.       
  456.     // 執行第三步,遍歷子數組,並將每個元素和樞軸對比,並改變元素位置  
  457.     for (int i = front; i < tail; i++)  
  458.     {  
  459.         // 如果當前元素小於樞軸,則將該元素和邊界互換,並將交換後的邊界往後移一位  
  460.         if (array[i] < pivot)  
  461.         {  
  462.             temp = array[boundary];  
  463.             array[boundary] = array[i];  
  464.             array[i] = temp;  
  465.               
  466.             boundary++;  
  467.         }  
  468.     }  
  469.       
  470.     // 執行第四步,將樞軸與邊界後的第一個元素交換  
  471.     temp = array[tail];  
  472.     array[tail] = array[boundary];  
  473.     array[boundary] = temp;  
  474.       
  475.     // 返回邊界位置  
  476.     return boundary;  
  477. }  
  478.   
  479. 快速排序實現如下:  
  480. static void quickSort(int[] array, int front, int tail)  
  481. {  
  482.     if (front < tail)  
  483.     {  
  484.         int pivotPosition = partition(array,front,tail);  
  485.         // 遞歸實現子數組的劃分,每次根據不同邊界來劃分  
  486.         // 劃分樞軸的左數組  
  487.         quickSort(array,front,pivotPosition - 1);  
  488.         // 劃分樞軸的右數組  
  489.         quickSort(array,pivotPosition + 1,tail);  
  490.     }  
  491. }  
  492. 注意:快速排序在最好情況下(每個樞軸在它的子數組的中間劃分之後就會終止,即樞軸爲每個子數組的中值)的最大運行時間是O(nlogn),最差情況下(在每個階段,樞軸恰好是它的子數組的最小數據項-升序)的運行時間是O(n^2),幸好,快速排序的平均情況下的時間是O(nlogn);快速排序的空間需求在平均情況下是O(nlogn),最壞情況下是O(n)。因此,快速排序比插入、選擇、冒泡排序的效率都要高,但是,在n值較小的情況下,其它排序方法會比快速更快。高效的排序法應該是結合快速排序和其它排序來實現數組排序。當數組很大時,我們先採用快速排序,但一旦劃分出子數組變得很小時(這時數組元素已大部分被排序),我們應該停止遞歸快速排序,而採用另一種非遞歸排序算法。  
  493.    
  494. 內容總結  
  495.    算法的效率分析標準從三方面:簡單清晰、空間、時間。  
  496.    算法增長率的計算:時間、指令執行次數、代數計算  
  497.    每種算法的三種情況分析,最佳、平均、最差。  
  498.    代數表述法與大O表示法  
  499.    查找算法:線性查找、二分查找,排序算法:插入排序、選擇排序、冒泡排序、快速排序等。  
  500.    遞歸的方法調用棧原理  
  501.    尾遞歸的優化  
  502.   
  503.    
  504. 獨立實踐  
  505. 1,  實現對員工數據的二分查找、排序(普通排序算法和高級算法各用一種)。  
  506. 2,  編寫遞歸實現Fibonacci和Factorial 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章