各種排序算法、十大排序算法

目錄

二分查找

冒泡排序

選擇排序

插入排序

希爾排序

歸併排序

快速排序

堆排序

計數排序

桶排序

基數排序

外部排序與歸併排序(強調一種思想)

動態規劃要點:


二分查找

//不使用遞歸實現: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. 針對填充數組進行關鍵字查找, 查找成功後記得判斷該元素是否來源於後來填充的那部分元素

參考鏈接:https://www.cnblogs.com/penghuwan/p/8021809.html#_label3

 

冒泡排序

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;
}

 

計數排序

https://mp.weixin.qq.com/s/WGqndkwLlzyVOHOdGK7X4Q(建議閱讀鏈接!優化穩定版計數排序不在下文中)

適用於數據比較集中的情況,有一定範圍,且範圍不是很大(此種情況性能比ologn快)

如:20個隨機整數,【0,10】,用最快的速度排序

先前的排序算法都是基於元素比較,而計數排序是利用數組下標來確定元素的正確位置

思路:根據整數的最大值max與最小值min之差dis,建立長度爲dis的數組,然後遍歷數組將每個數放在【數組-min】對應的位置

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

空間複雜度o(k=max-min,創建長度爲k的數組用於計數值出現的次數)

注意,以下情況不適用:

1.當數列最大最小值差距過大時,並不適用計數排序。

比如給定20個隨機整數,範圍在0到1億之間,這時候如果使用計數排序,需要創建長度1億的數組。不但嚴重浪費空間,而且時間複雜度也隨之升高。

2.當數列元素不是整數,並不適用計數排序。

如果數列中的元素都是小數,比如25.213,或是0.00000001這樣子,則無法創建對應的統計數組。這樣顯然無法進行計數排序。

對於這些侷限性,另一種線性時間排序算法(桶排序)做出了彌補

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)

參考鏈接:https://www.cnblogs.com/codeMedita/p/7425291.html

 

動態規劃要點:

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

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

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

  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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章