程序猿必備排序算法及其時間複雜度分析

經常使用的時間複雜度

常數階\(O(1)\)

說明: 只要代碼中沒有複雜的循環條件,不管代碼的函數是多少,一概爲常數階\(O(1)\)java

int i=1;
    int j=3;
    int m=0;
    m=i+j;
    ....

對數階 \(O(log_2n)\)

說明: 存在循環體,在不考慮循環體的代碼執行狀況,該循環體本該執行n次,可是循環體將改變循環變量i的大小,每執行一次,i就擴到兩倍,即i=i*2,這樣就致使循環體會加速趨近終止條件n;假設通過x次後,退出循環體,則有\(2^x>=n\),所以,\(x=log_2n\);同理,當\(i=i*3時,x=log_3n\),退出循環的速度更快,時間複雜度更小。git

while(i<n){
        i=i*2;
    }

線性階\(O(n)\)

說明: 存在循環體。循環體內代碼執行的次數隨着規模n的變化而變化,而且是線性變化程序員

for(int i=0;i<n;i++){
    int j=o;
    j++;
}

線性對數階\(O(nlog_2n)\)

說明: 將時間複雜度爲\(O(log_2n)\)的代碼重複執行n次,即爲線性對數階\(O(nlog_2n)\)算法

//n爲一個固定的常數
    //i和j均爲循環變量
    while(i<n){//線性節階
        while(j<n){//對數階
            j=j*2;
        }
    }

平方階\(O(n^2)\)

說明: 複雜度爲\(O(n)\)的代碼執行了n次。數組

for(int i=0;i<n;i++){//執行n次
        for(int j=0;j<n;j++){//執行n次
            int m=0;
            m++;
        }
    }

立方階O(n^3)

說明 和平方階原理同樣,時間複雜度爲\(O(n^2)\)的代碼執行n次緩存

for(int i=0;i<n;i++){//執行n次
        for(int j=0;j<n;j++){//執行n次
           for(int k=0;k<n;k++){
               int m=0;
               m++;
           }
        }
    }

綜上分析不難發現:時間複雜度的大小關係爲:\(O(1)<O(log_2n)<O(n)<O(nlog_2n)<O(n^2)<O(n^k)\)函數

程序員必備排序算法

程序猿必備的排序算法有:優化

  • 插入排序
    • 簡單直接插入
    • 希爾排序
  • 選擇排序
    • 簡單選擇排序
    • 堆排序
  • 交換排序
    • 冒泡排序
    • 快速排序
  • 歸併排序
  • 基數排序(桶排序)

冒泡排序(Bubble Sort)

基本原理: 將待排序的數組從左到右(下標從小到大), 依次選取元素,並和其相鄰的元素比較大小,若是逆序,則交換兩個元素的位置,每進行一輪,數組較大的元素會移動到數組的尾部,如同水中泡泡逐漸上浮同樣。重重複進行多輪操做,直到數組有序爲止。
圖解案例
以3 9 -1 10 20 爲例進行講解
冒泡排序
冒泡排序的幾個關鍵點ui

  • 進行arr.length-1趟排序
  • 每一趟排序均會將前面未被排序的最大元素「冒泡」到後半部分
  • 每一趟進行兩兩元素的比較,n個元素須要比較n-1次便可
  • 因爲每一趟的任務都是將前面未經排序的部分中較大的元素「冒泡」到後面,也就是說通過i趟以後,數組的後面的i個元素都是已經排好序的。所以第i趟只須要進行arr.lenth-1-i次比較便可

Java代碼實現部分編碼

/**
 * 優化的地方,當某一趟中,發現沒有進行過任何交換操做,則代表數組已經有序了,此時能夠當即結束操做,沒必要要進行循環操做。
 * 
 * */
public class BubbleSort{
    public static void bubbleSort(int arr[]){
        int temp = 0;
        boolean flag = false;// 表示是否進行過交換
        for (int i = 0; i < arr.length - 1; i++) {//進行arr.length-1趟排序
            for (int j = 0; j < arr.length - 1 - i; j++) {//每一趟排序都通過arr.length-1-i次兩兩比較
                if (arr[j] > arr[j + 1]) {
                    // 交換
                    flag = true;
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
            //優化代碼
            if (flag == false) {
                // 沒有發生過交換,代表是有序的
                break;
            } else {
                flag = false;// 重置flag,進行下次判斷
            }
        }
    }
}

時間複雜度分析
很顯然,兩個嵌套for循環,每一層for循環的時間複雜度均爲線性複雜度\(O(n)\),故冒泡排序算法的複雜度爲\(O(n^2)\)


快速排序(QuickSort)

基本原理:快速排序屬於交換排序,從數組中一個元素做爲基準元素(首元素或者中間元素),根據基準元素將數組分紅兩個部分,前半部分數組的元素均小於該基準值,後半部分元素均大於該基準值;而後使用遞歸的思想,對這兩個部分使用一樣的方法進行快速排序。
圖解案例

  • 以[4,7,6,5,3,2,8,1]數組爲例
    快速排序
    快速排序的幾個關鍵點
  • 選基準元素(首、尾元素或者中間元素)
  • 根據基準元素劃分數組,使得基準的前面元素均大於基準,基準後面的元素均小於該值
    Java代碼實現
public void QuickSort(int[] array,int left,int right){

        int l=left;
        int r=right;
        int pivot=array[(right+left)/2];//選取中間值爲基準值
        while(l<r){
            //從左邊開始找,直到找到大於或等於基準元素的位置爲止
            while(array[l]<pivot){
                l++;
            }
            //當找到了大於或等於基準元素的位置時,跳到右邊開始尋找
            while(array[r]>pivot){
                r++;
            }
            //此時,array[r]<=pivot<=array[l],所以交換順序
            int tmp=array[r];
            array[r]=array[l];
            array[l]=tmp;
            //注意:當array[r]==pivot或者array[l]==pivot,移動l或者r,否者
            //在這種狀況下會進入死循環
            if(array[l]==pivot){
                r--;
            }
            if(array[r]==pivot){
                l++;
            }
        }
        //當l==r時,須要錯開l和r,
        if(l==r){
            l++;
            r--;
        }
        if(left<r){
            QuickSort(array,left,r);
        }
        if(right>r){
            QuickSort(array,r,right);
        }
    }

時間複雜度: 顯然選擇排序的時間複雜度爲\(O(nlon^2n)\),


選擇排序(Selection Sort)

基本原理: 從array[0]array[n-1]中選擇一個最小的元素與array[0]交換位置,此時array[0]爲最小的元素;而後從array[1]array[n-1]中選取最小的元素與array[1]交換位置;選擇的範圍由全局全部變成0;因此須要通過n-1次選擇和交換的過程。
圖解案例

  • 思路圖解:
  • 初始數組:101,34,119,1
  • 第一趟:1,34,119,101
  • 第二趟:1,34,119,101
  • 第三趟:1,34,101,119

選擇排序的幾個關鍵點

  • 對於長度爲n的元素須要通過n-1次選擇的過程,由於最後一個元素不須要選擇,它是惟一,固然最小。
  • 在每以選擇的時候須要找到該次選擇的最小值,隨着選擇次數的遞增,所選擇的數組範圍逐漸變小

java代碼

public void SelectionSort(int[] array){
        
        for(int i=0;i<array.length-1;i++){//通過n-1輪選擇
            int minIndex=i;//初始最小值對應的索引
            int min=array[minIndex];//定義一個初始最小值
            for(int j=i;j<array.length;j++){//選擇最小值[i,length-1];索引從0開始
                if(array[j]<min){//找出當前選擇範圍的最小值
                    min=array[j];//更新最小值
                    minIndex=j;//更新最小值對應的索引
                }
            }
            //交換最小值和array[i]的值
            if(minIndex!=i){//不然不必交換
                //交換兩個數組元素的標準步驟
                array[minIndex]=array[i];
                array[i]=min;
            }
        }
    }

時間複雜度: 顯然選擇排序的時間複雜度爲\(O(n^2)\),緣由很簡單:兩個嵌套的for循環均爲\(O(n)\),注意:\(O(n-1)=O(n)\)


插入排序

基本原理: 將待排序數組array[0]~array[n-1]從邏輯上分爲有序數組和無序數組,初始條件:(array[0])爲有序數組,(array[1],..array[n-1])爲無序數組;則插入排序的基本過程是:逐一將無序數組中的元素插入到有序數組中去,每完成一次插入操做,有序數組中的元素加一,無序數組中的元素減一,直到全部的無序數組變成有序數組。
圖解案例

  • 原始數組:3,1,2,5,4
    • 第一次插入:3 1 2 5 4--->1 3 2 5 4
    • 第二次插入:1 3 2 5 4--->1 2 3 5 4
    • 第三次插入:1 2 3 5 4---->1 2 3 5 4 (實際上沒有進行任何插入操做)
    • 第4次操做 1 2 3 5 4 --->1 2 3 4 5

插入排序的幾個關鍵點

  • 從array[1]元素做爲有序數組和無序數組的分割點
  • 插入操做的代碼實現,首先肯定待插入的元素insertVal,對應的索引index,再肯定須要插入的位置insertIndex=index-1,插入操做爲:array[inserIndex]向後移動一位;
public void insertSort(int[] array){
        for(int i=1;i<array.length;i++){//從第一個元素開始
            int insertVal=array[i];//待插入的值爲當前索引指向的值(在尋找待插入位置時會覆蓋這個值,須要記錄下來)
            int insertIndex=i-1;//即將要插入的位置爲當前索引的前一個位置
            ////將當前元素與有序數組中的元素逐一比較,找到要插入的位置
            while(insertIndex>=0 && array[insertIndex]>insertVal){
                array[insertIndex+1]=array[insertIndex];//空出待插入的位置出來
                insertIndex--;
            }
            //開始執行插入操做,同時有個優化點,當插入的位置爲當前索引i時,不須要插入
            //因爲退出while時,insertIndex-1了,故真實的insertIndex=insertIndex+1;
            if(insertIndex!=i){
                array[insertIndex+1]=insertVal;
            }
        }
    }

時間複雜度: 顯然選擇排序的時間複雜度爲\(O(n^2)\)


希爾排序

基本原理: 將待排序的數組按照某個預約的增量gap分組,對每一個分組都採用直接插入排序的方式進行排序;隨後逐步按照某個策略縮小gap(如:gap=gap/2)並進行分組,使用直接插入排序算法分別對這些數組進行排序,當gap=1時,只有一個分組,使用希爾排序完成最後的一次排序。
圖解案例
希爾排序
希爾排序的關鍵步驟

  • 分組(gap逐漸縮小)
  • 對每一個分組使用直接插入排序

Java代碼實現部分

public void ShellSort(int[] array){
        //初始化gap爲數組長度的一半,每一次分組長度縮小一半
        for(int gap=array.lentgh/2;gap>0;gap=gap/2){
            //對當前分組中的全部組元素進行直接插入排序
            for(int i=gap;i<array.length;i++){ 
                int j=i;
                int tmp=array[j];//暫時保存待插入的值
                if(array[j]<array[j-gap]){//優化,其餘狀況不須要進行插入
                    while(j-gap>=0 && tmp<array[j-gap]){
                        array[j]=array[j-gap];//空出待插入的位置
                        j-=gap;
                    }
                    //退出while循環時,證實找到了合適的插入位置
                    array[j+gap]=tmp;
                }
               
            }
        }
    }

時間複雜度
第一層for循環爲分組功能,其時間複雜度爲:\(log_2n\),內部希爾排序最差爲\(O(n^2)\)


歸併排序

基本原理:採用分治思想和遞歸思想。歸併排序主要分爲兩大過程:分解和合並過程,分解是將數組二分至只有一個元素爲止。合併的過程當中同時進行排序,兩邊元素逐一比較,較小的元素填充到緩衝數組中。另外歸併排序須要額外的緩存數組。
圖解案例
分解過程使用了遞歸思想。
遞歸分解
合併過程爲:
合併
java代碼

public void mergeSort(int[] array,int left,int right,int[] temp){
	//遞歸分解
	//那麼遞歸的結束結束條件:left>=right
	if(left>=right){
		return;
	}
	int mid=(left+rightight)/2;
	//向左邊遞歸
	mergeSort(array,int left,mid,temp);
	//向右遞歸
	mergeSort(array,int mid+1,right,temp);
	merge(array,left,mid,right,temp);
	
}
/**
合併過程
**/
public void merge(int[] array,int left,int mid,int right,int[] temp){
	int i=left;//索引i範圍爲[left,mid];
	int j=mid+1;//索引j的範圍爲[mid+1,right];
	int t=0;//tmp額外數組的索引
	while(i<=mid && j<=right){
		if(array[i]<array[j]){
			temp[t]=array[i];
			t++;
			i++;
		}else{
			temp[t]=array[j];
			t++;
			j++;
		}
	}
	//當該循環退出的時候,有兩種種狀況(不可能同時超出邊界,最多同時到達邊界,但最後仍是有一個先)
	//超出邊界:
	//1.i>mid,右邊數組有剩餘
	//2.j>right,左邊數組有剩餘
	if(i>mid){
		while(j<=right){
			temp[t]=array[j];
			t++;
			j++;
		}
	}
	if(j>right){
		while(i<=mid){
			temp[t]=array[i];
			t++;
			i++;
		}
	}
	//將temp數組中的元素複製到array中
	//注意,temp中的有效部分並非從0開始,而是從[left,right]
	//所以只要複製的範圍是[left,right];
	t=0;//特別注意
	int tmpLeft=left;
	while(tmpLeft<=right){
		array[tmpLeft]=temp[t];
		t++;
		tmpLeft++;
	}
}

時間複雜度
第一層for循環爲分組功能,其時間複雜度爲:\(log_2n\),內部希爾排序最差爲\(O(nlog_2n)\)


桶排序

圖解
直接看圖解,語言難於生動的描述此過程
桶排序1
桶排序2
基本步驟:
(1)初始化1個二維數組10*array.length;表示10個桶,標號爲:0-9,每一個桶(用一個數組表示)最多容納array.length個元素。
(2)得到待排序數組中最大元素的最高位,依次按個位、十位、...最高位進行排序。
(3)第一輪,按個位進行選取元素,將個位放進對應的桶編號中,當全部的元素均放進桶中後,按順序讀出每一個桶中的元素,讀取到的序列做爲該輪排序的結果。再以該輪排序的結果做爲輸入條件,依照十位進行排序;最後直到全部的位數都完成。
編碼過程當中的幾個關鍵點

  • 獲取元素的個位、十位、百位:array[i]/1%10 array[i]/10%10 array[i]/100%10
  • 獲取某個元素的最多有幾位數:(n+"").length(),將整型強制轉化爲字符串類型,而後使用String的length()方法。
  • 因爲每完成一次「裝桶」操做,須要從每一個桶中讀取序列,每一個桶中個數不一,所以,能夠定義一個數組用於記錄每一個桶中存放了多少個元素。
  • 每一排序過程當中:桶的編號和位值是一一對應的關係.
    java代碼
public class RadixSort {
    public static void radixSort(int[] arr){
        //第一輪:針對每一個元素的個位進行排序
        
        //定義一個2維數組,表示10個桶,每一個桶都是一個一維數組
        //爲了防止數據的溢出,則每一個一維數組,大小定爲arr.length
        //很明顯,基數排序是使用空間換時間的經典排序算法
        int[][] bucket=new int[10][arr.length];
        //每一個桶都須要一個遊標指針。
        //所以咱們能夠定義一個數組來記錄每一個桶的遊標
        int[] bucketElementCounts=new int[10];
        //獲得數組中最大數的位數
        int max=arr[0];
        for(int i=1;i<arr.length;i++){
            if(arr[i]>max){
                max=arr[i];
            }
        }
        //技巧:獲取整數的最大位數
        int maxLength=(max+"").length();
        for(int i=0,n=1;i<maxLength;i++,n*=10){
            for (int j = 0; j < arr.length; j++) {
                // 取出每一個元素的個位、十位、百位。。。
                int digitOfElement = arr[j] / n % 10;
                bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
                bucketElementCounts[digitOfElement]++;
            }
            // 按照桶的順序依次取出數據,並放入原來的數組
            int index = 0;
            // 遍歷每個桶
            for (int k = 0; k < bucketElementCounts.length; k++) {
                // 若是桶中有數據,咱們才放入數據到原數組
                if (bucketElementCounts[k] != 0) {
                    // 取出該個桶中的數據
                    for (int l = 0; l < bucketElementCounts[k]; l++) {
                        // 取出元素放入到原數組中
                        arr[index] = bucket[k][l];
                        index++;
                    }
                }
                // 第一輪處理後,須要將每一個桶的清零
                bucketElementCounts[k] = 0;
            }
        }  
    }
}
相關文章
相關標籤/搜索