算法(Algorithm)是指解題方案的準確而完整的描述,是一系列解決問題的清晰指令,算法表明着用系統的方法描述解決問題的策略機制。java
簡單點說,算法就是解決問題的方法。確切來講它是相對於計算機程序的,大多數狀況並不與具體某一種編程語言有關,但今天咱們採用java語言實現算法示例。原諒我是一隻Android小菜鳥,就算不原諒,你又能拿我怎麼滴?哈哈,開個玩笑,回到正題,方法有千千萬萬種,相信你們王者榮耀上分也是想了好多辦法,嘗試不一樣的位置,不一樣英雄,每一個英雄不一樣的打法。最後發現,毫無卵用(手動滑稽)。那是你沒找到好的方法,好比你能夠找代練啊,而咱們的主題算法的優劣主要取決於時間複雜度和空間複雜度。那麼有人問了,什麼是時間複雜度,什麼又是空間複雜度呢?
git
時間複雜度是指執行算法所須要的計算工做量。咱們見得更多的是這樣的寫法,O(1)、O(n)。到底什麼意思呢?我相信更多的小夥伴想要的是這樣的解讀方式。github
int a=1;
int b=2;
int c=a+b;複製代碼
相似於這種,執行語句的頻度均爲1,即便有上千萬條,時間複雜度仍是O(1),只不過執行時間是一個很大的常數。面試
for(int i=0;i<n;i++){
int a=1;
int b=2;
int c=a+b;
}複製代碼
在一個規模爲n的循環中,無論是n次,仍是n-1次,對於時間複雜度都是線性的,取n,記爲O(n)。算法
int i = 1;
while (i <= n){
i = i*2;
}複製代碼
這個可能理解上有點困難,但也很簡單,每次執行i都會乘以2,其實就是2的x次冪小於等於n,求得x爲log2n,因此時間複雜度O(log2n)。shell
for (i = 0; i < n; ++i){
for (j = 0; j < n; j++){
printf ("%d\n", j);
}
}複製代碼
相似於這種在規模爲n的循環中又嵌套了一層規模爲n的循環,那麼時間複雜度就爲O(n^2),同理n的屢次冪就是多層嵌套。編程
空間複雜度是對一個算法在運行過程當中臨時佔用存儲空間大小的量度。通常來講咱們不考慮空間複雜度,大多狀況下爲O(1),像遞歸可能會達到O(n)。api
穩定性就是一組元素,其中有重複的元素,好比2113,咱們把前面的1記爲1a,後面的1記爲1b,那麼原始數據爲21a1b3,若是排序後重復元素的相對位置不變,那麼,這個排序是穩定的。如上例子,排序完應該是1a1b23纔是穩定的,而不是1b1a23。數組
排序算法是算法的入門知識,但思想能夠用於不少算法中。什麼是排序?排序就是將一組對象按照某種邏輯順序從新排列的過程。其實在咱們的api中提供了不少優秀的排序算法,那咱們爲何還要去學習它?緣由很簡單,它屬於入門,有助於你理解其它更高大上的算法,同時它也是咱們解決其餘問題的第一步。懂了它,你又向大佬靠近了一步。最重要的是面試官看你排序算法這麼6,內心想這個確定是個大佬,必定要留住他,到時候就是你裝逼的時候了。bash
我都懶得說排序算法有幾種了,由於我根本不知道,一種排序算法可能對應多種變體,此次我給你們介紹8種常見的經典排序算法。
直接插入排序很好理解,就是從一組元素中取一個元素(確定是有序的,就一個嘛,稱有序元素組),而後在剩下的元素中每次取一個元素使勁地往有序的元素組插,插到你滿意爲止。
是否是很好理解?若是以爲仍是有點抽象,沒有關係,每一個算法,我都會分爲3個步驟講解,思想-拆解分析-java代碼實現-運行結果。
爲了方便起見,排序的原始數據爲5201314,很正規,有重複元素,沒毛病。這裏給你們一個小意見,像碰到/2或者說*2這種,用位運算更佳哦,但本文爲了好理解採用了前者,哈哈。
咱們在實現直接插入排序的時候每每取第一個元素成立有序元素組,而後它後面的元素一個一個瘋狂插。
頗有層次感是否是?在插的時候也有小技巧的,要溫柔,要循循漸進。由於每當咱們插入一個元素,都是有序的,有序的說明什麼?越後面確定越大,因此咱們只要從後面開始比較就好了(你非要從前面插,我也沒辦法),咱們從後一直往前比較,直到碰到小於或等於插入的元素爲止,而後咱們乖乖的插到它後面就好了。
public static void insertSort(int[] array) {
//從第2個開始往前插
for (int i = 1, n = array.length; i < n; i++) {
int temp = array[i];//保存第i個值
int j = i - 1;//從有序數組的最後一個開始
for (; j >= 0 && array[j] > temp; j--) {
array[j + 1] = array[j];//從後往前比較,大於temp的值都得後移
}
array[j + 1] = temp;//碰到小於或等於的數中止,因爲多減了1,因此加上1後,賦值爲插入值temp
}
System.out.println("直接插入排序後:" + Arrays.toString(array));
}複製代碼
從代碼的實現來分析,運用咱們剛剛學的知識,一般狀況下最外層是一個n規模的循環,內部又有一個規模爲n的循環,所以平均時間複雜度爲O(n^2);最壞的狀況是內部的循環所有走一遍,好比咱們插入了一個最小的值,所以最差時間複雜度爲O(n^2);最好的狀況就是裏面的循環不用走,好比咱們插入了一個最大的值,所以最好時間複雜度爲O(n)。其中空間複雜度爲O(1)。因爲咱們是碰到小於或等於的數才中止,因此並不影響重複元素的相對位置,所以直接插入排序是穩定的。
希爾排序也是插入排序的一種,是直接插入排序算法的一種更高效的改進版本。
希爾排序是基於插入排序的如下兩點性質而提出改進方法的:
說了這麼多,其實就是插得不夠理想,根據直接插入排序的時間複雜度,最好的狀況能夠達到線性的程度,通常狀況下,卻不是這樣的,每次插入一個,可能要移動大量數據,咱們但願在執行直接插入前,可以儘可能的保持有序。
取一個增量d1<n,使得距離爲d1的元素分在一組,每組進行直接插入排序,而後再取d2<d1,進行排序,直到全部元素都在一組,即增量爲1。
public static void shellSort(int[] array) {
for (int n = array.length, d = n / 2; d > 0; d /= 2) {//取增量爲長度的一半,每次減半,直到d=1,可是d=1必須得排序,所以最後的判斷爲d>0
for (int x = 0; x < d; x++) {//分組
for (int i = x + d; i < n; i += d) {//每組進行直接插入排序
int temp = array[i];
int j = i - d;
for (; j >= 0 && array[j] > temp; j = j - d) {
array[j + d] = array[j];
}
array[j + d] = temp;
}
}
}
System.out.println("希爾排序後:" + Arrays.toString(array));
}複製代碼
希爾排序的最好與最壞時間複雜度同直接插入排序,平均時間複雜度爲O(n^1.3),不要問我1.3怎麼來的,這跟增量的取值有關係,空間複雜度爲O(1),因爲在最後一次直接排序前,通過分組排序,因此可能重複元素的相對位置會交換,所以它是不穩定的。
我記得當初老師讓咱們寫一個排序算法,我第一個想的就是這個,可能大多數都是這個?很厲害了是否是?至少也是有名的排序算法。
在一組元素中,暴力找出最小的元素與第一個位置的元素交換,而後從剩下的元素中,選取最小的,與第二個位置交換,以此類推。
public static void selectSort(int[] array) {
for (int i = 0, n = array.length; i < n; i++) {
int j = i + 1;
int temp = array[i];
int position = i;
for (; j < n; j++) {
if (array[j] < temp) {
temp = array[j];
position = j;
}
}
array[position] = array[i];
array[i] = temp;
}
System.out.println("簡單選擇排序後:" + Arrays.toString(array));
}複製代碼
從代碼上看,簡單選擇排序彷佛沒有什麼最好最壞的時候,老是這麼暴力,時間複雜度老是爲O(n^2),空間複雜度爲O(1),因爲每次取到最小值後都要與前面位置元素交換,所以破壞了元素的相對位置,因此它是不穩定的。
說堆排序以前,必須說一下堆的概念:
徹底二叉樹中任一非葉子結點的關鍵字均不大於(或不小於)其左右孩子(若存在)結點的關鍵字。而咱們這裏取不小於,也稱之爲大根堆。
利用大根堆的性質,每次把元素組建堆,取出最大值,放入最後,直到最後一位,排序完成。
以此類推,直到完成最後一個,排序完成。
public static void heapSort(int[] array) {
//從第一個非葉子結點開始,建堆
int n = array.length;
int startIndex = (n - 1 - 1) / 2;
for (int i = startIndex; i >= 0; i--) {
maxHeapify(array, n, i);
}
//末尾與頭交換,交換後調整最大堆
for (int i = n - 1; i > 0; i--) {
int temp = array[0];
array[0] = array[i];
array[i] = temp;
maxHeapify(array, i, 0);
}
System.out.println("堆排序後:" + Arrays.toString(array));
}
/**
* 建立最大堆
*
* @param array 元素組
* @param heapSize 須要建立最大堆的大小,通常在sort的時候用到,由於最大值放在末尾,末尾就再也不納入最大堆了
* @param index 當前須要建立最大堆的位置
*/
private static void maxHeapify(int[] array, int heapSize, int index) {
int left = index * 2 + 1;//左子節點
int right = left + 1;//右子節點
int largest = index;
if (left < heapSize && array[index] < array[left]) {
largest = left;
}
if (right < heapSize && array[largest] < array[right]) {
largest = right;
}
//獲得最大值後可能須要交換,若是交換了,其子節點可能就不符合堆要求了,須要從新調整
if (largest != index) {
int temp = array[index];
array[index] = array[largest];
array[largest] = temp;
maxHeapify(array, heapSize, largest);
}
}複製代碼
從代碼直接看時間複雜度其實挺難,我這裏直接給答案,堆排序的平均、最差、最壞時間複雜度都是O(nlog2n),由於它永遠都是一個套路,對這個時間複雜度怎麼來的,能夠網上搜搜,我相信你是棒棒的。空間複雜度爲O(1),由於它須要建堆還要從新調整堆,確定是無法保證元素的相對位置的,因此它是不穩定的。
冒泡排序能夠想象一下,魚吐泡泡,一個一個泡泡往上冒。
元素之間兩兩比較,最小數往上冒,或者最大數向下沉,直到排序完成。
咱們這裏以往上冒爲例。
public static void bubbleSort(int[] array) {
int n = array.length;
for (int i = 0; i < n - 1; i++) {
for (int j = n - 1 - 1; j >= i; j--) {
if (array[j + 1] < array[j]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
System.out.println("第" + i + "趟:" + Arrays.toString(array));
}
System.out.println("冒泡排序後:" + Arrays.toString(array));
}複製代碼
若是你單純從上面的代碼看,平均、最好、最差時間複雜度都是O(n^2),若是看過其它文章的同窗可能會說最好時間複雜度應該是O(n),那是由於加入了標誌位,具體代碼我不貼了,空間複雜度是O(1),因爲一直是兩兩比較,並無改變相對位置的操做,因此是穩定的。
選擇一個基數,通常咱們選擇第一個數,而後把大於該數的放右邊,小於該數的放左邊,而後分別對左右兩邊用一樣的方法處理,直到排序結束。
public static void quickSort(int[] array) {
_quickSort(array, 0, array.length - 1);
System.out.println("快速排序後:" + Arrays.toString(array));
}
private static int getMiddle(int[] array, int low, int high) {
int tmp = array[low]; //數組的第一個做爲基數
while (low < high) { //直到指針重合一趟完成
while (low < high && array[high] >= tmp) {
high--;
}
array[low] = array[high]; //找到比基數小的
while (low < high && array[low] <= tmp) {
low++;
}
array[high] = array[low]; //找到比基數大的
}
array[low] = tmp; //基數歸位
System.out.println(Arrays.toString(array));
return low; //返回基數的位置
}
private static void _quickSort(int[] array, int low, int high) {
if (low < high) {
int middle = getMiddle(array, low, high); //基於第一個數將array數組進行一分爲二
_quickSort(array, low, middle - 1); //左邊進行遞歸排序
_quickSort(array, middle + 1, high); //右邊進行遞歸排序
}
}複製代碼
關於快速排序的時間複雜度,表示三言兩語真的說不清楚,感興趣的朋友能夠翻閱相關書籍,有能力的能夠本身證實- -!最好時間複雜度爲O(nlog2n),狀況爲可以正好根據基數平均劃分元素組,最差時間複雜度爲O(n^2),狀況爲元素組呈正序或者逆序狀態,平均時間複雜度爲O(nlog2n),由於快速排序是遞歸進行的,須要牽涉到遞歸深度,空間複雜度爲O(nlog2n),準確來講是平均空間複雜度,因爲快速排序在跟基數比較的時候,可能會交換而破壞了元素之間的相對位置,所以快速排序是不穩定的。
採用經典分治思想,將一個元素組劃分多個有序的小元素組,而後將這些小元素組合併成一個有序的元素組。
public static void mergeSort(int[] array) {
sort(array, 0, array.length - 1);
System.out.println("歸併排序:" + Arrays.toString(array));
}
private static void sort(int[] array, int left, int right) {
if (left < right) {
//找出中間索引
int center = (left + right) / 2;
//對左邊數組進行遞歸
sort(array, left, center);
//對右邊數組進行遞歸
sort(array, center + 1, right);
//合併
merge(array, left, center, right);
}
}
private static void merge(int[] array, int left, int center, int right) {
int[] tmpArr = new int[array.length];
int mid = center + 1;
int third = left;//third記錄中間數組的索引
int tmp = left;//複製時用到的索引
while (left <= center && mid <= right) {
//從兩個數組中取出最小的放入中間數組
if (array[left] <= array[mid]) {
tmpArr[third++] = array[left++];
} else {
tmpArr[third++] = array[mid++];
}
}
//剩餘部分依次放入中間數組
while (mid <= right) {
tmpArr[third++] = array[mid++];
}
while (left <= center) {
tmpArr[third++] = array[left++];
}
//將中間數組中的內容複製回原數組
while (tmp <= right) {
array[tmp] = tmpArr[tmp++];
}
System.out.println(Arrays.toString(array));
}複製代碼
因爲歸併排序就一個套路並且合併的時候是從左往右,所以不會破壞元素的相對位置,是穩定的,同時它的最好、最壞、平均時間複雜度都是O(nlog2n),簡單來講它是基於徹底二叉樹的,其深度爲log2n,每次合併操做都是一個n級規模,所以爲nlog2n,而空間複雜度除了深度log2n之外,咱們還須要臨時數組,所以空間複雜度爲O(n)=O(n)+O(log2n)。
因爲是遞歸,可能與理想輸出有所差距。
將一組元素進行桶分配,啥意思?好比數字250,百位是2,十位是5,個位是0,而這些個位,十位等就是所謂的桶。
因爲測試數據全是個位數,因此只要進行一次就結束了,若是有更高位的,將一直進行到最高位。
public static void radixSort(int[] array) {
int max = array[0];
final int length = array.length;
for (int i = 1; i < length; i++) {
if (array[i] > max) {
max = array[i];
}
}
int time = 0;//數組最大值位數
while (max > 0) {
max /= 10;
time++;
}
int k = 0; //從新放入數組的索引
int n = 1; //位值,如1,10,100
int m = 1; //當前在哪一位
int[][] temp = new int[10][length]; //數組的第一維表示該位數值,二維表示具體的值
int[] order = new int[10]; //數組order[i]用來表示該位是i的數的個數
while (m <= time) {
for (int num : array) {
int lsd = (num / n) % 10;//獲取該位的基數0-9
temp[lsd][order[lsd]] = num;
order[lsd]++;
}
for (int i = 0; i < 10; i++) {
if (order[i] != 0) {
for (int j = 0; j < order[i]; j++) {
array[k] = temp[i][j];//基於m位的從新放入數組中
k++;
}
}
order[i] = 0;//復位
}
System.out.println("第" + m + "位排序:" + Arrays.toString(array));
n *= 10;
k = 0;//復位
m++;
}
System.out.println("基數排序後:" + Arrays.toString(array));
}複製代碼
直接給答案,最優時間複雜度爲O(d(r+n)),最差時間複雜度爲O(d(r+n)),平均時間複雜度爲O(d(r+n)),空間複雜度爲O(rd+n),其中r表明關鍵字基數,d表明長度,n表明關鍵字個數,因爲是分配且從左往右,所以是穩定的。
代碼已經貼出來了,因爲測試數據確實比較簡單,你們能夠本身使用複雜的原始數據進行測試。
到這裏,8種常見的排序算法介紹的差很少了,若是你內心想的是,臥槽,這麼簡單,我立刻能夠在個人小本本上寫出來,那麼我也沒白寫這篇文章。若是你是一臉懵逼,我內心可能想的是,臥槽,居然沒糊弄過去。哈哈,無論怎樣,算法並非什麼高端的東西,其實你每天都在寫算法,只不過。。。嘿嘿。算法可能有高低之分,但適合本身的纔是最好的。儘可能領會其中的思想,來完善你的算法吧。而這篇文章只不過是拋磚引玉,同我上篇文章帶你領略clean架構的魅力,你看,是否是不少架構的文章?我發現我愈來愈自戀了,哈哈,但真的很但願更強的大佬能分享學習心得,我徹底同意打賞這種形式,甚至是你的小圈圈,但也得用點心啊。小弟,我心是用了,可是能力可能不足,若有錯誤,麻煩提出來,我及時修改。最後,感謝一直支持個人人!誒,差點沒堅持下來。
哦,對了,我猜小夥伴又要吐槽個人做圖,其實我以爲挺好的,不是嗎?
Github:github.com/crazysunj/