演算法之排序(Java版)-持續更新補充

kissjz發表於2018-08-03

開始複習排序,主要是按照《輕鬆學演算法》這本書的目錄來學習,同時搜尋網上的部落格文章來輔助,參考的來源均在文章底部標明。同樣,本文持續更新,程式碼書上只給了基礎排序程式碼(我稱之為原始排序程式碼),擴充套件的都是我自己寫的,均通過測試,如果你覺得有寫得不好或者能夠優化的地方,非常歡迎留言評論或者私聊QQ我,大家交流學習,共同進步~~

排序

一、桶排序

n為待排序的元素,m為桶的個數。排序只需要遍歷一遍所有元素,輸出就只需要遍歷一遍桶,是非常快速的排序。當待排序元素分佈很均勻的時候才能更有效的利用好空間,也彌補了桶排序犧牲空間換時間的不足。
當然,桶排序還有痛點:小數,符數。
實際使用的時候會根據實際情況採取巧妙的解決辦法,比如結合雜湊表,來提高空間利用率。

public class BucketSort {
    private int[] buckets;
    private int[] array;
    
    public BucketSort(int range, int[] array) {
        this.buckets = new int[range];
        this.array = array;
    }
    public void sort() {
        if(array != null && array.length > 1) {
            for(int i = 0; i < array.length - 1; i++) {
                buckets[array[i]]++;
            }
        }
    }
    public void print() {
        for(int i = buckets.length - 1; i >= 0; i--) {
            for(int j = 0; j < buckets[i]; j++) {
                System.out.println(i);
            }
        }
    }
}

二、氣泡排序

體育老師肯定明白啥叫氣泡排序,每次重新組成班級了,體育課一開始要分成幾個小隊,讓我們站成男女兩排,高個在前,跟周圍人比較,誰高誰到前面去,然後再報數一二一二,分成四隊,簡直完美!
氣泡排序最壞情況是要n-1輪,並且每輪的每次比較之後都需要交換位置,時間複雜度為O(n^2)。用到的額外空間就是一個臨時變數temp。同時氣泡排序是穩定的,遇到相同的元素並不會交換位置,所以對於同樣大小的元素相對位置不會改變。

原始的氣泡排序是非常low的,有兩種方法可以改進:

  1. 標記最後一次比較的位置,比如10,8,5,1,2,這個只需要經過一趟排序就能搞定。
  2. 一次冒泡兩個元素,對於每一趟比較,正向、反向分別進行把最大和最小的都冒出去,這樣可以使排序趟數減少一半。

還有一點可以加在這兩個方法之中,就是如果發現沒有交換,則說明全部有序,直接退出。
詳細看下面三段程式碼,分別對應著上面的原始冒泡標記最後位置一次冒兩個

//1. 原始冒泡
public class BubbleSort {
    private int[] array;
    
    public BubbleSort(int[] array) {
        this.array = array;
    }
    public void sort() {
        int length = array.length;
        
        if(length > 0) {
            for(int i = 1; i < length - 1; i++) {
                for(int j = 0; j < length - i; j++) {
                    if(array[j] > array[j + 1]) {
                        int temp = array[j];
                        array[j] = array[j + 1];
                        array[j + 1] = temp;
                    }
                }
            }
        }
    }
    public void sort2() {
        int length = array.length;
        
        if(length > 0) {
            for(int i = length -1; i > 0; i--) {
                for(int j = length - 1; j > length - 1 - i; j--) {
                    if(array[j] > array[j - 1]) {
                        int temp = array[j];
                        array[j] = array[j - 1];
                        array[j - 1] = temp;
                    }
                }
            }
        }
    }
    public void print() {
        for(int i = 0; i < array.length -1; i++) {
            System.out.println(array[i]);
        }
    }
}
//2. 標記最後交換位置冒泡

public class BubbleSort {
    private int[] array;
    private int mark;
    private boolean exchange = false;
    private int count;//用來記錄總趟數
    
