排序算法(二)希爾排序+歸併排序+快速排序+堆排序--O(nlogn)的排序

希爾排序

排序思想:希爾排序可以說是插入排序的一種變種。無論是插入排序還是冒泡排序,如果數組的最大值剛好是在第一位,要將它挪到正確的位置就需要 n - 1 次移動。當原數組的一個元素如果距離它正確的位置很遠的話,需要與相鄰元素交換多次才能到達正確的位置,這樣效率較低。希爾排序就是插入排序排序的一種簡單改進,交換不相鄰的元素以對數組的局部進行排序,以此來提升效率。

排序過程:

  1. 先讓數組中任意間隔爲 h 的元素有序,剛開始 h 的大小可以是 h = n / 2
  2. 接着讓 h = n / 4,讓 h 一直縮小,相當於不斷增大步長,當 h = 1 時,也就是此時數組中任意間隔爲1的元素有序,此時的數組就是有序的了。
package sorting;

import java.util.Scanner;

/**
 * @ClassName HillSort.java
 * @Description 希爾排序
 * @Author ZBW
 * @Date 2020年03月07日 19:20
 **/
public class HillSort {
    public static int[] sort(int array[]) {
        if (array == null || array.length < 2) {
            return array;
        }
        int n = array.length;
        // 對每組間隔爲 h的分組進行排序,剛開始 h = n / 2;
        for (int h = n / 2; h > 0; h /= 2) {
            //對各個局部分組進行插入排序
            for (int i = h; i < n; i++) {
                // 將array[i] 插入到所在分組的正確位置上
                insert(array, h, i);
            }
        }
        return array;
    }

    /**
     * 將array[i]插入到所在分組的正確位置上
     * array[i]] 所在的分組爲 ... array[i-2*h],array[i-h], array[i+h] ...
     */
    private static void insert(int[] array, int h, int i) {
        int temp = array[i];
        int k;
        for (k = i - h; k > 0 && temp < array[k]; k -= h) {
            array[k + h] = array[k];
        }
        array[k + h] = temp;
    }
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("請輸入待排序數據個數:");
        //輸入需要排序的數據個數
        int n = in.nextInt();
        int[] array = new int[n];
        System.out.println("請輸入待排序數據:");
        for (int i = 0; i < n; i++) {
            array[i] = in.nextInt();
        }
        int[] res = sort(array);
        print(res);
    }
    public static void print(int[] array) {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
    }
}

  • 時間複雜度爲O(nlogn),空間複雜度:O(1)
  • 屬於非穩定排序
  • 屬於原地排序

歸併排序

排序思想:將一個大的無序數組有序,我們可以把大的數組分成兩個,然後對這兩個數組分別進行排序,之後在把這兩個數組合併成一個有序的數組。由於兩個小的數組都是有序的,所以在合併的時候是很快的。

注意:在一個歸併排序中,可以將總的數組中n個元素分成logn個層次,每個層次的合併保持在O(n)的複雜度,那麼最後的算法時間複雜度爲O(nlogn)

排序過程:

  1. 通過遞歸的方式將大的數組一直分割,直到數組的大小爲 1,此時只有一個元素,那麼該數組就是有序的了
  2. 再把兩個數組大小爲1的合併成一個大小爲2的,再把兩個大小爲2的合併成4的 …
  3. 直到全部小的數組合並起來。
package sorting;

import java.util.Scanner;

/**
 * @ClassName MergeSort.java
 * @Description 歸併排序遞歸版本
 * @Author ZBW
 * @Date 2020年03月07日 22:18
 **/
public class MergeSort {

    private static int[] sort(int[] array) {
        merge(array, 0, array.length - 1);
        return array;
    }
    //遞歸使用歸併排序,對array[l...r]的範圍進行排序
    private static void merge(int[] array, int l, int r){
        if (l >= r) {
            return;
        }
        int mid = (l + r)/2;
        merge(array, l, mid);
        merge(array, mid + 1, r);
        //此處是一種優化,對於整體數組基本有序時的優化
        if (array[mid] > array[mid+1]) {
            mergeAll(array, l, mid, r);
        }
    }
    //將array[l...mid]和array[mid+1...r]兩部分進行歸併
    private static void mergeAll(int[] array, int l, int mid, int r) {
        int[] temp = new int[r-l+1];
        for (int i = l; i <= r; i++) {
            temp[i-l] = array[i];
        }
        int i = l, j = mid+1;
        for (int k = l; k <= r; k++) {

            if (i > mid) {
                array[k] = temp[j-l];
                j++;
            } else if (j > r) {
                array[k] = temp[i-l];
                i++;
            } else if (temp[i-l] < temp[j-l]) {
                array[k] = temp[i-l];
                i++;
            } else {
                array[k] = temp[j-l];
                j++;
            }
        }
    }

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("請輸入待排序數據個數:");
        //輸入需要排序的數據個數
        int n = in.nextInt();
        int[] array = new int[n];
        System.out.println("請輸入待排序數據:");
        for (int i = 0; i < n; i++) {
            array[i] = in.nextInt();
        }
        int[] res = sort(array);
        print(res);
    }

    public static void print(int[] array) {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
    }
}

  • 分析上面的sort()函數,時間複雜度爲O(nlogn),空間複雜度爲O(n)
  • 屬於穩定排序
  • 屬於非原地排序

