二分查找與幾種排序方法


  1. 遞歸二分查找
  2. 冒泡排序
  3. 選擇排序
  4. 插入排序
  5. 歸並排序
  6. 快速排序

1、遞歸二分查找

思想

使用二分查找的前提條件是數組元素必須已經排好序。

  • 二分查找法首先將關鍵字與數組的中間元素進行比較,考慮下面三種情形:
  • 如果關鍵字比中間元素小,那么只需在前一半數組元素中進行遞歸查找;
  • 如果關鍵字與中間元素相等,則匹配成功,查找結束。

 

代碼:

public static int binarySearch(int[] list, int key){
        return binarySearch(list, key, 0, list.length - 1);
    }
    public static int binarySearch(int[] list, int key , int low, int high){
        //沒有查找到
        if(low > high)
            return - low - 1;
        
        int mid = (low + high) / 2;
        if(key < list[mid]){
            return binarySearch(list, key, low, mid - 1);
        }else if(key == list[mid]){
            return mid;
        }else{
            return binarySearch(list, key, mid + 1, high);
        }
    }

 

------------------------------------------------- 排序算法 ----------------------------------------------

各排序算法的時間復雜度、空間復雜度、穩定性:

排序算法的穩定性:排序前后相等元素相對位置不變,則稱排序算法是穩定的;否則排序算法是不穩定的)

排序算法 平均時間復雜度 最壞時間復雜度 空間復雜度  穩定性 
 冒泡排序  O(n^2)  O(n^2)  O(1) 穩定 
 選擇排序  O(n^2)  O(n^2)  O(1) 不穩定
直接插入排序  O(n^2)  O(n^2)  O(1) 穩定
 歸並排序  O(nlogn)  O(nlogn)

 O(n)

穩定
 快速排序  O(nlogn)  O(n^2)  O(logn) 不穩定
 堆排序  O(nlogn)  O(nlogn)  O(1) 不穩定

 

冒泡排序:

  冒泡排序算法需要多次遍歷數組(N-1次),在每次遍歷中,比較連續相鄰的元素。如果一對元素是降序,則互換它們的值;否則保持不變。這樣每一次遍歷較大的值都沉到了后面:第一次遍歷區間為0~N-1,第一次遍歷后,最后一個元素成為數組中最大的數;第二次遍歷區間為0~N-2,第二次遍歷后,倒數第二個元素成為第二大的數……

  • 穩定性穩定。冒泡排序就是把小的元素往前調或者把大的元素往后調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。如果兩個元素相等,不會把他們倆交換,所以相同元素的前后順序並沒有改變,所以冒泡排序是一種穩定排序算法。
  • 時間復雜度O(n^2)。需要兩個for循環,每次只關注一個元素,平均時間復雜度為O(n^2),最壞的也是O(n^2)。
  • 空間復雜度O(1)。需要一個臨時變量來交換元素位置,所以空間復雜度O(1)。

選擇排序:

  選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩余元素里面給第二個元素選擇最小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因為只剩下它一個最大的元素了。

  • 穩定性不穩定。在一趟選擇中,如果當前元素比一個元素小,而該小的元素又出現在一個和當前元素相等的元素后面,那么交換后穩定性就被破壞了。比如序列5 8 5 2 9, 我們知道第一遍選擇第1個元素5會和2交換,那么原序列中2個5的相對前后順序就被破壞了,所以選擇排序不是一個穩定的排序算法。
  • 時間復雜度O(n^2)。需要兩個for循環,平均時間復雜度為O(n^2),最壞的也是O(n^2)。
  • 空間復雜度O(1)。需要一個臨時變量來交換元素位置,所以空間復雜度O(1)。

