你連基本算法都不知道我這麼敢要你?【八大排序算法】(原理、Java實現、動態圖)

八大排序算法Java實現

什麼是排序?

  • 排序是計算機內經常進行的一種操作,其目的是將一組“無序”的記錄序列調整爲“有序”的記錄序列。

  • 排序分爲內部排序和外部排序。

  • 若整個排序過程不需要訪問外存便能完成,則稱此類排序問題爲內部排序。

  • 反之,若參加排序的記錄數量很大,整個序列的排序過程不可能在內存中完成,則稱此類排序問題爲外部排序。

排序的分類

在這裏插入圖片描述

算法分析

img


1、冒泡排序

基本思想

  • 冒泡排序(Bubble Sort)是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。

算法描述

  1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
  3. 針對所有的元素重複以上的步驟,除了最後一個。
  4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

動態效果如下

img

Java代碼實現

/*
 * 冒泡排序
 * 相鄰元素比較,大的元素往後調
 */
public static void bubbleSort(int array[]){
	
	for(int i = array.length - 1 ; i >= 0 ; i--){
		
		boolean flag = false;     //設置一趟排序是否有交換的標識
		
		for(int j = 0 ; j < i ; j++){   //一趟冒泡排序
			
			if(array[j] > array[j+1]){
				swap(array, j, j+1);
				flag = true;    //標識發生了交換
			}
		}
		
		if(!flag)
			break;
	}
}

比較與總結

  • 由於冒泡排序只在相鄰元素大小不符合要求時才調換他們的位置, 它並不改變相同元素之間的相對順序, 因此它是穩定的排序算法。


2、選擇排序

基本思想

  • 選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

  • 選擇排序的主要優點與數據移動有關。如果某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對 n個元素的表進行排序總共進行至多 n-1 次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種。

算法描述

  1. 從未排序序列中,找到關鍵字最小的元素
  2. 如果最小元素不是未排序序列的第一個元素,將其和未排序序列第一個元素互換
  3. 重複1、2步,直到排序結束。

動圖效果如下

img

Java代碼實現

/*
 * 選擇排序
 * 每個位置選擇當前元素最小的
 */
public static void selectSort(int array[]){
	
	
	for(int i = 0 ; i < array.length-1 ; i++){
		
		int minPosition = i;
		int min = array[i];
		
		for(int j = i+1 ; j <array.length ; j++){
			
			if(array[j] < min){
				min = array[j];
				minPosition = j;
			}
			
		}
		//若i不是當前元素最小的,則和找到的那個元素交換
		if(i !=  minPosition){
			array[minPosition] = array[i];
			array[i] = min;
		}
	}
}

比較與總結

  • 選擇排序的簡單和直觀名副其實,這也造就了它”出了名的慢性子”,無論是哪種情況,哪怕原數組已排序完成,它也將花費將近n²/2次遍歷來確認一遍。即便是這樣,它的排序結果也還是不穩定的。 唯一值得高興的是,它並不耗費額外的內存空間。


3、插入排序

基本思想

  • 通常人們整理橋牌的方法是一張一張的來,將每一張牌插入到其他已經有序的牌中的適當位置。在計算機的實現中,爲了要給插入的元素騰出空間,我們需要將其餘所有元素在插入之前都向右移動一位。

算法描述

  • 一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:
  1. 從第一個元素開始,該元素可以認爲已經被排序
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描
  3. 如果該元素(已排序)大於新元素,將該元素移到下一位置
  4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
  5. 將新元素插入到該位置後
  6. 重複步驟2~5

動態效果如下

img

Java代碼實現

/*
 * 插入排序
 * 已經有序的小序列的基礎上,一次插入一個元素
 */
public static void insertSort(int array[]){
	
	for(int i = 1 ; i < array.length ; i++){
		
		int current = array[i];   //待排元素
		
		int j = i;
		for(; j > 0 && array[j - 1] > current ; j--){
			//向前掃描,只要發現待排元素比較小,就插入
			
			array[j] = array[j - 1];    //移出空位
			
		}
		
		array[j] = current;    //元素插入
	}
}

