排序演算法全總結

SnailClimb發表於2018-04-04

目錄:

前言

一 氣泡排序

二 選擇排序

三 插入排序

四 希爾排序

五 歸併排序

六 快速排序

七堆排序

八 幾種排序演算法的區別與排序演算法的應用

另外這幾天沒事在刷演算法題,感興趣的可以看看:

(1)劍指Offer之斐波那契數列問題和跳臺階問題

(2)劍指Offer之二維陣列查詢和替換空格問題

(3)劍指Offer之數值的整數次方和調整陣列元素順序

(4)劍指Offer之連結串列相關程式設計題

(5)劍指Offer之棧變佇列和棧的壓入、彈出序列

前言

這是在掘金寫的第一篇文章,花了幾天重新修改了一下。

  排序就是將一組物件按照某種邏輯順序重新排列的過程。比如信用卡賬單中的交易是按照日期排序的——這種排序很可能使用了某種排序演算法。在計算時代早期,大家普遍認為30%的計算週期都用在了排序上,今天這個比例可能降低了,大概是因為現在的排序演算法更加高效。現在這個時代資料可以說是無處不在,而整理資料的第一步往往就是進行排序。所有的計算機系統都實現了各種排序演算法以供系統和使用者使用。   

即使你只是使用標準庫中的排序函式,學習排序演算法仍然有很大的實際意義:

  • 排序演算法往往是我們解決其他問題的第一步
  • 排序演算法有助於我們理解其他演算法
  • 演算法在公司面試中佔有很大比例,排序演算法作為其中的重要組成部分,我們理所當然要學好了。

  另外,更重的是下面介紹的這些演算法都很經典,優雅而且高效,學習其中的精髓對自己提高自己的程式設計能力也有很大的幫助。   排序在商業資料處理和現代科學計算中有很重要的地位,它能夠應用於事務處理,組合優化,天體物理學,分子動力學,語言學,基因組學,天氣預報和很多其他領域。下面會介紹的一種排序演算法(快速排序)甚至被譽為20世紀科學和工程領域的十大演算法之一。後面我們會依次學習幾種經典的排序演算法,並高效地實現“優先佇列”這種基礎資料型別。我們將討論比較排序演算法的理論基礎並中借若干排序演算法和優先佇列的應用。

初級排序演算法

一 氣泡排序

圖片來源:維基百科

氣泡排序原理圖

1. 介紹: 

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

2. 原理:

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

氣泡排序原理圖
備註:圖片來源百度,侵刪

3. 實現程式碼

	public static void bubbleSort(int[] numbers) {
		int temp = 0;
		int size = numbers.length;
		for (int i = 0; i < size - 1; i++) {
			for (int j = 0; j < size - 1 - i; j++) {
				 // 交換兩數位置
				if (numbers[j] > numbers[j + 1])
				{
					temp = numbers[j];
					numbers[j] = numbers[j + 1];
					numbers[j + 1] = temp;
				}
			}
		}
	}
複製程式碼

4. 簡單優化

氣泡排序是最容易理解的一個排序演算法。我們實現完後,發現其效率並不是很高。當序列已經有序的時候。會進行一些多餘的比較。根據其兩兩比較的特性,可以推測出,如果一趟比較中連一次元素的交換操作都沒發生,那麼整個序列肯定是已經有序的了。據此給出優化版氣泡排序演算法。

	public void bubbleSort(int[] numbers) {
		// 初始化為無序狀態
		boolean sorted = false;
		int temp = 0;
		int size = numbers.length;
		for (int i = 0; i < size - 1&& !sorted; i++) {
			// 假設已經有序,若沒有發生交換,則sorted維持為true,下次迴圈將直接退出。
			sorted = true;
			for (int j = 0; j < size - 1 - i; j++) {
				if (numbers[j] > numbers[j + 1]) // 交換兩數位置
				{
					temp = numbers[j];
					numbers[j] = numbers[j + 1];
					numbers[j + 1] = temp;
					// 陣列無序
					sorted = false;
				}
			}
		}
	}
複製程式碼

簡單的對比圖:

排序用到的資料:

		int[] numbers = { 1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21,1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21,1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21,1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21};

複製程式碼

優化之前:

優化之前
優化之後:
優化之後

備註:程式執行時間和你的機器也有很大關係,大多數情況下優化後的氣泡排序速度都更快一些。

二 選擇排序

圖片來源:維基百科

選擇排序

1. 介紹: 

  選擇排序是另一個很容易理解和實現的簡單排序演算法。學習它之前首先要知道它的兩個很鮮明的特點。1,執行時間和輸入無關。為了找出最小的元素而掃描一遍陣列並不能為下一遍掃描提供任何實質性幫助的資訊。因此使用這種排序的我們會驚訝的發現,一個已經有序的陣列或者陣列內元素全部相等的陣列和一個元素隨機排列的陣列所用的排序時間竟然一樣長!而其他演算法會更善於利用輸入的初始狀態,選擇排序則不然。 2,資料移動是最少的。選擇排序的交換次數和陣列大小關係是線性關係。看下面的原理時可以很容易明白這一點。

2. 原理:

  在要排序的一組數中,選出最小的一個數與第一個位置的數交換;然後在 剩下的數 當中再找最小的與第二個位置的數交換,如此迴圈到倒數第二個數和最後一個數比較為止。

選擇排序原理圖

3. 實現程式碼

	public static void selectSort(int[] numbers) {
		// 陣列長度
		int size = numbers.length; 
		// 中間變數
		int temp = 0; 

		for (int i = 0; i < size; i++) {
			// 待確定的位置
			int k = i; 
			// 選擇出應該在第i個位置的數也就是選出剩下元素最小的那個
			for (int j = size - 1; j > i; j--) {
				if (numbers[j] < numbers[k]) {
					k = j;
				}
			}
			// 交換兩個數
			temp = numbers[i];
			numbers[i] = numbers[k];
			numbers[k] = temp;
		}
	}
複製程式碼

三 插入排序

圖片來源:維基百科

選擇排序原理圖

1. 介紹:

  通常人們整理橋牌的方法是一張一張的來,將每一張牌插入到其他已經有序牌中的適當位置。在計算機的實現中,為了給要插入的元素騰出空間,我們需要將其餘所有元素在插入之前都向右移動以為。這種演算法就叫,插入排序與選擇排序一樣,當前索引左邊的所有元素都是有序的,但他們的最終位置還不確定為了給更小的元素騰出空間,他們可能會被移動。但是當索引到達陣列的右端時,陣列排序就完成了。和選擇排序不同的是,插入排序所需的時間取決於輸入中元素的初始順序。也就是說對一個接近有序或有序的陣列進行排序會比隨機順序或是逆序的陣列進行排序要快的多。

2. 原理: 

  每步將一個待排序的記錄,按其順序碼大小插入到前面已經排序的字序列的合適位置(從後向前找到合適位置後),直到全部插入排序完為止。   

插入排序原理

3. 實現程式碼

	/**
	 * 插入排序
	 * 
	 * 從第一個元素開始,該元素可以認為已經被排序 取出下一個元素,在已經排序的元素序列中從後向前掃描
	 * 如果該元素(已排序)大於新元素,將該元素移到下一位置 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置 將新元素插入到該位置中 重複步驟2
	 * 
	 * @param numbers
	 *            待排序陣列
	 */
	public static void insertSort(int[] numbers) {
		int size = numbers.length;
		int temp = 0;
		int j = 0;

		for (int i = 0; i < size; i++) {
			temp = numbers[i];
			// 假如temp比前面的值小,則將前面的值後移
			for (j = i; j > 0 && temp < numbers[j - 1]; j--) {
				numbers[j] = numbers[j - 1];
			}
			numbers[j] = temp;
		}
	}
複製程式碼

四,希爾排序

圖片來源:維基百科

希爾排序

