數據結構與算法 整理筆記---O(nlogn)的排序算法

本系列文章爲慕課網相關課程筆記整理java

O(nlog)級別的排序算法

1 歸併排序

歸併排序(Merge)是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分爲若干個子序列,每一個子序列是有序的。而後再把有序子序列合併爲總體有序序列。git

歸併排序是創建在歸併操做上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個很是典型的應用。 將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲2-路歸併。歸併排序算法穩定,數組須要O(n)的額外空間,鏈表須要O(log(n))的額外空間,時間複雜度爲O(nlog(n)),算法不是自適應的,不須要對數據的隨機讀取。github

實現原理:算法

  1. 申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列;
  2. 設定兩個指針變量,最初位置分別指向兩個已經排好序的數組序列的起始位置;
  3. 比較兩個指針所指向的元素,選擇相對小的元素放入到申請的合併空間裏,並移動此指針到下一位置;
  4. 繼續重複步驟3直到某一指針達到序列尾;
  5. 當一個指針到達一個序列尾時,將另外一序列剩下的全部元素直接複製到合併序列尾。

1.1 第一個版本

public class MergeSort {
    public static void sort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return ;
        }

        int n = arr.length;
        mergeSort(arr, 0, n - 1);
    }

    public static void mergeSort(int[] arr, int low, int high) {
        if (low >= high) {
            return ;
        }

        int mid = low + (high - low) / 2;

        mergeSort(arr, low, mid);
        mergeSort(arr, mid + 1, high);
        merge(arr, low, mid, high);
    }

    public static void merge(int[] arr, int low, int mid, int high) {
        if (arr == null || arr.length == 0) {
            return ;
        }

        int n = arr.length;
        int[] aux = Arrays.copyOfRange(arr, 0, n + 1);

        int i = low, j = mid + 1;
        for (int k = low; k <= high; k++) {

            if (i > mid) {
                //[low, mid]已經處理完了
                arr[k] = aux[j - low];
                j++;
            } else if (j > high) {
                //[mid + 1, high]已經處理完了 
                arr[k] = aux[i - low];
            } else if (aux[i - low] < aux[j - low]) {
                arr[k] = arr[i - low];
                i++;
            } else {
                arr[k] = arr[j - low];
                j++;
            }
        }
    }
}

性能分析:
在近乎有序的數組排序時,效率較低數組

1.2 優化後的版本

方向 :dom

  1. 什麼時候不須要歸併?
    當arr[mid] <= arr[mid + 1]時,事實上是不須要歸併操做的,所以能夠增長判斷,arr[mid] > arr[mid + 1]時才歸併,注意此時並不能將歸併排序的時間複雜度降至O(n)級別(而插入排序能夠作到)。
  2. 是否有必要遞歸到底?
    能夠在數組長度小於某個閾值後,使用插入排序。
public static void mergeSort(int[] arr, int low, int high) {
        //優化點
        if (high - low <= 15) {
            insertionSort(arr, low, high);
            return ;
        }

        int mid = low + (high - low) / 2;

        mergeSort(arr, low, mid);
        mergeSort(arr, mid + 1, high);
        //優化點
        if (arr[mid] > arr[mid + 1]) {
            merge(arr, low, mid, high);
        }    
    }

1.3 自底向上的歸併 排序

public class MergeSortBU {
    public static void sort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return ;
        }

        int n = arr.length;

        for (int sz = 1; sz <= n; sz += sz) {
            for (int i = 0; i + sz < n; i += sz + sz) {
                //對arr[i...i+sz-1] 和 arr[i+sz...i+sz+sz-1]進行歸併
                //注意數組越界問題
                //1. 保證[i+sz,i+sz+sz-1]存在,則,i+sz < n
                //2. 保證i+sz+sz-1不越界
                //System.out.println("排序中:sz = " + sz +", i = " + i);
                //System.out.println("排序區間爲: [" + i + ", " + (i + sz - 1) + 
                //    "], 以及[" + (i + sz) + ", " + (i + sz + sz -1) + "].");
                if (arr[i + sz - 1] > arr[i + sz]) {
                    merge(arr, i, i + sz - 1, Math.min(i + sz + sz - 1, n - 1));
                }
            }
        }
    }

    public static void merge(int[] arr, int low, int mid,int high) {
        if (arr == null || arr.length == 0) {
            return;
        }

        int n = arr.length;

        int[] aux = Arrays.copyOfRange(arr, 0, n - 1);
        int i = low, j = mid + 1;

        for (int k = low; k <= high; k++) {
            if (i > mid) {
                arr[k] = aux[j - low];
                j++;
            } else if (j > high) {
                arr[k] = aux[i - low];
                i++;
            } else if (arr[i] < arr[j]) {
                arr[k] = aux[i - low];
            } else {
                arr[k] = aux[j - low];
            }
        }
    }
}

