排序算法
排序算法是一種比較簡單的算法,從我們一開始接觸計算機編程開始接觸的可能就是排序或者搜索一類的算法,但是因為排序在其他的一些算法中應用較多,所以為了提高性能已經研究了多種排序算法。目前區別排序算法主要還是以時間復雜度,空間復雜度,穩定性等來排序,接下來我們分別分析。
穩定性算法
區別一個排序算法是否是穩定算法只需看相同的關鍵字在排序完成后是否保持原來兩者的前后關系即可,比如對於[1,2,3,4,1],a[0]=a[4],a[0]在a[4]之前,穩定的排序在排序完成后a[0]依舊在a[4]的前面,反之則是不穩定排序算法。
冒泡排序
基本原理
冒泡排序(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次交換;比較操作次數固定為N2/2,時間復雜度為O(n2),空間復雜度為O(1)。
直接插入排序
插入排序是比較簡單的排序方法,插入排序將待排序數組分為兩部分,一部分是已排序部分,另一部分則是待排序部分。最開始僅第一個數字為已排序部分。然后每次從待排序部分取出一個數,同已排序部分的數據進行比較,選出剛好前一個數比該數小,后一個數比該數大(第一位除外),將該數放在這個位置。進過遍歷后整個數組有序。
選擇排序算法的運作如下:
- 將第一個數選擇為已排序部分,取第二個數同第一個數比較,如果大於第一個數則不變,小於則交換位置。上述過程完成后將前兩個數字作為已排序部分。
- 再次從待排序部分取出一個數字,重復上訴步驟找出該數的位置。從后向前掃描過程中,需要反復把已排序元素逐步向后挪位,為最新元素提供插入空間。
- 重復上面的步驟直到將數據全部遍歷完成則表示數組有序。
代碼
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操作
- 直到步長為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(n1.3)~o(n2)。
快速排序
快速排序是對冒泡排序的改進。
快排的基本步驟:
- 從待排序列中選取一個數(一般為第一個),進行一次排序,將大於該數的放在該數前面,小於該數的放在其后面。
- 上述操作將待排序列分為兩個獨立的部分,遞歸的進行上面的操作,直到序列無法再被分割。
- 最后一次排序后序列中是有序的。
代碼
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)的空間。
歸並排序
歸並是指將兩個及以上的有序序列合並成一個有序序列。
歸並排序步驟:
- 申請一個和待排序列長度相同的空間空間該空間用來存放合並后的序列
- 設定兩個數為對數組中位置的指向,最初位置分別為兩個已經排序序列的起始位置
- 比較兩個指針所指向的元素,選擇小的元素放入到合並空間,並移動被選擇的數的指針到下一位置
- 重復步驟3直到某一指針到達指定的序列尾部
- 將另一序列剩下的所有元素直接復制到合並序列尾
代碼
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算法是一種起源於歸並排序和插入排序的混合排序算法,設計初衷是為了在真實世界中的各種數據中可以有較好的性能。基本工作過程是:
- 掃描數組,確定其中的單調上升段和嚴格單調下降段,將嚴格下降段反轉;
- 定義最小基本片段長度,短於此的單調片段通過插入排序集中為長於此的段;
- 反復歸並一些相鄰片段,過程中避免歸並長度相差很大的片段,直至整個排序完成,所用分段選擇策略可以保證O(n log n)時間復雜性。