算法導論知識梳理(二):比較排序算法及其下限

排序算法簡介

常見的排序算法大概有十種,而這些排序算法大體能夠分爲兩類:基於比較的排序算法和非基於比較的排序算法。基於比較的排序算法包括:冒泡排序、選擇排序、插入排序、希爾排序、堆排序、歸併排序、快速排序;非基於比較的排序包括:計數排序、基數排序、桶排序。本文主要介紹基於比較的排序算法中的一部分,包括:冒泡排序、選擇排序、插入排序、歸併排序和快速排序。前三個主要是提供算法優化的思路,後二個主要是爲了鞏固分治法這一思想。同時在最後也會介紹爲何基於比較的排序算法在最壞狀況下,時間複雜度最快也只能達到O(nlogn)。算法

交換函數

首先先寫一個通用的交換函數,用在下面的這些算法中數組

function exchange(arr, i, j) {
  const tmp = arr[i];
  arr[i] = arr[j];
  arr[j] = tmp; 
}
複製代碼

優化算法:冒泡排序、選擇排序、插入排序

這三個排序由於比較簡單,也都是比較容易想到的,因此並不打算寫,相信你們應該也都是知道的怎麼寫的。這三種排序都是不須要另外開闢一塊跟n相關的內存空間用以存儲排序過程當中的變量,即不佔用額外內存或只佔用常數內存。另外一方面,這三種排序算法也都有改進空間。下面主要講的就是這些算法的優化算法,主要是爲了提供一些算法優化的思路。數據結構

優化的冒泡排序

經過引入一個標誌位isExchange,對上一輪冒泡的結果進行校驗,若是上一輪沒發生冒泡,則說明結果已是有序的,便可提早退出循環。函數

function BubbleSort(arr) {
  let isExchange = false;
  for(let i = 0; i < arr.length; i++) {
    isExchange = false;
    for(let j = i + 1; j < arr.length; j++) {
      if(arr[i] > arr[j]) {
        exchange(arr, i, j);
        isExchange = true;
      }
    }
    // 若上一輪未交換位置,則提早退出循環
    if(!isExchange) break;
  }
  return arr;
}
複製代碼

優化的選擇排序

在每一次循環中,相比於原來的每次只找一個最大或最小值,能夠經過同時找到最大值和最小值的方式來縮短一半的時間。性能

function SelectionSort(arr) {
  let maxIndex, minIndex;
  for(let i = 0; i < arr.length / 2; i++) {
    maxIndex = minIndex = i;
    // 找到未排序部分中的最大、最小值
    for(let j = i + 1; j < arr.length - i; j++) {
      if(arr[maxIndex] < arr[j]) maxIndex = j;
      else if(arr[minIndex] > arr[j]) minIndex = j; 
    }
    // 交換未排序部分中的最小值和第一位數
    if(minIndex !== i) {
      exchange(arr, i, minIndex);
      // 若是最大值是i,由於上面這一過程當中交換了i和minIndex,因此要修正maxIndex的指向
      if(maxIndex === i) maxIndex = minIndex;
    }
    // 交換未排序部分中的最大值和最後一位數
    if(maxIndex !== arr.length - 1 - i) exchange(arr, arr.length - 1 - i, maxIndex);
  }
  return arr;
}
複製代碼

優化的插入排序

在每次選擇插入點時,原來的方法時從頭開始匹配,這種方法在數據量大時及其費時,其實能夠經過二分查找法選擇插入點。優化

function InsertSort(arr) {
  let maxIndex, minIndex;
  for(let i = 1; i < arr.length; i++) {
    const tmp = arr[i];
    if(tmp < arr[i - 1]) {
      const searchIndex = BinarySearch(arr, 0, i - 1, arr[i]);
      // 將待插入下標以後的項所有右移一單位
      for(let j = i; j > searchIndex; j--) {
        arr[j] = arr[j - 1];
      }
      // 插入待插入項
      arr[searchIndex] = tmp;
    }
  }
  return arr;
}
/** * 二分查找 * lowIndex 查找項的最左側下標 * highIndex 查找項的最右側下標 * target 待查找的目標項 * */
function BinarySearch(arr, lowIndex, highIndex, target) {
  while(lowIndex < highIndex) {
    const halfIndex = Math.floor((highIndex + lowIndex) / 2);
    if(arr[halfIndex] > target) {
      highIndex = halfIndex;
    } else {
      lowIndex = halfIndex + 1;
    }
  }
  return lowIndex;
}
複製代碼

總結

以上優化算法都是對原算法的擴展,可是其時間複雜度依舊爲O(n2),優化的內容無非就是這兩塊內容:ui

  1. 某些特殊狀況下的時間複雜度,如:優化的冒泡排序
  2. 其時間複雜度函數T(n)的高階項的係數或者低階項,如:另外兩個

分治法的應用:歸併排序、快速排序

歸併排序

歸併排序是創建在歸併操做上的一種有效的排序算法,該算法是採用分治法的一個很是典型的應用。將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。 --百度百科spa

歸併排序能夠分爲如下步驟:翻譯

  1. 將數組分爲2部分,將這兩部分進行歸併,並將歸併結果合併
  2. 合併過程:從前日後分別對比這兩部分數組,小項優先於大項插入到新的數組
  3. 重複上訴步驟
