1. 程式人生 > >各種排序演算法

各種排序演算法

目錄

二分查詢

氣泡排序

選擇排序

插入排序

希爾排序

歸併排序

快速排序

堆排序

計數排序

桶排序

基數排序

二分查詢

//不使用遞迴實現:while迴圈,時間O(log2 N),空間O(1)
public static int commonBinarySearch(int[] arr,int key){
    int low = 0;
    int high = arr.length - 1;
    int middle = 0;         //定義middle
    if(key < arr[low] || key > arr[high] || low > high){
        return -1;
    }
    while(low <= high){
        middle = (low + high) / 2;
        if(arr[middle] > key){
            //比關鍵字大則關鍵字在左區域
            high = middle - 1;
        }else if(arr[middle] < key){
            //比關鍵字小則關鍵字在右區域
            low = middle + 1;
        }else{
            return middle;
        }
    }
    return -1;      //最後仍然沒有找到,則返回-1
}

//使用遞迴實現,時間O(log2 N),空間O(log2N )
public static int recursionBinarySearch(int[] arr,int key,int low,int high){
    if(key < arr[low] || key > arr[high] || low > high){
        return -1;
    }
    int middle = (low + high) / 2;          //初始中間位置
    if(arr[middle] > key){
        //比關鍵字大則關鍵字在左區域
        return recursionBinarySearch(arr, key, low, middle - 1);
    }else if(arr[middle] < key){
        //比關鍵字小則關鍵字在右區域
        return recursionBinarySearch(arr, key, middle + 1, high);
    }else {
        return middle;
    }
}

二分查詢優化:

1、插值查詢演算法 將mid=left + (right-left)/2 的計算更改為 mid = left + ((target-min)/(max-target))*(right-left),即更換1/2係數

2、斐波那契查詢演算法

https://images2017.cnblogs.com/blog/1060770/201712/1060770-20171211095643381-1866544360.png

  1. 根據待查詢陣列長度確定裴波那契陣列的長度(或最大元素值)
  2. 根據1中長度建立該長度的裴波那契陣列,再通過F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)生成裴波那契數列為陣列賦值
  3. 以2中的裴波那契陣列的最大值為長度建立填充陣列,將原待排序陣列元素拷貝到填充陣列中來, 如果有剩餘的未賦值元素, 用原待排序陣列的最後一個元素值填充
  4. 針對填充陣列進行關鍵字查詢, 查詢成功後記得判斷該元素是否來源於後來填充的那部分元素

氣泡排序

public static void bubbleSort(int[] data){
    if(data == null) return;
    for (int i = 0; i < data.length; i++) {
        for (int j = 1; j < data.length-i; j++) {
            if (data[j-1]>data[j]) {
                int temp = data[j];
                data[j]=data[j-1];
                data[j-1]=temp;
            }
        }
    }
    return;
}

優化版:解釋連結:https://mp.weixin.qq.com/s/wO11PDZSM5pQ0DfbQjKRQA

public static void bubbleSortUpdate(int[] data){
    if(data == null) return;
    //記錄最後一次交換的位置
    int lastExchangeIndex =0; //解決原陣列中後部分都為有序情況下的比較浪費
    //無序數列的邊界,每次比較只需要比到這裡為止
    int sortBorder = data.length;
    for (int i = 0; i < data.length; i++) {
        //有序標記,每一輪的初始是true,當有一輪比較沒有找到需要更換位置的資料時,可以直接退出整個迴圈了
        boolean isSorted = true;
        for (int j = 1; j < sortBorder; j++) {
            if (data[j-1]>data[j]) {
                int temp = data[j];
                data[j]=data[j-1];
                data[j-1]=temp;
                //有元素交換,所以不是有序,標記變為false
                isSorted = false;
                //把無序數列的邊界更新為最後一次交換元素的位置
                lastExchangeIndex  = j;
            }
        }
        sortBorder = lastExchangeIndex;
        if (isSorted)
            break;
    }
    return;
}

選擇排序

public static void selectionSort(int[] data){
    if(data == null) return;
    int curMinIndex = 0;
    for (int i = 0; i < data.length; i++) {
        curMinIndex=i;
        for (int j = i; j < data.length; j++) {
            if (data[curMinIndex]>data[j]) {
                curMinIndex = j;
            }
        }
        int temp = data[i];
        data[i]=data[curMinIndex];
        data[curMinIndex]=temp;
    }
}

插入排序

public static void insertionSort(int[] data){
    if(data == null) return;
    int now = 0;
    int index = 0;
    for (int i = 1; i < data.length; i++) {
        index = i;
        now = data[i];
        while (index>0&&data[index-1]>now) {
            data[index]=data[index-1];
            index--;
        }
        data[index] = now;
    }
}

希爾排序