上面的是遞歸版本,下面的是使用迭代進行歸併排序的版本:

package sorting;

import java.util.Scanner;

/**
 * @ClassName MergeSort2.java
 * @Description //TODO
 * @Author ZBW
 * @Date 2020年03月08日 20:53
 **/
public class MergeSort2 {

    public static int[] sort(int[] array) {

        for (int size = 1; size <= array.length; size += size) {
            for (int i = 0; i < array.length; i += size + size) {
                mergeAll(array, i, i+size-1, Math.min(i+size+size-1, array.length-1));
            }
        }
        return array;
    }

    private static void mergeAll(int[] array, int l, int mid, int r) {
        int[] temp = new int[r-l+1];
        for (int i = l; i <= r; i++) {
            temp[i-l] = array[i];
        }
        int i = l, j = mid+1;
        for (int k = l; k <= r; k++) {

            if (i > mid) {
                array[k] = temp[j-l];
                j++;
            } else if (j > r) {
                array[k] = temp[i-l];
                i++;
            } else if (temp[i-l] < temp[j-l]) {
                array[k] = temp[i-l];
                i++;
            } else {
                array[k] = temp[j-l];
                j++;
            }
        }
    }

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("請輸入待排序數據個數:");
        //輸入需要排序的數據個數
        int n = in.nextInt();
        int[] array = new int[n];
        System.out.println("請輸入待排序數據:");
        for (int i = 0; i < n; i++) {
            array[i] = in.nextInt();
        }
        int[] res = sort(array);
        print(res);
    }

    public static void print(int[] array) {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
    }
}

快速排序(20世紀對世界影響最大的算法之一)牛掰!

排序思路:
首先我們來看總體的排序過程

  1. 比如說將一個數組中的第一個元素作爲主元,之後,採用雙指針的思想,讓 i = left + 1(此處最外層left即爲0),讓 j = right
  2. 之後 i 和 j 向數組中間進行移動,如果 arr[i] >= 主元,那麼i停止,同理如果 arr[j] <= 主元,那麼 j 停止,此時將 arr[i] 與 arr[j] 進行交換,然後繼續這樣的過程直到 i >= j
  3. 這時,除了主元,在 arr[j] 之前的元素都將小於等於主元,在 arr[j] 之後的元素都將大於等於主元
  4. 此時將 arr[j] 和主元進行交換,就能看到滿意的情況,主元左邊元素均小於主元,右邊元素都大於主元
  5. 這個時候就可以採用分治的思想,對於主元的左右兩部分數組再分別遞歸地進行上述過程
  6. 當數組元素只有一個或者零個時,那麼數組整體就是有序的了。

注意:

  • 快排中最核心的部分就是劃分的partition過程,而且是需要藉助外部空間的
  • 快速排序中partition時主元的選取可以是任意的,不一定必須是第一個元素爲主元,可以選取第一個或者最後一個,也可以利用random隨機生成介於0到數組長度之間的一個整數作爲主元索引,將對應元素作爲主元
    在這裏插入圖片描述
    給出一張圖,可以結合上面的過程,自己動態得畫一畫這個排序得過程,立馬就明白了,下面給出具體代碼,如果還不是很明白,該部分末尾有一篇很優質得文章推薦。
package sorting;

import java.util.Scanner;

/**
 * @ClassName QuickSort.java
 * @Description 快速排序
 * @Author ZBW
 * @Date 2020年03月07日 22:20
 **/
public class QuickSort {
    private static int[] sort(int[] array) {
       quickSort(array, 0, array.length-1);
       return array;
    }
    //對於arr[l....r]部分進行排序
    private static void quickSort(int[] array, int l, int r) {
        if (l >= r) {
            return;
        }
        int p = partition(array, l, r);
        quickSort(array, l, p-1);
        quickSort(array, p+1, r);
    }

