主流排序演算法全面解析

煩囂的人發表於2019-08-07

以下如無特殊說明都是按照升序進行排序。
原始碼見最下方

比較類排序

交換排序

氣泡排序

定義

是一種簡單的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端--維基百科。

思想

氣泡排序很簡單,顧名思義每輪迴圈中將一個最大/最小的數通過交換一路到陣列頂部。

程式碼

public class BubbleSort {

    public static void main(String[] args) {
        int[] arr = {4, 12, 2, 8, 453, 1, 59, 33};
        for (int i = 0, length = arr.length; i < arr.length - 1; i++) {
            for (int j = 0, tempLength = length - 1 - i; j < tempLength; j++) {
                //如果當前數大於下一個數那麼和下一個數交換位置
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
            System.out.println(Arrays.toString(arr));
        }
    }
}

快速排序

定義

快速排序(Quicksort)是對氣泡排序的一種改進。由 C. A. R. Hoare 在 1960 年提出。它的基本思想是:通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列--百度百科。

思想

  1. 使用了分治的思想,先取一個數作為基數(一般選第一個數),然後將這個數移動到一個合適的位置使左邊的都比它小,右邊的都比他大
  2. 遞迴處理這個數左邊的數和右邊的數,直到所有的數都有序。直到所有的數都有序

程式碼

public class QuickSort {

    private static void deal(Integer[] arr, int start, int end) {
        if (start >= end) {
            return;
        }
        int base = arr[start], i = start, j = end;
        while (i < j) {
            //在右邊找一個比基數小的數,直到i,j相等
            while (arr[j] >= base && j > i) {
                j--;
            }

            //在左邊找一個比基數大的數,直到i,j相等
            while (arr[i] <= base && j > i) {
                i++;
            }
            //如果ij不相等,交換其值
            if (i < j) {
                ArrayUtil.swap(arr, i++, j--);
            }
        }
        //此時i等於j,交換基數和i/j,使左邊的數小於等於基數,右邊的數大於等於基數
        if (start != i) {
            ArrayUtil.swap(arr, start, i);
        }
        deal(arr, start, i - 1);
        deal(arr, j + 1, end);
    }

    public static void main(String[] args) {
        Integer[] arr = {1, 43, 2, 7, 5, 6, 555, 200, 21};
        deal(arr, 0, arr.length - 1);
        System.out.println("結果" + Arrays.toString(arr));
    }
}

插入排序

簡單插入排序

是一種簡單直觀的排序演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用 in-place 排序,因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間--維基百科。

思想

插入排序的思想很簡單直接:

  1. 從第一個元素開始,該元素可以認為已經被排序
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描
  3. 如果該元素(已排序)大於新元素,將該元素移到下一位置
  4. 重複步驟 3,直到找到已排序的元素小於或者等於新元素的位置
  5. 將新元素插入到該位置後
  6. 重複步驟 2~5

動圖如下:

主流排序演算法全面解析

程式碼

public class InsertSort {

    public static void sort(Integer[] arr) {
        for (int i = 0, length = arr.length; i < length; i++) {
            //有序部分從後向前比較,直到找到合適的位置
            int j = i, temp = arr[i];
            //如果arr[j-1]<=temp,說明arr[j]需為temp,否則將arr[j-1]向後移動一位
            for (; j > 0 && temp < arr[j - 1]; j--) {
                arr[j] = arr[j - 1];
            }
            arr[j] = temp;
            System.out.println("當前陣列狀態為:" + Arrays.toString(arr));
        }
    }