1. 介紹:

  這個排序咋一看名字感覺很高大上,這是以D.L.shell名字命名的排序演算法。   為了展示初級排序演算法性質的價值,我們來看一下基於插入排序的快速的排序演算法——希爾排序。對於大規模亂序的陣列插入排序很慢,因為它只會交換相鄰的元素,因此元素只能一點一點地從陣列的一端移動到另一端。如果最小的元素剛好在陣列的盡頭的話,那麼要將它移動到正確的位置要N-1次移動。希爾排序為了加快速度簡單地改進了插入排序,交換不相鄰的元素以對陣列的區域性進行排序,並最終用插入排序將區域性有序的陣列排序。

2. 原理:

先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。

希爾排序原理
由上訴排序過程可見,希爾排序的一個特點是:自序列的構成不是簡單地"逐段分割",而是相隔某個“增量”的記錄組成一個子序列。

3. 實現程式碼

 /**
	 * 希爾排序的原理:根據需求,如果你想要結果從大到小排列,它會首先將陣列進行分組,然後將較大值移到前面,較小值
	 * 移到後面,最後將整個陣列進行插入排序,這樣比起一開始就用插入排序減少了資料交換和移動的次數,可以說希爾排序是加強版的插入排序 拿陣列5, 2,
	 * 8, 9, 1, 3,4來說,陣列長度為7,當increment為3時,陣列分為兩個序列
	 * 5,2,8和9,1,3,4,第一次排序,9和5比較,1和2比較,3和8比較,4和比其下標值小increment的陣列值相比較
	 * 此例子是按照從大到小排列,所以大的會排在前面,第一次排序後陣列為9, 2, 8, 5, 1, 3,4
	 * 第一次後increment的值變為3/2=1,此時對陣列進行插入排序, 實現陣列從大到小排
	 */

	public static void shellSort(int[] data) {
		int j = 0;
		int temp = 0;
		// 每次將長度縮短為原來的一半
		for (int increment = data.length / 2; increment > 0; increment /= 2) {
			for (int i = increment; i < data.length; i++) {
				temp = data[i];
				for (j = i; j >= increment; j -= increment) {
				   // 如想從小到大排只需修改這裡
					if (temp > data[j - increment])
					{
						data[j] = data[j - increment];
					} else {
						break;
					}

				}
				data[j] = temp;
			}

		}
	}
複製程式碼

排序演算法進階

五 歸併排序

圖片來源:維基百科

歸併排序原理

1, 介紹

  歸併即將兩個有序的陣列歸併併成一個更大的有序陣列。人們很快根據這個思路發明了一種簡單的遞迴排序演算法:歸併排序。要將一個陣列排序,可以先(遞迴地)將它分成兩半分別排序,然後將結果歸併起來。歸併排序最吸引人的性質是它能保證任意長度為N的陣列排序所需時間和NlogN成正比;它的主要缺點也顯而易見就是它所需的額外空間和N成正比。簡單的歸併排序如下圖:

簡單的歸併排序

2,原理

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

合併方法:

設r[i…n]由兩個有序子表r[i…m]和r[m+1…n]組成,兩個子表長度分別為n-i +1、n-m。

1、j=m+1;k=i;i=i; //置兩個子表的起始下標及輔助陣列的起始下標
2、若i>m 或j>n,轉⑷ //其中一個子表已合併完,比較選取結束
3、//選取r[i]和r[j]較小的存入輔助陣列rf
        如果r[i]<r[j],rf[k]=r[i]; i++; k++; 轉⑵
        否則,rf[k]=r[j]; j++; k++; 轉⑵
4、//將尚未處理完的子表中元素存入rf
        如果i<=m,將r[i…m]存入rf[k…n] //前一子表非空
        如果j<=n ,  將r[j…n] 存入rf[k…n] //後一子表非空
5、合併結束。
複製程式碼

3,實現程式碼