插入排序:

  插入排序是重復的將新的元素插入一個排好序的子線性表中,直到整個線性表排好序。插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。當然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其后面,否則一直往前找直到找到它該插入的位置......

  • 穩定性穩定。如果碰見一個和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后順序沒有改變。
  • 時間復雜度O(n^2)。需要兩個for循環,平均時間復雜度為O(n^2)。最壞時間O(n^2),最好時間O(n),就是不用交換。
  • 空間復雜度O(1)。需要一個臨時變量來交換元素位置,所以空間復雜度O(1)。

歸並排序:

  歸並排序是使用分而治之法對數組排序。將數組分為兩半,對每部分遞歸地應用歸並排序。在兩部分都排好序后,對它們進行歸並。先什么都不管,把數組分為兩個子數組,一直遞歸把數組划分為兩個子數組,直到數組里只有一個元素,這時候才開始排序,讓兩個數組間排好序,依次按照遞歸的返回來把兩個數組進行排好序,到最后就可以把整個數組排好序。

  做法:分成兩個函數:1)划分數組;2)歸並兩個有序數組。划分時創建兩個臨時數組,將數組前半部分與后半部分復制到臨時數組中。歸並時將它們先排序再歸並到原始數組中。

  • 穩定性穩定。在短序列只有1個或2個元素時,1個元素不會交換,2個元素如果大小相等也不交換,這不會破壞穩定性。那么,在短的有序序列合並的過程中,穩定是否受到破壞?沒有,合並過程中我們可以保證如果兩個當前元素相等時,我們把處在前面的序列的元素保存在結果序列的前面,這樣就保證了穩定性。
  • 時間復雜度O(nlogn)。歸並排序將數組划分為兩個子數組,使用同樣的算法對子數組進行遞歸排序,然后將子數組進行歸並,因此T(n)=T(n/2)+T(n/2)+歸並用時。第一項是對數組的前半部分排序所需的時間,第二項是對數組的后半部分排序所需的時間。要歸並兩個數組,最多需要n-1次比較來比較兩個子數組中的元素,以及n次移動將元素移到臨時數組中,因此歸並總時間為2n-1。因此T(n)=T(n/2)+T(n/2)+2n-1=O(nlogn)。
  • 空間復雜度O(n)。歸並排序每次遞歸需要用到一個輔助表,長度與待排序的表相等,雖然遞歸次數是O(log2n),但每次遞歸都會釋放掉所占的輔助空間,所以下次遞歸的棧空間和輔助空間與這部分釋放的空間就不相關了,這樣每一個時刻需要O(n)個空間即可,因而空間復雜度還是O(n)。

快速排序:

   在數組中選擇一個基准元素(pivot),將數組分為兩部分,使得第一部分中的所有元素都小於等於pivot,第二部分的所有元素都大於pivot。對第一部分遞歸的應用快速排序算法,然后對第二部分遞歸的應用快速排序算法。

  • 穩定性不穩定。快速排序有兩個方向,右邊的j下標一直往左走,當a[j] > pivot,左邊的i下標一直往右走,當a[i] <= pivot。如果i和j都走不動了,i <= j, 交換a[i]和a[j],重復上面的過程,直到i>j,交換a[j]和pivot,完成一趟快速排序。在基准元素pivot和a[j]交換的時候,很有可能把前面的元素的穩定性打亂,比如序列為 5 3 3 4 3 8 9 10 11, 現在基准元素5和3交換就會把元素3的穩定性打亂,所以快速排序是一個不穩定的排序算法,不穩定發生在基准元素和a[j]交換的時刻。
  • 時間復雜度O(nlogn)
    • 最差情況下,划分由n個元素構成的數組需要進行n次比較和n次移動,划分所需時間為O(n),在最差情況下,基准元素每次會將數組划分為一個大的子數組和另外一個空數組,這個大的子數組的大小是在划分前的子數組的大小上減1,該算法需要()+()+...+2+1=O(n^2)時間。
    • 最佳情況下,基准元素每次將數組划分為規模大致相等的兩部分,則T(n)=T(n/2)+T(n/2)+划分用時,其中前兩項表示在兩個子數組上進行遞歸的快速排序用時,划分用時為O(n),則T(n)=T(n/2)+T(n/2)+n=O(nlogn)。
  • 空間復雜度O(logn)。快速排序每次遞歸都會返回一個中間值的位置。最優的情況下空間復雜度為O(logn) ,即每一次都平分數組的情況;最差的情況下空間復雜度為O(n) ,即退化為冒泡排序的情況。

