排序算法(Java)——那些年面試常見的排序算法

前言

  排序就是將一組對象按照某種邏輯順序從新排列的過程。好比信用卡帳單中的交易是按照日期排序的——這種排序極可能使用了某種排序算法。在計算時代早期,你們廣泛認爲30%的計算週期都用在了排序上,今天這個比例可能下降了,大概是由於如今的排序算法更加高效。如今這個時代數據能夠說是無處不在,而整理數據的第一步每每就是進行排序。全部的計算機系統都實現了各類排序算法以供系統和用戶使用。
  即便你只是使用標準庫中的排序函數,學習排序算法仍然有很大的實際意義:java

  • 排序算法每每是咱們解決其餘問題的第一步
  • 排序算法有助於咱們理解其餘算法
  • 算法在公司面試中佔有很大比例,排序算法做爲其中的重要組成部分,咱們理所固然要學好了。

  另外,更重的是下面介紹的這些算法都很經典,優雅並且高效,學習其中的精髓對本身提升本身的編程能力也有很大的幫助。
  排序在商業數據處理和現代科學計算中有很重要的地位,它可以應用於事務處理,組合優化,天體物理學,分子動力學,語言學,基因組學,天氣預報和不少其餘領域。下面會介紹的一種排序算法(快速排序)甚至被譽爲20世紀科學和工程領域的十大算法之一。後面咱們會依次學習幾種經典的排序算法,並高效地實現「優先隊列」這種基礎數據類型。咱們將討論比較排序算法的理論基礎並中借若干排序算法和優先隊列的應用。面試

第一章——初始初級排序算法

一,我知道的第一個排序算法——冒泡排序

1. 介紹: 

  這是比較簡單的一種的排序算法,應該是咱們大學最早接觸到的一個排序算法了。很簡單的一種排序算法,大學裏好像也是每次必考的吧!算法

2. 原理:

  比較相鄰的元素。若是第一個比第二個大,就交換他們兩個。 對每一對相鄰元素做一樣的工做,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。針對全部的元素重複以上的步驟,除了最後一個。持續每次對愈來愈少的元素重複上面的步驟,直到沒有任何一對數字須要比較。
代碼實現:shell

3. 實現代碼

public static void bubbleSort(int[] numbers) {
        int temp = 0;
        int size = numbers.length;
        for (int i = 0; i < size - 1; i++) {
            for (int j = 0; j < size - 1 - i; j++) {
                 // 交換兩數位置
                if (numbers[j] > numbers[j + 1])
                {
                    temp = numbers[j];
                    numbers[j] = numbers[j + 1];
                    numbers[j + 1] = temp;
                }
            }
        }
    }

二,另外一個簡單排序算法——選擇排序

1. 介紹: 

  選擇排序市一中很容易理解和實現的簡單排序算法。學習它以前首先要知道它的兩個很鮮明的特色。編程

  • 運行時間和輸入無關。爲了找出最小的元素而掃描一遍數組並不能爲下一遍掃描提供任何實質性幫助的信息。所以使用這種排序的咱們會驚訝的發現,一個已經有序的數組或者數組內元素所有相等的數組和一個元素隨機排列的數組所用的排序時間居然同樣長!而其餘算法會更善於利用輸入的初始狀態,選擇排序則否則。
  • 數據移動是最少的選擇排序的交換次數和數組大小關係是線性關係。看下面的原理時能夠很容易明白這一點。

2. 原理:

  在要排序的一組數中,選出最小的一個數與第一個位置的數交換;而後在剩下的數當中再找最小的與第二個位置的數交換,如此循環到倒數第二個數和最後一個數比較爲止。相似下圖:

代碼實現:數組

3. 實現代碼

public static void selectSort(int[] numbers) {
        // 數組長度
        int size = numbers.length; 
        // 中間變量
        int temp = 0; 

        for (int i = 0; i < size; i++) {
            // 待肯定的位置
            int k = i; 
            // 選擇出應該在第i個位置的數
            for (int j = size - 1; j > i; j--) {
                if (numbers[j] < numbers[k]) {
                    k = j;
                }
            }
            // 交換兩個數
            temp = numbers[i];
            numbers[i] = numbers[k];
            numbers[k] = temp;
        }
    }