問題解答:ide

  1. 在sz double以前,是否數組中有一部分元素沒法被處理到?
    在內層循環中解決了,sz += sz以前,i會對[i, i+sz-1], [i+sz,Math.min(i+sz+sz-1, n-1]這兩部分進行處理,所以不會有元素不被處理到的情形
  2. 歸併時如何解決問題區間a[i, i+sz-1]與區間b[i+sz,Math.min(i+sz+sz-1, n-1]二者長度不一致,是否能夠歸併?
    能夠的,每次歸併時,會對比兩個區間中數據的大小,直到有個區間先處理完爲止,與區間長度無關。

2 快速排序

快速排序是C.R.A.Hoare於1962年提出的一種劃分交換排序。它採用了一種分治的策略,一般稱其爲分治法(Divide-and-ConquerMethod)。性能

該方法的基本思想是:測試

  1. 先從數列中取出一個數做爲基準數。
  2. 分區過程,將比這個數大的數全放到它的右邊,小於或等於它的數全放到它的左邊。
  3. 再對左右區間重複第二步,直到各區間只有一個數。

2.1 第一個版本

public static void sort(int[] arr) {
    if (arr == null || arr.length == 0) {
        return;
    }
    int n = arr.length;
    quickSort(arr, 0, n - 1);
}

public static void quickSort(int[] arr, int low, int high) {
    if (low >= high) {
        return;
    }

    int p = partition(arr, low, high);
    quickSort(arr, low, p - 1);
    quickSort(arr, p + 1, high);
}

public static int partition(int[] arr, int low, int high) {
    int v = arr[low];

    //arr[low + 1....j] < v ; arr[j + 1...i] > v
    int j = low;
    for (int i = low + 1; i < high; i++) {
        if (arr[i] < v) {
            swap(arr, j + 1, i);
            j++;
        }
    }
    swap(arr, low, j);

    return j;
}

//最後一次展現swap方法
private static void swap(int[] arr, int i, int j) {
    int t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
}

測試時,若是數據量較大,會報java.lang.StackOverflowError,由於遞歸太多,棧內存不夠用了。優化

2.2 優化版本

方向:

  1. 數據量較小後轉化爲插入排序
  2. 在近乎有序的排序中,快排的遞歸樹的平衡度比歸併的遞歸樹的平衡度要差不少,且樹的深度可能不是logn,最壞狀況是待排序數組徹底有序,此時快排的時間複雜度退化爲O(n^2),所以在partition過程當中,選擇pivot值時的策略爲隨機選擇是很好的解決方案。

    public static void quickSort(int[] arr, int low, int high) {
        if (high - low < 16) {
            insertSort(arr, low, high);
        }
    
        int p = partition(arr, low, high);
        quickSort(arr, low, p - 1);
        quickSort(arr, p + 1, high);
    }
    
    public static int partition(int[] arr, int low, int high) {
        long seed = System.nanoTime();
        Random random = new Random(seed);
    
        int randomPos = random.nextInt(high - low + 1) + low;
        swap(arr, low, randomPos);
    
        int v = arr[low];
    
        int j = low;
        for (int i = low + 1; i < high; i++) {
            if (arr[i] < v) {
                swap(arr, j + 1, i);
                j++;
            }
        }
        swap(arr, low, j);
    
        return j;
    }
  3. 若是數組中包含大量相同元素,上述快排依舊會退化成O(n^2)。能夠修改patition過程當中,根據pivot值分組的處理算法。
public static int partition(int[] arr, int low, int high) {
    long seed = System.nanoTime();
    Random random = new Random(seed);

    int randomPos = random.nextInt(high - low + 1) + low;
    swap(arr, low, randomPos);
    int v = arr[low];

    int i = low + 1, j = high;
    while (true) {
        while (i < high && arr[i] < v) {
            i++;
        }

        while (j > low && arr[j] > v) {
            j--;
        }

        if (i > j) {
            break;
        }

        swap(arr, i, j);
        i++;
        j--;
    }

    return j;
}

我寫得最6的快排

public static int partition(int[] arr, int low, int high) {
    long seed = System.nanoTime();
    Random random = new Random(seed);

    int randomPos = random.nextInt(high - low + 1) + low;
    swap(arr, low, randomPos);

    int v = arr[low];

    while (low < high) {
        while (low < high && arr[high] >= v) {
            high--;
        }
        arr[low] = arr[high];
        while (low < high && arr[high] <= v) {
            low++;
        }
        arr[high] = arr[low];
    }

    arr[low] = v;
    return low;
}

2.3 三路快排

若是存在大量相同元素,三路快排性能更佳!

三路快排中途發展圖示
default

三路快排結束時圖示
default

import java.util.Random;

public class QuickSort3Ways {
    public static void sort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        int n = arr.length;
        quickSort3Ways(arr, 0, n - 1);
    }

    public static void quickSort3Ways(int[] arr, int low, int high) {
        if (low >= high) {
            return;
        }

        long seed = System.nanoTime();
        Random random = new Random(seed);

        int pos = random.nextInt(high - low + 1) + low;
        swap(arr, low, pos);

        int v = arr[low];

        int lt = low; //arr[low+1...lt] < v 初始區間爲空
        int gt = high + 1;// arr[gt...high] > v 初始區間爲空
        int i = low + 1; // arr[lt+1...i] = v 初始區間爲空

        while (i < gt) {
            if (arr[i] > v) {
                swap(arr, i, gt - 1);
                gt--;
            } else if (arr[i] < v) {
                swap(arr, lt + 1, i);
                lt++;
                i++;
            } else {
                i++;
            }
        }

        swap(arr, lt, low);
        quickSort3Ways(arr, low, lt - 1);
        quickSort3Ways(arr, gt, high);
    }
}

核心在於三個區間的邊界肯定,結合上述兩幅圖示好好理解。

思考題:

  1. 求一組數的逆序對
  2. 用O(n)級別的算法求解第n大的元素
相關文章
相關標籤/搜索