堆排序:

  堆排序使用一個二叉堆,它首先將所有的元素添加到一個堆上,然后不斷移除最大的元素以獲得一個排好序的線性表。

  該二叉堆是一個完全二叉樹,該二叉樹具有如下屬性:首先它是一個完全二叉樹(二叉樹的每層都是滿的,或者最后一層沒填滿並且最后一層的葉子都是靠最左放置的),其次每個結點大於或等於它的任意一個孩子

  • 穩定性不穩定
  • 時間復雜度O(nlogn)。

設h表示n個元素的堆的高度。由於堆是一顆完全二叉樹,所以第一層有1(2^0)個結點,第二層有2(2^1)個結點,....,第h層有2^(h-1)個結點,最后第h+1層最少有1個最多有2^h個結點。因此1+2+...+2^(h-1)<n<=1+2+...+2^(h-1)+2^h,即2^h < n+1 <= 2^(h+1),所以h < log(n+1) <= h+1。所以堆的高度為O(logn)。

    • 由於add方法會最追蹤從葉子結點到根節點的路徑,因此向堆中添加一個新元素最多需要h步,所以建立一個包含n個元素的數組的初始堆需要O(nlogn)時間。
    • 因為remove方法要追蹤從根節點到葉子結點的路徑,因此從堆中刪除根節點后重建堆最多需要n步。由於要調用n次remove方法,所以由堆產生一個有序數組需要時間為O(nlogn)。
  • 空間復雜度O(1)。只是需要一個臨時變量來交換元素位置,所以空間復雜度O(1)。

1、冒泡排序

動畫演示:http://liveexample.pearsoncmg.com/dsanimation/BubbleSortNeweBook.html

思路

依次比較相鄰的兩個數,將小數放在前面,大數放在后面

即在第一趟:首先比較第1個和第2個數,將小數放前,大數放后。然后比較第2個數和第3個數,將小數放前,大數放后,如此繼續……直至比較最后兩個數,將小數放前,大數放后。重復第一趟步驟,直至全部排序完成。

第一趟比較完成后,最后一個數一定是數組中最大的一個數,所以第二趟比較的時候最后一個數不參與比較;

第二趟比較完成后,倒數第二個數也一定是數組中第二大的數,所以第三趟比較的時候最后兩個數不參與比較;

依次類推,每一趟比較次數-1;

……

 

穩定性:穩定

時間復雜度:O(n^2)

空間復雜度:O(1)

代碼:

public static void bubbleSort(int[] array) {

        // 優化:如果在某次遍歷中沒有發生交換,那么就不用再進行下一次遍歷了,因為所有元素已經排好序了
        boolean needNextPass = true;

        //需要array.length-1次遍歷
        for(int k = 1; k < array.length && needNextPass; k++){

            needNextPass = false; //假設不需要進行下次遍歷

            //第k次遍歷處理前array.length-k個數
            for(int i = 0; i < array.length - k; i++){
                //依次比較相鄰兩個元素
                if(array[i] > array[i + 1]){
                    //交換
                    int temp = array[i];
                    array[i] = array[i + 1];
                    array[i + 1] = temp;

                    needNextPass = true;  //需要進行下次遍歷
                }
            }
        }
    }

2、選擇排序

動畫演示:http://liveexample.pearsoncmg.com/dsanimation/SelectionSortNeweBook.html

思路

找數組中最小的數,並將其和第一個數交換;

然后從剩下的數中找最小的數,和第二個交換;

