【經典算法】N003:排序三劍客升級版(Heap、Shell、Shaker)

3.1 Heap排序(選擇排序升級版)

選擇排序法的概念簡單,每次從未排序部份選一最小值,插入已排序部份的後端,其時間主要花費於在整個未排序部份尋找最小值,如果能讓搜尋最小值的方式加快,選擇排序法的速率也就可以加快,Heap排序法讓搜尋的路徑由樹根至最後一個樹葉,而不是整個未排序部份,因而稱之爲改良的選擇排序法。

3.1.1 解析過程

Heap排序法使用Heap Tree(堆積樹),樹是一種資料結構,而堆積樹是一個二元樹,也就是每一個父節點最多隻有兩個子節點(關於樹的詳細定義還請見資料結構書籍),堆積樹的 父節點若小於子節點,則稱之爲最小堆積(Min Heap),父節點若大於子節點,則稱之爲最大堆積(Max Heap),而同一層的子節點則無需理會其大小關係,例如下面就是一個堆積樹:
在這裏插入圖片描述
可以使用一維陣列來儲存堆積樹的所有元素與其順序,爲了計算方便,使用的起始索引是1而不是0,索引1是樹根位置,如果左子節點儲存在陣列中的索引爲s,則其父節點的索引爲s/2,而右子節點爲s+1,就如上圖所示,將上圖的堆積樹轉換爲一維陣列之後如下所示:
在這裏插入圖片描述
首先必須知道如何建立堆積樹,加至堆積樹的元素會先放置在最後一個樹葉節點位置,然後檢查父節點是否小於子節點(最小堆積),將小的元素不斷與父節點交換,直到滿足堆積樹的條件爲止,例如在上圖的堆積加入一個元素12,則堆積樹的調整方式如下所示:
在這裏插入圖片描述
建立好堆積樹之後,樹根一定是所有元素的最小值,您的目的就是:

  1. 將最小值取出
  2. 然後調整樹爲堆積樹

不斷重複以上的步驟,就可以達到排序的效果,最小值的取出方式是將樹根與最後一個樹葉節點交換,然後切下樹葉節點,重新調整樹爲堆積樹,如下所示:
在這裏插入圖片描述
調整完畢後,樹根節點又是最小值了,於是我們可以重覆這個步驟,再取出最小值,並調整樹爲堆積樹,如下所示:
在這裏插入圖片描述

如此重覆步驟之後,由於使用一維陣列來儲存堆積樹,每一次將樹葉與樹根交換的動作就是將最小值放至後端的陣列,所以最後陣列就是變爲已排序的狀態。

其實堆積在調整的過程中,就是一個選擇的行爲,每次將最小值選至樹根,而選擇的路徑並不是所有的元素,而是由樹根至樹葉的路徑,因而可以加快選擇的過程, 所以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;
	}

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章