面試中可能被問到的經常使用排序算法

排序算法

排序算法是一種比較簡單的算法,從咱們一開始接觸計算機編程開始接觸的可能就是排序或者搜索一類的算法,可是由於排序在其餘的一些算法中應用較多,因此爲了提升性能已經研究了多種排序算法。目前區別排序算法主要仍是以時間複雜度,空間複雜度,穩定性等來排序,接下來咱們分別分析。算法

穩定性算法

區別一個排序算法是不是穩定算法只需看相同的關鍵字在排序完成後是否保持原來二者的先後關係便可,好比對於[1,2,3,4,1],a[0]=a[4],a[0]在a[4]以前,穩定的排序在排序完成後a[0]依舊在a[4]的前面,反之則是不穩定排序算法。shell

冒泡排序

基本原理

冒泡排序(Bubble Sort)是一種比較簡單的排序算法。基本原理爲選定一個數做爲比較標準,遍歷整個數組比較兩個數的大小,若是順序不對則進行交換,知道沒有再須要交換的數爲止。冒泡排序是穩定的排序算法
冒泡排序算法的運做以下:編程

  1. 比較相鄰的兩個元素。並根據須要進行交換,若是須要正序,那麼就將較大的放在後面,倒敘則將較小的放在後面。
  2. 對每一組相鄰元素一樣的操做。這步作完後,最後的元素會是最大的數。
  3. 外循環對除已經選擇出的元素重複上面的步驟,直到沒有任何一對數字須要比較,表示排序已經完成。

代碼

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

複雜度

若是序列的初始狀態是正序的,一趟掃描便可完成排序,不須要交換操做。通過n次的循環後排序完成,因此時間複雜度爲O(n),整個過程沒有使用輔助空間,空間複雜度爲O(1)。數組

選擇排序

選擇排序(Selection sort)是一種很簡單排序算法。它要求是每一次從待排序的元素中選出最小(最大)的一個元素,存放在起始位置,而後再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到上一位已經排好序的後面。以此類推,直到所有待排序的數據元素排完。 選擇排序是不穩定的排序方法。多線程

選擇排序算法的運做以下:工具

  1. 第一次遍歷整個數據序列,查找出最大(小)的數。並將該數放在第一位置。
  2. 將已經排序好的位置除外,剩下的數據序列重複進行上面的操做。

代碼

public static void insertSort(int[] arr){
        for(int i=0;i<arr.length;i++){
            //一趟以後最小的數到了下標爲i的位置
            for(int j=i+1;j<arr.length;j++){
                if(arr[i] > arr[j]){
                    swap(arr, i, j);
                }
            }
        }
    }

複雜度

若是數據自己就是有序的,0次交換;最壞的狀況下須要進行n-1次交換;比較操做次數固定爲N^2/2,時間複雜度爲O(n^2),空間複雜度爲O(1)。性能

直接插入排序

插入排序是比較簡單的排序方法,插入排序將待排序數組分爲兩部分,一部分是已排序部分,另外一部分則是待排序部分。最開始僅第一個數字爲已排序部分。而後每次從待排序部分取出一個數,同已排序部分的數據進行比較,選出恰好前一個數比該數小,後一個數比該數大(第一位除外),將該數放在這個位置。進過遍歷後整個數組有序。大數據

選擇排序算法的運做以下:ui

  1. 將第一個數選擇爲已排序部分,取第二個數同第一個數比較,若是大於第一個數則不變,小於則交換位置。上述過程完成後將前兩個數字做爲已排序部分。
  2. 再次從待排序部分取出一個數字,重複上訴步驟找出該數的位置。從後向前掃描過程當中,須要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
  3. 重複上面的步驟直到將數據所有遍歷完成則表示數組有序。

代碼

public static void insertSort(int[] nums){
        int i,j;
        for(i=1;i<nums.length;i++){
            int temp = nums[i];
            //將元素後移
            for(j=i-1;j>=0&&temp<nums[j];j--){
                nums[j+1] = nums[j];
            }
            nums[j+1] = temp;
        }

    }

複雜度

在將n個元素的序列進行升序或者降序排列,採用插入排序最好狀況就是序列已是有序的了,在這種狀況下,須要進行的比較操做需n-1次便可。最壞狀況就是序列是反序的,那麼此時須要進行的比較共有n(n-1)/2次。平均來講插入排序算法複雜度爲 O(n^2)。因此插入排序不適合對於數據量比較大的排序應用。可是在須要排序的數據量很小或者若已知輸入元素大體上按照順序排列,插入排序的效率仍是不錯。線程

帶哨兵的插入排序

在插入排序的時候,咱們看到每一次進行比較都有兩次比較操做j>=0&&temp<nums[j],即既要保證不越界又要判斷數據是否符合條件,假設在反序的狀況下就幾乎多出一倍的比較次數。這裏咱們使用一個哨兵來消除掉多的比較操做。

代碼

public static void insertWithSentinelSort(int[] nums){
        int i,j;
        for(i=1;i<nums.length;i++){
            //將第一個元素指定爲哨兵
            //要求傳入的數組比原數組長度大1
            nums[0] = nums[i];
            //將元素後移
            //這裏只需比較數據是否符合條件
            for(j=i-1;nums[j]>nums[0];j--){
                nums[j+1] = nums[j];
            }
            nums[j+1] = nums[0];
        }
    }

添加哨兵的好處就是將本來的比較次數減小,提升了算法效率。

希爾排序

希爾排序是插入排序的一種更高效的改進版本。希爾排序是非穩定排序算法。

希爾排序是把記錄按下標的必定的步長進行分組,對每組數據使用直接插入排序算法排序;隨着步長逐漸減小,每組包含的關鍵詞愈來愈多,當步長爲1時,恰好就是一個插入排序。而在此時整個數據序列已經基本有序,插入排序在對幾乎已經排好序的數據操做時,效率高,能夠達到線性排序的效率。因此希爾排序的總體效率較高。

希爾排序的步驟:

  1. 選擇步長大小,根據步長將數據分組
  2. 循環對每一組進行排序
  3. 修改步長的大小(通常爲一半,也能夠經過步長數組指定),重複1-2操做
  4. 直到步長爲1進行排序後中止

代碼

public static void shellSort(int[] nums){
        int size = nums.length/2;
        int i,j;
        while(size>=1){
            for(i=0;i<nums.length;i++){
                for(j=i;j+size<nums.length;j+=size){
                    if(nums[j]>nums[j+size]){
                        swap(nums, j, j+size);
                    }
                }
            }
            size/=2;
        }
    }

複雜度

希爾排序的時間複雜度分析比較複雜,由於它和所選取的步長有着直接的關係。步長的選取沒有一個統一的定論,只須要使得步長最後爲1便可。希爾排序的時間複雜度根據所選取的步長不一樣時間複雜度範圍在o(n^1.3)~o(n^2)。

快速排序

快速排序是對冒泡排序的改進。

快排的基本步驟:

  1. 從待排序列中選取一個數(通常爲第一個),進行一次排序,將大於該數的放在該數前面,小於該數的放在其後面。
  2. 上述操做將待排序列分爲兩個獨立的部分,遞歸的進行上面的操做,直到序列沒法再被分割。
  3. 最後一次排序後序列中是有序的。

代碼

public static void quickSort(int[] nums, int low, int high){
        if(low<high){
            int partation = partition(nums, low, high);
            //這裏返回的low值的位置已經肯定了 因此不用參與排序
            quickSort(nums, 0, low-1);
            quickSort(nums, low+1, high);
        }
    }
    
    //進行一次排序 將待排序列分爲兩個部分
    public static int partition(int[] nums, int low, int high){
        //選取第一個值爲樞紐值
        int pivo = nums[low];
        while(low<high){
            while(low<high&&nums[high]>=pivo){
                high--;
            }
            nums[low] = nums[high];
            while(low<high&&nums[low]<=pivo){
                low++;
            }
            nums[high]=nums[low];
        }
        nums[low] = pivo;

        return low;
    }

複雜度

時間複雜度

在最優狀況下,Partition每次都劃分得很均勻,若是排序n個關鍵字,其遞歸的深度就爲log2n+1,即僅需遞歸log2n 次。時間複雜度爲O(nlogn)。

最糟糕的狀況就是待排序列爲須要排序方向的逆序。每次劃分只獲得一個比上一次劃分少一個記錄的子序列。這時快排退化爲冒泡排序。時間複雜度爲O(n^2)。

