比較排序和決策樹(轉載---這批動態圖確實不賴)
比較排序是通過一個單一且抽象的比較運算(比如“小於等於”)讀取列表元素,而這個比較運算則決定了每兩個元素中哪一個應該先出現在最終的排序列表中。
宣告:下面通過在維基百科中找到的非常完美的圖示來介紹一系列比較排序。
插入排序
在該系列的【演算法】1中我們便介紹了這個基本的演算法,它的比較過程如下:
以下是用插入排序對30個元素的陣列進行排序的動畫:
插入排序是最簡單的排序演算法,插入排序最差的複雜度是O(n^2)效率比較低適合少量資料進行排序,但是實現起來比較簡單。在一組無序元素中選取第二位插入排序的key值,並用key值與其前面臨近的元素做比較,如果大於key值就將其值後移一下,繼續上key值與前面的做比較並重復移動位置。如果key前面的值都小於key就將key值後面的元素值賦值給key重複上述動作。(其根本就是在一個最小的有序序列中不斷的插入資料且使其保持有序)
插入排序的實現:
private static void insertSort(int[] a) {
for(int j = 1; j < a.length; j++) {
int key = a[j];
int i = j - 1;
while(i >= 0 && a[i] > key) {
a[i + 1] = a [i];
i--;
}
a[i + 1] = key;
}
}
選擇排序
選擇排序的比較過程如下:
其動畫效果如下:
選擇排序是通過遍歷每一次都找出最小(最大)的數查找出來放在第一位,然後從第二個元素開始重複上邊的動作即可完成排序。選擇排序的時間複雜度為哦(n^2),且為非穩定排序演算法。
實現程式碼如下:
private static void choseSort(int[] a) { for(int i = 0; i < a.length; i++) { int lowIndex = i; int j = i + 1; while(j < a.length) { if(a[lowIndex] > a[j]) { lowIndex = j; } j++; } int temp = a[i]; a[i] = a[lowIndex]; a[lowIndex] = temp; } }
歸併排序
前面多次寫到歸併排序,它的比較過程如下:
歸併排序的動畫如下:
private static void margeSort(int[] a,int left, int right) {
if(left < right) {
int middle = (left + right)/2;
System.out.println("middle:" + middle);
margeSort(a,left, middle);
margeSort(a, middle + 1, right);
marge(a, left, right, middle);
}
}
private static void marge(int[] a,int i, int j, int middle) {
int[] temp = new int[a.length];
int k = i;
int tempFirst = i;
int mid = middle +1;
while(i <= middle&& j >= mid) {
if (a[i] < a[mid]) {
temp[k++] = a[i++];
} else{
temp[k++] = a[mid++];
}
}
while(i <= middle) {
temp[k++] = a[i++];
}
while(j >= mid) {
temp[k++] = a[mid++];
}
while (tempFirst <= j) {
a[tempFirst] = temp[tempFirst ++];
}
}
歸併排序(Merge)是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分為若干個子序列,每個子序列是有序的。然後再把有序子序列合併為整體有序序列。
歸併排序是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。 將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為2-路歸併。
實現程式碼:堆排序(非穩定)
在該系列的【演算法】4中我們便介紹了快排,構建堆的過程如下:
堆排序的動畫如下:
堆排序和歸併排序一樣複雜度都是nlogn。同時和插入排序一樣不需要額外的儲存空間。堆排序是建立在完全二叉樹的基礎之上的。在進行堆排序事前我們首先需要對無序序列進行大頂堆(或者小頂堆)的構建。大頂堆的根節點的值是整個無序序列中值最大,將大頂堆根節點和最後一個節點交換,同時對剩下的n-i個節點進行大頂堆的構建,構建成功後重新將根節點的值和n-i進行交換,交換後對剩下的n-i-1個節點重新構建大頂堆。如此重複。
程式碼實現如下:
/**
* heap sort
*/
public static void heapSort(int[] array) {
buildHeap(array);
for (int a: array) {
System.out.println(a);
}
System.out.println("**************");
for (int i = array.length -1;i > 0; i--) {
swap(array,0,i);
heapify(array,0, i);
}
}
/**
* 根據parentnode leftnode rightnode 調整位置
* 在調整位置後遞迴調整做出位置交換的子節點的位置是否符合大頂堆狀態
* @param array
* @param i
* @param length
*/
private static void heapify(int[] array, int i, int length) {
int left = i * 2 + 1;
int right = i * 2 + 2;
int max = 0;
if (array[left] > array[i] && left < length) {
max = left;
} else {
max = i;
}
if (right < length && array[right] > array[max]) {
max = right;
}
if (max != i) {
swap(array, i, max);
heapify(array, max, length);
}
}
/**
*構建大頂堆
* @param array
*/
private static void buildHeap(int[] array) {
for (int i = array.length/2 -1; i>= 0; i--) {
heapify(array,i, array.length);
}
}
/**
* 交換
* @param array
* @param i
* @param j
*/
private static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
快速排序
設要排序的陣列是A[0]……A[N-1],首先任意選取一個數據(通常選用陣列的第一個數)作為關鍵資料,然後將所有比它小的數都放到它前面,所有比它大的數都放到它後面,這個過程稱為一趟快速排序。值得注意的是,快速排序不是一種穩定的排序演算法,也就是說,多個相同的值的相對位置也許會在演算法結束時產生變動。
在該系列的【演算法】5中我們便介紹了快排,它的比較過程如下:
快速排序的動畫如下:
快速排序的例項程式碼:
<1>遞迴實現方法:
/**
* Created by cike on 16/4/16.
*
* 快速排序是在複雜度同為O(N * logN)的集中演算法中效率較高的一種.
* 同時快速排序中運用的分治思想.
*/
public class Quicksort {
public void quicksort(int n[], int left, int right) {
int dp;
if (left < right) {
dp = partition(n, left, right);
quicksort(n, left, dp - 1);
quicksort(n, dp +1, right);
}
}
private int partition (int n[], int left, int right) {
int pivot = n[left];
while (left < right) {
while (left < right && n[right] >= pivot)
right--;
if (left < right)
n[left++] = n[right];
while (left < right && n[left] <= pivot)
left++;
if (left < right)
n[right--] = n[left];
}
n[left] = pivot;
return left;
}
}
<2>非遞迴方法:
/**
* 非遞迴方法
*/
public void sort(int[] n, int left, int right) {
Stack<Integer> stack = new Stack<Integer>();
if (left < right) {
int mid = partition(n, left, right);
if (left < mid -1) {
stack.push(left);
stack.push(mid -1);
}
if (right > mid + 1) {
stack.push(mid +1);
stack.push(right);
}
while (!stack.empty()) {
right = stack.pop();
left = stack.pop();
mid = partition(n, left, right);
if (left < mid -1) {
stack.push(left);
stack.push(mid -1);
}
if (right > mid + 1) {
stack.push(mid + 1);
stack.push(right);
}
}
}
}
通過遞迴和和非遞迴方法的對比我們能更加清楚的看到快遞排序的分治思想。另外一些比較排序
以下這些排序同樣也是比較排序,但該系列中之前並未提到。
Intro sort
該演算法是一種混合排序演算法,開始於快速排序,當遞迴深度超過基於正在排序的元素數目的水平時便切換到堆排序。它包含了這兩種演算法優良的部分,它實際的效能相當於在典型資料集上的快速排序和在最壞情況下的堆排序。由於它使用了兩種比較排序,因而它也是一種比較排序。
氣泡排序
大家應該多少都聽過氣泡排序(也被稱為下沉排序),它是一個非常基本的排序演算法。反覆地比較相鄰的兩個元素並適當的互換它們,如果列表中已經沒有元素需要互換則表示該列表已經排好序了。
上面的描述中已經體現了比較的過程,因而氣泡排序也是一個比較排序,較小的元素被稱為“泡(Bubble)”,它將“浮”到列表的頂端。
儘管這個演算法非常簡單,但大家應該也聽說了,它真的非常的慢。
氣泡排序的過程如下:
氣泡排序的動畫演示:
其最好情況、最壞情況的執行時間分別是:Θ(n)、Θ(n2)。
程式碼實現:
private static void bubbleSort(int[] a) {
for (int i = 0;i < a.length; i++) {
for (int j = 1; j < a.length - i; j++) {
if (a[j -1] > a[j]) {
int temp = a[j];
a[j] = a[j - 1];
a[j - 1] = temp;
}
}
}
}
奇偶排序
奇偶排序和氣泡排序有很多類似的特點,它通過比較在列表中所有的單雙號索引的相鄰元素,如果有一對是錯誤排序(也就是前者比後者大),那麼將它們交換,之後不斷的重複這一步驟,直到整個列表排好序。
而鑑於此,它的最好情況、最壞情況的執行時間均和氣泡排序相同:Θ(n)、Θ(n2)。
奇偶排序的演示如下:
下面是C++中奇偶排序的示例:
<code class="hljs d has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">template</span> <<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> T> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> OddEvenSort (T a[], <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> n) { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">for</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> i = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span> ; i < n ; i++) { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (i & <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 'i' is odd</span> { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">for</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> j = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span> ; j < n ; j += <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>) { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (a[j] < a[j-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>]) swap (a[j-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>], a[j]) ; } } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">else</span> { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">for</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> j = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span> ; j < n ; j += <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>) { <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (a[j] < a[j-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>]) swap (a[j-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>], a[j]) ; } } } }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li></ul>
雙向氣泡排序
雙向氣泡排序也被稱為雞尾酒排序、雞尾酒調酒器排序、搖床排序、漣漪排序、洗牌排序、班車排序等。(再多再華麗麗的名字也難以彌補它的低效)
和氣泡排序的區別在於它是在兩個方向上遍歷列表進行排序,雖然如此但並不能提高漸近效能,和插入排序比起來也沒太多優勢。
它的最好情況、最壞情況的執行時間均和氣泡排序相同:Θ(n)、Θ(n2)。
排序演算法的下界
我們可以將排序操作進行得多塊?
這取決於計算模型,模型簡單來說就是那些你被允許的操作。
決策樹
決策樹(decision tree)是一棵完全二叉樹,它可以表示在給定輸入規模情況下,其一特定排序演算法對所有元素的比較操作。其中的控制、資料移動等其他操作都被忽略了。
這是一棵作用於3個元素時的插入排序的決策樹。標記為i:j的內部結點表示ai和aj之間的比較。
由於它作用於3個元素,因此共有A33=6種可能的排列。也正因此,它並不具有一般性。
而對序列<a1=7,a2=2,a3=5>和序列<a1=5,a2=9,a3=6>進行排序時所做的決策已經由灰色和黑色粗箭頭指出了。
決策樹排序的下界
如果決策樹是針對n個元素排序,那麼它的高度至少是nlgn。
在最壞情況下,任何比較排序演算法都需要做Ω(nlgn)次比較。
因為輸入資料的Ann種可能的排列都是葉結點,所以Ann≤l,由於在一棵高位h的二叉樹中,葉結點的數目不多於2h,所以有:
n!≤l≤2h
對兩邊取對數:
=> lg2h≥lgn!
=> lg2h=hlg2≥lgn!
又因為:
lg2<1
所以:
n≥lgn!=Ω(nlgn)
因為堆排序和歸併排序的執行時間上界均為O(nlgn),因此它們都是漸近最優的比較排序演算法。