    //對array[l...r]部分進行partition操作
    //返回p,使得array[l...p-1] < array[p]; array[p+1...r] > arr[p]
    //partition過程也是整個排序算法最爲核心的部分
    private static int partition(int[] array, int l, int r) {
        int v = array[l];
        int i = l + 1, j = r;
        while (true) {
            //i向右遍歷過程,如果比主元大就停止
            while (i <= j && array[i] <= v) {
                i++;
            }
            //j向左遍歷過程,如果比主元小就停止
            while (i <= j && array[j] >= v) {
                j--;
            }
            if (i >= j) {
                break;
            }
            //對二者進行交換
            int temp = array[j];
            array[j] = array[i];
            array[i] = temp;
        }
        //將arr[j]和主元繼進行交換,這樣主元之前的元素都小於主元,主元后的元素都大於主元
        array[l] = array[j];
        array[j] = v;
        return j;
    }

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("請輸入待排序數據個數:");
        //輸入需要排序的數據個數
        int n = in.nextInt();
        int[] array = new int[n];
        System.out.println("請輸入待排序數據:");
        for (int i = 0; i < n; i++) {
            array[i] = in.nextInt();
        }
        int[] res = sort(array);
        print(res);
    }

    public static void print(int[] array) {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
    }
}

優質文章推薦:別再問我快速排序了

  • 觀察上面的排序過程,時間複雜度爲O(nlogn),空間複雜度爲O(logn)
  • 屬於非穩定排序
  • 屬於原地排序

補充:

  1. 當選定第一個元素爲主元時,當數組基本有序時,每次只會對主元一邊的數組進行分割,這樣,分割次數會邊多,而算法的時間複雜度會退化爲O(n2),但是當對主元進行隨機選取的時候,就不一樣了,它的時間複雜度的期望值就是**O(logn)了,但是注意,只是期望,就是說快速排序退化成O(n2)**複雜度的概率就很低了,上面的代碼在這方面也可以進行優化
  2. 當一個數組中,有很多重複元素,partition操作都容易將數組劃分爲極度不平衡的兩部分,即使我們的主元選擇得很合適,這時候複雜度也會退化爲**O(n2)**的複雜度,上面的是已經進行優化過的版本
  3. 當然,提到了上面的問題,對於大量重複元素存在於數組中的情況,還可以進行三路快速排序,將整個數組劃分爲小於主元等於主元大於主元三部分區域

堆排序

堆:對應的應該是一個樹形結構,比如二叉堆
堆排序:堆排序就是把堆頂的元素與最後一個元素交換,交換之後破壞了堆的特性,我們再把堆中剩餘的元素再次構成一個大頂堆,然後再把堆頂元素與最後第二個元素交換….如此往復下去,等到剩餘的元素只有一個的時候,此時的數組就是有序的了。

在這裏插入圖片描述
二叉堆是一顆完全二叉樹,在堆中某個節點的值總是不大於其父節點的值,堆總是一顆完全二叉樹(最大堆),最小堆與之同理

package sorting;

import java.util.Arrays;
import java.util.Scanner;

/**
 * @ClassName HeapSort.java
 * @Description 堆排序
 * @Author ZBW
 * @Date 2020年03月15日 22:21
 **/
public class HeapSort {
    /**
     *  下沉操作,執行刪除操作相當於把最後
     *  一個元素賦給根元素之後,然後對根元素執行下沉操作
     */
    public static int[] downAdjust(int[] arr, int parent, int length) {
        //臨時保證要下沉的元素
        int temp = arr[parent];
        //定位左孩子節點位置
        int child = 2 * parent + 1;
        //開始下沉
        while (child < length) {
            //如果右孩子節點比左孩子小,則定位到右孩子
            if (child + 1 < length && arr[child] > arr[child + 1]) {
                child++;
            }
            //如果父節點比孩子節點小或等於,則下沉結束
            if (temp <= arr[child]) {
                break;
            }
            //單向賦值
            arr[parent] = arr[child];
            parent = child;
            child = 2 * parent + 1;
        }
        arr[parent] = temp;
        return arr;
    }

    //堆排序
    public static int[] heapSort(int[] arr, int length) {
        //構建二叉堆
        for (int i = (length - 2) / 2; i >= 0; i--) {
            arr = downAdjust(arr, i, length);
        }
        //進行堆排序
        for (int i = length - 1; i >= 1; i--) {
            //把堆頂的元素與最後一個元素交換
            int temp = arr[i];
            arr[i] = arr[0];
            arr[0] = temp;
            //下沉調整
            arr = downAdjust(arr, 0, i);
        }
        return arr;
    }
    //測試
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("請輸入待排序數據個數:");
        //輸入需要排序的數據個數
        int n = in.nextInt();
        int[] array = new int[n];
        System.out.println("請輸入待排序數據:");
        for (int i = 0; i < n; i++) {
            array[i] = in.nextInt();
        }
        array = heapSort(array, array.length);
        System.out.println(Arrays.toString(array));
    }
}

  • 堆排序的時間複雜度爲O(nlogn),空間複雜度爲O(1)
  • 屬於非穩定排序
  • 屬於原地排序
    最後,附上一張圖作爲總結:
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章