三,另外一個簡單排序算法——插入排序

1. 介紹: 

  一般人們整理橋牌的方法是一張一張的來,將每一張牌插入到其餘已經有序牌中的適當位置。在計算機的實現中,爲了給要插入的元素騰出空間,咱們須要將其他全部元素在插入以前都向右移動覺得。這種算法就叫插入排序。
 ** 與選擇排序同樣,當前索引左邊的全部元素都是有序的,但他們的最終位置還不肯定爲了給更小的元素騰出空間,他們可能會被移動。可是當索引到達數組的右端時,數組排序就完成了。
  和選擇排序不一樣的是,插入排序所需的時間取決於輸入中元素的初始順序。也就是說對一個接近有序或有序的數組進行排序會比隨機順序或是逆序的數組進行排序要快的多。**微信

2. 原理: 

  每步將一個待排序的記錄,按其順序碼大小插入到前面已經排序的字序列的合適位置(從後向前找到合適位置後),直到所有插入排序完爲止。相似下圖:
  
函數

3. 實現代碼

/**
     * 插入排序
     * 
     * 從第一個元素開始,該元素能夠認爲已經被排序 取出下一個元素,在已經排序的元素序列中從後向前掃描
     * 若是該元素(已排序)大於新元素,將該元素移到下一位置 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置 將新元素插入到該位置中 重複步驟2
     * 
     * @param numbers
     *            待排序數組
     */
    public static void insertSort(int[] numbers) {
        int size = numbers.length;
        int temp = 0;
        int j = 0;

        for (int i = 0; i < size; i++) {
            temp = numbers[i];
            // 假如temp比前面的值小,則將前面的值後移
            for (j = i; j > 0 && temp < numbers[j - 1]; j--) {
                numbers[j] = numbers[j - 1];
            }
            numbers[j] = temp;
        }
    }

四,希爾排序

1. 介紹:

  這個排序咋一看名字感受很高大上,這是以D.L.shell名字命名的排序算法。
  爲了展現初級排序算法性質的價值,咱們來看一下基於插入排序的快速的排序算法——希爾排序。對於大規模亂序的數組插入排序很慢,由於它只會交換相鄰的元素,所以元素只能一點一點地從數組的一端移動到另外一端。若是最小的元素恰好在數組的盡頭的話,那麼要將它移動到正確的位置要N-1次移動。希爾排序爲了加快速度簡單地改進了插入排序,交換不相鄰的元素以對數組的局部進行排序,並最終用插入排序將局部有序的數組排序。性能

2. 原理:

先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。學習

3. 實現代碼

/**
     * 希爾排序的原理:根據需求,若是你想要結果從大到小排列,它會首先將數組進行分組,而後將較大值移到前面,較小值
     * 移到後面,最後將整個數組進行插入排序,這樣比起一開始就用插入排序減小了數據交換和移動的次數,能夠說希爾排序是增強 版的插入排序 拿數組5, 2,
     * 8, 9, 1, 3,4來講,數組長度爲7,當increment爲3時,數組分爲兩個序列
     * 5,2,8和9,1,3,4,第一次排序,9和5比較,1和2比較,3和8比較,4和比其下標值小increment的數組值相比較
     * 此例子是按照從大到小排列,因此大的會排在前面,第一次排序後數組爲9, 2, 8, 5, 1, 3,4
     * 第一次後increment的值變爲3/2=1,此時對數組進行插入排序, 實現數組從大到小排
     */

    public static void shellSort(int[] data) {
        int j = 0;
        int temp = 0;
        // 每次將步長縮短爲原來的一半
        for (int increment = data.length / 2; increment > 0; increment /= 2) {
            for (int i = increment; i < data.length; i++) {
                temp = data[i];
                for (j = i; j >= increment; j -= increment) {
                   // 如想從小到大排只需修改這裏
                    if (temp > data[j - increment])
                    {
                        data[j] = data[j - increment];
                    } else {
                        break;
                    }

                }
                data[j] = temp;
            }

        }
    }

