今天開始看排序算法,漸漸 發現一個事實,腦子有點不夠用……
排序算法主要的性能指標
有三個:
1.時間性能;
2.輔助空間;
3.算法複雜度
(不是時間空間複雜度,就是純粹的代碼複雜程度)。
詳細概念就不抄了……反正總的來說,時間性能一般是大家最看重的,這篇文章裏一共會有7種排序算法。按算法複雜度分爲兩類:
簡單算法:冒泡排序、簡單選擇排序、直接插入排序;
改進算法:希爾算法、堆排序、歸併排序和快速排序。
一個一個來吧~
具體排序算法
1.冒泡排序(Bubble Sort)
public class BubbleSort {
public static void main(String[] args){
int[] a = {11,25,32,1,3,4,37,12,33,13,32,10,38,58,7,4,63,33,6,43,4,21,14,24,62,4,42,1};
Tool.print(a);
int[] b = bubbleSort(a);
Tool.print(b);
}
public static int[] bubbleSort(int[] a){
int length = a.length;
for(int i=0; i<length; i++){
for(int j=0;j<length-1-i;j++){
if(a[j] > a[j+1])
Tool.swap(a, j, j+1);
}
}
return a;
}
}
其中
Tool.print(a);
…
Tool.swap(a, j, j+1);
是我自己在同一個包下編寫的一個小工具類,這是鏈接。
代碼非常簡單,for雙循環,內循環中每兩個相鄰的數都進行比較,如果前邊的數比後邊的數大,兩個數就進行交換。外循環是控制保證每次數組中最大的數都能放到此次循環的最後面。
運行結果:
11 25 32 1 3 4 37 12 33 13 32 10 38 58 7 4 63 33 6 43 4 21 14 24 62 4 42 1
1 1 3 4 4 4 4 6 7 10 11 12 13 14 21 24 25 32 32 33 33 37 38 42 43 58 62 63
成功對數組進行了排序,其時間複雜度爲O(n^2)。
2.簡單選擇排序(Simple Selection Sort)
public class SimpleSelSort {
public static void main(String[] args){
int[] a = {11,25,32,1,3,4,37,12,33,13,32,10,38,58,7,4,63,33,6,43,4,21,14,24,62,4,42,1};
Tool.print(a);
int[] b = simpleSelSort(a);
Tool.print(b);
}
public static int[] simpleSelSort(int[] a){
int length = a.length;
int min;
for(int i=0; i<length; i++){
min = i;
for(int j=i+1; j<length; j++){
if(a[min]>a[j]){
min = j;
}
}
if(min!=i)
Tool.swap(a, i, min);
}
return a;
}
}
代碼跟冒泡排序類似,先拿出一個值a[i](一般都是數組第一個值a[0]),將它的數組下標保存到一個臨時變量min,此時a[min]指向a[0]。之後開始遍歷數組,如果有數比a[0]小,(例如說a[3])那就把找到的這個數a[3]的下標3賦給臨時變量min,此時a[min]指向a[3],每次遇到比a[min]小的數,都將它的下標賦給min。一遍遍歷結束後,我們得到了數組中最小值的下標(例如是10),之後我們將a[0]和a[10]交換,這時數組的最小值已經存到數組的第一位了。之後再拿出最小值的後面一位(a[1]),然後重複上述步驟,會得到數組中第二小的值並將其存入數組的第二位中。……最後我們會得到一個其中的數從小到大排列的數組。
11 25 32 1 3 4 37 12 33 13 32 10 38 58 7 4 63 33 6 43 4 21 14 24 62 4 42 1
1 1 3 4 4 4 4 6 7 10 11 12 13 14 21 24 25 32 32 33 33 37 38 42 43 58 62 63
排序完成,其時間複雜度和冒泡排序一樣,也是O(n^2),但實際上,效率比冒泡算法稍高。3.直接插入排序(Straight Insertion Sort)
public class StraitInsertSort {
public static void main(String[] args){
int[] a = {11,25,32,1,3,4,37,12,33,13,32,10,38,58,7,4,63,33,6,43,4,21,14,24,62,4,42,1};
Tool.print(a);
int[] b = straitInsertSort(a);
Tool.print(b);
}
public static int[] straitInsertSort(int[] unSorted){
int length = unSorted.length;
for(int i=1; i<length; i++){
if(unSorted[i-1]>unSorted[i]){
int temp;
int j = i;
temp = unSorted[i];
for(; j > 0 && unSorted[j-1] > temp;j --){
unSorted[j] = unSorted[j-1]; //複製一份給後一位
}
unSorted[j] = temp; //覆蓋後面數值相同兩位中的第一位
}
}
return unSorted;
}
}
其代碼邏輯跟上面兩個排序算法稍有不同,但也差距不大。本質上都是雙for循環,這也是它們時間複雜度都是O(n^2)的原因。直接插入算法是在執行內循環之前先進行一個大小判斷,例如如果兩個相鄰的數中前一個數unSorted[3]比後一個數unSorted[4]大(我們想得到的是從小到大的排序)那就將後一個數unSorted[4]存入一個臨時變量temp,之後開始進行循環判斷,如果unSorted[4]前邊的數有大於unSorted[4]的,那就把大的數往後移一位(此時由於unSorted[4]的值已經存入temp中,因爲unSorted[3]比unSorted[4]大,那麼將unSorted[3]的值直接賦給unSorted[4]的位置上,unSorted[4]的值我們仍然可以用。這樣之後unSorted[3]與被覆蓋後的unSorted[4](此時unSorted[4]的值是unSorted[3])都存放了unSorted[3]的值,那麼對unSorted[3]又可以進行判斷,修改的操作了)。直到遇見一個值,它比unSorted[4]小,那麼將unSorted[4]的值存到這個數的後一位(後一位和後後一位存放的是相同的值,不怕被覆蓋)。內循環完成,之後繼續外循環,完成排序。11 25 32 1 3 4 37 12 33 13 32 10 38 58 7 4 63 33 6 43 4 21 14 24 62 4 42 1
1 1 3 4 4 4 4 6 7 10 11 12 13 14 21 24 25 32 32 33 33 37 38 42 43 58 62 63
其時間複雜度爲O(n^2),實際中它的性能稍強於冒泡排序和簡單選擇排序。
4.希爾排序(Shell Sort)
public class Test1 {
public static void main(String[] args){
int[] a = getRandomArray(9546400); //生成一個長度爲9546400的數組
Tool.print("--------------------------------------");
int[] b = shellSort(a);
Tool.print(b);
}
public static int[] getRandomArray(int log){
int[] result = new int[log];
for (int i = 0; i < log; i++) {
result[i] = i;
}
for (int i = 0; i < log; i++) {
int random = (int) (log * Math.random());
int temp = result[i];
result[i] = result[random];
result[random] = temp;
}
return result;
}
public static int[] shellSort(int[] a) {
int d = a.length / 2;
while (true) {
// 把距離爲d的元素編爲一個組,掃描所有組
for (int i = d; i < a.length; i++) {
int j = 0;
int temp = a[i];
// 對距離爲d的元素組進行排序
for (j = i - d; j >= 0 && temp < a[j]; j = j - d) {
a[j + d] = a[j];
}
a[j + d] = temp;
}
if(d==1) return a;
d = d / 2; // 減小增量
}
}
}
代碼的邏輯是在一個while循環裏套兩個for循環,雖然看着套了2個循環,但是由於每次循環都會將工作完成一部分,所以代碼到最後反而效率很高。當d沒有到1的時候會一直執行for循環裏面的for循環,for循環中每個被分好的小組的相同位置的數會進行比較,如果前面的數比後面的大,那麼進行一次交換,之後反覆,直到 i超出數組的範圍,跳出循環,d變成自己之前的一半,繼續進行循環。最後得出正確數組。
5.堆排序(Heap Sort)
public class HeapSort {
public static void main(String[] args){
int[] a = Tool.getRandomArray(9546400); //生成一個長度爲9546400的數組
Tool.print("--------------------------------------");
int[] b = heapSort(a);
Tool.printL(b);
}
public static void heapAdjust(int[] array, int parent, int length) {
int temp = array[parent]; // temp保存當前父節點
int child = 2 * parent + 1; // 先獲得左孩子
while (child < length) {
if (child + 1 < length && array[child] < array[child + 1]) // 如果有右孩子結點,並且右孩子結點的值大於左孩子結點,則選取右孩子結點
child++;
if (temp >= array[child]) // 如果父結點的值已經大於孩子結點的值,則直接結束
break;
array[parent] = array[child]; // 把孩子結點的值賦給父結點
parent = child; // 選取孩子結點的左孩子結點,繼續向下篩選
child = 2 * child + 1;
}
array[parent] = temp;
}
public static int[] heapSort(int[] list) {
// 循環建立初始堆
int length = list.length;
for (int i = length / 2; i >= 0; i--) {
heapAdjust(list, i, length - 1);
}
// 進行n-1次循環,完成排序
for (int i = length - 1; i > 0; i--) {
// 最後一個元素和第一元素進行交換
Tool.swap(list, 0, i);
// 篩選 R[0] 結點,得到i-1個結點的堆
heapAdjust(list, 0, i);
}
return list;
}
}
看代碼可以看到,堆排序分爲兩個函數,其中heapSort()是最後的排序函數,而heapAjust()就是我們之前第一步裏所說的先對堆進行排序,而僅靠它並不能完成使得堆中最大值處於根節點,所以我們可以再heapSort()方法中看到有一個for()循環,它從堆長度的一半開始,逐步往堆頂進行heapAjust()操作,自己稍微畫草圖驗證一下就能知道,這樣做能保證總能從堆的最底層最後一個最後一個節點所在的二叉樹開始進行調整獲取最大值(有點繞),然後逐步往上進行調整,這樣能保證整個二叉樹中的數都能被遍歷,由下往上
而不會被遺漏。最終調整完後,整個數組的最大值就會在在二叉堆的根節點上(此時其它的節點都是無序而需要重新進行排序的,這點很重要)。在heapSort()的第二個for循環中交換根節點和最後一個節點(數組的最後一個值),然後繼續進行調整,數組中的最大值已經被放到數組最後一位,不參加排序,其餘繼續上述過程,最終得到一個從小到大排好序的有序數組。6.歸併排序(Merging Sort)
public class MergeSort {
public static void main(String[] args){
// int[] a = Tool.getRandomArray(9546400); //生成一個長度爲9546400的數組
int[] a = {11,25,32,1,3,4,37,12,33,13,32,10,38,58,7,4,63,33,6,43,4,21,14,24,62,4,42,1};
// Tool.print(a);
Tool.print("--------------------------------------");
// int[] b = bubbleSort(a);
int[] b = mergeSort(a);
Tool.printL(b);
}
public static void merge(int[] a, int low, int mid, int high) {
int i = low; // i是第一段序列的下標
int j = mid + 1; // j是第二段序列的下標
int k = 0; // k是臨時存放合併序列的下標
int[] b = new int[high - low + 1]; // array2是臨時合併序列
// 掃描第一段和第二段序列,直到有一個掃描結束
while (i <= mid && j <= high) {
// 判斷第一段和第二段取出的數哪個更小,將其存入合併序列,並繼續向下掃描
if (a[i] <= a[j]) {
b[k++] = a[i++];
} else {
b[k++] = a[j++];
}
}
// 若第一段序列還沒掃描完,將其全部複製到合併序列
while (i <= mid) {
b[k++] = a[i++];
}
// 若第二段序列還沒掃描完,將其全部複製到合併序列
while (j <= high) {
b[k++] = a[j++];
}
// 將合併序列複製到原始序列中
for (k = 0, i = low; i <= high; i++, k++) {
a[i] = b[k];
}
}
public static void mergePass(int[] a, int gap, int length) {
int i = 0;
// 歸併gap長度的兩個相鄰子表.如果i之後還有兩個gap的長度,就繼續循環否則跳出循環。
// 每次進行兩個gap之間的歸併,剛開始gap==1, 每兩個數進行歸併,確定大小後排好序
for (i = 0; i + 2 *gap-1 < length; i = i + 2 * gap) {
merge(a, i, i + gap - 1, i + 2 * gap - 1);
}
if (i + gap - 1 < length) {// 餘下兩個子表,後者長度小於gap
merge(a, i, i + gap - 1, length - 1);
}
}
public static int[] mergeSort(int[] a) {
for (int gap = 1; gap < a.length; gap = 2 * gap) {
mergePass(a, gap, a.length);
}
return a;
}
}
觀察代碼,可以發現代碼分爲三個方法merge(),mergePass(),mergeSort(),這三個方法逐漸上升層次,分別對應了不同的三個功能:7.快速排序(Quick Sort)
public class QuickSort {
public static void main(String[] args){
int[] a = Tool.getRandomArray(9546400); //生成一個長度爲9546400的數組
// int[] a = {11,25,32,1,3,4,37,12,33,13,32,10,
// 38,58,7,4,63,33,6,43,4,21,14,24,62,4,42,1};
// Tool.printL(a);
Tool.print("-------------------------------------" +
"-------------------------------------");
int[] b = quickSort(a, 0, a.length-1);
Tool.printL(b);
}
public static int[] quickSort(int[] a, int left, int right){
if(left<right){
int pivot = quickAdjust(a, left, right);
quickSort(a, left, pivot);
quickSort(a, pivot+1, right);
}
return a;
}
public static int quickAdjust(int[] a, int left, int right){
int pivot = a[left];
while(left < right){
while(pivot<=a[right] && left < right)
right--;
a[left] = a[right];
while(a[left]<=pivot && left < right)
left++;
a[right] = a[left];
}
a[left] = pivot;
return left;
}
}
代碼總體是比較明瞭的,簡單說一下吧,這個快速排序中用到了遞歸。先找一個分界點pivot(一般直接選取數組第一個值),分界點的作用是:先將數組通過分界點分爲兩個較小的數組,分界點前所有的數比分界點的數都小,分界點後的數比分界點都大( 其中quickAdjust()方法實現了這一功能),再將每個較小的數組繼續以相同的方法進行分離,排序……最終能得到一個排好序的數組。這樣算初步完成快速排序了,但是有一點,分界點pivot選的太過隨意,萬一它離最值點比較近,那麼快速排序的效率就會變小,所以有改進之處。一般做法是三數取中(median-of-three)法。即去三個掛念自先進行排序,再將中間數作爲分界點,一般是去左端、右端和中間三個數。代碼實現如下: public static int quickAdjust(int[] a, int left, int right){
// int pivot = a[left];
// 對pivot的取法進行優化
int pivot;
int mid = (left + right) /2;
if(a[left]>a[right]) //保持右端比左端大
Tool.swap(a, left, right);
if(a[mid]>a[right]) //保持右端比中間大
Tool.swap(a, mid, right);
//這時右端最大。只需要比較左端和中間,取較大值即可
if(a[mid]>a[left])
//保持左端處於中間值
Tool.swap(a, left, right);
pivot = a[left];
while(left < right){
while(pivot<=a[right] && left < right)
right--;
a[left] = a[right];
while(a[left]<=pivot && left < right)
left++;
a[right] = a[left];
}
a[left] = pivot;
return left;
}
總結
參考資料:
大話數據結構
http://www.cnblogs.com/kkun/archive/2011/11/23/2260265.html
http://www.cnblogs.com/jingmoxukong/tag/%E6%8E%92%E5%BA%8F/
算法可視化網站:
這幾個網站能較爲直觀地顯示各個算法的排序過程,能分步觀察,我覺得挺適合初學者的。
https://visualgo-translation.club/zh(可能得用梯子)
http://sorting.at/