/**
	 * 歸併排序 簡介:將兩個(或兩個以上)有序表合併成一個新的有序表
	 * 即把待排序序列分為若干個子序列,每個子序列是有序的。然後再把有序子序列合併為整體有序序列 時間複雜度為O(nlogn) 穩定排序方式
	 * 
	 * @param nums
	 *            待排序陣列
	 * @return 輸出有序陣列
	 */

	public static int[] mergeSort(int[] nums, int low, int high) {
		int mid = (low + high) / 2;
		if (low < high) {
			// 左邊
			mergeSort(nums, low, mid);
			// 右邊
			mergeSort(nums, mid + 1, high);
			// 左右歸併
			merge(nums, low, mid, high);
		}
		return nums;
	}

	/**
	 * 將陣列中low到high位置的數進行排序
	 * 
	 * @param nums
	 *            待排序陣列
	 * @param low
	 *            待排的開始位置
	 * @param mid
	 *            待排中間位置
	 * @param high
	 *            待排結束位置
	 */
	public static void merge(int[] nums, int low, int mid, int high) {
		int[] temp = new int[high - low + 1];
		int i = low;// 左指標
		int j = mid + 1;// 右指標
		int k = 0;

		// 把較小的數先移到新陣列中
		while (i <= mid && j <= high) {
			if (nums[i] < nums[j]) {
				temp[k++] = nums[i++];
			} else {
				temp[k++] = nums[j++];
			}
		}

		// 把左邊剩餘的數移入陣列
		while (i <= mid) {
			temp[k++] = nums[i++];
		}

		// 把右邊邊剩餘的數移入陣列
		while (j <= high) {
			temp[k++] = nums[j++];
		}

		// 把新陣列中的數覆蓋nums陣列
		for (int k2 = 0; k2 < temp.length; k2++) {
			nums[k2 + low] = temp[k2];
		}
	}
複製程式碼

六 快速排序

圖片來源:維基百科

簡單的歸併排序

前言:

  快速排序可能是應用最廣泛的排序演算法了。快速排序流行的原因主要因為它實現簡單,適用於不同的輸入資料且在一般應用中比其他排序演算法都要快得多。快速排序引人注目的特點包括它是原地排序(只需要一個很小的輔助棧),且將長度為N的陣列排序所需要的時間和NlgN成正比。我們之前提到的幾種排序演算法都無法將這兩個優點結合起來。另外,快速排序的內迴圈比大多數排序演算法都要短小,這意味著它無論是理論上還是實際中都要更快。它的主要缺點是非常脆弱,在實現時要非常小心才能避免低劣的效能。已經有無數例子顯示許多錯誤都能致使它在實際應用中的效能只有平方級別。不過還好,我們由這些缺點和教訓中大大改進了快速排序演算法,使它的應用更加廣泛。

1,介紹

   快速排序是一種分治的排序演算法。它將一個陣列分成兩個字陣列,將兩部分獨立地排序。 快速排序和歸併排序是互補的:歸併排序將陣列分成兩個字陣列分別排序,並將有序的字陣列歸併以將整個陣列排序;而快速排序將陣列排序的方式則是當兩個字陣列都有序時整個陣列也就自然有序了。在第一種情況中,遞迴呼叫發生在處理整個陣列之前;在第二種情況中,遞迴呼叫發生在處理整個陣列之後。在歸併排序中,一個陣列被等分為兩半;快速排序中,切分的位置取決於陣列的內容。快速排序的過程大致如下:

排序演算法全總結

2,原理

快速排序的基本思想

  通過一趟排序將待排序記錄分割成獨立的兩部分,一部分全小於選取的參考值,另一部分全大於選取的參考值。這樣分別對兩部分排序之後順序就可以排好了。 例子:

(a)一趟排序的過程:

一趟排序的過程
(b)排序的全過程:
排序的全過程

3,實現程式碼

	/**
	 * 查詢出中軸(預設是最低位low)的在numbers陣列排序後所在位置
	 * 
	 * @param numbers 帶查詢陣列
	 * @param low 開始位置
	 * @param high 結束位置
	 * @return 中軸所在位置
	 */
	public static int getMiddle(int[] numbers, int low, int high) {
		// 陣列的第一個作為中軸
		int temp = numbers[low]; 
		while (low < high) {
			while (low < high && numbers[high] > temp) {
				high--;
			}
			// 比中軸小的記錄移到低端
			numbers[low] = numbers[high];
			while (low < high && numbers[low] < temp) {
				low++;
			}
			// 比中軸大的記錄移到高階
			numbers[high] = numbers[low]; 
		}
		numbers[low] = temp; // 中軸記錄到尾
		return low; // 返回中軸的位置
	}

	/**
	 * 
	 * @param numbers 帶排序陣列
	 * @param low 開始位置
	 * @param high 結束位置
	 */
	public static void quick(int[] numbers, int low, int high) {
		if (low < high) {
			int middle = getMiddle(numbers, low, high); // 將numbers陣列進行一分為二
			quick(numbers, low, middle - 1); // 對低欄位表進行遞迴排序
			quick(numbers, middle + 1, high); // 對高欄位表進行遞迴排序
		}

	}

	/**
	 * 快速排序
	 * 快速排序提供方法呼叫
	 * @param numbers 帶排序陣列
	 */
	public static void quickSort(int[] numbers) {
		// 檢視陣列是否為空
		if (numbers.length > 0) 
		{
			quick(numbers, 0, numbers.length - 1);
		}
	}

複製程式碼

4,改進方法

我們只介紹一種常用的,具體程式碼就不貼出了。想去的可以去https://algs4.cs.princeton.edu/code/ 這個網站找。

三向切分快速排序

核心思想就是將待排序的資料分為三部分,左邊都小於比較值,右邊都大於比較值,中間的數和比較值相等.三向切分快速排序的特性就是遇到和比較值相同時,不進行資料交換, 這樣對於有大量重複資料的排序時,三向切分快速排序演算法就會優於普通快速排序演算法,但由於它整體判斷程式碼比普通快速排序多一點,所以對於常見的大量非重複資料,它並不能比普通快速排序多大多的優勢 。

七 堆排序

排序的全過程

介紹:

堆排序(Heapsort)是指利用堆積樹(堆)這種資料結構所設計的一種排序演算法,它是是對簡單選擇排序的改進。。可以利用陣列的特點快速定位指定索引的元素。堆分為大根堆和小根堆,是完全二叉樹。大根堆的要求是每個節點的值都不大於其父節點的值,即A[PARENT[i]] >= A[i]。在陣列的非降序排序中,需要使用的就是大根堆,因為根據大根堆的要求可知,最大的值一定在堆頂。

原理:

將待排序的序列構造成一個大頂堆。此時,整個序列的最大值就是堆頂的根節點。將它移走(其實就是將其與堆陣列的末尾元素交換,此時末尾元素就是最大值),然後將剩餘的n-1個序列重新構造成一個堆,這樣就會得到n個元素中的次最大值。如此反覆執行,就能得到一個有序序列了。

  • 堆節點的訪問

    通常堆是通過一維陣列來實現的。在陣列起始位置為0的情形中:

    • 父節點i的左子節點在位置2i+1;
    • 父節點i的右子節點在位置 2i+2;
    • 子節點i的父節點在位置floor((i-1/2)
  • 堆的操作

    在堆的資料結構中,堆中的最大值總是位於根節點(在優先佇列中使用堆的話堆中的最小值位於根節點)。堆中定義以下幾種操作:

    • 最大堆調整(Max_Heapify):將堆的末端子節點作調整,使得子節點永遠小於父節點
    • 建立最大堆(Build_Max_Heap):將堆所有資料重新排序
    • 堆排序(HeapSort):移除位在第一個資料的根節點,並做最大堆調整的遞迴運算

3, 實現程式碼

public class HeapSort {
    
    private int[] arr;
    
    public HeapSort(int[] arr){
        this.arr = arr;
    }
    
    /**
     * 堆排序的主要入口方法,共兩步。
     */
    public void sort(){
        /*
         *  第一步:將陣列堆化
         *  beginIndex = 第一個非葉子節點。
         *  從第一個非葉子節點開始即可。無需從最後一個葉子節點開始。
         *  葉子節點可以看作已符合堆要求的節點,根節點就是它自己且自己以下值為最大。
         */
        int len = arr.length - 1;
        int beginIndex = (len - 1) >> 1; 
        for(int i = beginIndex; i >= 0; i--){
            maxHeapify(i, len);
        }
        
        /*
         * 第二步:對堆化資料排序
         * 每次都是移出最頂層的根節點A[0],與最尾部節點位置調換,同時遍歷長度 - 1。
         * 然後從新整理被換到根節點的末尾元素,使其符合堆的特性。
         * 直至未排序的堆長度為 0。
         */
        for(int i = len; i > 0; i--){
            swap(0, i);
            maxHeapify(0, i - 1);
        }
    }
    
    private void swap(int i,int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
    /**
     * 調整索引為 index 處的資料,使其符合堆的特性。
     * 
     * @param index 需要堆化處理的資料的索引
     * @param len 未排序的堆(陣列)的長度
     */
    private void maxHeapify(int index,int len){
        int li = (index << 1) + 1; // 左子節點索引
        int ri = li + 1;           // 右子節點索引
        int cMax = li;             // 子節點值最大索引,預設左子節點。
        
        if(li > len) return;       // 左子節點索引超出計算範圍,直接返回。
        if(ri <= len && arr[ri] > arr[li]) // 先判斷左右子節點,哪個較大。
            cMax = ri;
        if(arr[cMax] > arr[index]){
            swap(cMax, index);      // 如果父節點被子節點調換,
            maxHeapify(cMax, len);  // 則需要繼續判斷換下後的父節點是否符合堆的特性。
        }
    }
    
    /**
     * 測試用例
     * 
     * 輸出:
     * [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9]
     */
    public static void main(String[] args) {
        int[] arr = new int[]{3,5,3,0,8,6,1,5,8,6,2,4,9,4,7,0,1,8,9,7,3,1,2,5,9,7,4,0,2,6};        
        new HeapSort(arr).sort();        
        System.out.println(Arrays.toString(arr));
    }
    
}
複製程式碼

幾種排序演算法的區別與排序演算法的應用

穩定性:

  如果一個排序演算法能夠保留陣列中重複元素的相對位置則可以被稱為是穩定的。這個性質在許多情況下很重要。例如:在考慮一個需要處理大量含有地理位置和時間戳的事件網際網路商業應用程式。首先,我們在時間發生時將它們挨個儲存在一個陣列中,這樣在陣列中它們已經是按照時間順序排序好了的。現在假設在進一步處理前將按照地理位置劃分。一種簡單的方法是將陣列按照位置排序。如果排序演算法不是穩定的,排序後的每個城市的交易可能不會再是按照時間順序排序的了。很多情況下,不熟悉排序穩定性的程式設計師在第一次遇到這種情形的時候會在使用不穩定排序演算法後把陣列弄的一團糟而一臉懵逼。上一篇文章中我們講到的插入排序和歸併排序都屬於穩定的,其他幾個(選擇,希爾,快速,堆排序)都是不穩定的。有很多辦法能夠將任意排序演算法程式設計穩定的,但一般只有在穩定性是必要的情況下穩定的排序演算法才有優勢。不要想當然認為演算法具有穩定性是理所當然的,但事實上沒有任何實際應用中常見的演算法不是用了大量額外的時間和空間才做到了這一點(研究人員開發了這樣的演算法,但應用程式設計師發現它們太複雜了,無法在實際開發中使用)。 上述例子如下圖所示:

排序演算法全總結

我們究竟該使用哪種演算法

  下表總結了我們面試常見的排序演算法的各種重要性質。除了希爾排序(它的複雜度只是一個近似),插入排序(它的複雜度取決於輸入元素的排列情況)和快速排序的兩個版本(它門的複雜度和概率有關,取決於輸入元素的分不清情況)之外,將這些執行時間的增長數量級乘以適當的常數就能夠大致估計出其執行時間。這裡的常數有時和演算法有關(比如堆排序的比較次數是歸併排序的兩倍,且兩者訪問陣列的次數都比快速排序多得多),但主要取決於演算法的實現,Java編譯器以及你的計算機,這些因素決定了需要執行的機器指令的數量以及每條指令所需的執行時間。最重要的是,因為這些都是常數,你能通過較小的N得到的實現資料和我們標準雙倍測試來推測較大N所需的執行時間。

排序演算法全總結
快速排序是最快的通用排序演算法。自從數十年前快速排序發明以來,它在無數計算機系統中的無數實現都已經證明了這一點。總的來說,快速排序之所以最快是因為它的內迴圈中的指令很少(而且它還能利用緩衝,因為它總算是順序地訪問資料),所以它的執行時間的增長數量級為~cNlgN,而這裡的c比其他線性對數級別的排序演算法的相應常數都要小。在使用三向切分優化之後,快速排序對於實際應用中可能出現的某些分佈的輸入變成線性級別的了,而其他的排序演算法仍然需要線性對數時間。 因此,在大多數實際情況中,快速排序是最佳的選擇。當然,面對排序演算法和各式計算機及系統,這麼一句乾巴巴的話可能很難讓人信服。例如我們已經見過一個明顯的例外:如果穩定性很重要而空間又不是問題,歸併排序可能是最好的。我們會在後續的深入學習中見到更多的例外。

Java系統庫的排序演算法:

  Java系統庫主要排序方法java.util.Arrays.sort()。更具不同的引數型別,它實際代表了一系列排序方法:

  • 每種原始資料型別都有一個不同排序方法;
  • 一個適用於所有實現了Comparable介面的資料型別的排序方法;
  • 一個適用於實現了比較器Comparator的資料型別的排序方法。 Java的系統程式設計師選擇對原始資料型別使用(三向切分的)快速排序,對引用型別使用歸併排序。這些選擇實際上也暗示用速度和空間(對於原始資料型別)來換取穩定性(對於引用型別)。 當為實際應用開發Java程式,你會發現Java的Arrays.sort()發現(可能加上你自己實現的compareTo()或者compare())已經基本夠用了,因為它使用的三向快速排序和歸併排序都是經典。 下面看一個重寫compareTo方法來實現按年齡排序的例項
package map;
import java.util.Set;
import java.util.TreeMap;

public class TreeMap2 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TreeMap<Person, String> pdata = new TreeMap<Person, String>();
		pdata.put(new Person("張三", 30), "zhangsan");
		pdata.put(new Person("李四", 20), "lisi");
		pdata.put(new Person("王五", 10), "wangwu");
		pdata.put(new Person("小紅", 5), "xiaohong");
	     //得到key的值的同時得到key所對應的值
		Set<Person> keys = pdata.keySet();
		for (Person key : keys) {
			System.out.println(key.getAge() + "-" + key.getName());

		}
	}
}

// person物件沒有實現Comparable介面,所以必須實現,這樣才不會出錯,才可以使treemap中的資料按順序排列
// 前面一個例子的String類已經預設實現了Comparable介面,詳細可以檢視String類的API文件,另外其他
// 像Integer類等都已經實現了Comparable介面,所以不需要另外實現了

class Person implements Comparable<Person> {
	private String name;
	private int age;

	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
	/**
	 *TODO重寫compareTo方法實現按年齡來排序
	 */
	@Override
	public int compareTo(Person o) {
		// TODO Auto-generated method stub
		if (this.age > o.getAge()) {
			return 1;
		} else if (this.age < o.getAge()) {
			return -1;
		}
		return age;
	}
}
複製程式碼

參考:

《演算法》

維基百科:https://zh.wikipedia.org/wiki/堆排序

歡迎關注我的微信公眾號(堅持原創,分享美文,分享各種Java學習資源,面試題,以及企業級Java實戰專案回覆關鍵字免費領取):

微信公眾號

相關文章