第二章——進階之歸併排序

1, 介紹

  歸併即將兩個有序的數組歸併併成一個更大的有序數組。人們很快根據這個思路發明了一種簡單的遞歸排序算法:歸併排序。要將一個數組排序,能夠先(遞歸地)將它分紅兩半分別排序,而後將結果歸併起來。歸併排序最吸引人的性質是它能保證任意長度爲N的數組排序所需時間和NlogN成正比;它的主要缺點也顯而易見就是它所需的額外空間和N成正比。簡單的歸併排序以下圖:
簡單的歸併排序

2,原理

  歸併(Merge)排序法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分爲若干個子序列,每一個子序列是有序的。而後再把有序子序列合併爲總體有序序列。
  合併方法:
  設r[i…n]由兩個有序子表r[i…m]和r[m+1…n]組成,兩個子表長度分別爲n-i +一、n-m。

一、j=m+1;k=i;i=i; //置兩個子表的起始下標及輔助數組的起始下標
二、若i>m 或j>n,轉⑷ //其中一個子表已合併完,比較選取結束
三、//選取r[i]和r[j]較小的存入輔助數組rf
        若是r[i]<r[j],rf[k]=r[i]; i++; k++; 轉⑵
        不然,rf[k]=r[j]; j++; k++; 轉⑵
四、//將還沒有處理完的子表中元素存入rf
        若是i<=m,將r[i…m]存入rf[k…n] //前一子表非空
        若是j<=n ,  將r[j…n] 存入rf[k…n] //後一子表非空
五、合併結束。

3,實現代碼

/**
     * 歸併排序 簡介:將兩個(或兩個以上)有序表合併成一個新的有序表
     * 即把待排序序列分爲若干個子序列,每一個子序列是有序的。而後再把有序子序列合併爲總體有序序列 時間複雜度爲O(nlogn) 穩定排序方式
     * 
     * @param nums
     *            待排序數組
     * @return 輸出有序數組
     */

    public static int[] mergeSort(int[] nums, int low, int high) {
        int mid = (low + high) / 2;
        if (low < high) {
            // 左邊
            mergeSort(nums, low, mid);
            // 右邊
            mergeSort(nums, mid + 1, high);
            // 左右歸併
            merge(nums, low, mid, high);
        }
        return nums;
    }

    /**
     * 將數組中low到high位置的數進行排序
     * 
     * @param nums
     *            待排序數組
     * @param low
     *            待排的開始位置
     * @param mid
     *            待排中間位置
     * @param high
     *            待排結束位置
     */
    public static void merge(int[] nums, int low, int mid, int high) {
        int[] temp = new int[high - low + 1];
        int i = low;// 左指針
        int j = mid + 1;// 右指針
        int k = 0;

        // 把較小的數先移到新數組中
        while (i <= mid && j <= high) {
            if (nums[i] < nums[j]) {
                temp[k++] = nums[i++];
            } else {
                temp[k++] = nums[j++];
            }
        }

        // 把左邊剩餘的數移入數組
        while (i <= mid) {
            temp[k++] = nums[i++];
        }

        // 把右邊邊剩餘的數移入數組
        while (j <= high) {
            temp[k++] = nums[j++];
        }

        // 把新數組中的數覆蓋nums數組
        for (int k2 = 0; k2 < temp.length; k2++) {
            nums[k2 + low] = temp[k2];
        }
    }

第三章——進階之快速排序

