經常使用排序算法的Java實現與分析

因爲須要分析算法的最好時間複雜度和最壞時間複雜度,所以這篇文章中寫的排序都是從小到大的升序排序。git

帶排序的數組爲arr,arr的長度爲N。時間複雜度使用TC表示,額外空間複雜度使用SC表示。算法

好多代碼都用到了交換arr[i]和arr[j]的地方,這裏先給出代碼。shell

private static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

 

(1)插入排序api

1.1直接插入排序數組

算法思想:dom

  初始時第一個元素是有序的,在插入元素arr[i]時,arr[0] ~arr[i - 1]已是有序的了,將arr[i]插入其中便可。ui

穩定性:spa

  若是遇到和arr[i]相等的元素,那麼將arr[i]放在後面,因此直接插入排序是穩定的。指針

複雜度分析:code

  arr基本有序(升序)時,直接插入排序的TC是O(N);arr降序時,TC是O(N*N);平均時間複雜度O(N*N)。直接插入排序的SC是O(1)。

算法實現:

public class InsertionSort_StraightInsertionSort {
    public static int[] sis(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                swap(arr, j, j + 1);
            }
        }
        return arr;
    }
}

1.2希爾排序

算法思想:

  希爾排序的實質就是分組插入排序,該方法又稱爲縮小增量排序。先將arr分割成若干個子序列(由相隔某個增量的元素組成),分別進行直接插入排序,而後依次縮小增量再進行排序。待整個序列中的元素基本有序(增量足夠小時),再對全體元素進行一次直接插入排序。

穩定性:

  不穩定

複雜度分析:

  TC最好爲O(N) ,最壞 O(N * N),平均TC爲O(N*1.3)。SC爲O(1)。

算法實現:

public class InsertionSort_ShellSort {
 public static int[] shellSort(int[] arr) {
     for (int gap = arr.length / 2; gap > 0; gap /= 2) {
         for (int i = gap; i < arr.length; i++) {
             for (int j = i - gap; j >= 0 && arr[j] > arr[j + gap]; j -= gap) {
                 swap(arr, j, j + gap);
             }
         }
     }
     return arr;
 }
}

(2)交換排序

2.1冒泡排序

算法思想:

  一次比較兩個元素,若是他們的順序錯誤就把他們交換過來。

穩定性:

  穩定

複雜度分析:

  arr升序時TC最好爲O(N) ,arr降序時TC最壞 O(N * N),平均TC爲O(N*N)。SC爲O(1)

算法實現:

public class Exchange_Bubble {
    public static int[] bubbleSort(int[] arr) {
        for (int i = arr.length; i != 0; i--) {
            for (int j = 1; j < i; j++) {
                if (arr[j] < arr[j - 1]) {
                    swap(arr, j - 1, j);
                }
            }
        }
        return arr;
    }
}

2.2快速排序

算法思想:

  1.先從數列中取出一個數做爲基準數;

  2.分區過程,將比這個數大的數全放到它的右邊,小於或等於它的數全放到它的左邊;

  3.再對左右區間重複第二步,直到各區間只有一個數。

穩定性:

  不穩定

複雜度分析:

  arr升序時TC最好爲O(NLogN) ,arr降序時TC最壞 O(N*N),平均TC爲O(NLogN)。SC爲O(LogN)

算法實現:

public class Exchange_QuickSort {

    public static int[] quickSort1(int[] arr) {
        return qs1(arr, 0, arr.length - 1);
    }

    private static int[] qs1(int[] arr, int left, int right) {
        if (left > right) {
            return arr;
        }
        int i = left;
        int j = right;
        int tmp = arr[left];
        while (i < j) {
            while (i < j && arr[j] >= tmp) {
                j--;
            }
            while (i < j && arr[i] <= tmp) {
                i++;
            }
            if (i < j) {
                swap(arr, i, j);
            }
        }
        arr[left] = arr[i];
        arr[i] = tmp;

        qs1(arr, left, i - 1);
        qs1(arr, i + 1, right);
        return arr;
    }
}

(3)選擇排序

