1. 程式人生 > >資料結構與演算法學習筆記——排序

資料結構與演算法學習筆記——排序

排序方法與複雜度歸類

(1)幾種最經典、最常用的排序方法:氣泡排序、插入排序、選擇排序、快速排序、歸併排序、計數排序、基數排序、桶排序。
(2)複雜度歸類
氣泡排序、插入排序、選擇排序 O(n^2)
快速排序、歸併排序 O(nlogn)
計數排序、基數排序、桶排序 O(n)

如何分析一個“排序演算法”

<1>演算法的執行效率
1. 最好、最壞、平均情況時間複雜度。
2. 時間複雜度的係數、常數和低階。
3. 比較次數,交換(或移動)次數。
<2>排序演算法的穩定性
1. 穩定性概念:如果待排序的序列中存在值相等的元素,經過排序之後,相等元素之間原有的先後順序不變。
2. 穩定性重要性:可針對物件的多種屬性進行有優先順序的排序。
3. 舉例:給電商交易系統中的“訂單”排序,按照金額大小對訂單資料排序,對於相同金額的訂單以下單時間早晚排序。用穩定排序演算法可簡潔地解決。先按照下單時間給訂單排序,排序完成後用穩定排序演算法按照訂單金額重新排序。
<3>排序演算法的記憶體損耗
原地排序演算法:特指空間複雜度是O(1)的排序演算法。

常見的排序演算法

 


氣泡排序


氣泡排序只會操作相鄰的兩個資料。每次冒泡操作都會對相鄰的兩個元素進行比較,看是否滿足大小關係要求,如果不滿足就讓它倆互換。

程式碼:

    public int[] bubbleSort(int[] a) {
		int n = a.length;
		if (n<=1) return a;
		
		//提前退出冒泡迴圈的標誌
		boolean flag = false;
		
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n-i-1; j++) {
				if (a[j]>a[j+1]) {//
					int temp = a[j];
					a[j] = a[j+1];
					a[j+1] = temp;
					
					flag = true;//表示有資料交換
				}
			}
			if (!flag) break; //沒有資料交換(說明已排好序無需再進行冒泡),提前退出
		}
		
		return a;
	}


插入排序


插入排序將陣列資料分成已排序區間和未排序區間。初始已排序區間只有一個元素,即陣列第一個元素。在未排序區間取出一個元素插入到已排序區間的合適位置,直到未排序區間為空。

程式碼:

    public int[] insertionSort(int[] a) {
		int n = a.length;
		if (n<=1) return a;
		
		for (int i = 1; i < n; i++) {
			int value = a[i];
			int j = i-1;
			for (; j >=0; j--) {
				if (a[j] > value) {
					a[j+1] = a[j];//移動資料
				}else {
					break;
				}
			}
			a[j+1] = value;//插入資料
		}
		
		return a;
	}


選擇排序


選擇排序將陣列分成已排序區間和未排序區間。初始已排序區間為空。每次從未排序區間中選出最小的元素插入已排序區間的末尾,直到未排序區間為空。
程式碼:

public int[] selectionSort(int[] a) {
		int n = a.length;
		
		for (int i = 0; i < a.length - 1; i++) {
			for (int j = i+1; j < a.length; j++) {
				//交換
				if (a[i] > a[j]) {
					int temp = a[i];
					a[i] = a[j];
					a[j] = temp;
				}
			}
		}
		
		return a;
	}

歸併排序

如果要排序一個數組,我們先把陣列從中間分成前後兩部分,然後對前後兩部分分別排序,再將排好序的兩部分合並在一起,這樣整個陣列就都有序了。

 實現思路:

merge-sort(p...r)表示,給下標從p到r之間的陣列排序。我們將這個排序問題轉化為了兩個子問 ,題, merge_sort(p...q)和merge-sort(q+1..r),其中下標q等於p和r的中間位置,也就是, (p+r)/2,當下標從p到q和從q+1到r這兩個子陣列都排好序之後,我們再將兩個有序的子數組合並在一起,這樣下標從p到r之間的資料就也排好序了。

程式碼:

 // 歸併排序演算法, a是陣列,n表示陣列大小
  public static void mergeSort(int[] a, int n) {
    mergeSortInternally(a, 0, n-1);
  }

  // 遞迴呼叫函式
  private static void mergeSortInternally(int[] a, int p, int r) {
    // 遞迴終止條件
    if (p >= r) return;

    // 取p到r之間的中間位置q
    int q = (p+r)/2;
    // 分治遞迴
    mergeSortInternally(a, p, q);
    mergeSortInternally(a, q+1, r);

    // 將A[p...q]和A[q+1...r]合併為A[p...r]
    merge(a, p, q, r);
  }

  private static void merge(int[] a, int p, int q, int r) {
    int i = p;
    int j = q+1;
    int k = 0; // 初始化變數i, j, k
    int[] tmp = new int[r-p+1]; // 申請一個大小跟a[p...r]一樣的臨時陣列
   
    // 1 排序
    while (i<=q && j<=r) {
      if (a[i] <= a[j]) {
        tmp[k++] = a[i++]; // i++等於i:=i+1
      } else {
        tmp[k++] = a[j++];
      }
    }

    // 2 判斷哪個子陣列中有剩餘的資料
    int start = i;
    int end = q;
    if (j <= r) {
      start = j;
      end = r;
    }

    // 3 將剩餘的資料拷貝到臨時陣列tmp
    while (start <= end) {
      tmp[k++] = a[start++];
    }

    // 4 將tmp中的陣列拷貝回a[p...r]
    for (i = 0; i <= r-p; ++i) {
      a[p+i] = tmp[i];
    }
  }

merge是這樣執行的:

程式碼分析:

 

 

 

快速排序

快排的思想:    如果要排序陣列中下標從p到r之間的一組資料,我們選擇p到r之間的任意一個數據作為pivot (分割槽點) 。-我們遍歷p到r之間的資料,將小於pivot的放到左邊,將大於pivot的放到右邊,將pivot放到中間。經過這一步驟之後,陣列p到r之間的資料就被分成了三個部分,前面p到q-1之間都是小於pivot的,中間是pivot,後面的q+1到r之間是大於pivot的。

快排利用的分而治之的思想

 

線性排序:

時間複雜度O(n)

我們把時間複雜度是線性的排序演算法叫作線性排序(Linear sort)常見的線性演算法有: 桶排序、計數排序、基數排序

特點:

非基於比較的排序演算法 

 

桶排序

 

桶排序,顧名思義,會用到“桶" ,核心思想是將要排序的資料分到幾個有序的桶裡,每個桶裡的資料再單獨進行排序。桶內排完序之後,再把每個桶裡的資料按照順序依次取出,組成的序列就是有序的了。

對排序的資料要求苛刻:

1, 要排序的資料需要很容易就能劃分成m個桶,並且,桶與桶之間有著天然的大小順序。

2 ,資料在各個桶之間的分佈是比較均勻的。

3 ,桶排序比較適合用在外部排序中。所謂的外部排序就是資料儲存在外部磁碟中,資料量比較大,記憶體有限,無法將資料全部載入到記憶體中。

 

 

計數排序

計數排序只能用在資料範圍不大的場景中,如果資料範圍k比要排序的資料n大很多,就不適合用計數排序了。

計數排序只能給非負整數排序,如果要排序的資料是其他型別的,要將其在不改變相對大小的情況下,轉化為非負整數。

程式碼:

 // 計數排序,a是陣列,n是陣列大小。假設陣列中儲存的都是非負整數。
  public static void countingSort(int[] a) {
	int n = a.length;
    if (n <= 1) return;

    // 查詢陣列中資料的範圍
    int max = a[0];
    for (int i = 1; i < n; ++i) {
      if (max < a[i]) {
        max = a[i];
      }
    }

    // 申請一個計數陣列c,下標大小[0,max]
    int[] c = new int[max + 1];
    for (int i = 0; i < max + 1; ++i) {
      c[i] = 0;
    }

    // 計算每個元素的個數,放入c中
    for (int i = 0; i < n; ++i) {
      c[a[i]]++;
    }

    // 依次累加
    for (int i = 1; i < max + 1; ++i) {
      c[i] = c[i-1] + c[i];
    }

    // 臨時陣列r,儲存排序之後的結果
    int[] r = new int[n];
    // 計算排序的關鍵步驟了,有點難理解
    for (int i = n - 1; i >= 0; --i) {
      int index = c[a[i]]-1;
      r[index] = a[i];
      c[a[i]]--;
    }

    // 將結果拷貝會a陣列
    for (int i = 0; i < n; ++i) {
      a[i] = r[i];
    }
  }