排序算法和搜索算法

前言

模塊化編程 不僅僅是爲了把算法記錄下來,更重要的是以後使用起來更方便,更細緻的瞭解他的實現。我們通過構造許多靜態方法庫(模塊),一個庫中的靜態方法也能夠調用到另一個庫中定義的靜態方法。這能夠帶來許多好處:
- 程序的整體代碼量很大時,每次處理的模塊大小仍然適中;
- 可以重用和共享代碼而無需重新實現;
- 很容易用改進的實現替換老的實現;
- 可以爲解決編程問題建立合適的抽象模型;
- 縮小調試範圍;

單元測試
Java編程最佳實踐之一,就是每個靜態方法庫都包含一個main()函數,測試庫中的方法,隨着模塊的成熟,我們可以把main()方法作爲一個開發用例,測試更多細節;也可以編寫成測試用例對所有代碼進行全面測試。當用例越來越複雜就可將它獨立成一個模塊。

編寫算法過程(算法設計以及面試中都可以用到)
- 編寫用例,在實現中把計算過程分解成可控的部分
- 明確靜態方法庫與之對應的API
- 實現API和能夠對方法進行獨立測試的main()函數

備註:雖然整體上都是使用基本類型測試的但是以上思想還是很重要的,在基本類型的基礎工具類中任然可以將一些不變的代碼封裝起來。簡化,解耦代碼。

二分搜索(又叫二分查找、折半搜索)

兩個基本的類:
BasicTool 放置算法需要的方法以供調用;
BasicAlgorithm 放置基本算法;
兩個類中各有一個main()函數作爲測試方法,測試方法的結果輸出是否正確。

通用代碼:

public class BasicTool {
    //比較兩個元素大小
    private static boolean less(Comparable v, Comparable w) {
        return v.compareTo(w) < 0;
    }
    //交換元素位置 包裝類
    private static void exch(Comparable[] a, int i, int j) {
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    //針對int基本類型的交換
    public static void swap(int[] arr, int one, int two) {
        int temp = arr[one];
        arr[one] = arr[two];
        arr[two] = temp;
    }

    public static boolean isSorted(Comparable[] a) { // 測試數組元素是否有序
        for (int i = 1; i < a.length; i++)
            if (less(a[i], a[i - 1])) return false;
        return true;
    }
}

二分搜索:

public class BasicAlgorithm {