比較與總結

  • 插入排序所需的時間取決於輸入元素的初始順序。例如,對一個很大且其中的元素已經有序(或接近有序)的數組進行排序將會比隨機順序的數組或是逆序數組進行排序要快得多。


4、快速排序

基本思想

  • 快速排序的基本思想:挖坑填數+分治法

  • 快速排序使用分治法(Divide and conquer)策略來把一個串行(list)分爲兩個子串行(sub-lists)。

  • 快速排序又是一種分而治之思想在排序算法上的典型應用。本質上來看,快速排序應該算是在冒泡排序基礎上的遞歸分治法。

  • 快速排序的名字起的是簡單粗暴,因爲一聽到這個名字你就知道它存在的意義,就是快,而且效率高!它是處理大數據最快的排序算法之一了。雖然 Worst Case 的時間複雜度達到了 O(n²),但是人家就是優秀,在大多數情況下都比平均時間複雜度爲 O(n logn) 的排序算法表現要更好。

算法描述

快速排序使用分治策略來把一個序列(list)分爲兩個子序列(sub-lists)。步驟爲:

  1. 從數列中挑出一個元素,稱爲"基準"(pivot)。
  2. 重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面(相同的數可以到任一邊)。在這個分區結束之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。
  3. 遞歸地(recursively)把小於基準值元素的子數列和大於基準值元素的子數列排序。

遞歸到最底部時,數列的大小是零或一,也就是已經排序好了。這個算法一定會結束,因爲在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。

動態效果如下

img

Java代碼實現

/*
 * 快速排序
 * 兩個方向,左邊的i下標一直往右走,當a[i] <= a[center_index],
 * 其中center_index是中樞元素的數組下標,一般取爲數組第0個元素。而右邊的j下標一直往左走,當a[j] > a[center_index]
 * 如果i和j都走不動了,i <= j, 交換a[i]和a[j],重複上面的過程,直到i>j
 * 交換a[j]和a[center_index],完成一趟快速排序
 * 樞軸採用三數取中法可以優化
 */
//遞歸快速排序
public static void quickSort(int a[]){
	qSort(a, 0, a.length - 1);
}
//非遞歸快速排序,手動利用棧來存儲每次分塊快排的起始點,棧非空時循環獲取中軸入棧  
public static void quickSortNonRecursion(int array[]){
	 if (array == null || array.length == 1) return;
     //存放開始與結束索引
     Stack<Integer> s = new Stack<Integer>(); 
     //壓棧       
     s.push(0); 
     s.push(array.length - 1); 
     //利用循環裏實現
     while (!s.empty()) { 
         int right = s.pop(); 
         int left = s.pop(); 
         //如果最大索引小於等於左邊索引,說明結束了
         if (right <= left) continue; 
                  
         int i = partition(array, left, right); 
         if (left < i - 1) {
             s.push(left);
             s.push(i - 1);
         } 
         if (i + 1 < right) {
             s.push(i+1);
             s.push(right);
         }
     } 
}
//遞歸排序,利用兩路劃分
public static void qSort(int a[],int low,int high){
	int pivot = 0;
	if(low < high){
		//將數組一分爲二
		pivot = partition(a,low,high);
		//對第一部分進行遞歸排序
		qSort(a,low,pivot);
		//對第二部分進行遞歸排序
		qSort(a,pivot + 1,high);
	}
}
//partition函數
public static int partition(int a[],int low,int high){
	
	int pivotkey = a[low];   //選取第一個元素爲樞軸記錄
	while(low < high){
		//將比樞軸記錄小的交換到低端
		while(low < high && a[high] >= pivotkey){
			high--;
		}
		//採用替換而不是交換的方式操作
        a[low] = a[high];
		//將比樞軸記錄大的交換到高端
		while(low < high && a[low] <= pivotkey){
			low++;
		}
		a[high] = a[low];
	}
	//樞紐所在位置賦值
	a[low] = pivotkey;
	//返回樞紐所在的位置
	return low;
}

比較與總結



5、歸併排序

基本思想

  • 歸併排序算法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分爲若干個子序列,每個子序列是有序的。然後再把有序子序列合併爲整體有序序列。

這個圖很有概括性,來自維基

算法描述

歸併排序可通過兩種方式實現:

  • 自上而下的遞歸
  • 自下而上的迭代

遞歸法(假設序列共有n個元素):

  1. 將序列每相鄰兩個數字進行歸併操作,形成 floor(n/2)個序列,排序後每個序列包含兩個元素;
  2. 將上述序列再次歸併,形成 floor(n/4)個序列,每個序列包含四個元素;
  3. 重複步驟2,直到所有元素排序完畢。

迭代法

  1. 申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列
  2. 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
  3. 比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
  4. 重複步驟3直到某一指針到達序列尾
  5. 將另一序列剩下的所有元素直接複製到合併序列尾

動態效果如下

img

Java代碼實現

/*
 * 歸併排序
 * 把序列遞歸地分成短序列
 * 遞歸出口是短序列只有1個元素(認爲直接有序)或者2個序列(1次比較和交換),
 * 然後把各個有序的短序列合併成一個有序的長序列,不斷合併直到原序列全部排好序
 */
//將有二個有序數列a[first...mid]和a[mid+1...last]合併。  
public static void merge(int a[], int first, int mid, int last, int temp[]){
	
	int i = first,j = mid+1;
	int k = 0;
	
	while(i <= mid && j<= last){
		if(a[i]<a[j])
			temp[k++] = a[i++];
		else
			temp[k++] = a[j++];
	}
	
	while(i <= mid)
		temp[k++] = a[i++];
	while(j <= last)
		temp[k++] = a[j++];
	
	for(i = 0 ; i < k ; i++)
		a[first+i] = temp[i];
}

//遞歸合併排序
public static void mSort(int a[], int first,int last, int temp[]){
	if(first < last){
		int mid = (first + last) / 2;
		mSort(a, first, mid, temp);
		mSort(a, mid+1, last, temp);
		merge(a, first, mid, last, temp);
		
	}
}
//提供通用歸併排序接口
public static void mergeSort(int a[]){
	int[] temp = new int[a.length];
	mSort(a, 0, a.length-1, temp);
}

比較與總結

  • 歸併排序是建立在歸併操作上的一種有效的排序算法,1945年由約翰·馮·諾伊曼首次提出。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用,且各層分治遞歸可以同時進行。


6、希爾排序

基本思想

  • 將待排序數組按照步長gap進行分組,然後將每組的元素利用直接插入排序的方法進行排序;每次再將gap折半減小,循環上述操作;當gap=1時,利用直接插入,完成排序。

  • 可以看到步長的選擇是希爾排序的重要部分。只要最終步長爲1任何步長序列都可以工作。一般來說最簡單的步長取值是初次取數組長度的一半爲增量,之後每次再減半,直到增量爲1。

算法描述

  1. 選擇一個增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  2. 按增量序列個數 k,對序列進行 k 趟排序;
  3. 每趟排序,根據對應的增量 ti,將待排序列分割成若干長度爲 m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲 1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。

效果圖如下

img

Java代碼實現

/*
 * 希爾排序
 * 按照不同步長對元素進行插入排序
 * 插入排序的一種
 */
public static void shellSort(int a[]){
	if(a == null || a.length == 0){
		return;
	}
	int len = a.length;
	//初始化增量
	int inc = len;
	do{
		//增量變化規則
		inc = inc / 3 + 1;
		for(int i = inc; i < len; i++){
			//待排元素
			int cur = a[i];
			int j = i;
			//向前掃描,只要發現待排元素比較小,就插入
			for(; j >= inc && a[j - inc] > cur; j -= inc){
				//移除空位
				a[j] = a[j - inc];
			}
			//元素插入
			a[j] = cur;
			
		}
	}while(inc > 1);
}

比較與總結

  • 希爾排序更高效的原因是它權衡了子數組的規模和有序性。排序之初,各個子數組都很短,排序之後子數組都是部分有序的,這兩種情況都很適合插入排序。


7、堆排序

1991年的計算機先驅獎獲得者、斯坦福大學計算機科學系教授羅伯特·弗洛伊德(Robert W.Floyd) 和威廉姆斯(J.Williams) 在1964年共同發明了著名的堆排序算法(Heap Sort)

基本思想

  • 此處以大頂堆爲例,堆排序的過程就是將待排序的序列構造成一個堆,選出堆中最大的移走,再把剩餘的元素調整成堆,找出最大的再移走,重複直至有序。