……一直進行下去,直到僅剩一個數           (是一個遞歸的過程)

 

穩定性:不穩定

時間復雜度:O(n^2)

空間復雜度:O(1)

代碼:

//主程序初始時候執行 selectionSort(array,0, array.length - 1)

public static void selectionSort(double[] array, int low, int high){
        if(low < high){
            int indexOfMin = low;    //最小數下標
            double min = array[low]; //最小數
            //找到最小的數min和最小數下標indexOfMin
            for(int i = low + 1; i <= high; i++){
                if(array[i] < min){
                    min = array[i];
                    indexOfMin = i;
                }
            }
            //交換第low個元素與min
            array[indexOfMin] = array[low];
            array[low] = min;
            
            //遞歸
            selectionSort(array, low + 1, high);
        }
    }

3、插入排序

動畫演示:http://liveexample.pearsoncmg.com/dsanimation/InsertionSortNeweBook.html

思路

重復的將新的元素插入一個排好序的子線性表中,直到整個線性表排好序。

[0,1,4,6,3]

我們要做的從a[1]開始,至於為什么不是a[0]。a[0]之前沒有與a[0]進行比較的元素。我們插入a[1],這個時候我們需要遍歷a[1]和a[1]之前的所有元素並進行比較。設置一個變量j,用來記錄第一個 比 a[1]元素小的那個元素的下標,也就是a[1]要插入的位置,這個時候跳出循環,並且沒發現一個比a[1]大的元素,就要令這個元素后移一位。依次類推的算法直到整個數組的最后一個。

 

穩定性:穩定

時間復雜度:O(n^2)

空間復雜度:O(1)

代碼:

public static void insertionSortt(int[] array){
        //從a[1]開始
        for(int i = 1; i < array.length; i++){
            int currentElement = array[i];
            int j; //標記元素要移動到的位置
            //先判斷currentElement前面一個數,如果要移動,移動后再判斷前面一個數,直到判斷到不需要移動或已經判斷到list[0]
            for(j = i - 1; j >=0 && array[j] > currentElement; j--){
                //將currentElement前面一個數向后移動一位
                array[j + 1] = array[j];
            }
            array[j + 1] = currentElement;
        }
    }

4、歸並排序

動畫演示:http://liveexample.pearsoncmg.com/dsanimation/MergeSortNeweBook.html

思想:

使用分而治之法對數組排序。將數組分為兩半,對每部分遞歸地應用歸並排序。在兩部分都排好序后,對它們進行歸並。

穩定性:穩定

時間復雜度:O(nlogn)

空間復雜度:O(n)

代碼:

public static void mergeSort(int[] array) {
        if (array.length > 1) {
            // 創建臨時數組firstHalf存放前面一半的元素
            int[] firstHalf = new int[array.length / 2];
            System.arraycopy(list, 0, firstHalf, 0, array.length / 2);
            // 遞歸
            mergeSort(firstHalf);

            // 創建臨時數組secondHalf存放后面一半的元素
            int lengthOfSecondHalf = array.length - array.length / 2;
            int[] secondHalf = new int[lengthOfSecondHalf];
            System.arraycopy(array, array.length / 2, secondHalf, 0,
                    lengthOfSecondHalf);
            // 遞歸
            mergeSort(secondHalf);

            // 歸並到原始數組array中
            merge(firstHalf, secondHalf, array);
        }
    }

    /** 歸並 */
    public static void merge(int[] array1, int[] array2, int[] temp) {
        /*
         * 歸並兩個有序數組array1、array2為一個臨時數組temp。
         * current1和current2指向array1和array2中要考慮的當前元素,重復比較array1和array2中的當前元素,並將較小的一個元素移動到temp中。
         * 如果較小元素在array1中,current1增加1;如果較小元素在array2中,current2增加1。
         * 最后array1或array2會剩余一個元素沒有移動,將它直接復制到temp中。
         */
        int current1 = 0;// array1中要考慮的當前元素
        int current2 = 0;// array2中要考慮的當前元素
        int current3 = 0;// temp中要考慮的當前元素

        while (current1 < array1.length && current2 < array2.length) {
            // 如果較小元素在array1中,current1增加1
            if (array1[current1] < array2[current2])
                temp[current3++] = array1[current1++];
            // 如果較小元素在array2中,current2增加1
            else
                temp[current3++] = array2[current2++];
        }

        while (current1 < array1.length)
            temp[current3++] = array1[current1++];

        while (current2 < array2.length)
            temp[current3++] = array2[current2++];
    }

5、快速排序

動畫演示:http://liveexample.pearsoncmg.com/dsanimation/QuickSortNeweBook.html

思想

在數組中選擇一個基准元素(pivot),將數組分為兩部分,使得第一部分中的所有元素都小於等於pivot,第二部分的所有元素都大於pivot。

對第一部分遞歸的應用快速排序算法,然后對第二部分遞歸的應用快速排序算法。

 

穩定性:不穩定

時間復雜度:O(nlogn),最壞時間復雜度O(n^2)

空間復雜度:O(logn)

 

代碼:

    /**快速排序算法*/
    public static void quickSort(int[] data, int start, int end) throws Exception{
        if(start >= end)
            return;
        
        //分而治之
        int index = partition(data, start, end);
        if(index > start)
            quickSort(data, start, index - 1);
        if(index < end)
            quickSort(data, index + 1, end);
        
    }
    /**重要*/
    //選擇一個基准元素,把data中比基准元素小的數字移到其左邊,比基准元素大的數字移到其右邊。並返回移動后基准元素下標
    public static int partition(int[] data, int low,int high) throws Exception{
        if(data == null || low < 0 || high >= data.length)
            throw new Exception("Invalid Parameters");
        
        //選擇第一個數作為基准元素
        int pivot = data[low];
        
        while(low < high){
            //從右向左找小於基准元素的值
            //注意一定要加等號,不然可能會出現死循環
            while(low < high && data[high] >= pivot)
                high--;
            data[low] = data[high];//賦值
            
            //從左向右找大於基准元素的值
            while(low < high && data[low] <= high)
                low++;
            data[high] = data[low];
        }
        //此時low與high在同一個位置,把pivot的值給low
        data[low] = pivot;
        
        return low;
        
    }

 6、堆排序

動畫演示:http://liveexample.pearsoncmg.com/dsanimation/HeapeBook.html

堆排序使用一個二叉堆,它首先將所有的元素添加到一個堆上,然后不斷移除最大的元素以獲得一個排好序的線性表。

堆排序使用一個二叉堆,該二叉堆是一個完全二叉樹,該二叉樹具有如下屬性:

  • 形狀:是一個完全二叉樹。(如果一棵二叉樹的每層都是滿的,或者最后一層沒填滿並且最后一層的葉子都是靠最左放置的,那么這棵二叉樹就是完全的。)
  • 堆屬性:每個結點大於或等於它的任意一個孩子

 

給堆添加一個結點:

首先將它添加到堆的末尾;
將最后一個結點作為當前節點; while(當前結點大於它的父節點){ 將當前結點和它的父節點交換; }

從對中刪除最大元素,即刪除根節點:

將最后一個結點替換根結點;
讓根結點成為當前結點;
while(當前結點具有子節點並且當前結點小於它的子結點){
    將當前結點和它的子節點交換;
}

這樣堆排序的步驟:首先使用Heap創建一個對象,使用add方法將所有元素添加到堆中,然后使用remove方法從堆中刪除所有元素,記錄刪除的元素就是一個有序數組了。

 

穩定性不穩定

時間復雜度O(nlogn)

  設h表示n個元素的堆的高度。由於堆是一顆完全二叉樹,所以第一層有1(2^0)個結點,第二層有2(2^1)個結點,....,第h層有2^(h-1)個結點,最后第h+1層最少有1個最多有2^h個結點。因此

  1+2+...+2^(h-1)< n <= 1+2+...+2^(h-1)+2^h,即2^h < n+1 <= 2^(h+1),

  所以h < log(n+1) <= h+1。所以堆的高度為O(logn)。 

  由於add方法會最追蹤從葉子結點到根節點的路徑,因此向堆中添加一個新元素最多需要h步,所以建立一個包含n個元素的數組的初始堆需要O(nlogn)時間。因為remove方法要追蹤從根節點到葉子結點的路徑,因此從堆中刪除根節點后重建堆最多需要n步。由於要調用n次remove方法,所以由堆產生一個有序數組需要時間為O(nlogn)。

空間復雜度O(1)。只是需要一個臨時變量來交換元素位置,所以空間復雜度O(1)。

代碼:

    public static void heapSort(int[] array) {
        Heap<Integer> heap = new Heap();
        //使用add方法將所有元素添加到堆中
        for(int i = 0; i < array.length; i++)
            heap.add(array[i]);
        //然后使用remove方法從堆中刪除所有元素。 以降序刪除這些元素,得到的結果就是升序的
        for(int i = array.length - 1; i >= 0; i--) 
            array[i] = heap.remove();
    }
----------------------------------------------------------------------------------
/**建一個堆類*/
class Heap<E extends Comparable<E>> {
    //利用list存結點,則根節點下標為0, 下標為i的父節點下標應為(i-1)/2,子節點應為2*i+1、2*i+2
    private ArrayList<E> list = new ArrayList<E>();
    //構造方法
    public Heap() {}
    
    /**向堆中添加元素*/
    public void add(E item) {
        //首先將它添加到堆的末尾
        list.add(item);
        
        //將最后一個結點作為當前結點
        int currentIndex = list.size() - 1;
        while(currentIndex > 0) {
            //父節點
            int parentIndex = (currentIndex - 1) / 2;
            //當當前節點大於它的父節點時,將當前節點和它的父節點交換
            if(list.get(currentIndex).compareTo(list.get(parentIndex)) > 0) {
                E temp = list.get(currentIndex);
                list.set(currentIndex, list.get(parentIndex));
                list.set(parentIndex, temp);
                //currentIndex移到上一層
                currentIndex = parentIndex;
            }else {
                break;
            }
        }
    }
    
    /**刪除堆中的最大元素(刪除根節點)*/
    public E remove() {
        if(list.size() == 0)
            return null;
        
        E removeItem = list.get(0);
        //用最后一個結點替換根結點,並刪除這個最大結點(原來的根結點)
        list.set(0, list.get(list.size() - 1));
        list.remove(list.size() - 1);
        
        //讓根節點成為當前結點
         int currentIndex = 0;
         //當當前結點具有子結點並且當前結點小於它的子結點時,將當前結點和它較大的子結點交換
         while(currentIndex < list.size()) {
             int leftIndex = currentIndex * 2 + 1;//左孩子結點
             int rightIndex = currentIndex * 2 + 2;//右孩子結點
             if(leftIndex >= list.size())
                 break; //說明沒有孩子結點
             
            //取左孩子結點和右孩子結點最大的結點,然后與當前結點比較
             int maxIndex = leftIndex;
             if(rightIndex < list.size()) {
                 if(list.get(rightIndex).compareTo(list.get(leftIndex)) > 0)
                     maxIndex = rightIndex;
             }
             if(list.get(currentIndex).compareTo(list.get(maxIndex)) < 0) {
                 E temp = list.get(currentIndex);
                 list.set(currentIndex, list.get(maxIndex));
                 list.set(maxIndex, temp);
                 //currentIndex移到下一層
                 currentIndex = maxIndex;
             }else {
                 break;
             }
         }
        
        return removeItem;
    }

}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM