數據結構與算法學習十二:希爾排序、快速排序(遞歸、好理解)、歸併排序(遞歸、難理解)

前言

  • 上節的三個排序算法:冒泡、選擇、插入,較爲簡單,好理解,使用比較、交換的思想。但也都是基礎。
  • 這節的三個排序算法:希爾、快速【看註釋比較容易理解思路】、歸併,難理解,使用遞歸的思想。
  • 這三個是難點,但也是重點。加油

一、希爾排序

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} 請從小到大排序. 請分別使用

  1. 希爾排序時, 對有序序列在插入時採用 交換法 , 並測試排序速度,相對較慢(容易理解)
  2. 希爾排序時, 對有序序列在插入時採用 移動法 , 並測試排序速度,相對較快(不易理解)
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 思路分析

  1. 先把數組中的一個數當做基準數,一般會把數組中最左邊的數當做基準數。
  2. 然後從兩邊進行檢索,先從右邊往左檢索比基準數小的,再從左邊往右檢索比基準數大的。
  3. 如果檢索到了,就停下,交換這兩個元素。然後繼續檢索。
  4. 如果這兩個索引指針相遇了,就退出循環。
  5. 將基準數與這兩個索引遇到的數字進行對換。
  6. 基準數在這裏就歸位了,左邊的數字都比基準數小,右邊的數字都比基準數大。
  7. 然後使用遞歸進行排序。

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++;
        }

    }


}

3.4 測試結果

在這裏插入圖片描述

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