1. 程式人生 > 實用技巧 >排序演算法(一)時間複雜度為O(n²)的排序演算法

排序演算法(一)時間複雜度為O(n²)的排序演算法

排序演算法(一)

排序演算法 時間複雜度 是否基於比較
冒泡、插入、選擇 O(n²)
快排、歸併 O(nlogn)
桶、計數、基數 O(n) ×

如何分析一個排序演算法

一、執行效率

  1. 最好情況、最壞情況、平均情況時間複雜度
  2. 時間複雜度的系統,常數、低階
  3. 比較次數和交換(移動)次數

二、記憶體消耗

記憶體消耗可以通過空間複雜度衡量。原地排序:特指空間複雜度為O(1)的排序演算法,不需要使用額外空間。

三、穩定性

穩定性:排序之前存在有兩個值相等的元素,排序之後,相等元素之間原有的相對位置不變

例子:訂單中有兩個屬性,金額和時間。現在希望想要按照金額由小到大進行排序,金額相同的再按照時間從早到晚排序。

解決辦法:利用排序演算法穩定性解決比較方便。首先按照時間從早到晚進行排序,排完序之後再使用穩定排序演算法對金額進行排序,因為穩定性,對於相同金額的訂單,排序前後的相對位置是不會發生改變,所以訂單就按照預期排好了。

排序演算法

排序演算法 是否為原地排序演算法 是否穩定 最好時間複雜度 平均時間複雜度 最壞時間複雜度
氣泡排序 是,空間複雜度為O(1) 穩定 O(n) O(n²) O(n²)
插入排序 是,空間複雜度為O(1) 穩定 O(n) O(n²) O(n²)
選擇排序 是,空間複雜度為O(1) 不穩定 O(n²) O(n²) O(n²)

一、氣泡排序

氣泡排序只會比較相鄰兩個元素進行比較,看是否滿足大小關係,不滿足則互換位置,每次冒泡會讓至少一個元素移動到它應該在的位置,重複n次。

移動元素次數等於逆序度

優化:當某次冒泡沒有資料交換時,就說明已經排好序了,不需要再繼續執行後續冒泡操作了。

// 氣泡排序,a表示陣列,n表示陣列大小
public void bubbleSort(int[] a, int n) {
  if (n <= 1) return;
 
 for (int i = 0; i < n; ++i) {
    // 提前退出冒泡迴圈的標誌位
    boolean flag = false;
    for (int j = 0; j < n - i - 1; ++j) {
      if (a[j] > a[j+1]) { // 交換
        int tmp = a[j];
        a[j] = a[j+1];
        a[j+1] = tmp;
        flag = true;  // 表示有資料交換      
      }
    }
    if (!flag) break;  // 沒有資料交換,提前退出
  }
}

二、插入排序

思想:動態向有序集合中插入資料,並一直保持集合有序

插入排序描述:將陣列分為兩部分,分別是:已排序區間和未排序區間。初始已排序區間只有一個元素,即陣列的第一個元素,之後不斷從未排序區間中取出元素,並插入到已排序區間的合適位置,保證已排序區間一直有序。重複這個過程直到未排序區間中元素為空。

移動元素次數等於逆序度

// 插入排序,a表示陣列,n表示陣列大小
public void insertionSort(int[] a, int n) {
  if (n <= 1) return;

  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; // 插入資料
  }
}

三、選擇排序

選擇排序思路:陣列也分為已排序區間和未排序區間,每次在未排序區間中從第一位元素開始找,找到最小元素,將其與未排序區間第一位元素交換位置,就變為了已排序區間的末尾。

// 選擇排序,a表示陣列,n表示陣列大小
public static void selectionSort(int[] a, int n) {
    if (n <= 1) return;

    for (int i = 0; i < n - 1; ++i) {
        // 查詢最小值
        int minIndex = i;
        for (int j = i + 1; j < n; ++j) {
            if (a[j] < a[minIndex]) {
                minIndex = j;
            }
        }

        // 交換
        int tmp = a[i];
        a[i] = a[minIndex];
        a[minIndex] = tmp;
    }
}

思考

氣泡排序和插入排序的時間空間複雜度,穩定性都相同,但為什麼插入排序更受歡迎?

氣泡排序中需要交換的元素個數,和選擇排序需要移動元素個數是固定相同的,即都為原始資料的逆序度

但是氣泡排序資料交換要比插入排序資料移動複雜,氣泡排序需要3個賦值操作,而插入排序只需要1個

//氣泡排序中資料的交換操作:
if (a[j] > a[j+1]) { // 交換
   int tmp = a[j];
   a[j] = a[j+1];
   a[j+1] = tmp;
   flag = true;
}

//插入排序中資料的移動操作:
if (a[j] > value) {
  a[j+1] = a[j];  // 資料移動
} else {
  break;
}

假設將一個賦值語句耗時時間為單位時間,分別使用氣泡排序和選擇排序對同一個逆序度為K的陣列進行排序:

氣泡排序:需要K次交換操作,每次3個賦值語句,所以總耗時時間為3*K;

插入排序:需要K次移動操作,每次1個賦值語句,所以總耗時時間我K;

所以,雖然時間複雜度都為O(n²),但插入排序的效能更加極致優化。如果需要對插入排序進一步優化,可以瞭解下希爾排序