前言:

  快速排序多是應用最普遍的排序算法了。快速排序流行的緣由主要由於它實現簡單,適用於不一樣的輸入數據且在通常應用中比其餘排序算法都要快得多。快速排序引人注目的特色包括它是原地排序(只須要一個很小的輔助棧),且將長度爲N的數組排序所須要的時間和NlgN成正比。咱們以前提到的幾種排序算法都沒法將這兩個優勢結合起來。另外,快速排序的內循環比大多數排序算法都要短小,這意味着它不管是理論上仍是實際中都要更快。它的主要缺點是很是脆弱,在實現時要很是當心才能避免低劣的性能。已經有無數例子顯示許多錯誤都能導致它在實際應用中的性能只有平方級別。不過還好,咱們由這些缺點和教訓中大大改進了快速排序算法,使它的應用更加普遍。

1,介紹

  快速排序是一種分治的排序算法。它將一個數組分紅兩個字數組,將兩部分獨立地排序。快速排序和歸併排序是互補的:歸併排序將數組分紅兩個字數組分別排序,並將有序的字數組歸併以將整個數組排序;而快速排序將數組排序的方式則是當兩個字數組都有序時整個數組也就天然有序了。在第一種狀況中,遞歸調用發生在處理整個數組以前;在第二種狀況中,遞歸調用發生在處理整個數組以後。在歸併排序中,一個數組被等分爲兩半;快速排序中,切分的位置取決於數組的內容。快速排序的過程大體以下:

2,原理

快速排序的基本思想
  經過一趟排序將待排序記錄分割成獨立的兩部分,一部分全小於選取的參考值,另外一部分全大於選取的參考值。這樣分別對兩部分排序以後順序就能夠排好了。
例子:
(a)一趟排序的過程:

(b)排序的全過程:

3,實現代碼

/**
     * 查找出中軸(默認是最低位low)的在numbers數組排序後所在位置
     * 
     * @param numbers 帶查找數組
     * @param low 開始位置
     * @param high 結束位置
     * @return 中軸所在位置
     */
    public static int getMiddle(int[] numbers, int low, int high) {
        // 數組的第一個做爲中軸
        int temp = numbers[low]; 
        while (low < high) {
            while (low < high && numbers[high] > temp) {
                high--;
            }
            // 比中軸小的記錄移到低端
            numbers[low] = numbers[high];
            while (low < high && numbers[low] < temp) {
                low++;
            }
            // 比中軸大的記錄移到高端
            numbers[high] = numbers[low]; 
        }
        numbers[low] = temp; // 中軸記錄到尾
        return low; // 返回中軸的位置
    }

    /**
     * 
     * @param numbers 帶排序數組
     * @param low 開始位置
     * @param high 結束位置
     */
    public static void quick(int[] numbers, int low, int high) {
        if (low < high) {
            int middle = getMiddle(numbers, low, high); // 將numbers數組進行一分爲二
            quick(numbers, low, middle - 1); // 對低字段表進行遞歸排序
            quick(numbers, middle + 1, high); // 對高字段表進行遞歸排序
        }

    }

    /**
     * 快速排序
     * 快速排序提供方法調用
     * @param numbers 帶排序數組
     */
    public static void quickSort(int[] numbers) {
        // 查看數組是否爲空
        if (numbers.length > 0) 
        {
            quick(numbers, 0, numbers.length - 1);
        }
    }

4,改進方法

咱們只介紹一種經常使用的,具體代碼就不貼出了。想去的能夠去https://algs4.cs.princeton.ed... 這個網站找。
三向切分快速排序
  核心思想就是將待排序的數據分爲三部分,左邊都小於比較值,右邊都大於比較值,中間的數和比較值相等.三向切分快速排序的特性就是遇到和比較值相同時,不進行數據交換, 這樣對於有大量重複數據的排序時,三向切分快速排序算法就會優於普通快速排序算法,但因爲它總體判斷代碼比普通快速排序多一點,因此對於常見的大量非重複數據,它並不能比普通快速排序多大多的優點 。

歡迎關注個人微信公衆號(分享各類Java學習資源,面試題,以及企業級Java實戰項目回覆關鍵字免費領取):
微信公衆號

相關文章
相關標籤/搜索