3.1簡單選擇排序

算法思想:

  在要排序的一組數中,選擇出最小的數與第一個數交換;而後在剩下的數中,選擇出最小的數與第二個數交換,以此類推。

穩定性:

  不穩定

複雜度分析:

  arr升序時,只是避免了交換元素的操做,TC最好仍舊爲O(N*N) ;arr降序時TC最壞 O(N*N),平均TC爲O(N*N)。SC爲O(1)

算法實現:

public class SelectionSort_SimpleSelectionSort {
    public static int[] simpleSelectionSort(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            int minIdx = i;
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[minIdx]) {
                    minIdx = j;
                }
            }
            if (arr[minIdx] < arr[i]) {
                swap(arr, minIdx, i);
            }
        }
        return arr;
    }
}

3.2堆排序

算法思想:

  從小到大排序,構建大頂堆(堆首先是一棵徹底二叉樹,大頂堆每一個節點的值都不大於父節點的值),而後每次將堆頂元素和沒有排序的最後的元素交換,從新構建堆(即從新獲得最大的堆頂元素)。

穩定性:

  不穩定

複雜度分析:

  最好狀況:若是待排序數組是降序的,仍然須要O(N * logN)複雜度的比較操做,少了移動的操做;

  最壞狀況:若是待排序數組是升序的,不只須要O(N * logN)複雜度的比較操做,並且須要O(N * logN)複雜度的交換操做。總的時間複雜度仍是O(N * logN)。

  在最好和最壞狀況下,堆排序的時間複雜度都是O(NlogN)。堆排序時,因爲每次從新恢復堆的時間複雜度爲O(logN),共N - 1次堆調整操做,再加上前面創建堆時N / 2次向下調整,每次調整時間複雜度也爲O(logN)。兩次次操做時間相加仍是O(N * logN)。故堆排序的時間複雜度爲O(N * logN)。

  堆排序通常優於快速排序的重要一點是,數據的初始分佈狀況對堆排序的效率沒有大的影響

算法實現:

public class SelectionSort_HeapSort {
    public static int[] heapSort(int[] arr) {
        buildHeap(arr);
        for (int i = arr.length - 1; i >= 0; i--) {
            swap(arr, 0, i);//交換完後,大的元素跑到了數組的後面的部分  因此堆排序時 數組後面到前面逐漸變得有序
            heapify(arr, 0, i); //調整回大頂堆,即從新找到未排序部分的最大值
        }
        return arr;
    }
    private static void buildHeap(int[] arr) {
        for (int i = (arr.length - 1) / 2; i >= 0; i--) {
            heapify(arr, i, arr.length);
        }
    }
    //從index開始往下不斷調整大頂堆
    private static void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;
        int largest = index;
        while (left < heapSize) {
            if (arr[left] > arr[index]) {
                largest = left;
            }
            if (right < heapSize && arr[right] > arr[largest]) {
                largest = right;
            }
            if (largest != index) {
                swap(arr, largest, index);
            } else {
                break;
            }
            index = largest;
            left = index * 2 + 1;
            right = index * 2 + 2;
        }
    }
}

(4)不基於比較的排序

4.1桶排序

算法思想:

  桶排序可用於最大最小值相差較大的數據狀況,要求數據的分佈必須均勻,不然可能致使數據都集中到一個桶中。把數組 arr 劃分爲n個大小相同子區間(桶),每一個子區間各自排序,最後合併。計數排序是桶排序的一種特殊狀況,能夠把計數排序當成每一個桶裏只有一個元素的狀況。

  1.找出待排序數組中的最大值max、最小值min;

  2.咱們使用 動態數組ArrayList 做爲桶,桶裏放的元素也用 ArrayList 存儲。桶的數量爲(max-min)/arr.length+1;
  3.遍歷數組 arr,計算每一個元素 arr[i] 放的桶;4.每一個桶各自排序;5.遍歷桶數組,把排序好的元素放進輸出數組。

穩定性:

  穩定

複雜度分析:

  N個待排數據,M個桶,平均每一個桶[N/M]個數據的桶排序平均時間複雜度爲:O(N)+O(M*(N/M)log(N/M))=O(N+N(logN-logM))=O(N+N*logN-N*logM) 
  當N=M時,即極限狀況下每一個桶只有一個數據時。桶排序的最好效率可以達到O(N)。
  桶排序的平均時間複雜度爲線性的O(N+C),其中C=N*(logN-logM)。若是相對於一樣的N,桶數量M越大,其效率越高,最好的時間複雜度達到O(N)。
  桶排序的空間複雜度 爲O(N+M),若是輸入數據很是龐大,而桶的數量也很是多,則空間代價無疑是昂貴的。

算法實現:

public class NoCompare_BucketSort {
    public static int[] bucketSort(int[] arr) {
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int cur : arr) {
            max = Math.max(max, cur);
            min = Math.min(min, cur);
        }
        //計算桶的數量並構建初始的桶
        int bucketNum = (max - min) / arr.length + 1;
//        int bucketNum = (max - min) + 1;      //每一個桶中只有一個元素的時候就是計數排序
        List<List<Integer>> buckets = new ArrayList<>();
        for (int i = 0; i < bucketNum; i++) {
            buckets.add(new ArrayList<>());
        }
        //將每一個元素放到桶中
        for (int cur : arr) {
            int index = (cur - min) / arr.length;
//            int index = (cur - min);  //每一個桶中只有一個元素的時候就是計數排序
            buckets.get(index).add(cur);
        }
        //對每一個桶內的元素進行排序,可使用直接插入排序等排序方法
        int index = 0;
        for (List<Integer> bucket : buckets) {
            insertSort(bucket);
            for (int cur : bucket) {
                arr[index++] = cur;
            }
        }
        return arr;
    }
    
    private static void insertSort(List<Integer> bucket) {
        Integer[] arr = bucket.toArray(new Integer[bucket.size()]);
        for (int i = 1; i < arr.length; i++) {
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                swap(arr, j, j + 1);
            }
        }
        for (int i = 0; i < arr.length; i++) {
            bucket.set(i, arr[i]);
        }
    }
}

4.2基數排序

算法思想:

   基數排序(Radix Sort)是一種非比較型排序算法,它將整數按位數切割成不一樣的數字,而後按每一個位分別進行排序。

  基數排序的方式能夠採用MSD(Most significant digital)或LSD(Least significant digital),MSD是從最高有效位開始排序,而LSD是從最低有效位開始排序。固然咱們能夠採用MSD方式排序,按最高有效位進行排序,將最高有效位相同的放到一堆,而後再按下一個有效位對每一個堆中的數遞歸地排序,最後再將結果合併起來。可是,這樣會產生不少中間堆(高位排序比低位排序多了空間開銷)。因此,一般基數排序採用的是LSD方式。LSD基數排序實現的基本思路是將全部待比較數值(正整數)統一爲一樣的數位長度,數位較短的數前面補零。而後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成之後, 數列就變成一個有序序列。

須要注意的是,對每個數位進行排序的算法必須是穩定的,不然就會取消前一次排序的結果。

穩定性:

  穩定

複雜度分析:

  設待排序列爲n個記錄,d個關鍵碼,關鍵碼的取值範圍爲radix,則進行鏈式基數排序的時間複雜度爲O(d(n+radix)),其中,一趟分配時間複雜度爲O(n),一趟收集時間複雜度爲O(radix),共進行d趟分配和收集。

空間效率:須要2*radix個指向隊列的輔助空間,以及用於靜態鏈表的n個指針。

算法實現:

