算法就是編程的靈魂,不會算法的程序員只配作碼農。算法的學習也是有着階段性的,從入門到簡單,再到複雜,再到簡單。最後的簡單是當你達到必定高度以後對於問題可以準確的找到最簡單的解答。html
算法裏邊最經常使用也是最基本的就是排序算法和查找算法了,本文主要講解算法裏邊最經典的十大排序算法。在這裏咱們根據他們各自的實現原理以及效率將十大排序算法分爲兩大類:java
具體分類咱們上圖說明:程序員
這裏給出算法的時間複雜度,空間複雜度以及穩定性的對比整理,一樣經過圖片的形式給出:算法
穩定:若是a本來在b前面,而a=b,排序以後a仍然在b的前面。shell
不穩定:若是a本來在b的前面,而a=b,排序以後 a 可能會出如今 b 的後面。編程
時間複雜度:對排序數據的總的操做次數。反映當n變化時,操做次數呈現什麼規律。後端
空間複雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。數組
下面就一一對十大算法進行詳細的講解,會給出他們的基本思想,圖片演示,以及帶有詳細註釋的源碼。(本文全部的排序算法都是升序排序)緩存
冒泡排序能夠說是最簡單的排序之一了,也是大部分人最容易想到的排序。即對n個數進行排序,每次都是由前一個數跟後一個數比較,每循環一輪, 就能夠將最大的數移到數組的最後, 總共循環n-1輪,完成對數組排序。數據結構
比較相鄰的元素。若是第一個比第二個大,就交換他們兩個。
對每一對相鄰元素做一樣的工做,從開始第一對到結尾的最後一對。這步作完後,最後的元素會是最大的數。
針對全部的元素重複以上的步驟,除了最後一個。
持續每次對愈來愈少的元素重複上面的步驟,直到沒有任何一對數字須要比較。
當輸入的數據已是正序時,冒泡排序最快;當輸入的數據是反序時,冒泡排序最慢。
public static void bubbleSort(int[] arr) {
if (arr == null)
return;
int len = arr.length;
//i控制循環次數,長度爲len的數組只須要循環len-1次,i的起始值爲0因此i<len-1
for (int i = 0; i < len - 1; i++) {
// j控制比較次數,第i次循環內須要比較len-i次
// 可是因爲是由arr[j]跟arr[j+1]進行比較,因此爲了保證arr[j+1]不越界,j<len-i-1
for (int j = 0; j < len - i - 1; j++) {
// 若是前一個數比後一個數大,則交換位置將大的數日後放。
if (arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
複製代碼
選擇排序能夠說是冒泡排序的改良版,再也不是前一個數跟後一個數相比較, 而是在每一次循環內都由一個數去跟全部的數都比較一次,每次比較都選取相對較小的那個數來進行下一次的比較,並不斷更新較小數的下標。這樣在一次循環結束時就能獲得最小數的下標,再經過一次交換將最小的數放在最前面,經過n-1次循環以後完成排序。相對於冒泡排序來講,比較的次數並無改變,可是數據交換的次數大大減小。
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。
重複第二步,直到全部元素均排序完畢。
選擇排序是一種簡單直觀的排序算法,不管什麼數據進去都是 O(n²) 的時間複雜度。因此用到它的時候,數據規模越小越好。惟一的好處可能就是不佔用額外的內存空間了吧。
public static void selectSort(int[] arr) {
if (arr == null)
return;
int len = arr.length;
// i控制循環次數,長度爲len的數組只須要循環len-1次,i的起始值爲0因此i<len-1
for (int i = 0; i < len - 1; i++) {
// minIndex 用來保存每次比較後較小數的下標。
int minIndex = i;
// j控制比較次數,由於每次循環結束以後最小的數都已經放在了最前面,
// 因此下一次循環的時候就能夠跳過這個數,因此j的初始值爲i+1而不須要每次循環都從0開始,而且j<len便可
for (int j = i + 1; j < len; j++) {
//每比較一次都須要將較小數的下標記錄下來
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
// 當完成一次循環時,就須要將本次循環選取的最小數移動到本次循環開始的位置。
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
// 打印每次循環結束以後數組的排序狀態(方便理解)
System.out.println("第" + (i + 1) + "次循環以後效果:" + Arrays.toString(arr));
}
}
複製代碼
插入排序的思想打牌的人確定很容易理解,就是見縫插針。首先就默認數組中的第一個數的位置是正確的,即已經排序。而後取下一個數,與已經排序的數按從後向前的順序依次比較, 若是該數比當前位置排好序的數小,則將排好序的數的位置向後移一位。 重複上一步驟,直到找到合適的位置。 找到位置後就結束比較的循環,將該數放到相應的位置。
將第一待排序序列第一個元素看作一個有序序列,把第二個元素到最後一個元素當成是未排序序列。
從頭至尾依次掃描未排序序列,將掃描到的每一個元素插入有序序列的適當位置。(若是待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面。)
public static void insertSort(int[] arr) {
if (arr == null)
return;
int len = arr.length;
// i控制循環次數,由於已經默認第一個數的位置是正確的,因此i的起始值爲1,i<len,循環len-1次
for (int i = 1; i < len; i++) {
int j = i;//變量j用來記錄即將要排序的數的位置即目標數的原位置
int target = arr[j];//target用來記錄即將要排序的那個數的值即目標值
// while循環用來爲目標值在已經排好序的數中找到合適的位置,
// 由於是從後向前比較,而且是與j-1位置的數比較,因此j>0
while (j > 0 && target < arr[j - 1]) {
// 當目標數的值比它當前位置的前一個數的值小時,將前一個數的位置向後移一位。
// 而且j--使得目標數繼續與下一個元素比較
arr[j] = arr[j - 1];
j--;
}
// 更目標數的位置。
arr[j] = target;
//打印每次循環結束以後數組的排序狀態(方便理解)
System.out.println("第" + (i) + "次循環以後效果:" + Arrays.toString(arr));
}
}
複製代碼
希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。但希爾排序是非穩定排序算法。
希爾排序是基於插入排序的如下兩點性質而提出改進方法的:
插入排序在對幾乎已經排好序的數據操做時,效率高,便可以達到線性排序的效率;
但插入排序通常來講是低效的,由於插入排序每次只能將數據移動一位。
希爾排序的基本思想是:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。
選擇一個增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
按增量序列個數 k,對序列進行 k 趟排序;
每趟排序,根據對應的增量 ti,將待排序列分割成若干長度爲 m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲 1 時,整個序列做爲一個表來處理,表長度即爲整個序列的長度。
public static void shellSort(int[] arr) {
if (arr == null)
return;
int len = arr.length; // 數組的長度
int k = len / 2; // 初始的增量爲數組長度的一半
// while循環控制按增量的值來劃不一樣分子序列,每完成一次增量就減小爲原來的一半
// 增量的最小值爲1,即最後一次對整個數組作直接插入排序
while (k > 0) {
// 裏邊其實就是升級版的直接插入排序了,是對每個子序列進行直接插入排序,
// 因此直接將直接插入排序中的‘1’變爲‘k’就能夠了。
for (int i = k; i < len; i++) {
int j = i;
int target = arr[i];
while (j >= k && target < arr[j - k]) {
arr[j] = arr[j - k];
j -= k;
}
arr[j] = target;
}
// 不一樣增量排序後的結果
System.out.println("增量爲" + k + "排序以後:" + Arrays.toString(arr));
k /= 2;
}
}
複製代碼
整體歸納就是從上到下遞歸拆分,而後從下到上逐步合併。
先把待排序數組分爲左右兩個子序列,再分別將左右兩個子序列拆分爲四個子子序列,以此類推直到最小的子序列元素的個數爲兩個或者一個爲止。
將最底層的最左邊的一個子序列排序,而後將從左到右第二個子序列進行排序,再將這兩個排好序的子序列合併並排序,而後將最底層從左到右第三個子序列進行排序..... 合併完成以後記憶完成了對數組的排序操做(必定要注意是從下到上層級合併,能夠理解爲遞歸的層級返回)
申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列;
設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置;
比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置;
重複步驟 3 直到某一指針達到序列尾;
將另外一序列剩下的全部元素直接複製到合併序列尾。
和選擇排序同樣,歸併排序的性能不受輸入數據的影響,但表現比選擇排序好的多,由於始終都是 O(nlogn) 的時間複雜度。代價是須要額外的內存空間。
/** * 遞歸拆分 * @param arr 待拆分數組 * @param left 待拆分數組最小下標 * @param right 待拆分數組最大下標 */
public static void mergeSort(int[] arr, int left, int right) {
int mid = (left + right) / 2; // 中間下標
if (left < right) {
mergeSort(arr, left, mid); // 遞歸拆分左邊
mergeSort(arr, mid + 1, right); // 遞歸拆分右邊
sort(arr, left, mid, right); // 合併左右
}
}
/** * 合併兩個有序子序列 * @param arr 待合併數組 * @param left 待合併數組最小下標 * @param mid 待合併數組中間下標 * @param right 待合併數組最大下標 */
public static void sort(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1]; // 臨時數組,用來保存每次合併年以後的結果
int i = left;
int j = mid + 1;
int k = 0; // 臨時數組的初始下標
// 這個while循環可以初步篩選出待合併的了兩個子序列中的較小數
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 將左邊序列中剩餘的數放入臨時數組
while (i <= mid) {
temp[k++] = arr[i++];
}
// 將右邊序列中剩餘的數放入臨時數組
while (j <= right) {
temp[k++] = arr[j++];
}
// 將臨時數組中的元素位置對應到真真實的數組中
for (int m = 0; m < temp.length; m++) {
arr[m + left] = temp[m];
}
}
複製代碼
快速排序使用分治法(Divide and conquer)策略來把一個串行(list)分爲兩個子串行(sub-lists)。快速排序又是一種分而治之思想在排序算法上的典型應用。本質上來看,快速排序應該算是在冒泡排序基礎上的遞歸分治法。
快速排序也採用了分治的策略,這裏引入了‘基準數’的概念。
從數列中挑出一個元素,稱爲 「基準」(pivot);
從新排序數列,全部元素比基準值小的擺放在基準前面,全部元素比基準值大的擺在基準的後面(相同的數能夠到任一邊)。在這個分區退出以後,該基準就處於數列的中間位置。這個稱爲分區(partition)操做;
遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
在平均情況下,快速排序排序 n 個項目須要要 Ο(nlogn) 次比較,在最壞情況下則須要 Ο(n2) 次比較,但這種情況並不常見。事實上,快速排序一般明顯比其餘 Ο(nlogn) 算法更快,由於它的內部循環(inner loop)能夠在大部分的架構上頗有效率地被實現出來。
快速排序的最壞運行狀況是 O(n²),好比說順序數列的快排。但它的平攤指望時間是 O(nlogn),且 O(nlogn) 記號中隱含的常數因子很小,比複雜度穩定等於 O(nlogn) 的歸併排序要小不少。因此,對絕大多數順序性較弱的隨機數列而言,快速排序老是優於歸併排序。
/** * 分區過程 * @param arr 待分區數組 * @param left 待分區數組最小下標 * @param right 待分區數組最大下標 */
public static void quickSort(int[] arr, int left, int right) {
if (left < right) {
int temp = qSort(arr, left, right);
quickSort(arr, left, temp - 1);
quickSort(arr, temp + 1, right);
}
}
/** * 排序過程 * @param arr 待排序數組 * @param left 待排序數組最小下標 * @param right 待排序數組最大下標 * @return 排好序以後基準數的位置下標,方便下次的分區 */
public static int qSort(int[] arr, int left, int right) {
int temp = arr[left]; // 定義基準數,默認爲數組的第一個元素
while (left < right) { // 循環執行的條件
// 由於默認的基準數是在最左邊,因此首先從右邊開始比較進入while循環的判斷條件
// 若是當前arr[right]比基準數大,則直接將右指針左移一位,固然還要保證left<right
while (left < right && arr[right] > temp) {
right--;
}
// 跳出循環說明當前的arr[right]比基準數要小,那麼直接將當前數移動到基準數所在的位置,而且左指針向右移一位(left++)
// 這時當前數(arr[right])所在的位置空出,須要從左邊找一個比基準數大的數來填充。
if (left < right) {
arr[left++] = arr[right];
}
// 下面的步驟是爲了在左邊找到比基準數大的數填充到right的位置。
// 由於如今須要填充的位置在右邊,因此左邊的指針移動,若是arr[left]小於或者等於基準數,則直接將左指針右移一位
while (left < right && arr[left] <= temp) {
left++;
}
// 跳出上一個循環說明當前的arr[left]的值大於基準數,須要將該值填充到右邊空出的位置,而後當前位置空出。
if (left < right) {
arr[right--] = arr[left];
}
}
// 當循環結束說明左指針和右指針已經相遇。而且相遇的位置是一個空出的位置,
// 這時候將基準數填入該位置,並返回該位置的下標,爲分區作準備。
arr[left] = temp;
return left;
}
複製代碼
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似徹底二叉樹的結構,並同時知足堆積的性質:即子結點的鍵值或索引老是小於(或者大於)它的父節點。堆排序能夠說是一種利用堆的概念來排序的選擇排序。分爲兩種方法:
大頂堆:每一個結點的值都大於它的左右子結點的值,升序排序用大頂堆。
小頂堆:每一個結點的值都小於它的左右子結點的值,降序排序用小頂堆。
建立一個堆 H[0……n-1];
把堆首(最大值)和堆尾互換;
把堆的尺寸縮小 1,並調用 shift_down(0),目的是把新的數組頂端數據調整到相應位置;
重複步驟 2,直到堆的尺寸爲 1。
public static void heapSort(int[] arr) {
if (arr == null) {
return;
}
int len = arr.length;
// 初始化大頂堆(從最後一個非葉節點開始,從左到右,由下到上)
for (int i = len / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, len);
}
// 將頂節點和最後一個節點互換位置,再將剩下的堆進行調整
for (int j = len - 1; j > 0; j--) {
swap(arr, 0, j);
adjustHeap(arr, 0, j);
}
}
/** * 整理樹讓其變成堆 * @param arr 待整理的數組 * @param i 開始的結點 * @param j 數組的長度 */
public static void adjustHeap(int[] arr, int i, int j) {
int temp = arr[i];// 定義一個變量保存開始的結點
// k就是該結點的左子結點下標
for (int k = 2 * i + 1; k < j; k = 2 * k + 1) {
// 比較左右兩個子結點的大小,k始終記錄二者中較大值的下標
if (k + 1 < j && arr[k] < arr[k + 1]) {
k++;
}
// 經子結點中的較大值和當前的結點比較,比較結果的較大值放在當前結點位置
if (arr[k] > temp) {
arr[i] = arr[k];
i = k;
} else { // 說明已是大頂堆
break;
}
}
arr[i] = temp;
}
/** * 交換數據 * @param arr * @param num1 * @param num2 */
public static void swap(int[] arr, int num1, int num2) {
int temp = arr[num1];
arr[num1] = arr[num2];
arr[num2] = temp;
}
複製代碼
計數排序採用了一種全新的思路,再也不是經過比較來排序,而是將待排序數組中的最大值+1做爲一個臨時數組的長度,而後用臨時數組記錄待排序數組中每一個元素出現的次數。最後再遍歷臨時數組,由於是升序,因此從前到後遍歷,將臨時數組中值>0的數的下標循環取出,依次放入待排序數組中,便可完成排序。計數排序的效率很高,可是實在犧牲內存的前提下,而且有着限制,那就是待排序數組的值必須 限制在一個肯定的範圍。
public static void countSort(int[] arr) {
if (arr == null)
return;
int len = arr.length;
// 保存待排序數組中的最大值,目的是肯定臨時數組的長度(必須)
int maxNum = arr[0];
// 保存待排序數組中的最小值,目的是肯定最終遍歷臨時數組時下標的初始值(非必需,只是這樣能夠加快速度,減小循環次數)
int minNum = arr[0];
// for循環就是爲了找到待排序數組的最大值和最小值
for (int i = 1; i < len; i++) {
maxNum = maxNum > arr[i] ? maxNum : arr[i];
minNum = minNum < arr[i] ? minNum : arr[i];
}
// 建立一個臨時數組
int[] temp = new int[maxNum + 1];
// for循環是爲了記錄待排序數組中每一個元素出現的次數,並將該次數保存到臨時數組中
for (int i = 0; i < len; i++) {
temp[arr[i]]++;
}
// k=0用來記錄待排序數組的下標
int k = 0;
// 遍歷臨時數組,從新爲待排序數組賦值。
for (int i = minNum; i < temp.length; i++) {
while (temp[i] > 0) {
arr[k++] = i;
temp[i]--;
}
}
}
複製代碼
桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的肯定。爲了使桶排序更加高效,咱們須要作到這兩點:
在額外空間充足的狀況下,儘可能增大桶的數量
使用的映射函數可以將輸入的 N 個數據均勻的分配到 K 個桶中
桶排序利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的肯定。爲了使桶排序更加高效,咱們須要作到這兩點:
在額外空間充足的狀況下,儘可能增大桶的數量
使用的映射函數可以將輸入的 N 個數據均勻的分配到 K 個桶中
當輸入的數據能夠均勻的分配到每個桶中的時候桶排序最快,當輸入的數據被分配到了同一個桶中的時候最慢。
public static void bucketSort(int[] arr) {
if (arr == null)
return;
int len = arr.length;
// 定義桶的個數,這裏k的值要視狀況而定,這裏咱們假設待排序數組裏的數都是[0,100)之間的。
int k = 10;
// 用嵌套集合來模擬桶,外層集合表示桶,內層集合表示桶裏邊裝的元素。
List<List<Integer>> bucket = new ArrayList<>();
//for循環初始化外層集合即初始化桶
for (int i = 0; i < k; i++) {
bucket.add(new ArrayList<>());
}
// 循環是爲了將待排序數組中的元素經過映射函數分別放入不一樣的桶裏邊
for (int i = 0; i < len; i++) {
bucket.get(mapping(arr[i])).add(arr[i]);
}
// 這個循環是爲了將全部的元素個數大於1的桶裏邊的數據進行排序。
for (int i = 0; i < k; i++) {
if (bucket.size() > 1) {
// 由於這裏是用集合來模擬的桶因此用java寫好的對集合排序的方法。
// 其實應該本身寫一個方法來排序的。
Collections.sort(bucket.get(i));
}
}
// 將排好序的數從新放入待排序數組中
int m = 0;
for (List<Integer> list : bucket) {
if (list.size() > 0) {
for (Integer a : list) {
arr[m++] = a;
}
}
}
}
/** * 映射函數 * @param num * @return */
public static int mapping(int num) {
return num / 10;
}
複製代碼
基數排序是一種非比較型整數排序算法,其原理是將整數按位數切割成不一樣的數字,而後按每一個位數分別比較。因爲整數也能夠表達字符串(好比名字或日期)和特定格式的浮點數,因此基數排序也不是隻能使用於整數。
public static void main(String[] args) {
int[] arr = {720, 6, 57, 88, 60, 42, 83, 73, 48, 85};
redixSort(arr, 10, 3);
System.out.println(Arrays.toString(arr));
}
public static void redixSort(int[] arr, int radix, int d) {
// 緩存數組
int[] tmp = new int[arr.length];
// buckets用於記錄待排序元素的信息
// buckets數組定義了max-min個桶
int[] buckets = new int[radix];
for (int i = 0, rate = 1; i < d; i++) {
// 重置count數組,開始統計下一個關鍵字
Arrays.fill(buckets, 0);
// 將data中的元素徹底複製到tmp數組中
System.arraycopy(arr, 0, tmp, 0, arr.length);
// 計算每一個待排序數據的子關鍵字
for (int j = 0; j < arr.length; j++) {
int subKey = (tmp[j] / rate) % radix;
buckets[subKey]++;
}
for (int j = 1; j < radix; j++) {
buckets[j] = buckets[j] + buckets[j - 1];
}
// 按子關鍵字對指定的數據進行排序
for (int m = arr.length - 1; m >= 0; m--) {
int subKey = (tmp[m] / rate) % radix;
arr[--buckets[subKey]] = tmp[m];
}
rate *= radix;
}
}
複製代碼
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。