    public BubbleSort(int[] array) {
        this.array = array;
    }
    public void sort() {
        int length = array.length;
        mark = length - 1;
        if(length > 0) {
            for(int i = 1; i < length - 1; i++) {
                exchange = false;
                int markIndex = mark;
                for(int j = 0; j < markIndex; j++) {
                    if(array[j] > array[j + 1]) {
                        exchange = true;
                        mark = j + 1;
                        int temp = array[j];
                        array[j] = array[j + 1];
                        array[j + 1] = temp;
                    }
                }
//                如果發現沒有交換,則說明全部有序,退出
                if(!exchange) {
                    break;
                }
                count++;
            }
        }
    }
    public void print() {
        for(int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
        System.out.println("總趟數:" + count);
    }
}
//3. 一次冒泡兩個
public class BubbleSort {
    private int[] array;
    private boolean exchange = false;
    private int count;
    
    public BubbleSort(int[] array) {
        this.array = array;
    }
    public void sort() {
        int length = array.length;
        
        if(length > 0) {
            for(int i = 1; i < length - 1; i++) {
                exchange = false;
                for(int j = 0; j < length - i; j++) {
                    if(array[j] > array[j + 1]) {
                        exchange = true;
                        int temp = array[j];
                        array[j] = array[j + 1];
                        array[j + 1] = temp;
                    }
                }
                for(int j = length - i; j > 0; j--) {
                    if(array[j] < array[j - 1]) {
                        exchange = true;
                        int temp = array[j];
                        array[j] = array[j - 1];
                        array[j - 1] = temp;
                    }
                }
                count++;
                if(!exchange) {
                    break;
                }
            }
        }
    }
    public void print() {
        for(int i = 0; i < array.length -1; i++) {
            System.out.println(array[i]);
        }
        System.out.println("總趟數:" + count);
    }
}

三、快速排序

名字很吊,快速排序,確實也是,基本是相同數量級所有排序演算法中,平均效能最好的,而且簡單,用的是分治的思想。
快速排序可以說是氣泡排序的改進,冒泡每次只能交換相鄰的元素,而快速排序是跳躍式的交換。顯然,快速排序最差的情況就是每次都需要交換,是O(n^2),時間複雜度一般是O(nlogn)
因為使用的是原來的陣列空間,但是由於每次劃分之後遞迴呼叫,會消耗棧的空間,所以空間複雜度一般為O(logn)。同樣,最差的情況是每次只完成了一個元素,那就是O(n)了。
快速排序是不穩定的,也就是說每次排序對相同值的元素的相對位置會發生改變,氣泡排序則不會。

同樣的,相對於原始的快速排序,書裡面也給了幾個改進措施。

  1. 三者取中。如果每次選取基準值都取第一個數,那可能造成每次都需要移動,是演算法時間複雜度變為O(n^2)。這裡解決辦法是,每次取頭,尾,中的三個數,取中間大小的作為基準值。
  2. 根據規模大小改變演算法。在資料量較小的時候切換為其他演算法進行排序,一般為5~25。實現略。
  3. 分三個區間。大於基準數,小於基準數,和等於基準數的三個區間,這樣每次只遞迴大於和小於部分的。實現略。
  4. 並行處理。因為快速排序只對陣列中每一小段範圍排序,對其它段沒有影響,所以可以使用多執行緒並行處理。實現略。
    tip:實現省略的幾個以後學得更深入了可能會回來再補充。
//1. 原始快速排序
public class QuickSort {
    private int[] array;
    
    public QuickSort(int[] array) {
        this.array = array;
    }
    
    public void sort() {
        quickSort(array, 0, array.length - 1);
    }
    public void print() {
        for(int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
    }
    public void quickSort(int[] src, int begin, int end) {
        if(begin < end) {
           int key = src[begin];
           int i = begin;
           int j = end;
           
           while(i < j) {
               while(i < j && src[j] > key) {
                   j--;
               }
               if(i < j) {
                   src[i] = src[j];
                   i++;
               }
               while(i < j && src[i] < key) {
                   i++;
               }
               if(i < j) {
                   src[j] = src[i];
                   j--;
               }
           }
           src[i] = key;
           quickSort(src, begin, i - 1);
           quickSort(src, i + 1, end);
        }
    }
}
//2. 基準值三者取中
/*
*這個參考了別人的部落格(連結在底部),但看見他的方法略微繁瑣,結合書中原始的快速排序程式碼,加以了改進。
*
*/
public class QuickSort {
    private int[] array;
    private int count;//記錄遞迴次數
    
    public QuickSort(int[] array) {
        this.array = array;
    }
    
    public void sort() {
        quickSort(array, 0, array.length - 1);
    }
    public void print() {
        for(int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
        System.out.println("遞迴的次數:" + count);
    }

    private void quickSort(int[] src, int begin, int end) {
        if(begin < end) {
            count++;
           int i = begin;
           int j = end;
           int mid = (begin + end) / 2;
           findMiddle(src, begin, mid, end);
           int key = src[i];
           
           while(i < j) {
               while(i < j && src[j] > key) {
                   j--;
               }
               if(i < j) {
                   src[i] = src[j];
                   i++;
               }
               while(i < j && src[i] < key) {
                   i++;
               }
               if(i < j) {
                   src[j] = src[i];
                   j--;
               }
           }
           src[i] = key;
           quickSort(src, begin, i - 1);
           quickSort(src, i + 1, end);
        }
    }
    /*這個方法同時完成了兩件事,找到中間值,並且把中間值和begin位置的值互換
            從而巧妙的把後續求解過程轉換成了原始的快速排序過程
    */
    private static void findMiddle(int[] src, int begin, int mid, int end) {
        if((src[mid] > src[begin] && src[mid] < src[end]) || (src[mid] > src[end] && src[mid] < src[begin])) {
            swap(src, begin, mid);
        }else if((src[end] > src[begin] && src[end] < src[mid]) || (src[end] > src[mid] && src[end] < src[begin])) {
            swap(src, begin ,end);
        }else {
            return;
        }
    }
    private static void swap(int[] src, int a, int b) {
        int temp = src[a];
        src[a] = src[b];
        src[b] = temp;
    }
}

四、插入排序

有兩種,一種是直接插入排序,一種是二分插入排序。
直接插入排序的思想:分為兩列,一列已經排好序,一列沒有,沒有排好序的那列的值一個個加入已經排序好的那列。二分插入排序是在直接插入排序的基礎上使用二分查詢來找到插入的位置。
直接插入排序時間複雜度是O(n^2),空間複雜度是O(1),因為是陣列內部自己排序,後面新加入的按照一個個與前面已經排好序的比較,再移動,所以可以保持相同值的相對位置不變,所以是穩定的

插入排序發揮最好的場景是當數列已經近似有序的時候,所以一般與快速排序搭配使用。也就是在快速排序的分割槽規模達到一定的值比如5~25的時候,而往往這時候每個分割槽中也近似有序了,所以正好可以切換為使用插入排序來輔助。

希爾排序

原始的直接插入排序的改進就是傳說中的————希爾排序
基本思想:把待排序的數列按照一定間隔分組,然後對各個組的數列進行插入排序。這個間隔(說間隔有點不準確,想不到更好的詞了,意思不理解可以直接看程式碼,或者百度一下再回來~)叫做增量,增量逐漸減少到1為止。

對於希爾排序的時間複雜度,因為增量的序列不一定,所以時間複雜度也不確定。增量序列有幾種方法,這裡先不記錄了。
如果採用每除以2的增量選擇,最好情況仍是待排序陣列本身有序,O(n),最壞情況是O(n^2)。一般認為希爾排序的平均時間複雜度為O(n^(1.3))
希爾排序中會進行分組,排序,同樣的值的元素如果不在同一個組裡,其相對位置可能變化,所以希爾排序是不穩定的,這與插入排序不同。

//1. 直接插入排序
public class InsertSort {
    private int[] array;
    
    public InsertSort(int[] array) {
        this.array = array;
    }
    
    public void sort() {
        if(array == null) {
            throw new RuntimeException("array is null");
        }
        int length = array.length;
        if(length > 1) {
            for(int i = 1; i < length; i ++) {
                int j = i;
                int temp = array[i];
                for(; j > 0 && array[j - 1] > temp; j--) {
                    array[j] = array[j - 1];
                }
                array[j] = temp;
            }
        }
    }
    public void print() {
        for(int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
    }
}
//2. 希爾排序
/*
*這裡對於增量的處理,採取每次對長度取半的方式
*/
public class ShellSort {
    private int[] array;
    
    public ShellSort(int[] array) {
        this.array = array;
    }
    
    //這裡跟書上不同,我改進了一下( ̄︶ ̄)↗ (說不定效能反而下降了。。)
    public void sort() {
        int length = array.length;
        for(int k = length/2; k > 0; k/=2) {
            for(int i = k; i < length; i++) {
                int j = i;
                int temp = array[i];
                for(; j >= k && array[j - k] > temp; j-=k) {
                    array[j] = array[j - k];
                }
                array[j] = temp;
            }
        }
    }
    public void print() {
        for(int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
    }
}

五、選擇排序

思想:每次選擇最大或最小的數與第一個交換。
比較的次數與數列長度有關,而外部也需要遍歷也與長度有關,所以不管在什麼情況下,時間複雜度總是O(n^2)。由於不需要一個個移動,時間複雜度比氣泡排序略好。
穩定性,比如6,6`,1,第一次交換後變為:1,6`,6。發現兩個6的相對順序改變了,所以不穩定
改進
有點跟氣泡排序一次冒兩個泡類似,可以同時尋找到最大、最小分別與第一個和最後一個交換。

//1. 簡單選擇排序
public class SelectSort {
    private int[] array;
    
    public SelectSort(int[] array) {
        this.array = array;
    }
    
    public void sort() {
        int length = array.length;
        for(int i = 0; i < length; i++) {
            int minIndex = i;
            for(int j = i + 1; j < array.length; j++) {
                if(array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
            if(minIndex != i) {
                int temp = array[minIndex];
                array[minIndex] = array[i];
                array[i] = temp;
            }
        }
    }
    public void print() {
        for(int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
    }
}
//2. 同時選擇最大和最小
public class SelectSort {
    private int[] array;
    
    public SelectSort(int[] array) {
        this.array = array;
    }
    
    public void sort() {
        int length = array.length;
        int i = 0;
        for(; i < length - i; i++) {
            int minIndex = i;
            int maxIndex = length - i - 1;
            
            for(int j = i + 1; j < length - i; j++) {
                if(array[j] < array[minIndex]) {
                    minIndex = j;
                }
                if(array[j] > array[maxIndex]) {
                    maxIndex = j;
                }
            }
            if(minIndex != i) {
                swap(array, minIndex, i);
            }
            if(maxIndex != length - i - 1) {
                swap(array, maxIndex, length - i - 1);
            }
        }
    }
    public void print() {
        for(int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
    }
    private void swap(int[] array, int a ,int b) {
        int temp = array[a];
        array[a] = array[b];
        array[b] = temp;
    }
}

上面提到的演算法,除了快速排序空間複雜度是O(logn)之外,其他的都是O(1)。
關於穩定性
穩定性是指排序之後原有數列中相同值的相對次序是否發生改變,沒有改變則是穩定的。
穩定性的好處,1. 如果穩定,那麼上一趟排序的結果可以被下一趟使用。2.可以避免多餘的比較或移動。

選擇排序演算法考慮時的注意點

  1. 待排序的數列長度
  2. 記錄本身的資料量(通常根據記錄中的部分內容排序,也需要考慮一下其他部分資料量的大小)
  3. 待排序數列元素結構和分佈(比如,分佈集中可以考慮桶排序)
  4. 對排序穩定性的要求

參考:

  1. 《輕鬆學演算法》趙燁
  2. 《圖解排序演算法(五)之快速排序——三數取中法》


相關文章