3.1 Heap排序(選擇排序升級版)
選擇排序法的概念簡單,每次從未排序部份選一最小值,插入已排序部份的後端,其時間主要花費於在整個未排序部份尋找最小值,如果能讓搜尋最小值的方式加快,選擇排序法的速率也就可以加快,Heap排序法讓搜尋的路徑由樹根至最後一個樹葉,而不是整個未排序部份,因而稱之爲改良的選擇排序法。
3.1.1 解析過程
Heap排序法使用Heap Tree(堆積樹),樹是一種資料結構,而堆積樹是一個二元樹,也就是每一個父節點最多隻有兩個子節點(關於樹的詳細定義還請見資料結構書籍),堆積樹的 父節點若小於子節點,則稱之爲最小堆積(Min Heap),父節點若大於子節點,則稱之爲最大堆積(Max Heap),而同一層的子節點則無需理會其大小關係,例如下面就是一個堆積樹:
可以使用一維陣列來儲存堆積樹的所有元素與其順序,爲了計算方便,使用的起始索引是1而不是0,索引1是樹根位置,如果左子節點儲存在陣列中的索引爲s,則其父節點的索引爲s/2,而右子節點爲s+1,就如上圖所示,將上圖的堆積樹轉換爲一維陣列之後如下所示:
首先必須知道如何建立堆積樹,加至堆積樹的元素會先放置在最後一個樹葉節點位置,然後檢查父節點是否小於子節點(最小堆積),將小的元素不斷與父節點交換,直到滿足堆積樹的條件爲止,例如在上圖的堆積加入一個元素12,則堆積樹的調整方式如下所示:
建立好堆積樹之後,樹根一定是所有元素的最小值,您的目的就是:
- 將最小值取出
- 然後調整樹爲堆積樹
不斷重複以上的步驟,就可以達到排序的效果,最小值的取出方式是將樹根與最後一個樹葉節點交換,然後切下樹葉節點,重新調整樹爲堆積樹,如下所示:
調整完畢後,樹根節點又是最小值了,於是我們可以重覆這個步驟,再取出最小值,並調整樹爲堆積樹,如下所示:
如此重覆步驟之後,由於使用一維陣列來儲存堆積樹,每一次將樹葉與樹根交換的動作就是將最小值放至後端的陣列,所以最後陣列就是變爲已排序的狀態。
其實堆積在調整的過程中,就是一個選擇的行爲,每次將最小值選至樹根,而選擇的路徑並不是所有的元素,而是由樹根至樹葉的路徑,因而可以加快選擇的過程, 所以Heap排序法纔會被稱之爲改良的選擇排序法。
3.1.2 代碼執行
package com.gavinbj.leetcode.classic;
import java.util.Arrays;
public class SortUtils {
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] number = new int[10];
for (int i = 0; i < 10; i++) {
number[i] = (int) (Math.random() * 100);
}
heapSort(number);
}
public static void heapSort(int[] number) {
int[] tmp = new int[number.length + 1];
// 構建一個存儲數組
for (int i = 1; i < tmp.length; i++) {
tmp[i] = number[i - 1];
}
createHeap(tmp);
System.out.println("初始化堆數組:" + Arrays.toString(number));
int m = number.length;
while (m > 1) {
swap(tmp, 1, m);
m--;
int p = 1;
int s = 2 * p;
while (s <= m) {
if (s < m && tmp[s + 1] < tmp[s]) {
s++;
}
if (tmp[p] <= tmp[s]) {
break;
}
swap(tmp, p, s);
p = s;
s = 2 * p;
}
}
// 這邊將排序好的臨時數組設定回原數組
for (int i = 0; i < number.length; i++) {
number[i] = tmp[i + 1];
}
System.out.println("高級選擇排序(HeapSort)結果:" + Arrays.toString(number));
}
/**
* A:初始化堆數組
*
* @param tmp
*/
private static void createHeap(int[] tmp) {
int[] heap = new int[tmp.length];
for (int i = 0; i < heap.length; i++) {
heap[i] = -1;
}
for (int i = 1; i < heap.length; i++) {
heap[i] = tmp[i];
int s = i;
int p = i / 2;
while (s >= 2 && heap[p] > heap[s]) {
swap(heap, p, s);
s = p;
p = s / 2;
}
}
for (int i = 1; i < tmp.length; i++) {
tmp[i] = heap[i];
}
}
private static void swap(int[] number, int i, int j) {
int t;
t = number[i];
number[i] = number[j];
number[j] = t;
}
}
3.2 Shell排序(插入排序升級版)
插入排序法由未排序的後半部前端取出一個值,插入已排序前半部的適當位置,概念簡單但速度不快。排序要加快的基本原則之一,是讓後一次的排序進行時,儘量利用前一次排序後的結果,以加快排序的速度,Shell排序法即是基於此一概念來改良插入排序法。
3.2.1 解析過程
Shell排序法最初是D.L Shell於1959所提出,假設要排序的元素有n個,則每次進行插入排序時並不是所有的元素同時進行時,而是取一段間隔。Shell首先將間隔設定爲n/2,然後跳躍進行插入排序,再來將間隔n/4,跳躍進行排序動作,再來間隔設定爲n/8、n/16,直到間隔爲1之後的最 後一次排序終止,由於上一次的排序動作都會將固定間隔內的元素排序好,所以當間隔越來越小時,某些元素位於正確位置的機率越高,因此最後幾次的排序動作將 可以大幅減低。
舉個例子來說,假設有一未排序的數字如右:89 12 65 97 61 81 27 2 61 98
數字的總數共有10個,所以第一次我們將間隔設定爲10 / 2 = 5,此時我們對間隔爲5的數字進行排序,如下所示:
畫線連結的部份表示 要一起進行排序的部份,再來將間隔設定爲5 / 2的商,也就是2,則第二次的插入排序對象如下所示:
再來間隔設定爲2 / 2 = 1,此時就是單純的插入排序了,由於大部份的元素都已大致排序過了,所以最後一次的插入排序幾乎沒作什麼排序動作了:
將間隔設定爲n/2是D.L Shell最初所提出,在教科書中使用這個間隔比較好說明,然而Shell排序法的關鍵在於間隔的選定,不同的間隔選定法可以將Shell排序法的速度再加快。
3.2.2 代碼執行
package com.gavinbj.leetcode.classic;
import java.util.Arrays;
public class SortUtils {
public static void main(String[] args) {
int[] number = new int[10];
for (int i = 0; i < 10; i++) {
number[i] = (int) (Math.random() * 100);
}
shellSort(number);
System.out.println("ShellSort結果:" + Arrays.toString(number));
}
/**
* Shell排序,改進版插入排序
* @param number
*/
public static void shellSort(int[] number) {
int gap = number.length / 2;
while (gap > 0) {
for (int k = 0; k < gap; k++) {
for (int i = k + gap; i < number.length; i += gap) {
for (int j = i - gap; j >= k; j -= gap) {
if (number[j] > number[j + gap]) {
swap(number, j, j + gap);
} else
break;
}
}
}
gap /= 2;
}
}
private static void swap(int[] number, int i, int j) {
int t;
t = number[i];
number[i] = number[j];
number[j] = t;
}
}
3.3 Shaker排序(冒泡排序升級版)
Shaker排序又叫雞尾酒排序或雙向冒泡排序,它是冒泡排序的一種輕微改進。與冒泡排序相同,Shaker排序也是一種穩定排序算法。不同的是,普通的冒泡排序算法僅是從低到高比較序列裏的每個元素,或者說普通的冒泡排序算法只能每次從前向後按一個次序進行遍歷,而Shaker排序方法每次遍歷卻包括兩個方向,先從前向後再從後向前,在從前往後遍歷後能記錄最後發生交換的兩個元素位置,從後往前遍歷時就從這個位置開始。這種雙向交替比較不僅可以使小的浮上水面,同時也會使大的沉倒水底,因而較普通的冒泡算法在效率上有所改進。
3.3.1 解析過程
在Shaker排序法中,具有n個待排序元素的數據表將分n/2個階段來完成排序操作。每個階段的Shaker排序都包括一個從左向右的遍歷和一個從右向左的遍歷。在遍歷過程中會將兩個相鄰元素進行比較,如果它們的順序是非正常的就將它們做交換。
傳統的冒泡排序的交換的動作並不會一直進行至數組的最後一個,而是會進行至MAX-i-1,所以排序的過程中,數組右方排序好的元素會一直增加,使得左邊排序的次數逐漸減少,方括號括住的部份表示已排序完畢。如例所示:
排序前:95 27 90 49 80 58 6 9 18 50
- 27 90 49 80 58 6 9 18 50 [95] 95浮出
- 27 49 80 58 6 9 18 50 [90 95] 90浮出
- 27 49 58 6 9 18 50 [80 90 95] 80浮出
- 27 49 6 9 18 50 [58 80 90 95] …
- 27 6 9 18 49 [50 58 80 90 95] …
- 6 9 18 27 [49 50 58 80 90 95] …
- 6 9 18 [27 49 50 58 80 90 95]
Shaker排序也使用如上概念,如果讓左邊的元素也具有這樣的性質,讓左右兩邊的元素都能先排序完成,如此未排序的元素會集中在中間,由於左右兩邊同時排序,中間未排序的部份將會很快的減少。方法就在於氣泡排序的雙向進行,先讓氣泡排序由左向右進行,再來讓氣泡排序由右往左進行,如此完成一次排序的動作,而您必須使用left與right兩個標識來記錄左右兩端已排序的元素位置。
Shaker排序的例子如下所示:
排序前:45 19 77 81 13 28 18 19 77 11
- 往右排序:19 45 77 13 28 18 19 77 11 [81]
向左排序:[11] 19 45 77 13 28 18 19 77 [81] - 往右排序:[11] 19 45 13 28 18 19 [77 77 81]
向左排序:[11 13] 19 45 18 28 19 [77 77 81] - 往右排序:[11 13] 19 18 28 19 [45 77 77 81]
向左排序:[11 13 18] 19 19 28 [45 77 77 81] - 往右排序:[11 13 18] 19 19 [28 45 77 77 81]
向左排序:[11 13 18 19 19] [28 45 77 77 81]
如上所示,括號中表示左右兩邊已排序完成的部份,當left > right時,則排序完成。
3.3.2 代碼執行
package com.gavinbj.leetcode.classic;
import java.util.Arrays;
public class SortUtils {
public static void main(String[] args) {
int[] number = new int[10];
for (int i = 0; i < 10; i++) {
number[i] = (int) (Math.random() * 100);
}
shakerSort(number);
System.out.println("ShakerSort結果:" + Arrays.toString(number));
}
/**
* Shaker排序(改良版冒泡排序)
* @param number
*/
public static void shakerSort(int[] number) {
int i, left = 0, right = number.length - 1, shift = 0;
while (left < right) {
// 向右進行氣泡排序
for (i = left; i < right; i++) {
if (number[i] > number[i + 1]) {
swap(number, i, i + 1);
shift = i;
}
}
right = shift;
// 向左進行氣泡排序
for (i = right; i > left; i--) {
if (number[i] < number[i - 1]) {
swap(number, i, i - 1);
shift = i;
}
}
left = shift;
}
}
private static void swap(int[] number, int i, int j) {
int t;
t = number[i];
number[i] = number[j];
number[j] = t;
}
}