前言
下面會講到一些簡單的排序演算法(均基於java實現),並給出實現和效率分析。
使用的基類如下:
注意:抽象函式應為public的,我就不改程式碼了
- public abstract class Sortable {
- protected String LABLE="排序演算法";
- //比較兩個數(使用了Integer中sort的原始碼)
- protected int compare(int x, int y) {
- return (x < y) ? -1 : ((x == y) ? 0 : 1);
- }
- //同上,不過返回改為bool
- protected boolean less(int x,int y){
- return compare(x,y) <0;
- }
- //交換陣列中的兩個值
- protected void exch(int[] a,int i,int j){
- Integer temp = a[i];
- a[i] = a[j];
- a[j] = temp;
- }
- //子類需要實現的排序演算法
- public abstract void sort(int[] a);
- public String getLABLE() {
- return LABLE;
- }
- }
氣泡排序
最常見的,畢竟老師教給我們的的第一種排序演算法。實現起來很簡單,不過實際應用很少(正常情況下),複雜度O(n²)。
原理
趟一趟的比,每一趟中,迴圈剩餘的數,和後一個進行比較,若比它小則交換。這樣一趟下來最小的在第一個,最大的在最後一個。總共比n-1趟。
實現
- import com.anxpp.sort.base.Sortable;
- /**
- * 最簡單的氣泡排序
- *
- * @author anxpp.com
- * 原理:比較相鄰兩個元素,從第一對開始比較一直到最後一對,若順序不對就交換(感覺就像冒泡一樣)。
- * 一趟比較後,最大(或最小)的會位於最後的位置,然後再以類似方式比較前面的元素。
- */
- public class BubbleSort extends Sortable {
- public BubbleSort(){
- super.LABLE = "氣泡排序";
- }
- @Override
- public void sort(int[] a) {
- for(int i=0;i<a.length-1;i++){
- for(int j=0;j<a.length-1-i;j++){
- if(less(a[j+1],a[j])){
- exch(a,j,j+1);
- }
- }
- }
- }
- }
優化
上面的演算法,無論的你的資料怎麼樣,始終都要比n²次,效率很低。若你的資料區域性有序,經過幾趟交換以後,已經有序,則不用繼續往下比。效率會高很多(絕大多數情況下)。優化程式碼如下:
- import com.anxpp.sort.base.Sortable;
- /**
- * 設定標誌優化後的氣泡排序
- * @author anxpp.com
- *
- * 原理:比較相鄰兩個元素,從第一對開始比較一直到最後一對,若順序不對就交換(感覺就像冒泡一樣)。
- * 一趟比較後,最大(或最小)的會位於最後的位置,然後再以類似方式比較前面的元素。
- * 優化:傳統的氣泡排序,總是要比較那麼多次,如果在某趟完成後,並無交換表示資料已經有序,所以設定
- * 一個標誌,如某趟比較完成後沒有發生,則不再繼續後面的運算直接返回即可,其實,有時候效率反而會比傳統的低!
- * 其他:據說分而治之也能有用到冒泡,這裡就不深究了...
- */
- public class BetterBubbleSort extends Sortable {
- public BetterBubbleSort(){
- super.LABLE = "氣泡排序優化";
- }
- @Override
- public void sort(int[] a) {
- boolean didSwap;
- for(int i=0;i<a.length-1;i++){
- didSwap = false;
- for(int j=0;j<a.length-1-i;j++){
- if(less(a[j+1],a[j])){
- exch(a,j,j+1);
- didSwap = true;
- }
- }
- if(!didSwap) return ;
- }
- }
- }
選擇排序
和冒泡複雜度一樣O(n²),但是時間上可能會比冒泡稍微快一點,因為交換的次數比冒泡少。
原理
選擇排序可以說是最好理解的演算法。就是每次遍歷一趟,找出最小的數,放到最前端。(這裡說的是最前,是指無序的佇列中的最前)
實現
- import com.anxpp.sort.base.Sortable;
- /**
- * 選擇排序
- * @author anxpp.com
- *
- */
- public class SelectionSort extends Sortable {
- public SelectionSort(){
- super.LABLE = "選擇排序";
- }
- @Override
- public void sort(int[] a) {
- for(int i=0;i<a.length;i++){
- int min=i;
- for(int j=i+1;j<a.length;j++){
- if(less(a[j],a[min])){
- min = j;
- }
- }
- exch(a,i,min);
- }
- }
- }
插入排序
時間複雜度O(n²)。
原理
遍歷未排序序列。把未排序數列的第一個數和已排序數列的每一個數比較,若比它大則交換。經典的理解方式就是理解成摸牌時候理牌的順序。我上面的實現是直接互動數字,若是把大的數直接往後移效率還會更高。
實現
- import com.anxpp.sort.base.Sortable;
- /**
- * 插入排序
- * @author anxpp
- *
- */
- public class InsertionSort extends Sortable {
- public InsertionSort(){
- super.LABLE = "插入排序";
- }
- @Override
- public void sort(int[] a) {
- for(int i=1;i<a.length;i++){
- for(int j=i;j>0;j--){
- if(less(a[j],a[j-1])){
- exch(a,j,j-1);
- }
- else break;
- }
- }
- }
- }
適合插入排序的資料
當你的資料是基本有序的時候且資料量小,利用插入排序的時候,效率會很高。若資料為逆序的話,效率很低。
希爾排序
可以看出是插入排序的一種優化,或者是預處理。希爾排序就是先進行h-sort,也就是讓間隔為h的元素都是有序的。普通的插入排序就是1-sort。
原理
主要就是選定一個h的有序陣列來進行預排序。這樣最後進行插入排序的時候,能使資料區域性有序。就算交換的話,交換的次數也不會很多。這樣h序列稱為遞增序列。希爾的效能很大部分取決於遞增序列.一般來說我們使用這個序列3x + 1.
實現
- import com.anxpp.sort.base.Sortable;
- /**
- * 希爾排序
- * @author anxpp.com
- *
- */
- public class ShellSort extends Sortable {
- public ShellSort(){
- super.LABLE = "希爾排序";
- }
- @Override
- public void sort(int[] a) {
- int h=1;
- while(h<a.length/3){
- h=3*h+1;
- }
- while(h>=1){
- for(int i=h;i<a.length;i++){
- for(int j=i;j>=h;j=j-h){
- if(less(a[j],a[j-h])){
- exch(a,j,j-h);
- }
- else break;
- }
- }
- h=h/3;
- }
- }
- }
效能
對於希爾排序的效能其實無法準確表示。介於O(nlogn)和O(n²)之間,大概在n的1.5次冪左右。
希爾排序對於中大型資料的排序效率是很高的,而且佔用空間少,程式碼量短。而且就算是很大的資料,用類似快排這種高效能的排序方法,也僅僅只比希爾快兩倍或者不到。
歸併排序
複雜度O(nlogn).
核心思想就是採用分而治之的方法,遞迴的合併兩個有序的陣列。效率比較高,缺點是空間複雜度高,會用到額外的陣列。
原理
核心程式碼是合併的函式。合併的前提是保證左右兩邊的陣列分別有序,在合併之前和之後在Java中我們可以用斷言來保證陣列有序。合併的原理其實也很簡單,先把a陣列中的內容複製到額外儲存的temp陣列中去。分別用兩個index指向a陣列的起始位置和中間位置,保證a陣列左右兩邊有序,比如i,j。現在開始從頭掃描比較左右兩個陣列,若a[i]<=a[j],則把a[i]放到temp陣列中去,且i向前走一步。反正則放a[j],且j走一步。若其中一個陣列走完了,則把另一個陣列剩餘的數直接放到temp陣列中。我們用遞迴的方式來實現左右兩邊有序。遞迴到陣列只有1個數時肯定是有序的,再合併2個數,再退出來合併4個數,以此類推。
實現
- import com.anxpp.sort.base.Sortable;
- /**
- * 歸併排序
- * @author anxpp.com
- *
- */
- public class MergeSort extends Sortable {
- public MergeSort(){
- super.LABLE = "歸併排序";
- }
- int[] temp ;
- private void merge(int[] a, int l, int m, int h){
- for(int i=l;i<=h;i++){
- temp[i]=a[i];
- }
- int i=l;
- int j=m+1;
- for(int k=l;k<=h;k++){
- if(i>m) a[k]=temp[j++];
- else if(j>h) a[k]=temp[i++];
- else if(less(temp[i],temp[j])) a[k]=temp[i++];
- else a[k] = temp[j++];
- }
- }
- private void sort(int[] a,int l,int h) {
- if(l<h){
- int mid = (l+h)/2;
- sort(a,l,mid);
- sort(a,mid+1,h);
- if (!less(a[mid+1], a[mid])) return;
- merge(a,l,mid,h);
- }
- }
- @Override
- public void sort(int[] a) {
- temp = new int[a.length];
- sort(a,0,a.length-1);
- }
- }
優化
歸併排序對小陣列排序時,由於會有多重的遞迴呼叫,所以速度沒有插入排序快。可以在遞迴呼叫到小陣列時改採用插入排序。小陣列的意思是差不多10個數左右。
如果遞迴時判斷已經有序則不用繼續遞迴。也可以增加效率。
- private void sort(int[] a,int l,int h) {
- if(l<h){
- int mid = (l+h)/2;
- sort(a,l,mid);
- sort(a,mid+1,h);
- if (!less(a[mid+1], a[mid])) return;
- merge(a,l,mid,h);
- }
- }
另外在合併時互動兩個陣列的順序,能節省複製陣列到輔助陣列的時間,但節省不了空間。
適用範圍
如果你對空間要求不高,且想要一個穩定的演算法。那麼可以使用歸併排序。
快速排序
傳說中最快的排序演算法,聽說能裸寫快排,月薪可上10k...
快排平均情況下時間複雜度O(nlogn),最糟糕情況O(n²)。O(n²)主要是因為選定的主元是極端值造成的,比如說最大值,最小值。不過這種情況一般很少出現,所以在進行快排之前我們需要對陣列進行亂序,儘量避免這種情況的發生。
原理
第一步打亂陣列。
然後也是分治法。歸併是先分再合併。快排是先排序再分別排序兩邊。
排序過程核心思想是為了選出一個數,把陣列分成左右兩邊,左邊比主元小,右邊比主元大。
選定第一個數作為主元。然後設定兩個index指向陣列首尾,比如i,j。接著從兩邊向中間掃描,分別用a[i]和a[j]和主元比較。若兩邊位置不對則交換a[i]和a[j],比如說a[i]在掃描過程中遇到a[i]>主元,那麼則停止掃描,因為我們需要左邊的數小於主元,反正右邊也一樣等到a[j]也停下來,則交換a[i]和a[j]。
得到中間的位置之後再分別左右遞迴排序。
實現
- import com.anxpp.sort.base.Sortable;
- /**
- * 快速排序
- * @author u
- *
- * 原理:選擇一個基準元素,通過一趟掃描,將資料分成大於和不大於基準元素的兩部分(分別在基準元素的兩邊),此時
- * 基準元素就在未來排好後的正確位置,然後遞迴使用類似的方法處理這個基準元素兩邊的部分。
- * 既然用了遞迴,難免在空間上的效率不高...
- * 平均效能通常被認為是最好的
- */
- public class quickSort extends Sortable {
- public quickSort(){
- super.LABLE = "快速排序";
- }
- /**
- *
- * @param a 要排序的列表
- * @param low 左邊位置
- * @param high 右邊位置
- */
- private void sort(int[] a,int low,int high){
- //左
- int l =low;
- //右
- int h = high;
- //基準值
- int k = a[low];
- //判斷一趟是否完成
- while(l<h){
- //若順序正確就比較下一個
- while(l<h&&a[h]>=k)
- h--;
- if(l<h){
- int temp = a[h];
- a[h] = a[l];
- a[l] = temp;
- l++;
- }
- while(l<h&&a[l]<=k)
- l++;
- if(l<h){
- int temp = a[h];
- a[h] = a[l];
- a[l] = temp;
- h--;
- }
- }
- if(l>low) sort(a,low,l-1);
- if(h<high) sort(a,l+1,high);
- }
- @Override
- public void sort(int[] a) {
- sort(a,0,a.length-1);
- }
- }
優化
第一步的隨機打亂陣列,雖然會耗費一定時間,但卻是必要的。同樣的小陣列的排序,快排不如插入排序。所以小陣列可以直接採用插入排序。主元的選擇方式可以有多種,比如隨機選擇主元。或者選取三個數,取中位數為主元,但是會耗費一定時間。
適用範圍
雖然快速排序是不穩定的。但快速排序通常明顯比其他Ο(nlogn)演算法更快,因為它的內部迴圈很小。快速排序在對重複資料的排序時,會重複劃分資料進行排序。雖然效能也還行,但這裡可以進行改進,就是下面介紹的三向切分排序。
三向切分
快速排序的一種改進,使快排在有大量重複元素的資料,同樣能保持高效。
原理
基本原理和快排差不多。三向切分的時候在劃分陣列時不是分為兩組,而是分成三組。
- 小於主元
- 和主元相等
- 大於主元
實現
- public class ThreeWaySort extends Sortable {
- public void sort(int[] a,int l ,int h) {
- if(l>=h) return;
- int v = a[l];
- int i=l;
- int lv=l;
- int gh=h;
- while(i<=gh){
- int cmpIndex = compare(a[i],v);
- if(cmpIndex<0) exch(a,i++,lv++);
- else if(cmpIndex>0) exch(a,i,gh--);
- else i++;
- }
- sort(a,l,lv-1);
- sort(a,gh+1,h);
- }
- @Override
- void sort(int[] a) {
- sort(a,0,a.length-1);
- }
- }
堆排序
時間複雜度O(nlogn),堆排序主要用二叉堆實現,在講堆排序之前我們可以要先了解下二叉堆。
二叉堆
所謂的二叉堆用一顆二叉樹表示,也就是每一個節點都大於它的左右子節點。也就是說根節點是最大的。
二叉樹用陣列儲存,可以用下標來表示節點。比如i這個節點的父節點為i/2,左兒子為2*i,右兒子為2*i+1.
堆的操作主要有兩種上浮和下沉。主要對應兩種情況,比如在陣列末尾新增節點,此時需要上浮節點,保證二叉堆的特點。反之在替換根節點是則需要下沉操作。
原理
分為兩步。
- 把陣列排成二叉堆的順序
- 調換根節點和最後一個節點的位置,然後對根節點進行下沉操作。
實現
適用範圍
堆排序也是不穩定的。
堆排序在空間和時間上都是O(nlogn),且沒有最糟情況,但在平均情況下比快排慢。
所以現在大部分應用都是用的快排,因為它的平均效率很高,幾乎不會有最糟情況發生。
但如果你的應用非常非常重視效能的保證,比如一些醫學上的監控之類的。
那麼可以使用堆排序。還有一個堆排序的缺點,是它無法利用快取,幾乎很少和相鄰元素的比較。
執行時間比較
使用下面的程式碼測試以上排序演算法:
- import java.util.Random;
- import com.anxpp.sort.base.Sortable;
- /**
- * 測試排序演算法
- * @author anxpp.com
- *
- */
- public class TestSort {
- //需要排序的數字長度為LEN
- private final static int LEN = 30000;
- //最大值為MAX
- private final static int MAX = 99999;
- public static void main(String[] args){
- //初始化排序演算法
- Sortable[] sortables = {
- new BubbleSort(),new BetterBubbleSort(),new SelectionSort(),
- new InsertionSort(),new ShellSort(),new MergeSort(),
- new BetterMergeSort(),new quickSort(),new ThreeWayQuickSort()};
- //產生源資料
- Random random = new Random();
- random.setSeed(System.currentTimeMillis());
- int[][] a = new int[sortables.length][LEN];
- int i = 0;
- while(i++ < LEN-1){
- int num = random.nextInt(MAX);
- int j = 0;
- while(j<sortables.length)
- a[j++][i] = num;
- }
- //排序
- for(i = 0;i<sortables.length;i++){
- System.out.println(sortables[i].getLABLE()+":");
- // print(a[i]);
- // sortTime(a[i],sortables[i]);
- System.out.println(sortTime(a[i],sortables[i]));
- // print(a[i]);
- }
- }
- public static int sortTime(int[] a,Sortable sortable){
- long start = System.currentTimeMillis();
- sortable.sort(a);
- return (int) (System.currentTimeMillis()-start);
- }
- public static void print(int[] a){
- for(int i = 0;i<a.length;i++)
- System.out.print(a[i] + ",");
- System.out.println();
- }
- }
下面是本人的一次執行結果:
- 氣泡排序:
- 2348
- 氣泡排序優化:
- 2660
- 選擇排序:
- 250
- 插入排序:
- 907
- 希爾排序:
- 12
- 歸併排序:
- 7
- 歸併排序優化:
- 5
- 快速排序:
- 6
- 三向切分快速排序:
- 15
推薦一個很好的網站,對各種演算法進行了總結,和動畫描述:sorting-algorithms