    public static void main(String[] args) {
        Integer[] arr = {1, 65, 32, 12, 21};
        InsertSort.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

希爾排序

定義

希爾排序,也稱遞減增量排序演算法,是插入排序的一種更高效的改進版本。希爾排序是非穩定排序演算法。

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的資料操作時,效率高,即可以達到線性排序的效率
  • 但插入排序一般來說是低效的,因為插入排序每次只能將資料移動一位

思想

  1. 取一個小於陣列長度 n 的整數 n1,將所有間隔為 n1 的數分成一組,對各組進行直接插入排序.然後 n=n1;
  2. 重複上述操縱,直到 n1=1 進行一次完整的插入排序後結束。

程式碼

public class ShellSort {

    public static void sort(Integer[] arr) {
        int n1 = arr.length / 2;
        // 也可將do/while替換成尾遞迴
        do {
            //共n1組資料需要進行直接插入排序
            for (int start = 0; start < n1; start++) {
                //對一組執行插入排序,第一個數為arr[start],增量為n1
                for (int i = start; i < arr.length; i += n1) {
                    int j = i, temp = arr[i];
                    for (; j > start && temp < arr[j - n1]; j -= n1) {
                        arr[j] = arr[j - n1];
                    }
                    arr[j] = temp;
                }
            }
            n1 /= 2;
        } while (n1 >= 1);
    }

    public static void main(String[] args) {
        Integer[] arr = {1, 65, 32, 12, 21};
        ShellSort.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

選擇排序

簡單選擇排序

定義

是一種簡單直觀的排序演算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

思想

簡單選擇排序顧名思義,每次從無序部分選出一個最大的數,和無序部分的最後一個值交換,重複 n-1 次後所有的值都變成有序狀態.

動畫如下:

主流排序演算法全面解析

程式碼

public class SimpleSelectSort {

    public static void sort(Integer[] arr) {
        int length = arr.length;
        for (int i = 0; i < length - 1; i++) {
            int maxIndex = 0;
            for (int j = 1; j < length - i; j++) {
                if (arr[j] > arr[maxIndex]) {
                    maxIndex = j;
                }
            }
            ArrayUtil.swap(arr, maxIndex, length - i);
        }
    }

    public static void main(String[] args) {
        Integer[] arr = {1, 65, 32, 12, 21};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

堆排序

定義

堆排序(英語:Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子節點的鍵值或索引總是小於(或者大於)它的父節點。

不瞭解的可以看看這篇,翻譯的挺好的。

思想

  1. 構建大頂堆(升序用大頂堆,降序用小頂堆) ,i=arr.lengh-1,n=arr.lengh
  2. 將 arr[0]和 arr[i]互換,
  3. i--
  4. 重新將 arr 0 到 i 構建為大頂堆
  5. 重複 2,3,4 直到 i=1

構建大頂堆過程如下:

  • 從最後一個非葉子節點開始從下往上進行調整。
  • 將該節點的值調整為 max(節點值,直接子節點值),注意如果產生了交換操作還要調整被交換節點,讓其也是 max(節點值,直接子節點值),直到被交換節點無子節點

程式碼

public class HeapSort {


    private static void sort(Integer[] arr) {
        int n = arr.length;
        //構建大頂堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            adjustHeap(arr, i, n);
        }
        //排序
        for (int i = n - 1; i > 0; i--) {
            ArrayUtil.swap(arr, 0, arr[i]);
            adjustHeap(arr, 0, i);
        }
    }

    /**
     * Description: 調整堆
     *
     * @param arr    陣列
     * @param index  調整index處的對結構
     * @param length 堆大小
     * @author fanxb
     * @date 2019/7/31 19:50
     */
    private static void adjustHeap(Integer[] arr, int index, int length) {
        if (index >= length) {
            return;
        }
        int maxIndex = index;
        for (int i = 2 * index + 1; i < length - 1 && i <= 2 * index + 2; i++) {
            if (arr[maxIndex] < arr[i]) {
                maxIndex = i;
            }
        }
        //如果進行了交換,還要調整被交換節點
        if (maxIndex != index) {
            ArrayUtil.swap(arr, maxIndex, index);
            adjustHeap(arr, maxIndex, length);
        }
    }

    public static void main(String[] args) {
        Integer[] arr = {1, 65, 32, 334, 12, 21, 65, 112, 444443};
        ShellSort.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

歸併排序

二路歸併

定義

歸併排序(英語:Merge sort,或 mergesort),是建立在歸併操作上的一種有效的排序演算法,效率為主流排序演算法全面解析。1945 年由約翰·馮·諾伊曼首次提出。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用,且各層分治遞迴可以同時進行。--維基百科

思想

歸併排序的核心思想是將兩個有序的陣列合併成一個大的陣列,這個過程稱為 1 次歸併。

一次歸併過程如下(arr1,arr2 兩個有序陣列,arr3 存放排序後的陣列,i=0,j=0,k=0):

  1. 如果arr1[i]<=arr2[j],那麼arr3[k]=arr1[i]i++,k++;否則 arr3[k]=arr2[j]j++,k++;

  2. 重複 1,直到某個有序陣列全部加入到 arr3 中,然後將另外一個陣列剩餘的部分加到 arr3 中即可。

但是一個無須陣列顯然不能直接拆成兩個有序陣列,這就需要用到分治的思想。將陣列一層一層的拆分,直到單個陣列的長度為 1(長度為 1 的陣列可以認為是有序的),然後再反過來一層層進行歸併操作,那麼最後陣列就變成有序的了。

排序過程動圖如下(來自Swfung8):

主流排序演算法全面解析

程式碼

public class MergeSort {

    /**
     * Description:
     *
     * @param arr   待排序陣列
     * @param start 開始下標
     * @param end   結束下標
     * @author fanxb
     * @date 2019/8/6 9:29
     */
    public static void mergeSort(Integer[] arr, int start, int end) {
        if (start >= end) {
            return;
        }
        int half = (start + end) / 2;
        //歸併左邊
        mergeSort(arr, start, half);
        //歸併右邊
        mergeSort(arr, half + 1, end);
        //合併
        merge(arr, start, half, end);
    }

    /**
     * Description:
     *
     * @param arr arr
     * @author fanxb
     * @date 2019/8/5 17:36
     */
    public static void merge(Integer[] arr, int start, int half, int end) {
        ArrayList<Integer> tempList = new ArrayList<>();
        int i = start, j = half + 1;
        // 迴圈比較,將較小的放到tempList中
        while (i <= half && j <= end) {
            if (arr[i] <= arr[j]) {
                tempList.add(arr[i]);
                i++;
            } else {
                tempList.add(arr[j]);
                j++;
            }
        }
        if (i > half) {
            //說明第一個陣列已經完了,將第二個陣列的剩餘部分放到tempList中
            while (j <= end) {
                tempList.add(arr[j]);
                j++;
            }
        } else {
            //說明第二個陣列已經完了,將第一個陣列剩餘部分放到tempList中
            while (i <= half) {
                //說明第二個陣列處理完了
                tempList.add(arr[i]);
                i++;
            }
        }
        //最後將tempList複製到arr中
        for (int k = 0, length = tempList.size(); k < length; k++) {
            arr[start + k] = tempList.get(k);
        }

    }

    public static void main(String[] args) {
        Integer[] arr = {4, 3, 1, 2, 5, 4, 2};
        mergeSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }
}

非比較排序

計數排序

定義

計數排序不以比較為基礎,核心在於將數 a 存放在 arr[a]上,排序速度超級快,但是要求輸入的數必須是有確定範圍的整數。

思想

假設對於範圍 0-100 的整數進行排序

  1. 定義長度為 101 的陣列 arr,並將值初始化為 0
  2. 讀取一個數 a,然後 arr[a]++
  3. 遍歷 arr,陣列上的每個值表示對應下標的數的出現次數。

程式碼

public class CountSort {
    /**
     * Description:
     *
     * @param arr 待排序陣列
     * @return void
     * @author fanxb
     * @date 2019/8/6 17:36
     */
    public static void sort(Integer[] arr, Integer minValue, Integer maxValue) {
        int range = maxValue - minValue + 1;
        Integer[] numCount = new Integer[range];
        Arrays.fill(numCount, 0);
        for (Integer item : arr) {
            item = item - minValue;
            numCount[item]++;
        }
        int count = 0;
        for (int i = 0; i < range; i++) {
            if (numCount[i] == 0) {
                continue;
            }
            for (int j = 0; j < numCount[i]; j++) {
                arr[count] = minValue + i;
                count++;
            }
        }
    }


    public static void main(String[] args) {
        Integer[] arr = {1, 65, 32, 334, 12, 21, 65, 112, 444443};
        sort(arr, 1, 444443);
        System.out.println(Arrays.toString(arr));
    }
}

PS

計數排序有很多的變種,下面列舉幾種:

  1. 存在負數怎麼辦?

很簡單,先進行一次遍歷將正數負數分開,在分別進行排序,負數取反後再排。

  1. 有空間限制且陣列非常大怎麼辦?

這裡可以利用檔案來實現。先將超大的陣列按照規則分成幾個部分,分別存到檔案中(比如 1-1000000 放在檔案 1 中,1000001-2000000 放在檔案 2 中,以此類推)就將超大的陣列分成了小的陣列,然後再分別計數排序即可。

基數排序

定義

是一種非比較型整數排序演算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。基數排序的發明可以追溯到 1887 年赫爾曼·何樂禮在打孔卡片製表機(Tabulation Machine)上的貢獻。--來自維基百科

基數排序可以採用 LSD(從高位開始),MSD(從低位開始),這裡以 MSD 為例。

(有興趣的可以思考思考如何用 LSD 實現,目前網上絕大多數都是 MSD 實現的)

思想

  1. 先建立 10 個桶,分別對應數字 0-9。
  2. 從左往右取第一位的數字,放到對應的桶中
  3. 依次從桶中取出數字(要按照先進先出的原則)放到源陣列中。
  4. 重複 2,3 步驟,依次對第二、第三。。。位的數字排序,直到最大位數處理完畢。

為什麼能夠這樣排序呢?第一遍排序完畢後,所有的數是按照個位排序的,對於所有小於 10 的數來說,他們已經是相對有序(並不是說位置不再變化,只是相對順序不再變化)的了,在第二輪對十位排序時,所有的個位數都將被放到 0 桶了,用先進先出策略處理這些個位數,取出時個位數還是有序的。

第二輪排序後所有小於 10 的數的位置已經確定且不再變化,大於 10 小於 100 的數的位置已經相對有序.在第三輪中所有小於 100 的數都將被放到 0 桶,這時相對有序就變成了絕對的了,取出後位置不再變化。

第三輪排序後所有小於 100 的數的位置已經確定且不再變化。以此類推直到全部排序完成。

動圖如下:

程式碼


public class RadixSort {

    @SuppressWarnings("unchecked")
    public static void sort(Integer[] arr) {
        //定義桶
        LinkedList<Integer>[] buckets = new LinkedList[10];
        for (int i = 0; i < 10; i++) {
            buckets[i] = new LinkedList<>();
        }
        int size = arr.length;
        //當前處理第幾位的數
        int count = 0;
        while (true) {
            //是否繼續進位
            boolean isContinue = false;
            //將數放到桶中
            for (int i = 0; i < size; i++) {
                int temp = arr[i] / (int) Math.pow(10, count) % 10;
                if (!isContinue && temp != 0) {
                    // 如果存在一個數取的值不為0,說明還要繼續迴圈。
                    isContinue = true;
                }
                buckets[temp].addLast(arr[i]);
            }
            if (!isContinue) {
                return;
            }
            //從桶中取出放到arr中,注意以什麼順序放進去的就要以什麼順序取出來(先進先出)
            int index = 0;
            for (int i = 0; i < 10; i++) {
                Integer item;
                while ((item = buckets[i].pollFirst()) != null) {
                    arr[index++] = item;
                }
            }
            //位數+1
            count++;
        }
    }

    public static void main(String[] args) {
        Integer[] arr = {4, 31, 1, 29, 5, 4, 2};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

本文原創釋出於:www.tapme.top/blog/detail/20190806

原始碼:github

相關文章