快排的平均複雜度爲O(nlogn),證實過程較長,直接貼個連接吧。

空間複雜度

被快速排序所使用的空間,根據上面咱們實現的代碼來看,在任何遞歸調用前,僅會使用固定的額外空間。然而,若是須要產生 o(logn)嵌套遞歸調用,它須要在他們每個存儲一個固定數量的信息。由於最好的狀況最多須要O(logn)次的嵌套遞歸調用,因此它須要O(logn)的空間。最壞狀況下須要 O(n)次嵌套遞歸調用,所以須要O(n)的空間。

歸併排序

歸併是指將兩個及以上的有序序列合併成一個有序序列。

歸併排序步驟:

  1. 申請一個和待排序列長度相同的空間空間該空間用來存放合併後的序列
  2. 設定兩個數爲對數組中位置的指向,最初位置分別爲兩個已經排序序列的起始位置
  3. 比較兩個指針所指向的元素,選擇小的元素放入到合併空間,並移動被選擇的數的指針到下一位置
  4. 重複步驟3直到某一指針到達指定的序列尾部
  5. 將另外一序列剩下的全部元素直接複製到合併序列尾

代碼

public static void mergeSort(int[] nums, int[] temp, int left, int right){
        if(left<right){
            int mid = (left+right)/2;
            mergeSort(nums, temp,left,mid);
            mergeSort(nums, temp,mid+1,right);
            merge(nums,temp, mid, left, right);
        }
    }

    public static void merge(int[] nums, int[] temp, int mid, int left, int right){
        int i=left,j=mid+1,k=0;
        while(i<=mid&&j<=right){
            if(nums[i]<nums[j]){
                temp[k++] = nums[i++];
            }else {
                temp[k++] = nums[j++];
            }
        }

        while(i<=mid){
            temp[k++] = nums[i++];
        }

        while(j<=right){
            temp[k++] = nums[j++];
        }
        //將temp中的元素所有拷貝到原數組中
        //這裏必須將原來排好序的數組值複製回去
        //不然後續的對比前面排序長的數組排序時會出錯
        //好比4 1 2 3   講過排序後分爲1 4 和2 3兩組
        //若是沒有將值複製回去那麼合併後將是2 3 4 1
        k=0;
        while(left<=right){
            nums[left++] = temp[k++];
        }
    }

複雜度

歸併排序是一種效率高且穩定的算法。可是卻須要額外的空間。

歸併排序的比較是分層次來歸併的。第一次將序列分爲兩部分,第二次將第一次獲得的兩部分各自分爲兩部分。最後分割合併就相似一課二叉樹。其平均時間複雜度爲O(nlogn)。空間複雜度由於其須要額外長度爲n的輔助空間,其空間複雜度爲O(n)。

大數據量排序

上面演示的代碼也被成爲2-路歸併排序,其核心思想是將覺得數組中先後響鈴的兩個有序序列合併爲一個有序序列。可是實際上平時咱們不會使用這種排序方式。

可是歸併排序使用場景仍是不少的,特別是在對數量較大的序列進行排序是,好比目前咱們有大量的數據存儲在文本中,如今須要對其進行排序。因爲內存的限制沒有辦法一次性加載全部的數據,這時候咱們就可使用歸併排序,將大的文件分割爲若干份小文件,分別對這些小文件的數據進行排序後使用歸併排序再將其進行排序。而且排序過程當中可使用多線程等手段提升算法效率。

TIMSort

在JDK中,Arrays工具類爲咱們提升了各類類型的排序方法,Arrays.sort在JDK1.6及以前使用的是歸併排序,在1.7開始使用的是TimSort排序。

TimSort算法是一種起源於歸併排序和插入排序的混合排序算法,設計初衷是爲了在真實世界中的各類數據中能夠有較好的性能。基本工做過程是:

  1. 掃描數組,肯定其中的單調上升段和嚴格單調降低段,將嚴格降低段反轉;
  2. 定義最小基本片斷長度,短於此的單調片斷經過插入排序集中爲長於此的段;
  3. 反覆歸併一些相鄰片斷,過程當中避免歸併長度相差很大的片斷,直至整個排序完成,所用分段選擇策略能夠保證O(n log n)時間複雜性。
相關文章
相關標籤/搜索