function MergeSort(arr) {
  if(arr.length < 2) return arr;
  const midIndex = Math.floor(arr.length / 2);
  const left = MergeSort(arr.slice(0, midIndex));
  const right = MergeSort(arr.slice(midIndex, arr.length));
  return Merge(left, right);
}
function Merge(left, right) {
  let result = [];
  let i = j = 0;
  while(i < left.length && j < right.length) {
    if(left[i] < right[j])
      result.push(left[i++]);
    else
      result.push(right[j++]);
  }
  if(i === left.length) result = result.concat(right.slice(j));
  if(j === right.length) result = result.concat(left.slice(i));
  return result;
}
複製代碼

快速排序

經過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的全部數據都比另一部分的全部數據都要小,而後再按此方法對這兩部分數據分別進行快速排序,整個排序過程能夠遞歸進行,以此達到整個數據變成有序序列。 --百度百科code

快速排序就像它的名字同樣,在基於比較的排序算法中,其排序速度是最快的。由於隨着數據規模的變大,其時間複雜度是近似線性增加。

快速排序可分爲如下步驟:

  1. 隨機選取某個數組中的值,通常取位於中間的值
  2. 將小於這個值的數放在左邊數組,大於這個值的數放在右邊數組
  3. 分別對左邊數組和右邊數組執行上述操做
function QuickSort(arr) {
  if(arr.length <= 1) return arr;
  const midIndex = Math.floor(arr.length / 2);
  const left = [];
  const right = [];
  for(let i = 0; i < arr.length; i++) {
    if(i === midIndex) continue;

    if(arr[i] < arr[midIndex]) left.push(arr[i]);
    else right.push(arr[i]);
  }
  return QuickSort(left).concat([arr[midIndex]], QuickSort(right));
}
複製代碼

決策樹模型

比較排序能夠被抽象爲一棵決策樹,決策樹是一棵徹底二叉樹,它能夠表示在給定的輸入規模狀況下,某一特定排序算法對全部元素的比較操做。其中控制、數據移動等其餘操做都被忽略。

見下圖:

在決策樹中,從根結點到任意一個可達葉結點之間的最長簡單路徑的長度,表示的是對應的排序算法中最壞狀況下的比較次數。所以,一個比較排序算法中的最壞狀況比較次數就等於其決策樹的高度。

書上這裏爲何說的是最壞狀況?這是由於只有在最壞狀況下,即對任意兩個數之間都進行了一次比較操做,比較次數纔會是從根結點出發到某一個具體葉結點。然而在大多數狀況下,咱們爲了提高算法性能,會經過相似上面提過的一些優化手段來提早結束比較。

對於一棵每一個排列都是可達葉結點的決策樹來講,樹的高度徹底能夠被肯定。假設一棵這種決策樹的高度爲h,葉結點總數爲l,對應於對n個元素所作的比較排序,那麼咱們能夠輕易獲得n! <= l,又由於,對於任意一棵二叉樹而言,其葉結點數 l <= 2^h,滿二叉樹的葉結點數爲2^h。

因此獲得n! <= l <= 2^h

兩邊同時取對數,獲得 h >= log(n!)

又由於log(n!) = Θ(nlogn)(這是一個定理)

因此能獲得h >= Ω(nlogn)

即在最壞的狀況下,任何比較排序都須要作Ω(nlogn)次比較。

證實log(n!) = Θ(nlogn)

log(n!) = log1 + log2 + ... + logn <= logn + logn + ... + logn <= nlogn

獲得log(n!) = O(nlogn)

而後取n!的後一半,即n/2...n

log(n!) = log1 + log2 + ... + logn >= log(n/2 + 1) + log(n/2 + 2) + ... + log(n - 1) + logn >= log(n/2) + log(n/2) + ... log(n/2) + log(n/2) >= n/2log(n/2)

獲得log(n!) = Ω(nlogn)

因此log(n!) = Θ(nlogn)

總結

上訴結論翻譯成大白話就是:在基於對任意的兩個數都進行過比較的狀況下(即不提早結束排序算法,好比優化後的冒泡排序就是提早結束的,這種狀況映射到決策樹模型上就是取的是從跟節點出發到某一個葉結點之間的路徑下的某一段路徑,而不是整段路徑),任何基於比較的排序,至少都須要進行nlogn次比較。

而從這必定理出發,能夠獲得:堆排序和歸併排序實際上是漸進最優的比較排序算法。由於這二者在最好、最壞、平均狀況下的時間複雜度都爲O(nlogn),即它們的運行時間上界達到了O(nlogn)。而快速排序在最壞狀況下,即劃分極不平衡的狀況下,即用來比較用的中間值恰好是最大/最小值,其就退化爲插入排序。但爲何實際狀況中每每快排的使用率更高?1、快排能夠作到基於原數組進行排序,不產生額外的內存開銷;2、快排的高階項的係數較低,貌似是1.38,有興趣的能夠本身去查查資料。在實際狀況下,每每是經過多種排序算法的結合來完成咱們所須要的排序算法。例如:js中的sort方法就是對小數組插入排序,對大數組使用快速排序;內省排序是從快排開始,當遞歸深度超過必定深度後轉爲堆排序。

本章內容到這裏就結束了,下一節內容:基本數據結構。

相關文章
相關標籤/搜索