算法描述

  1. 先將初始序列K[1…n]K[1…n]建成一個大頂堆, 那麼此時第一個元素K1K1最大, 此堆爲初始的無序區.
  2. 再將關鍵字最大的記錄K1K1 (即堆頂, 第一個元素)和無序區的最後一個記錄 KnKn 交換, 由此得到新的無序區K[1…n−1]K[1…n−1]和有序區K[n]K[n], 且滿足K[1…n−1].keys⩽K[n].keyK[1…n−1].keys⩽K[n].key
  3. 交換K1K1 和 KnKn 後, 堆頂可能違反堆性質, 因此需將K[1…n−1]K[1…n−1]調整爲堆. 然後重複步驟2, 直到無序區只有一個元素時停止。

動圖效果如下

堆排序過程

Java代碼實現

/*
 * 堆排序
 * 調整最大堆,交換根元素和最後一個元素。
 * 參數說明:
 *     a -- 待排序的數組
 */
public static void heapSort(int[] a) {
	if(a == null || a.length == 0){
		return;
	}
	int len = a.length;
	//從尾部開始,調整成最大堆
	for(int i = len / 2 - 1; i >= 0; i--){
		maxHeapDown(a, i, len - 1);
	}
	
	//從最後一個元素開始對序列進行調整,不斷縮小調整的範圍直到第一個元素
	for(int i = len - 1; i >= 0; i--){
		//交換a[0]和a[i]。交換後,a[i]是a[0..i]中最大
		int tmp = a[0];
		a[0] = a[i];
		a[i] = tmp;
		//調整a[0..i - 1],使得a[0..i - 1]仍然是一個最大堆
		maxHeapDown(a, 0, i - 1);
	}
}

/*
 * 注:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
 *     其中,N爲數組下標索引值,如數組中第1個數對應的N爲0。
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     lo -- 被下調節點的起始位置(一般爲0,表示從第1個開始)
 *     hi   -- 截至範圍(一般爲數組中最後一個元素的索引)
 */
private static void maxHeapDown(int[] a, int lo, int hi){
	//記錄當前結點位置
	int curIndex = lo;
	//記錄左孩子結點
	int left = 2 * curIndex + 1;
	//記錄當前結點的值
	int curVal = a[curIndex];
	
	//保證curIndex,leftIndex,rightIndex中,curIndex對應的值最大
	for(; left <= hi; curIndex = left, left = 2 * left + 1){
		//左右孩子中選擇較大者
		if(left < hi && a[left] < a[left + 1]){
			left++;
		}
		if(curVal >= a[left]){
			break;
		}else{
			a[curIndex] = a[left];
			a[left] = curVal;
		}
	}
}

比較與總結

  • 由於堆排序中初始化堆的過程比較次數較多, 因此它不太適用於小序列。 同時由於多次任意下標相互交換位置, 相同元素之間原本相對的順序被破壞了, 因此, 它是不穩定的排序。


8、基數排序

基本思想

  • 它是這樣實現的:將所有待比較數值(正整數)統一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。

基數排序按照優先從高位或低位來排序有兩種實現方案:

  • MSD(Most significant digital) 從最左側高位開始進行排序。先按k1排序分組, 同一組中記錄, 關鍵碼k1相等, 再對各組按k2排序分成子組, 之後, 對後面的關鍵碼繼續這樣的排序分組, 直到按最次位關鍵碼kd對各子組排序後. 再將各組連接起來, 便得到一個有序序列。MSD方式適用於位數多的序列
  • LSD (Least significant digital)從最右側低位開始進行排序。先從kd開始排序,再對kd-1進行排序,依次重複,直到對k1排序後便得到一個有序序列。LSD方式適用於位數少的序列

算法描述

我們以LSD爲例,從最低位開始,具體算法描述如下:

  1. 取得數組中的最大數,並取得位數;
  2. arr爲原始數組,從最低位開始取每個位組成radix數組;
  3. 對radix進行計數排序(利用計數排序適用於小範圍數的特點);

動態效果圖如下

基數排序LSD動圖演示

Java代碼實現

