排序算法是一種比較簡單的算法,從咱們一開始接觸計算機編程開始接觸的可能就是排序或者搜索一類的算法,可是由於排序在其餘的一些算法中應用較多,因此爲了提升性能已經研究了多種排序算法。目前區別排序算法主要仍是以時間複雜度,空間複雜度,穩定性等來排序,接下來咱們分別分析。算法
區別一個排序算法是不是穩定算法只需看相同的關鍵字在排序完成後是否保持原來二者的先後關係便可,好比對於[1,2,3,4,1],a[0]=a[4],a[0]在a[4]以前,穩定的排序在排序完成後a[0]依舊在a[4]的前面,反之則是不穩定排序算法。shell
冒泡排序(Bubble Sort)是一種比較簡單的排序算法。基本原理爲選定一個數做爲比較標準,遍歷整個數組比較兩個數的大小,若是順序不對則進行交換,知道沒有再須要交換的數爲止。冒泡排序是穩定的排序算法
冒泡排序算法的運做以下:編程
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)是一種很簡單排序算法。它要求是每一次從待排序的元素中選出最小(最大)的一個元素,存放在起始位置,而後再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到上一位已經排好序的後面。以此類推,直到所有待排序的數據元素排完。 選擇排序是不穩定的排序方法。多線程
選擇排序算法的運做以下:工具
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
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時,恰好就是一個插入排序。而在此時整個數據序列已經基本有序,插入排序在對幾乎已經排好序的數據操做時,效率高,能夠達到線性排序的效率。因此希爾排序的總體效率較高。
希爾排序的步驟:
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)。
快速排序是對冒泡排序的改進。
快排的基本步驟:
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)的空間。
歸併是指將兩個及以上的有序序列合併成一個有序序列。
歸併排序步驟:
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-路歸併排序,其核心思想是將覺得數組中先後響鈴的兩個有序序列合併爲一個有序序列。可是實際上平時咱們不會使用這種排序方式。
可是歸併排序使用場景仍是不少的,特別是在對數量較大的序列進行排序是,好比目前咱們有大量的數據存儲在文本中,如今須要對其進行排序。因爲內存的限制沒有辦法一次性加載全部的數據,這時候咱們就可使用歸併排序,將大的文件分割爲若干份小文件,分別對這些小文件的數據進行排序後使用歸併排序再將其進行排序。而且排序過程當中可使用多線程等手段提升算法效率。
在JDK中,Arrays工具類爲咱們提升了各類類型的排序方法,Arrays.sort在JDK1.6及以前使用的是歸併排序,在1.7開始使用的是TimSort排序。
TimSort算法是一種起源於歸併排序和插入排序的混合排序算法,設計初衷是爲了在真實世界中的各類數據中能夠有較好的性能。基本工做過程是: