基本思想前端
給定一個數組,咱們把數組裏的元素統統倒入到水池中,這些元素將經過相互之間的比較,按照大小順序一個一個地像氣泡同樣浮出水面。java
實現算法
每一輪,從雜亂無章的數組頭部開始,每兩個元素比較大小並進行交換,直到這一輪當中最大或最小的元素被放置在數組的尾部,而後不斷地重複這個過程,直到全部元素都排好位置。其中,核心操做就是元素相互比較。後端
例題分析數組
給定數組 [2, 1, 7, 9, 5, 8],要求按照從左到右、從小到大的順序進行排序。數據結構
解題思路dom
從左到右依次冒泡,把較大的數往右邊挪動便可。函數
首先指針指向第一個數,比較第一個數和第二個數的大小,因爲 2 比 1 大,因此兩兩交換,[1, 2, 7, 9, 5, 8]。學習
接下來指針往前移動一步,比較 2 和 7,因爲 2 比 7 小,二者保持不動,[1, 2, 7, 9, 5, 8]。到目前爲止,7 是最大的那個數。優化
指針繼續往前移動,比較 7 和 9,因爲 7 比 9 小,二者保持不動,[1, 2, 7, 9, 5, 8]。如今,9 變成了最大的那個數。
再日後,比較 9 和 5,很明顯,9 比 5 大,交換它們的位置,[1, 2, 7, 5, 9, 8]。
最後,比較 9 和 8,9 比 8 大,交換它們的位置,[1, 2, 7, 5, 8, 9]。通過第一輪的兩兩比較,9 這個最大的數就像冒泡同樣冒到了數組的最後面。
接下來進行第二輪的比較,把指針從新指向第一個元素,重複上面的操做,最後,數組變成了:[1, 2, 5, 7, 8, 9]。
在進行新一輪的比較中,判斷一下在上一輪比較的過程當中有沒有發生兩兩交換,若是一次交換都沒有發生,就證實其實數組已經排好序了。
實現代碼
public static void bubbleSort(int[] nums) { // 定義一個布爾變量 hasChange,用來標記每輪遍歷中是否發生了交換 boolean hasChange = true; for (int i = 0; i < nums.length - 1 && hasChange; i++) { // 每輪遍歷開始,將 hasChange 設置爲 false hasChange = false; // 進行兩兩比較,若是發現當前的數比下一個數還大,那麼就交換這兩個數,同時記錄一下有交換髮生 for (int j = 0; j < nums.length - 1 - i; j++) { if (nums[j] > nums[j+1]) { swap(nums, j, j+1); hasChange = true; } } } } // 交換數組中的兩個數 public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
算法分析
空間複雜度
假設數組的元素個數是 n,因爲在整個排序的過程當中,咱們是直接在給定的數組裏面進行元素的兩兩交換,因此空間複雜度是 O(1)。
時間複雜度
給定的數組按照順序已經排好
在這種狀況下,咱們只須要進行 n−1 次的比較,兩兩交換次數爲 0,時間複雜度是 O(n)。這是最好的狀況。
給定的數組按照逆序排列
在這種狀況下,咱們須要進行 n(n-1)/2 次比較,時間複雜度是 O(n2)。這是最壞的狀況。
給定的數組雜亂無章
在這種狀況下,平均時間複雜度是 O(n2)。
因而可知,冒泡排序的時間複雜度是 O(n2)。它是一種穩定的排序算法。(穩定是指若是數組裏兩個相等的數,那麼排序先後這兩個相等的數的相對位置保持不變。)
基本思想
不斷地將還沒有排好序的數插入到已經排好序的部分。
特色
在冒泡排序中,通過每一輪的排序處理後,數組後端的數是排好序的;而對於插入排序來講,通過每一輪的排序處理後,數組前端的數都是排好序的。
例題分析
對數組 [2, 1, 7, 9, 5, 8] 進行插入排序。
解題思路
首先將數組分紅左右兩個部分,左邊是已經排好序的部分,右邊是尚未排好序的部分,剛開始,左邊已排好序的部分只有第一個元素 2。接下來,咱們對右邊的元素一個一個進行處理,將它們放到左邊。
實現代碼
public static void insertionSort(int[] nums) { // 將數組的第一個元素看成已經排好序的,從第二個元素,即 i 從 1 開始遍歷數組 for (int i = 1, j, current; i < nums.length; i++) { // 外圍循環開始,把當前 i 指向的值用 current 保存 current = nums[i]; // 指針 j 內循環,和 current 值比較,若 j 所指向的值比 current 值大,則該數右移一位 for (j = i - 1; j >= 0 && nums[j] > current; j--) { nums[j + 1] = nums[j]; } // 內循環結束,j+1 所指向的位置就是 current 值插入的位置 nums[j + 1] = current; } }
算法分析
空間複雜度
假設數組的元素個數是 n,因爲在整個排序的過程當中,是直接在給定的數組裏面進行元素的兩兩交換,空間複雜度是 O(1)。
時間複雜度
給定的數組按照順序已經排好
只須要進行 n-1 次的比較,兩兩交換次數爲 0,時間複雜度是 O(n)。這是最好的狀況。
給定的數組按照逆序排列
在這種狀況下,咱們須要進行 n(n-1)/2 次比較,時間複雜度是 O(n2)。這是最壞的狀況。
給定的數組雜亂無章
在這種狀況下,平均時間複雜度是 O(n2)。
因而可知,和冒泡排序同樣,插入排序的時間複雜度是 O(n2),而且它也是一種穩定的排序算法。
基本思想
核心是分治,就是把一個複雜的問題分紅兩個或多個相同或類似的子問題,而後把子問題分紅更小的子問題,直到子問題能夠簡單的直接求解,最原問題的解就是子問題解的合併。歸併排序將分治的思想體現得淋漓盡致。
實現
一開始先把數組從中間劃分紅兩個子數組,一直遞歸地把子數組劃分紅更小的子數組,直到子數組裏面只有一個元素,纔開始排序。
排序的方法就是按照大小順序合併兩個元素,接着依次按照遞歸的返回順序,不斷地合併排好序的子數組,直到最後把整個數組的順序排好。
例題分析
例題:利用歸併排序算法對數組 [2, 1, 7, 9, 5, 8] 進行排序。
解題思路
首先不斷地對數組進行切分,直到各個子數組裏只包含一個元素。
接下來遞歸地按照大小順序合併切分開的子數組,遞歸的順序和二叉樹裏的前序遍歷相似。
合併數組 [1, 2, 7] 和 [5, 8, 9] 的操做步驟以下。
合併之因此能成功,先決條件必須是兩個子數組都已經分別排好序了。
實現代碼
public static void mergeSort(int[] arr, int lo, int hi) { // 判斷是否只剩下最後一個元素 if (lo >= hi) { return; } // 從中間將數組分紅兩個部分 int mid = lo + (hi - lo) / 2; // 分別遞歸地將左右兩半排好序 mergeSort(arr, lo, mid); mergeSort(arr, mid + 1, hi); // 將排好序的左右兩半合併 merge(arr, lo, mid, hi); } // 歸併 public static void merge(int[] nums, int lo, int mid, int hi) { // 複製一份原來的數組 int[] copy = nums.clone(); // 定義一個 k 指針表示從什麼位置開始修改原來的數組,i 指針表示左半邊的起始位置,j 表示右半邊的起始位置 int k = lo, i = lo, j = mid + 1; while(k <= hi) { if(i > mid) { nums[k++] = copy[j++]; } else if(j > hi) { nums[k++] = copy[i++]; } else if(copy[j] < copy[i]) { nums[k++] = copy[j++]; } else { nums[k++] = copy[i++]; } } }
其中,While 語句比較,一共可能會出現四種狀況。
算法分析
空間複雜度
因爲合併 n 個元素須要分配一個大小爲 n 的額外數組,合併完成以後,這個數組的空間就會被釋放,因此算法的空間複雜度就是 O(n)。歸併排序也是穩定的排序算法。
時間複雜度
歸併算法是一個不斷遞歸的過程。
舉例:數組的元素個數是 n,時間複雜度是 T(n) 的函數。
解法:把這個規模爲 n 的問題分紅兩個規模分別爲 n/2 的子問題,每一個子問題的時間複雜度就是 T(n/2),那麼兩個子問題的複雜度就是 2×T(n/2)。當兩個子問題都獲得瞭解決,即兩個子數組都排好了序,須要將它們合併,一共有 n 個元素,每次都要進行最多 n-1 次的比較,因此合併的複雜度是 O(n)。由此咱們獲得了遞歸複雜度公式:T(n) = 2×T(n/2) + O(n)。
對於公式求解,不斷地把一個規模爲 n 的問題分解成規模爲 n/2 的問題,一直分解到規模大小爲 1。若是 n 等於 2,只須要分一次;若是 n 等於 4,須要分 2 次。這裏的次數是按照規模大小的變化分類的。
以此類推,對於規模爲 n 的問題,一共要進行 log(n) 層的大小切分。在每一層裏,咱們都要進行合併,所涉及到的元素其實就是數組裏的全部元素,所以,每一層的合併複雜度都是 O(n),因此總體的複雜度就是 O(nlogn)。
基本思想
快速排序也採用了分治的思想。
實現
把原始的數組篩選成較小和較大的兩個子數組,而後遞歸地排序兩個子數組。
舉例:把班裏的全部同窗按照高矮順序排成一排。
解法:老師先隨機地挑選了同窗 A,讓全部其餘同窗和 A 比高矮,比 A 矮的都站在 A 的左邊,比 A 高的都站在 A 的右邊。接下來,老師分別從左邊和右邊的同窗裏選擇了同窗 B 和 C,而後不斷地篩選和排列下去。
在分紅較小和較大的兩個子數組過程當中,如何選定一個基準值(也就是同窗 A、B、C 等)尤其關鍵。
例題分析
對數組 [2, 1, 7, 9, 5, 8] 進行排序。
解題思路
實現代碼
public static void quickSort(int[] nums, int lo, int hi) { // 判斷是否只剩下一個元素,是則直接返回 if (lo >= hi) { return; } // 利用partition函數找到一個隨機基準點 int p = partition(nums, lo, hi); // 遞歸地對基準點左半邊和右半邊的數進行排序 quickSort(nums, lo, p - 1); quickSort(nums, p + 1, hi); } // 得到基準值 public static int partition(int[] nums, int lo, int hi) { // 隨機選擇一個數做爲基準值,nums[hi] 就是基準值 swap(nums, randRange(lo, hi), hi); int i, j; // 從左到右用每一個數和基準值比較,若比基準值小,則放到指針 i 所指向的位置。循環完畢後,i 指針以前的數都比基準值小 for (i = lo, j = lo; j < hi; j++) { if (nums[j] <= nums[hi]) { swap(nums, i++, j); } } // 末尾的基準值放置到指針 i 的位置,i 指針以後的數都比基準值大 swap(nums, i, j); // 返回指針 i,做爲基準點的位置 return i; } // 獲取隨機值 public static int randRange(int lo, int hi) { return (int) (lo + Math.random() * (hi - lo + 1)); } // 交換數組中的兩個數 public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
算法分析
空間複雜度
和歸併排序不一樣,快速排序在每次遞歸的過程當中,只須要開闢 O(1) 的存儲空間來完成交換操做實現直接對數組的修改,又由於遞歸次數爲 logn,因此它的總體空間複雜度徹底取決於壓堆棧的次數,所以它的空間複雜度是 O(logn)。
時間複雜度
最優狀況:被選出來的基準值都是當前子數組的中間數。
這樣的分割,能保證對於一個規模大小爲 n 的問題,能被均勻分解成兩個規模大小爲 n/2 的子問題(歸併排序也採用了相同的劃分方法),時間複雜度就是:T(n) = 2×T(n/2) + O(n)。
把規模大小爲 n 的問題分解成 n/2 的兩個子問題時,和基準值進行了 n-1 次比較,複雜度就是 O(n)。很顯然,在最優狀況下,快速排序的複雜度也是 O(nlogn)。
最壞狀況:基準值選擇了子數組裏的最大或者最小值
每次都把子數組分紅了兩個更小的子數組,其中一個的長度爲 1,另一個的長度只比原子數組少 1。劃分過程和冒泡排序的過程相似,算法複雜度爲 O(n2)。
tips:能夠經過隨機地選取基準值來避免出現最壞的狀況。
基本思想
堆排序是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似徹底二叉樹的結構,並同時知足堆的性質:即子結點的鍵值或索引老是小於(或者大於)它的父節點。
實現
實現代碼
public static void heapSort(int[] arr) { len = arr.length; if (len < 1) { return; } //1.構建一個大根堆 buildMaxHeap(arr); //2.循環將堆首位(最大值)與末位交換,而後再從新調整最大堆 while(len > 0) { swap(arr, 0, len-1); len--; adjustHeap(arr, 0); } } // 創建大根堆 public static void buildMaxHeap(int[] arr) { // 從最後一個非葉子節點開始向上構造大根堆 for (int i = (len / 2 -1); i >= 0; i--) { adjustHeap(arr, i); } } // 調整使之成爲大根堆 public static void adjustHeap(int[] arr, int i ) { int maxIndex = i; // 若是有左子樹且左子樹大於父節點,則將最大指針指向左子樹 if (i * 2 < len && arr[i * 2] > arr[maxIndex]) { maxIndex = i * 2; } // 若是有右子樹且右子樹大於父節點,則將最大指針指向右子樹 if (i * 2 + 1 < len && arr[i * 2 + 1] > arr[maxIndex]) { maxIndex = i * 2 + 1; } // 若是父節點不是最大值,則將父節點與最大值交換並遞歸調整與父節點交換的位置 if (maxIndex != i) { swap(arr, maxIndex, i); adjustHeap(arr, maxIndex); } } // 交換數組中的兩個數 public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
算法分析
時間複雜度
堆排序是一種選擇排序,總體主要由構建初始堆+交換堆頂元素和末尾元素並重建堆兩部分組成。其中構建初始堆經推導複雜度爲O(n),在交換並重建堆的過程當中,需交換n-1次,而重建堆的過程當中,根據徹底二叉樹的性質,[log2(n-1),log2(n-2)...1]逐步遞減,近似爲nlogn。因此堆排序時間複雜度通常認爲就是O(nlogn)級。
基本思想
計數排序的核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。做爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有肯定範圍的整數。
計數排序(Counting sort)是一種穩定的排序算法。計數排序使用一個額外的數組C,其中第i個元素是待排序數組A中值等於i的元素的個數。而後根據數組C來將A中的元素排到正確的位置。它只能對整數進行排序。
實現
動圖演示
實現代碼
public static void countingSort(int[] arr) { if (arr.length == 0) { return; } int bias, min = arr[0], max = arr[0]; // 1.確認數組中的最大值最小值 for (int i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } if (arr[i] < min) { min = arr[i]; } } bias = 0 - min; // bias記錄新數組的下標偏移量 int[] bucket = new int[max - min + 1]; // 2.統計並存入新數組 for (int i = 0; i < arr.length; i++) { bucket[arr[i] + bias]++; } int index = 0, i = 0; // 3.反向填充目標數組 while(index < arr.length) { if (bucket[i] != 0) { arr[index] = i - bias; bucket[i]--; index++; } else { i++; } } }
算法分析
當輸入的元素是 n 個 0 到 k 之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。因爲用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,須要大量時間和內存。
最佳狀況:T(n) = O(n+k) 最差狀況:T(n) = O(n+k) 平均狀況:T(n) = O(n+k)
思想
桶排序
是計數排序的升級版。當數列取值範圍過大,或者不是整數時不能適用計數排序,這時可使用桶排序來解決問題。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的肯定。
桶排序 (Bucket sort)
的工做的原理:假設輸入數據服從均勻分佈,將數據分到有限數量的桶裏,每一個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)
實現
每個桶(bucket)表明一個區間範圍,裏面能夠承載一個或多個元素。桶排序的第一步,就是建立這些桶,肯定每個桶的區間範圍:
具體創建多少個桶,如何肯定桶的區間範圍,有不少不一樣的方式。咱們這裏建立的桶數量等於原始數列的元素數量,除了最後一個桶只包含數列最大值,前面各個桶的區間按照比例肯定。
區間跨度 = (最大值-最小值)/ (桶的數量 - 1)
第二步,遍歷原始數列,把元素對號入座放入各個桶中:
第三步,每一個桶內部的元素分別排序(顯然,只有第一個桶須要排序):
第四步,遍歷全部的桶,輸出全部元素:
0.5,0.84,2.18,3.25,4.5
到此爲止,排序結束。
實現代碼
public static void bucketSort(double[] array){ //1.獲得數列的最大值和最小值,並算出差值d double max = array[0]; double min = array[0]; for(int i=1; i<array.length; i++) { if(array[i] > max) { max = array[i]; } if(array[i] < min) { min = array[i]; } } double d = max - min; //2.初始化桶 int bucketNum = array.length; ArrayList<LinkedList<Double>> bucketList = new ArrayList<LinkedList<Double>>(bucketNum); for(int i = 0; i < bucketNum; i++){ bucketList.add(new LinkedList<Double>()); } //3.遍歷原始數組,將每一個元素放入桶中 for(int i = 0; i < array.length; i++){ int num = (int)((array[i] - min) * (bucketNum-1) / d); bucketList.get(num).add(array[i]); } //4.對每一個通內部進行排序 for(int i = 0; i < bucketList.size(); i++){ //JDK底層採用了歸併排序或歸併的優化版本 Collections.sort(bucketList.get(i)); } //5.輸出所有元素 int index = 0; for(LinkedList<Double> list : bucketList){ for(double element : list){ array[index] = element; index++; } } }
算法分析
時間複雜度:O(N + C)
對於待排序序列大小爲 N,共分爲 M 個桶,主要步驟有:
通常使用較爲快速的排序算法,時間複雜度爲 O ( N l o g N ),實際的桶排序過程是以鏈表形式插入的。
整個桶排序的時間複雜度爲:
O ( N ) + O ( M ∗ ( N / M ∗ l o g ( N / M ) ) ) = O ( N ∗ ( l o g ( N / M ) + 1 ) )
當 N = M 時,複雜度爲 O ( N )
空間複雜度:O(N+M)
基本思想
和前面介紹的幾種排序不一樣,拓撲排序應用的場合再也不是一個簡單的數組,而是研究圖論裏面頂點和頂點連線之間的性質。拓撲排序就是要將這些頂點按照相連的性質進行排序。
要能實現拓撲排序,得有幾個前提:
拓撲排序通常用來理清具備依賴關係的任務。
舉例:假設有三門課程 A、B、C,若是想要學習課程 C 就必須先把課程 B 學完,要學習課程 B還得先學習課程 A,因此得出課程的學習順序應該是 A -> B -> C。
實現
例題分析
有一個學生想要修完 5 門課程的學分,這 5 門課程分別用 一、二、三、四、5 來表示,如今已知學習這些課程有以下的要求:
課程 2 和 4 依賴於課程 1
課程 3 依賴於課程 2 和 4
課程 4 依賴於課程 1 和 2
課程 5 依賴於課程 3 和 4
那麼這個學生應該按照怎樣的順序來學習這 5 門課程呢?
解題思路
能夠把 5 門課程當作是一個圖裏的 5 個頂點,用有向線段按照它們的相互關係連起來,因而得出下面的有向圖。
首先能夠看到,這個有向圖裏沒有環,不管從哪一個頂點出發,都不會再回到那個頂點。而且,這個圖裏並無孤島的出現,所以,咱們能夠對它進行拓撲排序。
方法就是,一開始的時候,對每一個頂點統計它們各自的前驅(也就是入度):1(0),2(1),3(2),4(1),5(2)。
通常來講,一個有向無環圖能夠有一個或多個拓撲排序的序列。
實現代碼
運用廣度優先搜索的方法對這個圖的結構進行遍歷。在構建這個圖的過程當中,用一個連接矩陣 adj 來表示這個圖的結構,用一個 indegree 的數組統計每一個頂點的入度,重點看如何實現拓撲排序。
void topologicalSort() { Queue<Integer> q = new LinkedList(); // 定義一個隊列 q // 將全部入度爲 0 的頂點加入到隊列 q for (int v = 0; v < V; v++) { if (indegree[v] == 0) q.add(v); } // 循環,直到隊列爲空 while (!q.isEmpty()) { int v = q.poll(); // 每次循環中,從隊列中取出頂點,即爲按照入度數目排序中最小的那個頂點 print(v); // 將跟這個頂點相連的其餘頂點的入度減 1,若是發現那個頂點的入度變成了 0,將其加入到隊列的末尾 for (int u = 0; u < adj[v].length; u++) { if (--indegree[u] == 0) { q.add(u); } } } }
算法分析
時間複雜度
統計頂點的入度須要 O(n) 的時間,接下來每一個頂點被遍歷一次,一樣須要 O(n) 的時間,因此拓撲排序的時間複雜度是 O(n)。