轉載請注明出處
作者:@ZJXin
說明:本文所寫的排序,默認按從小到大排序。由于本人水平有限,如有不正確的地方歡迎留言指出,謝謝!
排序算法是面試中經常會被問到的問題,甚至會要求手寫算法,本文對一些常用的排序算法做了總結。本文涉及的代碼全部都運行驗證過。(堆排序沒有給出代碼實現)
1. 直接插入排序
算法思想
每次將無序區的第一個記錄按關鍵字插入到有序區的合適位置。
算法實現步驟
- 取出無序區第一個元素,并將該值賦值給一個臨時變量
- 在已經排序的元素序列中從后向前掃描
- 如果所取元素大于新元素,將該元素向后移動一個位置
- 重復步驟三,直到找到已排序的元素小于或者等于新元素的位置
- 將新元素插入到該位置中
- 重復1~5
算法流程圖
Java代碼實現
/**
* 直接插入排序算法
*
* @param nums 待排序數組
*/
public void insertSort(int []nums){
//數組長度
int length = nums.length;
//要插入的數
int insertNum;
for(int i=1;i<length;i++){//遍歷數組
int j = i-1;//已排好序的元素序列的最大下標
insertNum = nums[i];
while(j>=0 && nums[j]>insertNum){
//首先判斷j是否>=0,不然將可能產生數組溢出
//序列從后往前循環
//將大于insertNum的數往后挪一格
nums[j+1] = nums[j--];
}
//將需要插入的數放在插入的位置
nums[j+1] = insertNum;
}
}
算法分析
- 時間復雜度
- 最佳情況:O(n)
- 最壞情況:O(n^2)
- 平均時間復雜度:O(n^2)
- 空間復雜度:O(1)
- 穩定性:穩定
- 改進:希爾排序(縮小增量排序)
2. 希爾排序(縮小增量排序)
算法思想
希爾排序是對直接插入排序的一種改進。希爾排序將整個待排序序列按增量dk劃分為m個子序列,不斷縮小增量dk,重復這一過程,直到dk減少到1,對整個序列進行一次直接插入排序,因此,希爾排序也被稱為縮小增量排序。
希爾排序與直接插入排序的不同之處在于,直接插入排序每次對相鄰記錄進行比較,記錄只移動一個位置,而希爾排序每次對相隔較遠(即增量)的記錄進行比較,使得記錄移動時跨越多個記錄,實現宏觀上的調整。當增量為1時,此時序列已基本有序,可將前邊的各趟的調整看做最后一趟的預處理。
算法實現步驟
- 選擇一個增量序列t1、t2、t3...tk(其中tk=1)
- 按增量序列個數k,對待排序序列進行k趟排序
- 每趟排序,根據相應的增量dk,進行插入排序
算法流程圖
Java代碼實現
/**
* 希爾排序(縮小增量排序)
* 不穩定
*
* @param nums 待排序數組
*/
public void sheelSort(int []nums){
int dk = nums.length;
do{
//縮小增量
//此處縮小增量可以自己設置,一般縮小當前的一半
dk = dk/2;
sheelInsert(nums, dk);//進行一次希爾排序
}while(dk>0);//當增量為1時停止
}
/**
* 希爾排序一趟排序
* 如果把增量設置為1,則是簡單的插入排序
*
* @param nums 待排序數組
* @param dk 增量
*/
public void sheelInsert(int []nums,int dk){
int length = nums.length;
int insertNum;//待插入的值
for(int i=dk;i<length;i++){
int j=i-dk;
insertNum = nums[i];
while(j>=0 && nums[j]>insertNum){
//同樣的,應該先檢測j是否比0小,否則可能產生數組溢出
//向后移動dk位
nums[j+dk] = nums[j];
j = j-dk;
}
nums[j+dk] = insertNum;//把待插入的 值放到正確的位置
}
}
算法分析
- 時間復雜度
- 最佳情況:O(n)
- 最壞情況:O(n^2)
- 平均情況:O(nlogn)
- 空間復雜度:O(1)
- 穩定性:不穩定
3. 簡單選擇排序
算法思想
每趟在未排序的序列中找到最小的元素,存放到未排序序列的起始位置。
算法實現步驟
- 首先在未排序序列中找到最小元素,存放到排序序列的起始位置
- 再從剩余未排序元素中繼續尋找最小元素,然后放到已排序序列的末尾。
- 重復上述步驟,直到所有元素均排序完畢。
算法流程圖
Java代碼實現
/**
* 簡單選擇排序
* 每一個循環后再交換,簡單選擇排序
*
* @param nums 待排序數組
*/
public void selsectSort(int []nums){
int length = nums.length;//數組長度,將這個值提出來,提高速度
for(int i=0; i<length; i++){//循環次數
int key = nums[i];
int position = i;
for(int j=i+1;j<length;j++){//選出最小的值和位置
if(key>nums[j]){
key = nums[j];
position = j;
}
}
//交換位置
nums[position] = nums[i];
nums[i] = key;
}
}
算法分析
- 時間復雜度
- 最壞、最佳、平均:O(n^2)
- 空間復雜度:O(1)
- 穩定性:不穩定
4. 堆排序
算法思想
堆排序是對簡單排序的優化,利用大頂堆所有非葉子結點均不小于其左右孩子結點這一特性來排序。
算法實現步驟
- 將待排序列建成一個大頂堆
- 將頂堆結點與隊尾結點交換位置,堆長度減1
- 調整剩余結點為堆
- 重復步驟2~3
算法流程圖
算法分析
- 時間復雜度
- 最壞、最好、平均:O(nlogn)
- 空間復雜度:O(1)
- 穩定性:不穩定
5. 冒泡排序
算法思想
冒泡排序每趟排序,對相鄰兩個元素進行比較,如果順序錯誤則交換過來,每趟排序后,都將把未排序序列的最大值放在最后邊。每趟排序,越小的元素會經由交換,慢慢地"浮"到數列頂端,這也是該算法名字的由來。
算法實現步驟
- 將序列中所有元素相鄰兩兩比較,將最大的放在最后面。
- 將剩余序列中所有元素相鄰兩兩比較,將最大的放在最后面。
- 重復第二步,直到只剩下一個數。
算法流程圖
Java代碼實現
/**
* 冒泡排序
*
* @param nums 待排序數組
*/
public void bubbleSort(int []nums){
int length = nums.length;
int temp;
for(int i=0; i<length; i++){
for(int j=0; j<length-i-1; j++){
if(nums[j]>nums[j+1]){
//交換相鄰兩個數
temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
}
算法分析
- 時間復雜度:
最佳、最壞、平均:O(n^2) - 空間復雜度:O(1)
- 穩定性:穩定
算法改進
- 改進方案一
設置一標志性變量pos,用于記錄每趟排序中最后一次進行交換的位置。由于pos位置之后的記錄均已交換到位,故在進行下一趟排序時只要掃描到pos位置即可。
/**
* 冒泡排序改進版本一
* 通過設置標志位來減少遍歷次數
*
* @param nums
*/
public void betterBubbleSort1(int []nums){
int length = nums.length;
int i= length -1; //初始時,最后位置保持不變
while ( i> 0) {
int pos= 0; //每趟開始時,無記錄交換
for (int j = 0; j< i; j++){
if (nums[j]> nums[j+1]) {
pos= j; //記錄交換的位置
int tmp = nums[j];
nums[j]=nums[j+1];
nums[j+1]=tmp;
}
i= pos; //為下一趟排序作準備
}
}
}
- 改進方案二
若某一趟排序中未進行一次交換,則排序結束 。
/**
* 冒泡排序改進版本二
*
* @param nums 待排序數組
*/
public void betterBubbleSort2(int[] nums ) {
int len = nums .length;
boolean flag = true;
while (flag) {
flag = false;
for (int i = 0; i < len - 1; i++) {
if (nums [i] > nums [i + 1]) {
int temp = nums [i + 1];
nums [i + 1] = nums [i];
nums [i] = temp;
flag = true;
}
}
len--;
}
}
6. 快速排序
算法思想
分治法。通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,然后在分別對這兩部分記錄進行排序。
算法實現步驟
- 從數列中挑出一個元素,稱為“基準”
- 重新排序數列,所有比基準小的元素放在基準前邊,所有比基準大的放在基準后邊,該基準最后處于數列中間位置。
- 遞歸地把小于基準值元素和大于基準值元素的子序列快速排序
算法流程圖
Java代碼實現
/**
* 快速排序
* 不穩定
*
* @param nums 待排序數組
* @param left 開始位置
* @param right 結束位置
*/
public void quickSort(int []nums,int left,int right){
if(left<right){
int base = nums[left];//選定的基準
int low = left;
int high = right;
while(low<high){
while(low<high && nums[high]>base){
//先從后往前遍歷,直到數值比基準小
high--;
}
//把數值比基準小的放到基準左邊
nums[low] = nums[high];
while(low<high && nums[low]<base){
//從前往后遍歷,直到數值比基準大
low++;
}
//把數值比基準大的放到基準右邊
nums[high] = nums[low];
}
//把基準放到準確位置去
nums[low] = base;
//用同樣的方法,遞歸調用基準左邊的數組以及右邊的數組
quickSort(nums, left, low-1);
quickSort(nums, low+1, right);
}
}
算法分析
- 時間復雜度
- 最佳情況:O(nlogn)
- 最壞情況:O(n^2)
- 平均復雜度:O(nlogn)
- 空間復雜度:O(nlogn)
- 穩定性:不穩定
7.歸并排序
算法思想
分治法。歸并排序將待排序列一分為二,并對每個子數組遞歸排序,然后再把這兩個排好序的子數組合并為一個有序的數組。
算法實現步驟
- 把長度為n的輸入序列分為兩個長度為n/2的子序列
- 對這兩個子序列分別采用歸并排序
- 將兩個排序好的子序列合并成一個最終的排序序列
算法流程圖
Java代碼實現
/**
* 歸并排序
* 穩定的排序算法
*
* @param nums 待排序數組
* @param low 待排序數組的起點
* @param high 待排序數組的終點
*/
public static void mergeSort(int []nums,int low,int high){
//算出分割點,2-路歸并即數組的中點
int mid = (high+low)/2;
if(low<high){
//遞歸分割
mergeSort(nums, low, mid);
mergeSort(nums, mid+1, high);
//調用歸并方法,將分割的進行歸并
merge(nums, low, mid, high);
}
}
/**
* 一次2-路歸并
* 可以想象成將兩個鏈表,按照升序的方法組合成一條鏈表
*
* @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,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數組
//新數組的長度有可能比nums數組短
for(int t=0;t<temp.length;t++){
nums[t+low] = temp[t];
}
}
算法分析
- 時間復雜度
- 最佳、最壞、平均:O(nlogn)
- 空間復雜度:O(n)
- 穩定定:穩定
- 優劣
- 優點:穩定性,性能不受輸入數據的影響
- 缺點:需要線性的額外空間
8. 基數排序
算法思想
先將所有關鍵字統一為相同位數,位數較少的數前邊補0.然后從最低位開始依次向高位進行排序,直到按最高位排序完成,關鍵字序列就成為有序序列。基數排序基于分別排序,分別收集,所以是穩定的。適用于很長的數的排序。
算法實現步驟
- 確定排序趟次,即確定最大的數的位數
- 從最低位按照分配和收集進行排序,直至最高位。
- 在每一趟,分配按照相應關鍵字的曲子將記錄加入到r個不同的隊列,收集是從小到大以此將r個隊列收尾相接成數組。
算法流程圖
從圖片可以看出,最大的數只有3位,進行3趟排序。先從個位進行排序,第二趟對十位數進行排序,最后對百位數進行排序。
Java代碼實現
/**
* 基數排序
*
* @param nums
*/
public static void radixSort(int []nums){
//確定排序趟次
int time = sortTime(nums);
//建立10個隊列
List<ArrayList> queue = new ArrayList<ArrayList>();
for (int i = 0; i < 10; i++) {
ArrayList<Integer> queue1 = new ArrayList<Integer>();
queue.add(queue1);
}
//進行time次分配和收集
for (int i = 0; i < time; i++) {
//分配數組元素;
for (int j = 0; j < nums.length; j++) {
//得到數字的第time+1位數
//先取余,再做除法
int x = nums[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
ArrayList<Integer> queue2 = queue.get(x);
queue2.add(nums[j]);
queue.set(x, queue2);
}
//元素計數器
int count = 0;
//收集隊列元素
for (int k = 0; k < 10; k++) {
while (queue.get(k).size() > 0) {
//如果該隊列有分配到元素,對其進行收集
ArrayList<Integer> queue3 = queue.get(k);
nums[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
//查看第N趟排序后的結果
/*
System.out.print("\n"+"第"+i+"趟排序結果:");
for(int m=0;m<nums.length;m++){
System.out.print(nums[m]+" ");
}
*/
}
}
/**
* 計算基數排序的遍歷趟次
*
* @param nums 待排序序列
* @return 返回一個int類型,表示需要遍歷的趟次
*/
public static int sortTime(int []nums){
//首先確定排序的趟數
//通過待排序列的最大數來確定趟數time
int max = nums[0];
for (int i = 1; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
}
int time = 0;
//判斷位數;
while (max > 0) {
max /= 10;
time++;
}
return time;
}
算法分析
- 時間復雜度
- 最佳、最壞、平均:O(mn)
- 空間復雜度:O(n)
- 穩定性:穩定
總結
算法 | 平均復雜度 | 最佳時間復雜度 | 最壞時間復雜度 | 空間復雜度 | 穩定性 |
---|---|---|---|---|---|
直接插入排序 | O(n2) | O(n) | O(n^2) | O(1) | 穩定 |
希爾排序 | O(nlogn) | O(n) | O(n^2) | O(1) | 不穩定 |
選擇排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不穩定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不穩定 |
冒泡排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 穩定 |
快速排序 | O(nlogn) | O(nlogn) | n(n^2) | O(nlogn) | 不穩定 |
歸并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 穩定 |
基數排序 | O(nm) | O(nm) | O(nm) | O(n) | 穩定 |