用了幾個月磕磕絆絆的總算把《算法導論》一書看完了,在此寫篇博客總結一下學習到的知識。
首先先放上《算法導論》的思維導圖:
由於本人的理解能力有限,故部分較難懂的內容沒有加入到該思維導圖中。
1.排序
排序問題是我們日常生活中經常遇到的一個問題,因此算法導論也把排序作為整個算法介紹的入門篇。在這么多排序算法里面,目前經典的排序算法有以下幾種:
1.插入排序
對於少量元素的排序,插入排序是一個有效的算法。它的工作方式就像我們排序撲克牌一樣,每次把一張新的撲克牌插入到已經排好序的序列中。假設排序序列的長度為n,由於插入排序每次加入一個新的元素都需要遍歷幾乎整個排好序的序列,故他的時間復雜度為O(n^2)。
以下為插入排序的Java代碼:
- public class Algorithm {
- public static void main(String[] args) {
- int A[] = {5, 2, 4, 6, 1, 3};
- InsertionSort(A);
- for(int num : A)
- System.out.println(num);
- }
- // 插入排序
- public static void InsertionSort(int A[]) {
- for (int i = 1; i < A.length; i++) {
- int key = A[i];
- int j = i-1;
- while(j >= 0 && A[j] > key) {
- A[j+1] = A[j];
- j--;
- }
- A[j+1] = key;
- }
- }
- }
插入排序還有一種變種,稱為希爾排序,即Shell Sort,也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本,是一種非穩定排序算法。希爾排序是把數組按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的元素越來越多,當增量減至1時,整個數組恰被分成一組,算法便終止。由於希爾排序的時間復雜度與增量的選取有關,在此不作深入討論。
希爾排序的Java代碼如下所示:
- public class Algorithm {
- public static void main(String[] args) {
- int A[] = { 5, 2, 4, 1, 3, 6 };
- int ds[] = { 4, 2, 1 }; // 最后為1使得排序最終成功
- ShellSort(A, ds);
- for (int num : A)
- System.out.println(num);
- }
- /**
- * 希爾排序
- *
- * @param nums
- * 待排序數組
- * @param ds
- * 增量數組
- */
- public static void ShellSort(int nums[], int ds[]) {
- for (int d : ds) {
- // 分為d組
- for (int i = 0; i < d; i++) {
- // 插入排序
- for (int j = 0; j + d < nums.length; j += d) {
- int key = nums[j + d];
- int k = j;
- while (k >= 0 && key < nums[k]) {
- nums[k+d] = nums[k];
- k -= d;
- }
- nums[k+d] = key;
- }
- }
- }
- }
- }
2.冒泡排序
冒泡排序與插入排序類似,也是一種較為簡單的排序算法。冒泡排序會重復地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來,因此越大的元素會經由交換慢慢“浮”到數列的頂端,這就是這種排序方法命名的原因。由於需要兩層循環遍歷數組,所以冒泡排序的時間復雜度為O(n^2)。
以下為冒泡排序的Java代碼:
- public class Algorithm {
- public static void main(String[] args) {
- int A[] = {5, 2, 4, 6, 1, 3};
- BubbleSort(A);
- for(int num : A)
- System.out.println(num);
- }
- // 冒泡排序
- public static void BubbleSort(int A[]) {
- for (int i = 0; i < A.length; i++) {
- for (int j = 1; j < A.length - i; j++) {
- // 如果前面的元素比后面的大,則發生交換
- if (A[j-1] > A[j]) {
- int temp = A[j];
- A[j] = A[j-1];
- A[j-1] = temp;
- }
- }
- }
- }
- }
實際上面的冒泡排序代碼還可以進行一點小優化,要是循環中沒有發生交換則可直接退出:
- // 冒泡排序(優化)
- public static void BubbleSortII(int A[]) {
- boolean swap = false; // 是否發生過交換
- for (int i = 0; i < A.length; i++) {
- swap = false;
- for (int j = 1; j < A.length - i; j++) {
- // 如果前面的元素比后面的大,則發生交換
- if (A[j-1] > A[j]) {
- swap = true;
- int temp = A[j];
- A[j] = A[j-1];
- A[j-1] = temp;
- }
- }
- // 沒有發生過交換,已經排序完畢,可跳出循環
- if (swap == false)
- break;
- }
- }
3.歸並排序
要提到歸並排序就不得不講到分治法(Divide and Conquer),分治法的思想是:把原問題分解成為幾個規模較小,但類似於原問題的子問題,遞歸的去求解這些子問題,然后再合並這些子問題的解來建立原問題的解。歸並排序就是是采用分治法的一個非常典型的應用。它是建立在歸並操作上的一種有效的排序算法,該算法將已有序的子序列合並,得到完全有序的序列。用撲克牌來舉例:在排序一副撲克牌的時候,我們現將其分成兩疊大小相近的撲克牌,分別排序,然后我們再把這兩疊已經排好序的撲克牌合並成一副更大的排好序的撲克牌,此時只需要每次比較兩疊撲克牌的第一張牌即可。歸並排序的圖解如下:

設序列中有N個元素需要排序,則由上圖易得可以把排序過程分為logN(以2為低的對數)次處理,每次循環N次,故歸並排序的時間復雜度為O(N*logN)。
歸並排序的Java代碼如下:
- public class Algorithm {
- public static void main(String[] args) {
- int A[] = {5, 2, 4, 6, 1, 3};
- MergeSort(A, 0, A.length-1);
- for(int num : A)
- System.out.println(num);
- }
- public static void MergeSort(int A[], int start, int end) {
- if (start < end) {
- // 中點
- int mid = (start+end)/2;
- // 子序列分別排序
- MergeSort(A, start, mid);
- MergeSort(A, mid+1, end);
- // 合並
- // 把子序列存到新數組中
- int leftLen = mid-start+1, rightLen = end-mid;
- int leftCounter = 0, rightCounter = 0, numCounter = start;
- int L[] = new int[leftLen], R[] = new int[rightLen];
- for (int i = 0; i < leftLen; i++)
- L[i] = A[start+i];
- for (int i = 0; i < rightLen; i++)
- R[i] = A[mid+1+i];
- // 比較子序列第一項元素
- while(leftCounter < leftLen && rightCounter < rightLen) {
- if(L[leftCounter] < R[rightCounter])
- A[numCounter++] = L[leftCounter++];
- else
- A[numCounter++] = R[rightCounter++];
- }
- // 把剩余的子序列加到后面
- while(leftCounter < leftLen)
- A[numCounter++] = L[leftCounter++];
- while(rightCounter < rightLen)
- A[numCounter++] = R[rightCounter++];
- }
- }
- }
- public class Algorithm {
- public static void main(String[] args) {
- int A[] = { 5, 2, 4, 1, 3, 6};
- MergeSort(A);
- for (int num : A)
- System.out.println(num);
- }
- public static void MergeSort(int A[]) {
- int len = A.length;
- int temp[] = new int[len];
- int leftMin, leftMax, rightMin, rightMax; // leftMin ~ leftMax, rightMin
- // ~ rightMax
- for (int i = 1; i < len; i *= 2) {
- leftMin = leftMax = rightMin = rightMax = 0;
- while (leftMin < len) {
- rightMin = leftMax = leftMin + i;
- rightMax = rightMin + i;
- if (rightMax > len)
- rightMax = len;
- if (rightMin > rightMax)
- leftMax = rightMin = rightMax;
- int counter = 0;
- while (leftMin < leftMax && rightMin < rightMax)
- temp[counter++] = A[leftMin] > A[rightMin] ? A[rightMin++]
- : A[leftMin++];
- while (leftMin < leftMax)
- A[--rightMin] = A[--leftMax];
- while (counter > 0)
- A[--rightMin] = temp[--counter];
- leftMin = rightMax;
- }
- }
- }
- }
4.堆排序
首先我們來看看什么是堆:

如上圖所示(二叉)堆是一個數組,它可以被看成一個近似的完全二叉樹,樹上每一個結點對應數組中的一個元素。除了最底層外,該樹是完全充滿的,而且是從左到右填充。在堆中,給定一個結點下標i(對於起始下標為1而言),則它的父節點為i/2,它的左孩子下標為i*2,右孩子下標為i*2+1。堆中結點的高度被定義為該結點到葉結點的最長簡單路徑,由數學公式可得含N個元素的堆高度為logN。
二叉堆可以分為兩種形式:最大堆和最小堆,他們除了滿足堆的基本性質外,最大堆滿足:除了根結點外,所有結點的值小於等於父節點,最小堆反之。在堆排序算法中,我們使用最大堆,最小堆通常用於構造優先隊列。
以下為Java的堆排序:
- public class Algorithm {
- public static void main(String[] args) {
- int A[] = { 5, 2, 4, 6, 1, 3 };
- HeapSort(A);
- for (int num : A)
- System.out.println(num);
- }
- public static void HeapSort(int A[]) {
- BuildMaxHeap(A);
- for (int i = A.length - 1; i > 0; i--) {
- // 把根結點和最后的結點對調
- int temp = A[i];
- A[i] = A[0];
- A[0] = temp;
- // 對根結點進行最大堆性質維護
- MaxHeapify(A, i, 0);
- }
- }
- /**
- * 建立最大堆
- *
- * @param A
- * 數組
- */
- private static void BuildMaxHeap(int A[]) {
- int heapSize = A.length;
- // heapSize/2 ~ heapSize-1 均為葉結點,對非葉結點調用維護最大堆性質方法即可
- for (int i = heapSize / 2 - 1; i >= 0; i--)
- MaxHeapify(A, heapSize, i);
- }
- /**
- * 維護最大堆的性質,調整下標為i的結點位置
- * @param A 數組
- * @param heapSize 堆大小
- * @param index 結點下標
- */
- private static void MaxHeapify(int A[], int heapSize, int index) {
- int left = index*2+1, right = index*2+2, largest = index;
- // 選取父結點,左結點,右結點中值最大的當父結點
- if (left < heapSize && A[left] > A[index])
- largest = left;
- if (right < heapSize && A[right] > A[largest])
- largest = right;
- // 若子結點充當了父結點,對子結點遞歸調用方法維護最大堆性質
- if (largest != index) {
- int temp = A[largest];
- A[largest] = A[index];
- A[index] = temp;
- MaxHeapify(A, heapSize, largest);
- }
- }
- }
首先來看看MaxHeapify方法,該方法是用於維護最大堆性質的方法。若方法調整的結點發生了交換,則對其子結點遞歸的調用該方法繼續維護最大堆性質,故該方法的調用次數與堆的高度有關,時間復雜度為O(h) = O(logN)。
再來看看BuildMaxHeap方法,該方法用於把一個無序的數組構造成一個最大堆。該方法自底向上對非葉結點調用MaxHeapify,咋看其時間復雜度為O(N*logN),但由數學推導可得其緊確時間復雜度為線性時間,此處不給出證明。
最后再來看HeapSort方法,堆排序首先把一個數組構造成最大堆,然后每次讓堆的根結點(堆最大的元素)和堆最后的結點交換,並減少堆的大小,然后再對根結點調用MaxHeapify方法調整其位置。堆排序總共調用了N次MaxHeapify方法,故其時間復雜度為O(N*logN)
5.快速排序
快速排序也被稱為霍爾排序,雖然快速排序的最壞時間復雜度為O(N^2),但是快速排序通常是實際排序應用中最好的選擇,因為他的平均性能很好,期望時間復雜度為O(N*lgN)。快速排序與歸並排序類似,都使用了分治思想。快速排序每次從數組中選擇一個元素作為主元,把比主元小的元素放在其前面,把比主元大的元素方法主元的后面,然后再對其前后兩個子數組進行相同的操作。
快速排序的Java代碼如下所示:
- public class Algorithm {
- public static void main(String[] args) {
- int A[] = {5, 2, 4, 6, 1, 3};
- QuickSort(A, 0, A.length-1);
- for(int num : A)
- System.out.println(num);
- }
- public static void QuickSort(int A[], int start, int end) {
- if (start < end) {
- // 主元
- int key = A[end];
- int i = start-1;
- for (int j = start; j < end; j++) {
- // 比key小的數放在前面
- if (A[j] < key) {
- i++;
- int temp = A[j];
- A[j] = A[i];
- A[i] = temp;
- }
- }
- i++;
- A[end] = A[i];
- A[i] = key;
- // 對子數組進行同樣的操作
- QuickSort(A, start, i-1);
- QuickSort(A, i+1, end);
- }
- }
- }
上面的代碼固定選取當前數組的最后一個元素作為主元,如果想要快速排序的平均性能更好,可以隨機選取數組中的元素作為主元來減少出現最壞情況的概率。