public class NoCompare_RadixSort {
    public static int[] radixSort(int[] arr) {
        int maxLen = getMaxBit(arr);//獲得arr中最大的數的長度
        int[] tmp = new int[arr.length];
        int[] count = new int[10];// 計數器
        int radix = 1;
        for (int i = 1; i <= maxLen; i++) {// 進行maxLen次排序
            Arrays.fill(count, 0);// 每次分配前清空計數器

            for (int j = 0; j < arr.length; j++) {
                int k = (arr[j] / radix) % 10;// 統計每一個桶中的記錄數
                count[k]++;
            }
            for (int j = 1; j < 10; j++) {
                count[j] = count[j - 1] + count[j];// 將tmp中的位置依次分配給每一個桶
            }
            for (int j = arr.length - 1; j != -1; j--) {// 將全部桶中的記錄依次收集到tmp中
                int k = (arr[j] / radix) % 10;
                tmp[count[k] - 1] = arr[j];
                count[k]--;
            }
            for (int j = 0; j < arr.length; j++) {// 將臨時數組的內容複製到arr中
                arr[j] = tmp[j];
            }
            radix = radix * 10;
        }
        return arr;
    }

    // 獲得arr中最大的數的長度
    private static int getMaxBit(int[] arr) {
        int maxNum = Integer.MIN_VALUE;
        for (int cur : arr) {
            maxNum = Math.max(maxNum, cur);
        }
        return String.valueOf(maxNum).length();
    }
}

(5)歸併排序

算法思想:

  將數組分紅二組A,B,若是這二組組內的數據都是有序的,那麼就能夠很方便的將這二組數據進行合併。
  能夠將A,B組各自再分紅二組。依次類推,當分出來的小組只有一個數據時,
  能夠認爲這個小組組內已經達到了有序,而後再合併相鄰的二個小組就能夠了。這樣經過先遞歸的分解數列,再合併數列就完成了歸併排序。

穩定性:

  穩定

複雜度分析:

  最好、最壞、平均TC都是O(NlogN)。SC是O(N)

算法實現:

 

public class MergeSort {
    //將arr[first~mid]和arr[mid~last]合併到tmp中
    private static void mergeArray(int[] arr, int first, int mid, 
            int last, int[] tmp) {
        int begin1 = first;
        int begin2 = mid + 1;
        int end1 = mid;
        int end2 = last;
        int k = 0;
        while (begin1 <= end1 && begin2 <= end2) {
            if (arr[begin1] < arr[begin2]) {
                tmp[k++] = arr[begin1++];
            } else {
                tmp[k++] = arr[begin2++];
            }
        }
        while (begin1 <= end1) {
            tmp[k++] = arr[begin1++];
        }
        while (begin2 <= end2) {
            tmp[k++] = arr[begin2++];
        }
        //將輔助數組tmp的數據寫回arr
        for (begin1 = 0; begin1 < k; begin1++) {
            arr[first + begin1] = tmp[begin1];
        }
    }
    public static int[] mergeSort(int[] arr, int first, 
            int last, int[] tmp) {
        if (first < last) {
            int mid = (first + last) / 2;
            mergeSort(arr, first, mid, tmp);//左邊有序
            mergeSort(arr, mid + 1, last, tmp);//右邊有序
            mergeArray(arr, first, mid, last, tmp);//將兩個有序數列合併
        }
        return arr;
    }
}

 

此外,爲了驗證結果的正確性,能夠寫個和Arrays.sort()方法比較的代碼。getIntArr獲得一個大小是size,最大值小於bound的int數組。arrEquals比較兩個int數組是否相等。

public class MyUtil {
    public static int[] getIntArr(int size, int bound) {
        int[] arr = new int[size];
        Random r = new Random();
        for (int i = 0; i < size; i++) {
            arr[i] = r.nextInt(bound);
        }
        return arr;
    }
    
    public static boolean arrEquals(int[] arr1, int[] arr2) {
        for (int i = 0; i < arr1.length; i++) {
            if (arr1[i] != arr2[i]) {
                return false;
            }
        }
        return true;
    }
}

以直接插入排序爲例,咱們能夠這麼用MyUtil。

public static void main(String[] args) {
        int times = 0;
        while (times++ < 100) {
            int[] arr = MyUtil.getIntArr(50, 100);
            int[] brr = arr.clone();
            Arrays.sort(arr);
            if (!MyUtil.arrEquals(arr, sis(brr))) {
                System.out.println("Something Wrong");
            }
        }
        System.out.println("Everything is OK.");
}
相關文章
相關標籤/搜索