文章目錄
前言
- 上節的三個排序算法:冒泡、選擇、插入,較爲簡單,好理解,使用比較、交換的思想。但也都是基礎。
- 這節的三個排序算法:希爾、快速【看註釋比較容易理解思路】、歸併,難理解,使用遞歸的思想。
- 這三個是難點,但也是重點。加油
一、希爾排序
1.1 簡單插入排序存在的問題
我們看簡單的插入排序可能存在的問題.
數組 arr = {2,3,4,5,6,1} 這時需要插入的數 1(最小), 這樣的過程是:
{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}
結論
: 當需要插入的數是較小的數時,後移的次數明顯增多,對效率有影響.
1.2 基本介紹
希爾排序是希爾(Donald Shell)於1959年提出的一種排序算法。希爾排序也是一種插入排序,它是 簡單插入排序 經過改進之後的一個更高效的版本,也稱爲 縮小增量排序
。
1.3 思路分析
1.3.1希爾排序法基本思想
希爾排序是**把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序
;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止
**
1.3.2希爾排序法示意圖
1.4 代碼實現
有一羣小牛, 考試成績分別是 {8,9,1,7,2,3,5,4,6,0} 請從小到大排序. 請分別使用
- 希爾排序時, 對有序序列在插入時採用
交換法
, 並測試排序速度,相對較慢(容易理解) - 希爾排序時, 對有序序列在插入時採用
移動法
, 並測試排序速度,相對較快(不易理解)
package com.feng.ch09_sort;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
/*
* 希爾排序:
* 對插入排序的 進一步優化
* 使用到了 分組,並且縮小增量的 思想。
* 第一次分組,gap(分組的次數)=【數組大小】/2,
* 第二次分組,gap = (第一次分組次數)/2,
* 第三次分組,gap = (上一次分組次數)/2,
*
* 有兩種方式:
* 第一種:交換式
* 裏面使用了冒泡排序的 交換思想
* 效率低,容易理解 80000數據排序使用 14-19 S
*
* 第二種:移位式
* 裏面使用了 簡單插入排序的 插入移位思想
* 效率高,不易理解 8萬數據: 1S , 80萬數據: 1S, 800萬數據:4S
* */
public class S4_ShellSort {
public static void main(String[] args) {
int[] array = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
System.out.println("初始數組:");
System.out.println(Arrays.toString(array));
System.out.println();
System.out.println("排序後");
// deductionShellSortBychange(array);
int[] ints = shellSortByChange(array); // 交換式
System.out.println(Arrays.toString(ints));
// 測試 80000 個數據排序 所用的時間
System.out.println();
System.out.println("測試 80000 個數據 採用希爾排序(交換式) 所用的時間:");
testTime(); // 交互式:19S, 移位式:1S
}
/*
* 測試一下 希爾排序的速度O(n^2), 給 80000 個數據,測試一下
* */
public static void testTime() {
// 創建一個 80000個的隨機的數組
int array2[] = new int[80000];
for (int i = 0; i < 80000; i++) {
array2[i] = (int) (Math.random() * 8000000); // 生成一個[ 0, 8000000] 數
}
// System.out.println(Arrays.toString(array2)); // 不在打印,耗費時間太長
long start = System.currentTimeMillis(); //返回以毫秒爲單位的當前時間
System.out.println("long start:" + start);
Date date = new Date(start); // 上面的也可以不要,但是我想測試
System.out.println("date:" + date);
SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
System.out.println("排序前的時間是=" + format.format(date));
shellSortByChange(array2); // 交互式
// shellSortByMove(array2); // 移位式
System.out.println();
long end = System.currentTimeMillis();
Date date2 = new Date(end); // 上面的也可以不要,但是我想測試
System.out.println("排序後的時間是=" + format.format(date2));
System.out.println("共耗時" + (end - start) + "毫秒");
System.out.println("毫秒轉成秒爲:" + ((end - start) / 1000) + "秒");
}
/*
* 對交換式的希爾排序進行優化 -》》移位法
* */
public static int[] shellSortByMove(int[] array) {
// 增量gap, 並逐步的縮小增量
for (int gap = array.length / 2; gap > 0; gap /= 2) { // 共分 3 組
// 從第 gap 個元素,逐個對其所在的組進行直接插入排序
for (int i = gap; i < array.length; i++) {
// 這裏使用了 前面學習的 直接插入排序
int j = i;
int temp = array[j];
if (array[j] < array[j - gap]) {
while ((j - gap) >= 0 && temp < array[j - gap]) {
//移動
array[j] = array[j - gap];
j -= gap;
}
//當退出while後,就給temp找到插入的位置
array[j] = temp;
}
}
}
return array;
}
/*
* 交換式排序 整合
* */
public static int[] shellSortByChange(int[] array) {
int temporary = 0;
int count = 0;
for (int gap = array.length / 2; gap > 0; gap /= 2) { // 三次分組。
for (int i = gap; i < array.length; i++) { // 有幾組,循環遍歷幾次,也就比較幾次。
// 遍歷各組中所有的元素(共 gap 組,每組有 個元素),步長爲 gap
// 這裏使用了交換:也就是前面學習的 冒泡排序的思想
for (int j = i - gap; j >= 0; j -= gap) {
if (array[j] > array[j + gap]) {
temporary = array[j];
array[j] = array[j + gap];
array[j + gap] = temporary;
}
}
}
// System.out.println("希爾排序第" + (++count) + "輪=" + Arrays.toString(array));
}
return array;
}
/*
* 希爾排序:採用交換式
* 使用逐步推導的方式
* */
public static int[] deductionShellSortBychange(int[] array) {
// 臨時變量,存放交換的數據
int temporary = 0;
/*
* 希爾排序的第 1 輪排序
* */
System.out.println("第 1 輪排序");
// 因爲第 1 輪排序,是將 10 個數據分成了5組 ,所以要比較 5 次, i 從 5 開始
for (int i = 5; i < array.length; i++) { // 共遍歷 5 次,即 比較 5 次
// 遍歷各組中所有的元素(共 5 組,每組有2個元素),步長爲 5
for (int j = i - 5; j >= 0; j -= 5) { // 僅比較一次,走一次邏輯,就知道啦
if (array[j] > array[j + 5]) {
temporary = array[j];
array[j] = array[j + 5];
array[j + 5] = temporary;
}
}
}
System.out.println(Arrays.toString(array));
/*
* 希爾排序的第 2 輪排序
* */
System.out.println("第 2 輪排序");
// 因爲第 1 輪排序,是將 10 個數據分成了 5/2 = 2組
for (int i = 2; i < array.length; i++) {
// 遍歷各組中所有的元素(共 5 組,每組有2個元素),步長爲 5
for (int j = i - 2; j >= 0; j -= 2) {
if (array[j] > array[j + 2]) {
temporary = array[j];
array[j] = array[j + 2];
array[j + 2] = temporary;
}
}
}
System.out.println(Arrays.toString(array));
/*
* 希爾排序的第 3 輪排序
* */
System.out.println("第 3 輪排序");
// 因爲第 3 輪排序,是將 10 個數據分成了 2/2 = 1組
for (int i = 1; i < array.length; i++) {
// 遍歷各組中所有的元素(共 5 組,每組有2個元素),步長爲 5
for (int j = i - 1; j >= 0; j -= 1) {
if (array[j] > array[j + 1]) {
temporary = array[j];
array[j] = array[j + 1];
array[j + 1] = temporary;
}
}
}
System.out.println(Arrays.toString(array));
return array;
}
}
1.5 測試結果
二、快速排序
2.1 基本介紹
快速排序(Quicksort)是對冒泡排序的一種改進。
基本思想是
:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再 按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行
,以此達到整個數據變成有序序列
2.2 思路分析
- 先把數組中的一個數當做基準數,一般會把數組中最左邊的數當做基準數。
- 然後從兩邊進行檢索,先從右邊往左檢索比基準數小的,再從左邊往右檢索比基準數大的。
- 如果檢索到了,就停下,交換這兩個元素。然後繼續檢索。
- 如果這兩個索引指針相遇了,就退出循環。
- 將基準數與這兩個索引遇到的數字進行對換。
- 基準數在這裏就歸位了,左邊的數字都比基準數小,右邊的數字都比基準數大。
- 然後使用遞歸進行排序。
2.3 代碼實現
package com.feng.ch09_sort;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
/*
* 快速排序:
* 對冒泡排序的優化
*
* 時間複雜度:O(nlog2n)
* 速度測試 : 8萬數據: 1S不到, 80萬數據: 1S 不到, 800萬數據:1S不到 ,速度超級快
* */
public class S5_QuickSort {
public static void main(String[] args) {
int[] array = {-9, 78, 0, 23, -567, 70, -1, 900, 4561};
System.out.println("初始數組:");
System.out.println(Arrays.toString(array));
System.out.println();
System.out.println("排序後:");
quickSort(array, 0, array.length - 1);
System.out.println(Arrays.toString(array));
// 測試 80000 個數據排序 所用的時間
System.out.println();
System.out.println("測試 80000 個數據 採用快速排序(交換式) 所用的時間:");
testTime(); // 8萬數據: 1S不到, 80萬數據: 1S 不到, 800萬數據:2-3
}
/*
* 測試一下 快速排序的速度, 給 80000 個數據,測試一下
* */
public static void testTime() {
// 創建一個 80000個的隨機的數組
int array2[] = new int[8000000];
for (int i = 0; i < 8000000; i++) {
array2[i] = (int) (Math.random() * 8000000); // 生成一個[ 0, 8000000] 數
}
// System.out.println(Arrays.toString(array2)); // 不在打印,耗費時間太長
long start = System.currentTimeMillis(); //返回以毫秒爲單位的當前時間
System.out.println("long start:" + start);
Date date = new Date(start); // 上面的也可以不要,但是我想測試
System.out.println("date:" + date);
SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
System.out.println("排序前的時間是=" + format.format(date));
quickSort(array2, 0, array2.length - 1); // 移位式
System.out.println();
long end = System.currentTimeMillis();
Date date2 = new Date(end); // 上面的也可以不要,但是我想測試
System.out.println("排序後的時間是=" + format.format(date2));
System.out.println("共耗時" + (end - start) + "毫秒");
System.out.println("毫秒轉成秒爲:" + ((end - start) / 1000) + "秒");
}
/*
* 快速排序方法:通俗異同,直接看註釋即可
*
* @param array 需要排序的數組
* @param left 最左邊的下標,也是索引
* @param right 最右邊的下標,也是索引
* */
public static void quickSort(int[] array, int left, int right) {
// 進行判斷,如果左邊索引比右邊索引要大,是不合法的,直接使用return 結束這個方法
if (right < left) {
return;
}
// 定義變量保存基準數,這裏的基準數用最左邊的數來代替。
int base = array[left];
// 定義變量 i ,指向最左邊
int i = left;
// 定義變量 j, 指向最右邊
int j = right;
// 當 i 和 j 不相遇的時候,就在循環中進行檢索。
while (i != j) {
// 先 由j 從右往左檢索比基準數小的,如果檢索到比基準數小的就停下
// 如果檢索比基準數大的或者相等的,就繼續檢索
while (array[j] >= base && i<j) {
j--; // j 從右往左移動
}
// i 從左往右檢索。找比基準數 大的,
while (array[i] <= base && i<j) {
i++;// i 從左往右移動
}
// 如果代碼走到這兒,i 和 j 都停下了,。然後交換 i 和 j 位置的元素
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
/*
* 如果上面的 while 循環條件不成立了,會跳出這個循環,往下執行。
* 如果這個條件不成立 說明 i 和 j 相遇了,也就是相等
* 如果 i 和 j 相遇了,就交換基準數這個元素 和相遇位置的元素。
* 把 相遇位置的元素 賦值 給基準數這個位置的元素
* */
array[left] = array[i];
// 把基準數賦值給相遇位置的元素
array[i] = base;
// 基準數在這裏就歸位了,左邊的數字都比基準數小,右邊的數字都比基準數大。
// 對基準數的 左邊進行排序
quickSort(array, left, i-1);
// 排右邊
quickSort(array, j+1, right);
}
}
2.4 測試結果
三、歸併排序
3.1 基本介紹
歸併排序(MERGE-SORT)是利用 歸併
的思想實現的排序方法,該算法採用經典的**分治
**(divide-and-conquer)策略(分治法將問題 分
(divide)成一些小的問題然後遞歸求解,而 治
(conquer)的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。
3.2 思路分析
說明:
可以看到這種結構很像一棵完全二叉樹,本文的歸併排序我們採用遞歸去實現(也可採用迭代的方式去實現)。分階段可以理解爲就是遞歸拆分子序列的過程。
3.3 代碼實現
package com.feng.ch09_sort;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
/*
* 歸併排序
*
* 速度測試: 8萬數據: 117Ms, 80萬數據: 395Ms, 800萬數據:2-3S
* */
public class S6_MergetSort {
public static void main(String[] args) {
int[] array = {8, 4, 5, 7, 1, 3, 6, 2}; // 8-> merge 7次, 80000-> merge 80000-1=7999次
int[] temp = new int[array.length];
System.out.println("原始數組:");
System.out.println(Arrays.toString(array));
System.out.println();
System.out.println("排序後:");
mergetSort(array,0, array.length-1, temp);
System.out.println(Arrays.toString(array));
// 測試 80000 個數據排序 所用的時間
System.out.println();
System.out.println("測試 80000 個數據 採用歸併排序(交換式) 所用的時間:");
testTime(); // 8萬數據: 1S不到, 80萬數據: 1S 不到, 800萬數據:2-3
}
/*
* 測試一下 歸併排序的速度, 給 80000 個數據,測試一下
* */
public static void testTime() {
// 創建一個 80000個的隨機的數組
int array2[] = new int[8000000];
for (int i = 0; i < 8000000; i++) {
array2[i] = (int) (Math.random() * 8000000); // 生成一個[ 0, 8000000] 數
}
// System.out.println(Arrays.toString(array2)); // 不在打印,耗費時間太長
long start = System.currentTimeMillis(); //返回以毫秒爲單位的當前時間
System.out.println("long start:" + start);
Date date = new Date(start); // 上面的也可以不要,但是我想測試
System.out.println("date:" + date);
SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
System.out.println("排序前的時間是=" + format.format(date));
int[] temp = new int[array2.length];
mergetSort(array2,0, array2.length-1, temp); // 移位式
System.out.println();
long end = System.currentTimeMillis();
Date date2 = new Date(end); // 上面的也可以不要,但是我想測試
System.out.println("排序後的時間是=" + format.format(date2));
System.out.println("共耗時" + (end - start) + "毫秒");
System.out.println("毫秒轉成秒爲:" + ((end - start) / 1000) + "秒");
}
/*
* 分 + 合的方法
* */
public static void mergetSort(int[] array, int left, int right, int[] temp) {
if (left < right) {// 數據校驗
int mid = (left + right) / 2; // 中間索引
// 向左遞歸 分解
mergetSort(array, left, mid, temp);
// 向右遞歸 分解
mergetSort(array, mid + 1, right, temp);
// 以上爲分解
// 以下這一句爲合併
merge(array, left, mid, right, temp);
}
}
/*
* 合併的方法
*
* @param array 排序的原始數組
* @param left 左邊有序序列的初始索引
* @param mid 中間索引
* @param right 右邊索引
* @param temp 做中轉的數組
* */
public static void merge(int[] array, int left, int mid, int right, int[] temp) {
// System.out.println("XXXX");
int i = left; // 初始化 i, 左邊有序序列的初始索引
int j = mid + 1; // 初始化j, 右邊有序序列的初始索引
int t = 0; // 指向 temp 數組的 當前索引。
// (一)
// 先把左右兩邊(有序)的數據按照規則填充到 temp數組
// 直到 左右兩邊的有序序列,有一邊處理完畢爲止
while (i <= mid && j <= right) {
/*
* 如果 左邊的有序序列 的當前元素,小於等於右邊有序序列的 當前元素
* 既然左邊的當前元素,填充到 temp 數組
* 然後 t++, i++
* */
if (array[i] <= array[j]) {
temp[t] = array[i];
t += 1;
i += 1;
} else {
// 如果 右邊的有序序列的當前元素 小於 左邊有序序列的當前元素,則將右邊的當前元素填充到 temp數組,然後 t++, j++
temp[t] = array[j];
t += 1;
j += 1;
}
}
/*
* (二)
* 當代碼走到這裏,說明 左邊 或者 右邊 ,已經有一方 有剩餘的。
* 把有 剩餘數據 的一邊的數據 依次全部填充到 temp
* */
// 左邊 有序序列還有剩餘的元素,就全部填充到 temp
while (i <= mid) {
temp[t] = array[i];
t++;
i++;
}
// 右邊 有序序列還有剩餘的元素,就全部填充到 temp
while (j <= right) {
temp[t] = array[j];
t++;
j++;
}
/*
* (三)
* 將 temp 數組的元素拷貝到 array
* 注意,並不是每次都拷貝所有
* */
t = 0;
int tempLeft = left;//
// 第一次 合併: tempLeft = 0, right = 1, // tempLeft = 2, right = 3 // tempLeft = 0, right = 3
// 最後一次,tempLeft = 0 right = 7
// System.out.println("tempLeft=" + tempLeft + ",right=" + right);
while (tempLeft <= right) {
array[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
}