    public static int binarySearch(int a[], int target) {
        //數組必須有序
        int low = 0;
        int high = a.length - 1;
        while (low <= high) {
        //被查找的元素要不存在,要麼必然存在於a[low....high]
          int mid = low + (high - low) / 2;
            if (target < a[mid]) {
                high = mid - 1;
            } else if (target > a[mid]) {
                low = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        int[] a = {1, 2, 5, 5, 6, 8, 9};
        System.out.println(binarySearch(a, 6) + "");
    }

}

擴展:

二分搜索的遞歸(Recursion)實現:

    public static int binarySearchRecursion(int a[], int target, int low, int high) {
        if (low > high) return -1;
        //被查找的元素要不存在,要麼必然存在於a[low....high]
        int mid = low + (high - low) / 2;
        if (target < a[mid])
            return binarySearchRecursion(a, target, low, mid - 1);
        else if (target > a[mid])
            return binarySearchRecursion(a, target, mid + 1, high);
        else return mid;
    }

    public static void main(String[] args) {
        int[] a = {1, 2, 5, 5, 6, 8, 9};
        System.out.println(binarySearchRecursion(a, 6, 0, a.length - 1) + "");
    }

在擴展下:

遞歸要注意啥,有啥好處?

更加簡潔易懂。代碼少了其實需要我們寫遞歸的時候注意的也要多三點:

  • 遞歸總有一個最簡單的情況——方法的第一條總是一個包含return的條件語句
if (low > high) return -1;
  • 遞歸調用總是嘗試去解決一個規模更小的子問題,這樣才能收斂到最簡單的情況,就像low和high的差值越來越小,搜索的區間(規模)越來越小
  • 遞歸調用的父問題和嘗試解決的子問題之間不能有交集,雖然low和high的差值越來越小,但是父問題和子問題操作的區間是不同的,也就是他們操作的數組部分是不同的

排序算法

擴展:

Java API自帶排序方法:Arrays.sort() 他的排序大體上對於Java基本類型使用的是快速排序,對對象數組使用的是歸併排序,但是Java8對於排序進一步優化,根據不同類型和剩餘需要排序元素的個數,來細化使用排序的類型(比如插入排序等)。

原因:使用不同類型的排序算法主要是由於快速排序是不穩定的,而合併排序是穩定的。這裏的穩定是指比較相等的數據在排序之後仍然按照排序之前的前後順序排列。對於基本數據類型,穩定性沒有意義,而對於對象類型,穩定性是比較重要的,因爲對象相等的判斷可能只是判斷關鍵屬性,最好保持相等對象的非關鍵屬性的順序與排序前一致。

另外一個原因是由於合併排序相對而言 比較次數 比快速排序少, 移動(對象引用的移動)次數 比快速排序多,而對於對象來說,移動是簡單的,只是引用的轉換,但是比較相對更加耗時。

合併排序的時間複雜度是nlogn, 快速排序的平均時間複雜度也是nlogn,但是合併排序的需要額外的n個引用的空間。

冒泡排序

它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。

比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。每趟比較交換完之後,最後的元素是最大的數,所以每趟結束後參與排序的元素個數n-1-i。
針對所有的元素重複以上的步驟,除了最後一個。
持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

時間複雜度

   o(n^2)

註釋部分爲優化部分。

    // 冒泡:n-1  n-1-i
    // 冒泡排序:兩兩比較,比較n-1趟,每一趟比較n-i-1次
    public static int[] bubbleSort(int[] a) {
        int n = a.length;
        for (int i = 0; i < n - 1; i++) {
            //boolean isSwap = false;
            for (int j = 0; j < n - 1 - i; j++) {
                if (a[j] > a[j + 1]) {
                    a[j + 1] = a[j] ^ a[j + 1];
                    a[j] = a[j] ^ a[j + 1];
                    a[j + 1] = a[j] ^ a[j + 1];
                    isSwap = true;
                }
            }
            //if (!isSwap) {
            //    return a;
            //}
        }
        return a;
    }

選擇排序

時間複雜度

   o(n^2)

首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小元素,然後放到排序序列末尾。以此類推,直到所有元素均排序完畢。

    //選擇排序:遍歷n-1趟元素,設置i爲最小數下標即哨兵,與 i+1 直到 n 個元素比較,記錄小數下標,如果最終記錄的min和開始設置的哨兵min(i)不一致,與下標爲i的元素交換位置
    public static int[] selectionSort(int[] a) {
        int n = a.length;
        for (int i = 0; i < n - 1; i++) {
            int min = i;
            for (int j = i + 1; j < n; j++) {
                if (a[min] > a[j]) {
                    min = j;
                }
            }
            if (min != i) {
                swap(a, min, i);
            }
        }
        return a;
    }

插入排序

時間複雜度

   o(n^2)

它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。

    //插入排序:左邊爲有序區域,遍歷第一個直到第n個,每次加入一個新的元素插入到前邊有序的元素中比較j和j-1元素如果a[j-1] > a[j]大小交換位置else測跳出這層循環
    public static int[] insertionSort(int[] a) {
        int n = a.length;
        for (int i = 1;i<n;i++) {
            for (int j = i;j>0;j--) {
                if (a[j] < a[j-1]) {
                    swap(a,j,j-1);
                } else {
                    break;
                }
            }
        }
        return a;
    }

歸併排序

時間複雜度

   O(n*log 
n) 

需要額外空間,針對兩個有序數組

該算法是採用分治法(Divide and Conquer)的一個非常典型的應用

申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列;
設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置;
比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置;
重複步驟3直到某一指針達到序列尾;
將另一序列剩下的所有元素直接複製到合併序列尾

    //歸併排序 遞歸方式    
    public static void mergeSort(int[] oldArr, int left, int right) {
        if (left >= right)//保證可分的數組至少有兩個元素 一個元素是不可分的
            return;
        //去一箇中間數
        int center = left + (right - left) / 2;//(right + left)/2
        //對左邊數組遞歸
        mergeSort(oldArr, left, center);
        //對右邊數組遞歸
        mergeSort(oldArr, center + 1, right);
        //合併
        merge(oldArr, left, center, right);
    }

    public static void merge(int[] oldArr, int left, int center, int right) {
        print(oldArr, left, center, right);
        //臨時新的數組
        int[] newArr = new int[right - left + 1];
        //左邊第一個元素
        int i = left;
        //右邊數組第一個元素
        int j = center + 1;
        //新數組指針
        int k = 0;

        //兩個指針指向的元素比較 誰的比較小就放到新數組裏 指針加1
        while (i <= center && j <= right) {
            if (oldArr[i] < oldArr[j])
                newArr[k++] = oldArr[i++];
            else
                newArr[k++] = oldArr[j++];
        }
        //兩個While只有一個可以運行 最後只有一個數組會剩餘元素 把剩餘元素移入數組,誰沒指完誰剩餘,誰放入數組
        while (i <= center) {
            newArr[k++] = oldArr[i++];
        }

        while (j <= right) {
            newArr[k++] = oldArr[j++];
        }

        //然後把新數組排好序的數覆蓋就數組中對應得數
        for (int kk = 0; kk < newArr.length; kk++) {
            oldArr[kk + left] = newArr[kk];
        }
        print(oldArr);


    }

    public static void print(int[] data, int left, int center, int right) {
        System.out.println();
        System.out.println("leftNum:" + left);
        System.out.println("centerNum:" + center);
        System.out.println("rightNum:" + right);

        System.out.println("left:");
        for (int i = left; i <= center; i++) {
            System.out.print(data[i] + "\t");
        }
        System.out.println();
        System.out.println("right:");
        for (int i = center + 1; i <= right; i++) {
            System.out.print(data[i] + "\t");
        }
        System.out.println();
    }

    public static void print(int[] data) {
        System.out.println();
        for (int i = 0; i < data.length; i++) {
            System.out.print(data[i] + "\t");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        int[] a = {1, 24, 54, 7, 2, 8, 23, 45, 1, 2, 3};
        mergeSort(a, 0, a.length - 1);
        for (int i : a) {
            System.out.print(i + " ");
        }

    }

合併流程打印結果

leftNum:0
centerNum:0
rightNum:1
left:
1   
right:
24  

1   24  54  7   2   8   23  45  1   2   3   

leftNum:0
centerNum:1
rightNum:2
left:
1   24  
right:
54  

1   24  54  7   2   8   23  45  1   2   3   

leftNum:3
centerNum:3
rightNum:4
left:
7   
right:
2   

1   24  54  2   7   8   23  45  1   2   3   

leftNum:3
centerNum:4
rightNum:5
left:
2   7   
right:
8   

1   24  54  2   7   8   23  45  1   2   3   

leftNum:0
centerNum:2
rightNum:5
left:
1   24  54  
right:
2   7   8   

1   2   7   8   24  54  23  45  1   2   3   

leftNum:6
centerNum:6
rightNum:7
left:
23  
right:
45  

1   2   7   8   24  54  23  45  1   2   3   

leftNum:6
centerNum:7
rightNum:8
left:
23  45  
right:
1   

1   2   7   8   24  54  1   23  45  2   3   

leftNum:9
centerNum:9
rightNum:10
left:
2   
right:
3   

1   2   7   8   24  54  1   23  45  2   3   

leftNum:6
centerNum:8
rightNum:10
left:
1   23  45  
right:
2   3   

1   2   7   8   24  54  1   2   3   23  45  

leftNum:0
centerNum:5
rightNum:10
left:
1   2   7   8   24  54  
right:
1   2   3   23  45  

1   1   2   2   3   7   8   23  24  45  54  

快速排序

最好時間複雜度:

   O(n*log 
n) 

最壞時間複雜度(不常見):

   O(n^2) 

平均時間複雜度:

   O(n*log 
n) 

事實上,快速排序通常明顯比其他Ο(n log n) 算法更快,因爲它的內部循環(inner loop)可以在大部分的架構上很有效率地被實現出來,且在大部分真實世界的數據,可以決定設計的選擇,減少所需時間的二次方項之可能性。

  • 從數列中挑出一個元素,稱爲 “基準”(pivot);
  • 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。
  • 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
//獲取標記位 並且根據標記爲元素大小來進行大於標記元素和小於標記元素的劃分
    public static int partition(int[] a, int low, int high) {
        //低位
        int i = low;
        //高位
        int j = high;

        int privotKey = a[low];

        while (i < j) {
            //低位高位交替執行 交換位置  等於的情況下對基本類型的穩定性沒有影響,內部某一個while循環完畢,交換時,privotKey總是在i或者是j的位置上
            while (i < j && a[j] >= privotKey) --j;
            swap(a, i, j);
            while (i < j && a[i] <= privotKey) ++i;
            swap(a, i, j);
        }

        return i;//返回high也可,while循環停止時,最後i == j
    }
    //遞歸除了標記元素之外的數組進行 partition
    public static void quickSort(int[] a, int low, int high) {

        if (low < high) {
            int privotLoc = partition(a, low, high);
            quickSort(a, low, privotLoc - 1);
            quickSort(a, privotLoc + 1, high);
        }

    }


    public static void main(String[] args) {
        int[] a = {1, 24, 54, 7, 2, 8, 23, 45, 1, 2, 3};
        quickSort(a, 0, a.length - 1);
        for (int i : a) {
            System.out.print(i + " ");
        }

    }

如果初始序列按關鍵碼有序或基本有序時,快排序反而蛻化爲冒泡排序。所以改進方法:

快速排序的改進在本改進算法中,只對長度大於k的子序列遞歸調用快速排序,讓原序列基本有序,然後再對整個基本有序序列用插入排序算法排序。實踐證明,改進後的算法時間複雜度有所降低,且當k取值爲 8 左右時,改進算法的性能最佳。

//partition不變,更改下遞歸區間。
public static void quickSortPro(int[] a, int low, int high, int k) {
        //長度大於k時遞歸, k爲指定的數
        if (high - low > k) {
            int privotLoc = partition(a, low, high);
            quickSort(a, low, privotLoc - 1);
            quickSort(a, privotLoc + 1, high);
        }

        bubbleSort(a);

    }

    public static void main(String[] args) {
        int[] a = {1, 24, 54, 7, 2, 8, 23, 45, 1, 2, 3, 6, 4, 78, 43, 67, 54, 32, 47};
        quickSortPro(a, 0, a.length - 1, 8);
        for (int i : a) {
            System.out.print(i + " ");
        }

    }

[參考]:

《算法》第四版

經典排序算法總結–冒泡、快排、插入、希爾、歸併、選擇

幾種常見排序算法

視覺直觀感受 7 種常用的排序算法

各種排序算法

排序分析細緻

八大排序算法

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