數組各種排序算法和復雜度分析


 

Java排序算法
1)分類:
  • 插入排序(直接插入排序、希爾排序)
  • 交換排序(冒泡排序、快速排序)
  • 選擇排序(直接選擇排序、堆排序)
  • 歸並排序
  • 分配排序(箱排序、基數排序)
所需輔助空間最多:歸並排序
所需輔助空間最少:堆排序
平均速度最快:快速排序
不穩定:快速排序,希爾排序,堆排序。
2)選擇排序算法的時候要考慮
  • 數據的規模 、
  • 數據的類型 、
  • 數據已有的順序。
  • 一般來說,當數據規模較小時,應選擇直接插入排序或冒泡排序。任何排序算法在數據量小時基本體現不出來差距。 考慮數據的類型,比如如果全部是正整數,那么考慮使用桶排序為最優。  考慮數據已有順序,快排是一種不穩定的排序(當然可以改進),對於大部分排好的數據,快排會浪費大量不必要的步驟。數據量極小,而起已經基本排好序,冒泡是最佳選擇。我們說快排好,是指大量隨機數據下,快排效果最理想。而不是所有情況。
3)總結:
——按平均的時間性能來分:    
  • 時間復雜度為O(nlogn)的方法有:快速排序、堆排序和歸並排序,其中以快速排序為最好;    
  • 時間復雜度為O(n2)的有:直接插入排序、起泡排序和簡單選擇排序,其中以直接插入為最好,特別是對那些對關鍵字近似有序的記錄序列尤為如此;     
  • 時間復雜度為O(n)的排序方法只有,基數排序。 當待排記錄序列按關鍵字順序有序時,直接插入排序和起泡排序能達到O(n)的時間復雜度;而對於快速排序而言,這是最不好的情況,此時的時間性能蛻化為O(n2),因此是應該盡量避免的情況。簡單選擇排序、堆排序和歸並排序的時間性能不隨記錄序列中關鍵字的分布而改變。
——按平均的空間性能來分(指的是排序過程中所需的輔助空間大小):     
  • 所有的簡單排序方法(包括:直接插入、起泡和簡單選擇)和堆排序的空間復雜度為O(1);
  • 快速排序為O(logn ),為棧所需的輔助空間;
  • 歸並排序所需輔助空間最多,其空間復雜度為O(n );
  • 鏈式基數排序需附設隊列首尾指針,則空間復雜度為O(rd )。
——排序方法的穩定性能:
  • 穩定的排序方法指的是,對於兩個關鍵字相等的記錄,它們在序列中的相對位置,在排序之前和 經過排序之后,沒有改變。
  • 當對多關鍵字的記錄序列進行LSD方法排序時,必須采用穩定的排序方法。
  • 對於不穩定的排序方法,只要能舉出一個實例說明即可。
  • 快速排序,希爾排序和堆排序是不穩定的排序方法。
各種排序算法Java版
 
/*
 * Copyright (c) 2005-2015 XXX Corporation. All rights reserved.
 *
 * Project Name: test
 * File Name:    SortTest.java
 * Package Name: XXX
 * date:         2016年7月28日
 *
 */
package dt;
import java.util.Arrays;
/**
 * ClassName:   SortTest <br/>
 * Description: TODO ADD DESCRIPTION. <br/>
 * date:        2016年7月28日 上午8:25:39 <br/>
 *
 * @author      danier
 */