基數排序:通過序列中各個元素的值,對排序的N個元素進行若干趟的“分配”與“收集”來實現排序。

  • 分配:我們將L[i]中的元素取出,首先確定其個位上的數字,根據該數字分配到與之序號相同的桶中
  • 收集:當序列中所有的元素都分配到對應的桶中,再按照順序依次將桶中的元素收集形成新的一個待排序列L[]。對新形成的序列L[]重複執行分配和收集元素中的十位、百位…直到分配完該序列中的最高位,則排序結束
/*
 * 基數排序
 * 按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位
 */
public  static void radixSort(int[] array,int d)
{
    int n=1;   //代表位數對應的數:1,10,100...
    int k=0;   //保存每一位排序後的結果用於下一位的排序輸入
    int length=array.length;
    int[][] bucket=new int[10][length];      //排序桶用於保存每次排序後的結果,這一位上排序結果相同的數字放在同一個桶裏
    int[] order=new int[length];    //用於保存每個桶裏有多少個數字
    while(n<d)
    {
        for(int num:array)    //將數組array裏的每個數字放在相應的桶裏
        {
            int digit=(num/n)%10;
            bucket[digit][order[digit]]=num;
            order[digit]++;
        }
        for(int i=0;i<length;i++)      //將前一個循環生成的桶裏的數據覆蓋到原數組中用於保存這一位的排序結果
        {
            if(order[i]!=0)        //這個桶裏有數據,從上到下遍歷這個桶並將數據保存到原數組中
            {
                for(int j=0;j<order[i];j++)
                {
                    array[k]=bucket[i][j];
                    k++;
                }
            }
            order[i]=0;      //將桶裏計數器置0,用於下一次位排序
        }
        n*=10;
        k=0;               //將k置0,用於下一輪保存位排序結果
    }
    
}

比較與總結

  • 基數排序更適合用於對時間, 字符串等這些 整體權值未知的數據 進行排序。

  • 基數排序不改變相同元素之間的相對順序,因此它是穩定的排序算法。

  • 基數排序 vs 計數排序 vs 桶排序

  • 這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差異:

    • 基數排序:根據鍵值的每位數字來分配桶

    • 計數排序:每個桶只存儲單一鍵值

    • 桶排序:每個桶存儲一定範圍的數值



八大排序算法總結

各種排序性能對比如下:

排序類型 平均情況 最好情況 最壞情況 輔助空間 穩定性
冒泡排序 O(n²) O(n) O(n²) O(1) 穩定
選擇排序 O(n²) O(n²) O(n²) O(1) 不穩定
直接插入排序 O(n²) O(n) O(n²) O(1) 穩定
折半插入排序 O(n²) O(n) O(n²) O(1) 穩定
希爾排序 O(n^1.3) O(nlogn) O(n²) O(1) 不穩定
歸併排序 O(nlog₂n) O(nlog₂n) O(nlog₂n) O(n) 穩定
快速排序 O(nlog₂n) O(nlog₂n) O(n²) O(nlog₂n) 不穩定
堆排序 O(nlog₂n) O(nlog₂n) O(nlog₂n) O(1) 不穩定
計數排序 O(n+k) O(n+k) O(n+k) O(k) 穩定
桶排序 O(n+k) O(n+k) O(n²) O(n+k) (不)穩定
基數排序 O(d(n+k)) O(d(n+k)) O(d(n+kd)) O(n+kd) 穩定

從時間複雜度來說:

  1. 平方階O(n²)排序:各類簡單排序:直接插入、直接選擇和冒泡排序
  2. 線性對數階O(nlog₂n)排序:快速排序、堆排序和歸併排序
  3. O(n1+§))排序,§是介於0和1之間的常數:希爾排序
  4. 線性階O(n)排序:基數排序,此外還有桶、箱排序

論是否有序的影響:

  • 當原表有序或基本有序時,直接插入排序和冒泡排序將大大減少比較次數和移動記錄的次數,時間複雜度可降至O(n);
  • 而快速排序則相反,當原表基本有序時,將蛻化爲冒泡排序,時間複雜度提高爲O(n2);
  • 原表是否有序,對簡單選擇排序、堆排序、歸併排序和基數排序的時間複雜度影響不大。
    img
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章