public static void shellSort(int[] data){
    if(data==null || data.length<=1)
        return;
    //陣列長12 d=6  d=3
    for(int gap=data.length/2; gap>0; gap=gap/2){
        //i=6 7   /  3 4 5
        for(int i=gap;i<data.length;i++){
            int cur = i;
            int temp = data[i];
            //這個步驟類似於直接插入排序
            while (cur-gap>=0 && data[cur-gap]>temp) {
                data[cur] = data[cur-gap];
                cur = cur-gap;
            }
            data[cur]=temp;
        }
    }
}

歸併排序

public static void Merge(int[] data){
    if (data==null) return;
    //在排序前,先建好一個長度等於原陣列長度的臨時陣列,避免遞迴中頻繁開闢空間
    int[] temp = new int[data.length];
    sort(data,0,data.length-1,temp);
}
private static void sort(int[] data, int start, int end, int[] temp) {
    if(start<end){
        int mid = start + (end - start)/2;
        sort(data,start,mid,temp);//左邊歸併排序,使得左子序列有序
        sort(data,mid+1,end,temp);//右邊歸併排序,使得右子序列有序
        merge(data,start,mid,end,temp);//將兩個有序子數組合並操作
    }
}
private static void merge(int[] data, int start, int mid, int end, int[] temp) {
    int left = start;//左序列指標
    int right = mid+1;//右序列指標
    int tempIndex = 0;//臨時陣列指標
    while (left<=mid && right<=end){
        if(data[left]<=data[right]){
            temp[tempIndex++] = data[left++];
        }else {
            temp[tempIndex++] = data[right++];
        }
    }
    while(left<=mid){//將左邊剩餘元素填充進temp中
        temp[tempIndex++] = data[left++];
    }
    while(right<=end){//將右序列剩餘元素填充進temp中
        temp[tempIndex++] = data[right++];
    }
    tempIndex = 0;
    //將temp中的元素全部拷貝到原陣列中
    while(start <= end){
        data[start++] = temp[tempIndex++];
    }
}

快速排序

public static void quickSort(int[] data,int start,int end) {
   if (data==null) return;
   if (start>=end) return;
   //獲得start元素在原陣列中排序後的準確的位置索引
   int index = partition3(data,start,end);
   quickSort(data,start,index-1);
   quickSort(data,index+1,end);
}
//作用:根據輸入data【】,start與end,返回data[start]在排序陣列中準確的位置
private static int partition(int[] data, int start, int end) {
   if(start>=end)
          return end;
   //儲存目標值
   int target=data[start];
   //start是前面的哨兵,end是後面的哨兵
   while(end>start){
      //右哨兵從當前位置迴圈找到一個小於目標值的index
      while (end>start&&data[end]>target) 
         end--;
      //執行與左哨兵更換,並讓左哨兵走一步
      if (end>start) 
         data[start++] = data[end];
      //左哨兵迴圈找到一個大於目標值的index
      while(end>start&&data[start]<target)
         start++;
      //左哨兵與右哨兵交換,並讓右哨兵向左走一步
      if (end>start) 
         data[end--] = data[start];
   }
   //當執行到這裡,start=end
   data[start]=target;
   //System.out.println(start);
   return start;
}

堆排序

private static void heapSort(int[] data){
   if (data==null) return;
   //1.構建初始大頂堆
   //data.length/2-1定位到倒數第一個非葉子結點
   for (int i = data.length/2-1; i >= 0; i--) {
      adjustHeap(data,i,data.length);
   }
   //2.交換堆頂元素和末尾元素並重建堆
   for (int j = data.length-1; j >0; j--) {
      swapUtil.swap(data, 0, j);
      adjustHeap(data,0,j);
   }
}

//調整堆為最大堆,第二個引數i為需要考慮調整的節點,此處需要傳入第三個引數長度,因為最後搭建排序陣列的時候參加運算的陣列長度會減小
private static void adjustHeap(int[] data, int i, int length) {
   int temp = data[i];
   for (int j = 2*i+1; j < length; j=2*j+1) {
      //若當前節點的右子節點的值大於左子節點的值,則定位到右子節點
      if (j+1 < length && data[j+1]>data[j]) {//若為最小堆,則第二個>換為<號
         j++;
      }
      //若當前考慮的節點(子節點)大於其父節點,則將其賦值給父節點,不用進行交換,到退出迴圈時再交換
      if (data[j]>temp) {//若為最小堆,則這裡換為<號
         data[i] = data[j];
         i = j;
      }else {
         break;
      }
   }
   data[i]=temp;
}

計數排序

適用於資料比較集中的情況

時間複雜度o(n+k=遍歷n查詢最大最小值,建立計數陣列(max-min=k),再遍歷n計數每個值出現的次數,再遍歷新陣列k進行排序)

空間複雜度o(k=max-min,建立長度為k的陣列用於計數值出現的次數)

private static void CountSort(int[] data){
   if (data==null) return;
   int min = data[0];
   int max = data[0];
   for (int i = 0; i < data.length; i++) {
      if (data[i]>max) {
         max = data[i];
      }else if (data[i]<min) {
         min = data[i];
      }
   }
   //以上步驟只是為了找出最大最小值以便於建立臨時陣列,非計數排序必須,這裡只是因為輸入的陣列沒有規定陣列大小範圍
   int[] bucket = new int[max-min+1];
   for (int i = 0; i < data.length; i++) {
      bucket[data[i]-min]++;
   }
   for (int i = 0; i < bucket.length; i++) {
      if (bucket[i]!=0) {
         for (int j = 0; j < bucket[i]; j++) {
            System.out.print(i+min);
            System.out.print(" ");
         }
      }
   }
}

桶排序

適用於最大最小值相差較大的情況,但是值的分佈要夠均勻

時間複雜度o(n+k=遍歷原陣列n找到最大最小值,建立桶陣列,遍歷原陣列n將資料放入桶,排序每個桶內元素後遍歷桶取出資料k)

空間複雜度o(n+k=需要一個長度為n的陣列作為桶,每個桶裡面儲存一個數組List,陣列的每個位置區間大小為k,k=(max-min)/n+1(經過驗證,這個k最好要加1,使得程式魯棒性得以提升,即區間算出來後要加1))

public static void bucketSort(int[] arr){
    //新建一個大小為原陣列長度的陣列
    ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(arr.length);
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for(int i = 0; i < arr.length; i++){
        max = Math.max(max, arr[i]);
        min = Math.min(min, arr[i]);
        bucketArr.add(new ArrayList());
    }
    //每個桶的區間大小
    int bucketNum = (max - min) / arr.length+1;
    //將每個元素放入桶
    for(int i = 0; i < arr.length; i++){
        int num = (arr[i] - min) / bucketNum ;
        bucketArr.get(num).add(arr[i]);
    }
    //對每個桶進行排序
    for(int i = 0; i < bucketArr.size(); i++){
        Collections.sort(bucketArr.get(i));
    }
    System.out.println(bucketArr.toString());
}

基數排序

時間複雜度o(n*k=遍歷原陣列n得到陣列的最大值的位數k,再遍歷k遍原陣列n)

public static void radixSort(int[] a) {
    int exp;    // 指數。當對陣列按個位進行排序時,exp=1;按十位進行排序時,exp=10;...
    int max = getMax(a);    // 陣列a中的最大值
    // 從個位開始,對陣列a按"指數"進行排序
    for (exp = 1; max/exp > 0; exp *= 10)
        countSort2(a, exp);
}
private static void countSort2(int[] a, int exp) {
   int[] output = new int[a.length];    // 儲存"被排序資料"的臨時陣列
   LinkedList[] buckets = new LinkedList[10];
    for (int i = 0; i < buckets.length; i++) {
       buckets[i]=new LinkedList();
   }
    // 將資料儲存在buckets[]中
       for (int i = 0; i < a.length; i++){
           //int temp = (a[i]/exp)%10;
           buckets[(a[i]/exp)%10].offer(a[i]);
       }
       int temp = 0;
       // 將資料儲存到臨時陣列output[]中
       for (int j = 0; j < 10; j++) {
           while (buckets[j].peek()!=null) {
              output[temp++]=(int) buckets[j].poll();
          }
   }
       // 將排序好的資料賦值給a[]
       for (int i = 0; i < a.length; i++)
           a[i] = output[i];
       output = null;
       buckets = null;
}

有時,待排序的檔案很大,計算機記憶體不能容納整個檔案,這時候對檔案就不能使用內部排序了(這裡做一下說明,其實所有的排序都是在記憶體中做的,這裡說的內部排序是指待排序的內容在記憶體中就可以完成,而外部排序是指待排序的內容不能在記憶體中一下子完成,它需要做內外存的內容交換),外部排序常採用的排序方法也是歸併排序,這種歸併方法由兩個不同的階段組成:

1、採用適當的內部排序方法對輸入檔案的每個片段進行排序,將排好序的片段(成為歸併段)寫到外部儲存器中(通常由一個可用的磁碟作為臨時緩衝區),這樣臨時緩衝區中的每個歸併段的內容是有序的。

2、利用歸併演算法,歸併第一階段生成的歸併段,直到只剩下一個歸併段為止。

例如要對外存中4500個記錄進行歸併,而記憶體大小隻能容納750個記錄,在第一階段,我們可以每次讀取750個記錄進行排序,這樣可以分六次讀取,進行排序,可以得到六個有序的歸併段,如下圖:

每個歸併段的大小是750個記錄,記住,這些歸併段已經全部寫到臨時緩衝區(由一個可用的磁碟充當)內了,這是第一步的排序結果。

完成第二步該怎麼做呢?這時候歸併演算法就有用處了,演算法描述如下:

1、將記憶體空間劃分為三份,每份大小250個記錄,其中兩個用作輸入緩衝區,另外一個用作輸出緩衝區。首先對Segment_1和Segment_2進行歸併,先從每個歸併段中讀取250個記錄到輸入緩衝區,對其歸併,歸併結果放到輸出緩衝區,當輸出緩衝區滿後,將其寫到臨時緩衝區內,如果某個輸入緩衝區空了,則從相應的歸併段中再讀取250個記錄進行繼續歸併,反覆以上步驟,直至Segment_1和Segment_2全都排好序,形成一個大小為1500的記錄,然後對Segment_3和Segment_4、Segment_5和Segment_6進行同樣的操作。

2、對歸併好的大小為1500的記錄進行如同步驟1一樣的操作,進行繼續排序,直至最後形成大小為4500的歸併段,至此,排序結束。

以上對外部排序如何使用歸併演算法進行排序進行了簡要總結,提高外部排序需要考慮以下問題:

1、如何減少排序所需的歸併趟數。

2、如果高效利用程式緩衝區,使得輸入、輸出和CPU執行儘可能地重疊。

3、如何生成初始歸併段(Segment)和如何對歸併段進行歸併。

此演算法適用於用小記憶體排序大資料量的問題

假設要對1000G資料用2G記憶體進行排序

方法:每次把2G資料從檔案傳入記憶體,用一個“記憶體排序”演算法排好序後,再寫入外部磁碟的一個2G的檔案中,之後再從1000G中載入第二個2G資料。迴圈500遍。就得到500個檔案,每個檔案2G,檔案內部都是有序的。

然後進行歸併排序,比較第1/500和2/500的檔案,分別讀入750MB進入記憶體,記憶體剩下的500MB用來臨時儲存生成的資料,直到將兩個2G檔案合併成4G,再進行後面兩個2G檔案的歸併……。另外,也可以用歸併排序的思想同時對500個2G的檔案直接進行歸併

優化思路:

  1. 增設一個緩衝buffer,加速從檔案到記憶體的轉儲

        假設這個buffer已經由系統幫我們優化了

  1. 使用流水線的工作方式,假設從磁碟讀資料到記憶體為L,記憶體排序為S,排完寫磁碟為T,因為L和S都是IO操作,比較耗時間,所以可以用流水線,在IO操作的同時記憶體也在進行排序
  2. 以上流水線可能會出現記憶體溢位的問題,所以需要把記憶體分為3部分。即每個流水線持有2G/3的記憶體。
  3. 在歸併排序上進行優化,最後得到的500個2G檔案,每次掃描檔案頭找最小值,最差情況要比較500次,平均時間複雜度是O(n),n為最後得到的有序陣列的個數,優化思路是:維護一個大小為n的“最小堆”,每次返回堆頂元素(當前檔案頭數值最小的那個值),判斷彈出的最小值是屬於哪個檔案的,將哪個檔案此時的標頭檔案所指向的數再插入最小堆中,檔案指標自動後移,插入過程為logn,最小堆返回最小值為o(1),執行時空間複雜度為o(n)

將原問題拆解成若干子問題,同時儲存子問題的答案,使得每個子問題只求解一次,最終獲得原問題的答案

大多數動態規劃問題本質都是遞迴問題——重疊子問題——記憶化搜尋(自頂向下)

                                                                                                ——動態規劃(自底向上)

  1. 求一個問題的最優解
  2. 整體問題的最優解依賴各個子問題的最優解
  3. 子問題之間有相互重疊的更小的子問題
  4. 從上往下分析問題,從下往上求解問題

三個重要概念:最優子結構,邊界,狀態轉移公式

遞迴:記憶化搜尋——自上而下的解決問題

動態規劃——自下而上的解決問題

0-1揹包問題示例:

public int SingleArray() {
    int[] weight = {3,5,2,6,4}; //物品重量
    int[] val = {4,4,3,5,3}; //物品價值
    int length = weight.length;
    int w = 12;
    //如果是不需要裝滿,則初始化0,要裝滿則初始化Integer.MIN_VALUE
    int[] dp = new int[w+1];//+1的目的使得i位置代表體積為i
    for (int i = 0; i < length; i++) {
        //for(int j=weight[i];j<dp.length;j++)完全揹包問題(無限使用)使用此迴圈
        for (int j = dp.length-1; j >= weight[i] ; j--) {
            dp[j] = Math.max(dp[j],dp[j-weight[i]]+val[i]);
        }
    }
    return dp[w];
}

0-1揹包問題更詳細的參考連結:https://blog.csdn.net/ls5718/article/details/52227908