public class SortTest {
    static int[] a = { 5,17,16, 7,10, 9, 18,4,15,14, 3, 1, 19,0, 20,6,13, 2, 12,8,11 };
    /**
     * main: ADD DESCRIPTION. <br/>
     * 執行流程: (可選). <br/>
     * 使用方法: (可選). <br/>
     * 注意事項: (可選). <br/>
     *
     * @author danier
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        long begin = System.currentTimeMillis();
        SortTest st = new SortTest();
        st.bubbleSort();
        st.selectSort();
        st.insertSort();
        st.halfInsertSort();
        st.hillsort();
        st.mergeSort(0, a.length - 1);
        st.quickSort(0, a.length - 1);
        long over = System.currentTimeMillis();
        System.out.println("使用的時間為:" + (over - begin) + "毫秒");
        System.out.print(Arrays.toString(st.a));
    }
    //    復雜度分析:一共要比較 ((n-1)+(n-2)+...+3+2+1)=n*(n-1)/2次,所以時間復雜度是O(n^2)
    //    冒泡排序就是把小的元素往前調或者把大的元素往后調。比較是相鄰的兩個元素比較,
    //    交換也發生在這兩個元素之間。所以,如果兩個元素相等,我想你是不會再無聊地把他們倆交換一下的;
    //    如果兩個相等的元素沒有相鄰,那么即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,
    //    所以相同元素的前后順序並沒有改變,所以冒泡排序是一種穩定排序算法。
    public void bubbleSort() {
        for (int i = a.length - 1; i > 1; i--) {
            for (int j = 0; j < i; j++) {
                if (a[j] > a[j + 1]) swap(j, j + 1);
            }
        }
    }
 
    //    選擇排序是不穩定算法,最好的情是已經排好順序,只要比較n*(n-1)/2次即可,
    //    最壞情況是逆序排好的,那么還要移動O(n)次,由於是低階故而不考慮不難得出選擇排序的時間復雜度是O(n^2)
    //    比較拗口,舉個例子,序列5 8 5 2 9, 我們知道第一遍選擇第1個元素5會和2交換,那么原序列中2個5的相對前后順序就被破壞了,所以選擇排序不是一個穩定的排序算法
    public void selectSort() {
        for (int i = 0; i < a.length; i++) {
            int min = i;
            for (int j = i + 1; j < a.length; j++) {
                if (a[j] < a[min]) min = j;
            }
            swap(i, min);
        }
    }
    //    插入排序的思想是這樣的,第一層for循環表示要循環n次,且每次循環要操作的主體是a[i],第二層循環是對
    //    a[i]的具體操作,是從原數祖第i個位置起,向前比較,所以插入排序的平均時間復雜度也是O(n^2).
    //    比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,
    //    如果比它大則直接插入在其后面,否則一直往前找直到找到它該插入的位置。如果碰見一個和插入元素相等的,
    //    那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后順序沒有改變,
    //    從原無序序列出去的順序就是排好序后的順序,所以插入排序是穩定的。
    void insertSort() {
        for (int i = 1; i < a.length; i++) {
            int temp = a[i], j = i;
            while (j > 0 && a[j - 1] > temp) {
                a[j] = a[j - 1];
                j--;
            }
            a[j] = temp;
        }
    }
 
    //    1)        最好情況:序列是升序排列,在這種情況下,需要進行的比較操作需(n-1)次。后移賦值操作為0次。即O(n)
    //    2)        最壞情況:O(nlog2n)。
    //    3)        漸進時間復雜度(平均時間復雜度):O(nlog2n)
    //    希爾排序是不穩定的。因為在進行分組時,相同元素可能分到不同組中,改變相同元素的相對順序
    public void hillsort() { int h = 1; while (h < a.length / 3) { h = h * 3 + 1; } while (h > 0) { for (int i = 1; i < a.length; i++) { int temp = a[i], j = i; while (j > h - 1 && a[j - h] > temp) { a[j] = a[j - h]; j -= h; } a[j] = temp; } h = (h - 1) / 3; } }
//    二分查找排序是穩定的,不會改變相同元素的相對順序, 
    //    1. 時間復雜度:O(n^2)
    //    二分查找插入位置,因為不是查找相等值,而是基於比較查插入合適的位置,所以必須查到最后一個元素才知道插入位置。
    //    二分查找最壞時間復雜度:當2^X>=n時,查詢結束,所以查詢的次數就為x,而x等於log2n(以2為底,n的對數)。即O(log2n)
    //    所以,二分查找排序比較次數為:x=log2n
    //    二分查找插入排序耗時的操作有:比較 + 后移賦值。時間復雜度如下:
    //    1)        最好情況:查找的位置是有序區的最后一位后面一位,則無須進行后移賦值操作,其比較次數為:log2n  。即O(log2n)
    //    2)        最壞情況:查找的位置是有序區的第一個位置,則需要的比較次數為:log2n,需要的賦值操作次數為n(n-1)/2加上 (n-1) 次。即O(n^2)
    //    3)        漸進時間復雜度(平均時間復雜度):O(n^2)
    void halfInsertSort() {
        for (int i = 1; i < a.length; i++) {
            if (a[i] > a[i - 1]) {
                continue;
            }
            int temp = a[i], left = 0, right = i - 1;
            while (left <= right) {
                int mid = (left + right) / 2;
                if (a[mid] > temp)
                    right = mid - 1;
                else
                    left = mid + 1;
            }
            for (int j = i; j > left; j--) {
                a[j] = a[j - 1];
            }
            a[left] = temp;
        }
    }
   

public void
mergeSort(int left, int right) { if (left >= right) return; int mid = (left + right) / 2; mergeSort(left, mid); mergeSort(mid + 1, right); merge(left, mid, mid + 1, right); } private void merge(int lb, int le, int rb, int re) { int[] temp = new int[a.length]; int leftbegin = lb; int index = lb; while (lb <= le && rb <= re) { if (a[lb] < a[rb]) temp[index++] = a[lb++]; else temp[index++] = a[rb++]; } while (lb <= le) { temp[index++] = a[lb++]; } while (rb <= re) { temp[index++] = a[rb++]; } while (leftbegin <= re) { a[leftbegin] = temp[leftbegin]; leftbegin++; } }

1》歸並排序的步驟如下:

       Divide: 把長度為n的輸入序列分成兩個長度為n/2的子序列。
       Conquer: 對這兩個子序列分別采用歸並排序。      
       Combine: 將兩個排序好的子序列合並成一個最終的排序序列。
2》時間復雜度:
這是一個遞推公式(Recurrence),我們需要消去等號右側的T(n),把T(n)寫成n的函數。其實符合一定條件的Recurrence的展開有數學公式可以套,這里我們略去嚴格的數學證明,只是從直觀上看一下這個遞推公式的結果。當n=1時可以設T(1)=c1,當n>1時可以設T(n)=2T(n/2)+c2n,我們取c1和c2中較大的一個設為c,把原來的公式改為:
這樣計算出的結果應該是T(n)的上界。下面我們把T(n/2)展開成2T(n/4)+cn/2(下圖中的(c)),然后再把T(n/4)進一步展開,直到最后全部變成T(1)=c(下圖中的(d)):

 

 
       把圖(d)中所有的項加起來就是總的執行時間。這是一個樹狀結構,每一層的和都是cn,共有lgn+1層,因此總的執行時間是cnlgn+cn,相比nlgn來說,cn項可以忽略,因此T(n)的上界是Θ(nlgn)。
       如果先前取c1和c2中較小的一個設為c,計算出的結果應該是T(n)的下界,然而推導過程一樣,結果也是Θ(nlgn)。既然T(n)的上下界都是Θ(nlgn),顯然T(n)就是Θ(nlgn)。
 
 

4)快速排序算法思想

基於分治的思想,是冒泡排序的改進型。首先在數組中選擇一個基准點(該基准點的選取可能影響快速排序的效率,后面講解選取的方法),然后分別從數組的兩端掃描數組,設兩個指示標志(lo指向起始位置,hi指向末尾),首先從后半部分開始,如果發現有元素比該基准點的值小,就交換lo和hi位置的值,然后從前半部分開始掃秒,發現有元素大於基准點的值,就交換lo和hi位置的值,如此往復循環,直到lo>=hi,然后把基准點的值放到hi這個位置。一次排序就完成了。以后采用遞歸的方式分別對前半部分和后半部分排序,當前半部分和后半部分均有序時該數組就自然有序了。

 

 
 
//    平均時間復雜度O(nlogn),最壞時間復雜度O(n*n),輔助空間O(logn)<每次都要分給一個額外空間,而總共有logn次>
//    每次分成兩段,那么分的次數就是logn了,每一次處理需要n次計算,那么時間復雜度就是nlogn了!
//    根據平均情況來說是O(nlogn),因為在數據分布等概率的情況下對於單個數據來說在logn次移動后就會被放到正確的位置上了。
//    最壞是O(n^2).這種情況就是數組剛好的倒序,然后每次去中間元的時候都是取最大或者最小。
//    穩定性:不穩定。
 public static int partition(int []array,int lo,int hi){
//左右中抽取3個點,按照213的順序排序,以左節點2作為pivot
int mid=lo+(hi-lo)/2;
if(array[mid]>array[hi]){
swap(array[mid],array[hi]);
}
if(array[lo]>array[hi]){
swap(array[lo],array[hi]);
}
if(array[mid]>array[lo]){
swap(array[mid],array[lo]);
}
int key=array[lo]; //此時左節點lo值為key。后續准備放置到lo和hi重合的位置

while(lo<hi){
//從右開始找到比key值小的數據,寫入lo
while(array[hi]>=key&&hi>lo){
hi--;
}
array[lo]=array[hi];

//從左開始找到比key值大的數據,寫入hi
while(array[lo]<=key&&hi>lo){
lo++;
}
array[hi]=array[lo];
}

// lo和hi重合時候,將key放入。此時hi左面的數都小於key,右面數大於key
array[hi]=key;
return hi;
}

public static void swap(int a,int b){
int temp=a;
a=b;
b=temp;
}
public static void sort(int[] array,int lo ,int hi){
if(lo>=hi){
return ;
}
int index=partition(array,lo,hi);
sort(array,lo,index-1);
sort(array,index+1,hi);
}


 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM