1. 程式人生 > >【Java面試12】常用演算法(冒泡、插入、選擇、快速)和二叉樹詳解

【Java面試12】常用演算法(冒泡、插入、選擇、快速)和二叉樹詳解

常用演算法(冒泡、插入、選擇、快速)和二叉樹詳解

 

  同一問題可用不同演算法解決,而一個演算法的質量優劣將影響到演算法乃至程式的效率。演算法分析的目的在於選擇合適演算法和改進演算法。

  電腦科學中,演算法的時間複雜度是一個函式,它定量描述了該演算法的執行時間。這是一個關於代表演算法輸入值的字串的長度的函式。時間複雜度常用大O符號(Order)表述,不包括這個函式的低階項和首項係數。使用這種方式時,時間複雜度可被稱為是漸近的,它考察當輸入值大小趨近無窮時的情況。

定義

  在電腦科學中,演算法的時間複雜度是一個函式,它定量描述了該演算法的執行時間。這是一個關於代表演算法輸入值的字串的長度的函式。時間複雜度常用大O符號表述,不包括這個函式的低階項和首項係數。

演算法複雜度

  演算法複雜度分為時間複雜度和空間複雜度。其作用: 時間複雜度是指執行演算法所需要的計算工作量;而空間複雜度是指執行這個演算法所需要的記憶體空間。(演算法的複雜性體現在執行該演算法時的計算機所需資源的多少上,計算機資源最重要的是時間和空間(即暫存器)資源,因此複雜度分為時間和空間複雜度)。

時間複雜度

  1. 一般情況下,演算法的基本操作重複執行的次數是模組n的某一個函式f(n),因此,演算法的時間複雜度記做:T(n)=O(f(n))

分析:隨著模組n的增大,演算法執行的時間的增長率和 f(n) 的增長率成正比,所以 f(n) 越小,演算法的時間複雜度越低,演算法的效率越高。

  2. 在計算時間複雜度的時候,先找出演算法的基本操作,然後根據相應的各語句確定它的執行次數,再找出 T(n) 的同數量級(它的同數量級有以下:1,log(2)n,n,n log(2)n ,n的平方,n的三次方,2的n次方,n!),找出後,f(n) = 該數量級,若 T(n)/f(n) 求極限可得到一常數c,則時間複雜度T(n) = O(f(n))

  例:演算法:

  則有 T(n) = n 的平方+n的三次方,根據上面括號裡的同數量級,我們可以確定 n的三次方 為T(n)的同數量級

  則有 f(n) = n的三次方,然後根據 T(n)/f(n) 求極限可得到常數c

  則該演算法的時間複雜度:T(n) = O(n^3) 注:n^3即是n的3次方。

  3.在pascal中比較容易理解,容易計算的方法是:看看有幾重for迴圈,只有一重則時間複雜度為O(n),二重則為O(n^2),依此類推,如果有二分則為O(logn),二分例如快速冪、二分查詢,如果一個for迴圈套一個二分,那麼時間複雜度則為O(nlogn)。

 常用排序

 

名稱

複雜度

說明

備註

氣泡排序
Bubble Sort

O(N*N)

將待排序的元素看作是豎著排列的“氣泡”,較小的元素比較輕,從而要往上浮

 

插入排序

Insertion sort

O(N*N)

逐一取出元素,在已經排序的元素序列中從後向前掃描,放到適當的位置

起初,已經排序的元素序列為空

選擇排序

O(N*N)

首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小元素,然後放到排序序列末尾。以此遞迴。

 

快速排序

Quick Sort

O(n *log2(n))

先選擇中間值,然後把比它小的放在左邊,大的放在右邊(具體的實現是從兩邊找,找到一對後交換)。然後對兩邊分別使用這個過程(遞迴)。

 

堆排序HeapSort

O(n *log2(n))

利用堆(heaps)這種資料結構來構造的一種排序演算法。堆是一個近似完全二叉樹結構,並同時滿足堆屬性:即子節點的鍵值或索引總是小於(或者大於)它的父節點。

近似完全二叉樹

希爾排序

SHELL

O(n1+£)

0<£<1

選擇一個步長(Step) ,然後按間隔為步長的單元進行排序.遞迴,步長逐漸變小,直至為1.

 

箱排序
Bin Sort

O(n)

設定若干個箱子,把關鍵字等於 k 的記錄全都裝入到第k 個箱子裡 ( 分配 ) ,然後按序號依次將各非空的箱子首尾連線起來 ( 收集 ) 。

分配排序的一種:通過" 分配 " 和 " 收集 " 過程來實現排序。

氣泡排序

  氣泡排序(BubbleSort)的基本概念是:依次比較相鄰的兩個數,將小數放在前面,大數放在後面。即在第一趟:首先比較第1個和第2個數,將小數放前,大數放後。然後比較第2個數和第3個數,將小數放前,大數放後,如此繼續,直至比較最後兩個數,將小數放前,大數放後。

  氣泡排序流程至此第一趟結束,將最大的數放到了最後。在第二趟:仍從第一對數開始比較(因為可能由於第2個數和第3個數的交換,使得第1個數不再小於第2個數),將小數放前,大數放後,一直比較到倒數第二個數(倒數第一的位置上已經是最大的),第二趟結束,在倒數第二的位置上得到一個新的最大數(其實在整個數列中是第二大的數)。如此下去,重複以上過程,直至最終完成排序。

  由於在排序過程中總是小數往前放,大數往後放,相當於氣泡往上升,所以稱作氣泡排序。

編碼思路:

  用二重迴圈實現,外迴圈變數設為i,內迴圈變數設為j。假如有10個數需要進行排序,則外迴圈重複9次,內迴圈依次重複9,8,...,1次。每次進行比較的兩個元素都是與內迴圈j有關的,它們可以分別用a[j]和a[j+1]標識,i的值依次為1,2,...,9,對於每一個i,j的值依次為1,2,...10-i。

複製程式碼

/*
 * 氣泡排序
 */
public class BubbleSort {
  public static void main(String[] args) {
    int[] arr={9,8,7,6,5,4,3,2,1};
    System.out.println("排序前陣列為:");
    for(int num:arr){
      System.out.print(num+" ");
    }
    for(int i=0;i<arr.length-1;i++){//外層迴圈控制排序趟數
      for(int j=0;j<arr.length-1-i;j++){//內層迴圈控制每一趟排序多少次
        if(arr[j]>arr[j+1]){
          int temp=arr[j];
          arr[j]=arr[j+1];
          arr[j+1]=temp;
        }
      }
    } 
    System.out.println();
    System.out.println("排序後的陣列為:");
     for(int num:arr){
       System.out.print(num+" ");
     } 
  }
 }

複製程式碼

 

插入排序

  有一個已經有序的資料序列,要求在這個已經排好的資料序列中插入一個數,但要求插入後此資料序列仍然有序,這個時候就要用到一種新的排序方法--插入排序法,插入排序的基本操作就是將一個數據插入到已經排好序的有序資料中,從而得到一個新的、個數加一的有序資料,演算法適用於少量資料的排序,時間複雜度為O(n^2)。是穩定的排序方法。

  插入演算法把要排序的陣列分成兩部分:第一部分包含了這個陣列的所有元素,但將最後一個元素除外(讓陣列多一個空間才有插入的位置),而第二部分就只包含這一個元素(即待插入元素)。在第一部分排序完成後,再將這個最後元素插入到已排好序的第一部分中。

  1、將指標指向某個元素,假設該元素左側的元素全部有序,將該元素抽取出來,然後按照從右往左的順序分別與其左邊的元素比較,遇到比其大的元素便將元素右移,直到找到比該元素小的元素或者找到最左面發現其左側的元素都比它大,停止;

  2、此時會出現一個空位,將該元素放入到空位中,此時該元素左側的元素都比它小,右側的元素都比它大;

  3、指標向後移動一位,重複上述過程。每操作一輪,左側有序元素都增加一個,右側無序元素都減少一個。

編碼思路:

  需要兩層迴圈,第一層迴圈index表示上述例子中的指標,即遍歷從座標為1開始的每一個元素;第二層迴圈從leftindex=index-1開始,leftindex--向左遍歷,將每一個元素與i處的元素比較,直到j處的元素小於i出的元素或者leftindex<0;遍歷從i到j的每一個元素使其右移,最後將index處的元素放到leftindex處的空位處。

複製程式碼

public class InsertSort {
    private int[] array;
    private int length;
    
    public InsertSort(int[] array){
        this.array = array;
        this.length = array.length;
    }
    
    public void display(){        
        for(int a: array){
            System.out.print(a+" ");
        }
        System.out.println();
    }
    
    /**
     * 插入排序方法
     */
    public void doInsertSort(){
        for(int index = 1; index<length; index++){//外層向右的index,即作為比較物件的資料的index
            int temp = array[index];//用作比較的資料
            int leftindex = index-1;
            while(leftindex>=0 && array[leftindex]>temp){//當比到最左邊或者遇到比temp小的資料時,結束迴圈
                array[leftindex+1] = array[leftindex];
                leftindex--;
            }
            array[leftindex+1] = temp;//把temp放到空位上
        }
    }
    
    public static void main(String[] args){
        int[] array = {38,65,97,76,13,27,49};
        InsertSort is = new InsertSort(array);
        System.out.println("排序前的資料為:");
        is.display();
        is.doInsertSort();
        System.out.println("排序後的資料為:");
        is.display();
    }
}

複製程式碼

 

選擇排序

  選擇排序(Selection sort)是一種簡單直觀的排序演算法。它的工作原理是每一次從待排序的資料元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到全部待排序的資料元素排完。 選擇排序是不穩定的排序方法。

  1、從第一個元素開始,分別與後面的元素向比較,找到最小的元素與第一個元素交換位置;

  2、從第二個元素開始,分別與後面的元素相比較,找到剩餘元素中最小的元素,與第二個元素交換;

  3、重複上述步驟,直到所有的元素都排成由小到大為止。

程式設計思路:

  需要兩次迴圈,第一層迴圈i表示每輪指標指向的位置,將最小值min初始化為第i個元素,第二層迴圈從j=i+1開始,分別與min比較,如果小於min,則更新min的值,內層迴圈結束後;交換min元素和第i個元素的位置。以此類推進行下一輪迴圈,直到i=length時停止迴圈。當i=length時,說明小的元素已經全部移到了左面,因此無需進行內層迴圈了。

複製程式碼

package com.test.insertsort;
/**
 * 選擇排序
 * @author Administrator
 *
 */
public class ChooseSort {
    private int[] array;
    private int length;
    
    public ChooseSort(int[] array){
        this.array = array;
        this.length = array.length;
    }
    
    /**
     * 列印陣列中的所有元素
     */
    public void display(){
        for(int i: array){
            System.out.print(i+" ");
        }
        System.out.println(); 
    }
    
    /**
     * 選擇排序演算法
     */
    public void chooseSort(){
        for(int i=0; i<length-1; i++){
            int minIndex = i;
            for(int j=minIndex+1;j<length;j++){
                if(array[j]<array[minIndex]){
                    minIndex = j;
                }
            }
            int temp = array[i];
            array[i] = array[minIndex];
            array[minIndex] = temp; 
        }
    }
    
    public static void main(String[] args){
        int[] array={100,45,36,21,17,13,7};
        ChooseSort cs = new ChooseSort(array);
        System.out.println("排序前的資料為:");
        cs.display();
        cs.chooseSort();
        System.out.println("排序後的資料為:");
        cs.display();
    }
} 

複製程式碼

快速排序

設要排序的陣列是A[0]……A[N-1],首先任意選取一個數據(通常選用陣列的第一個數)作為關鍵資料,然後將所有比它小的數都放到它前面,所有比它大的數都放到它後面,這個過程稱為一趟快速排序。值得注意的是,快速排序不是一種穩定的排序演算法,也就是說,多個相同的值的相對位置也許會在演算法結束時產生變動  

注:在待排序的檔案中,若存在多個關鍵字相同的記錄,經過排序後這些具有相同關鍵字的記錄之間的相對次序保持不變,該排序方法是穩定的;若具有相同關鍵字的記錄之間的相對次序發生改變,則稱這種排序方法是不穩定的。
要注意的是,排序演算法的穩定性是針對所有輸入例項而言的。即在所有可能的輸入例項中,只要有一個例項使得演算法不滿足穩定性要求,則該排序演算法就是不穩定的。 

排序演示

示例

假設使用者輸入瞭如下陣列:

 

下標

0

1

2

3

4

5

資料

6

2

7

3

8

9

建立變數i=0(指向第一個資料), j=5(指向最後一個數據), k=6(賦值為第一個資料的值)。

我們要把所有比k小的數移動到k的左面,所以我們可以開始尋找比6小的數,從j開始,從右往左找,不斷遞減變數j的值,我們找到第一個下標3的資料比6小,於是把資料3移到下標0的位置,把下標0的資料6移到下標3,完成第一次比較:

下標

0

1

2

3

4

5

資料

3

2

7

6

8

9

i=0 j=3 k=6

接著,開始第二次比較,這次要變成找比k大的了,而且要從前往後找了。遞加變數i,發現下標2的資料是第一個比k大的,於是用下標2的資料7和j指向的下標3的資料的6做交換,資料狀態變成下表:

下標

0

1

2

3

4

5

資料

3

2

6

7

8

9

i=2 j=3 k=6

稱上面兩次比較為一個迴圈。

接著,再遞減變數j,不斷重複進行上面的迴圈比較。

在本例中,我們進行一次迴圈,就發現i和j“碰頭”了:他們都指向了下標2。於是,第一遍比較結束。得到結果如下,凡是k(=6)左邊的數都比它小,凡是k右邊的數都比它大:

下標

0

1

2

3

4

5

資料

3

2

6

7

8

9

如果i和j沒有碰頭的話,就遞加i找大的,還沒有,就再遞減j找小的,如此反覆,不斷迴圈。注意判斷和尋找是同時進行的。

然後,對k兩邊的資料,再分組分別進行上述的過程,直到不能再分組為止。

注意:第一遍快速排序不會直接得到最終結果,只會把比k大和比k小的數分到k的兩邊。為了得到最後結果,需要再次對下標2兩邊的陣列分別執行此步驟,然後再分解陣列,直到陣列不能再分解為止(只有一個數據),才能得到正確結果。

複製程式碼

package com.test.insertsort;

/**
 * 劃分、遞迴、快排
 * @author bjh
 *
 */
public class QuickSort {
    
    /**待排序、劃分陣列*/
    private int[] array;
    /**陣列長度*/
    private int length;
    
    public QuickSort(int[] array){
        this.array = array;
        this.length = array.length;
    }
    
    /**
     * 列印元素
     */
    public void printArray(){
        for(int i=0; i<length; i++){
            System.out.print(array[i]+" ");
        }
        System.out.println();
    }
    
    
    /**
     * 劃分
     * @return 劃分的分界點
     */
    public int partition(int left, int right, int pivot){
        //左指標的起點,left-1是由於在後面的迴圈中,每迴圈一次左指標都要右移,
        //這樣可以確保左指標從左邊第一個元素開始,不然是從第二個開始
        int leftpoint = left-1;
        //右指標的起點,right+1是由於後面的迴圈中,每迴圈一次右指標都要左移,
        //這樣可以確保右指標從最右邊開始,不然是從倒數第二個開始
        int rightpoint = right+1;
        while(true){
            //找到左邊大於pivot的資料,或者走到了最右邊仍然沒有找到比pivot大的資料
            while(leftpoint<right && array[++leftpoint]<pivot);
            //找到右邊小於pivot的資料,或者走到了最左邊仍然沒有找到比pivot小的資料
            while(rightpoint>left && array[--rightpoint]>pivot);
            //左指標和右指標重疊或相交
            if(leftpoint >= rightpoint){
                break;
            }else{
                //交換左邊大的和右邊小的資料
                swap(leftpoint,rightpoint);
            }
        }
        //返回分界點,即右邊子陣列中最左邊的點
        return leftpoint;
    }
    
    
    /**
     * 交換資料
     */
    public void swap(int leftpoint,int rightpoint){
        int temp = array[leftpoint];
        array[leftpoint] = array[rightpoint];
        array[rightpoint] = temp;
    }
    
    public static void main(String args[]){
        int[] array = {99,78,26,17,82,36,9,81,22,100,30,20,17,85};
        QuickSort qs = new QuickSort(array);
        System.out.println("劃分前的資料為:");
        qs.printArray();
        int bound = qs.partition(0, array.length-1, 50);
        System.out.println("劃分後的資料為:");
        qs.printArray();
        System.out.println("劃分的分界點為:" + array[bound] + ",分界點的座標為:" + bound);
    }

}

複製程式碼

二叉樹遍歷

樹的特徵和定義


  樹是一種重要的非線性資料結構,直觀地看,它是資料元素(在樹中稱為結點)按分支關係組織起來的結構,很象自然界中的樹那樣。樹結構在客觀世界中廣泛存在,如人類社會的族譜和各種社會組織機構都可用樹形象表示。樹在計算機領域中也得到廣泛應用,如在編譯源程式時,可用樹表示源程式的語法結構。又如在資料庫系統中,樹型結構也是資訊的重要組織形式之一。一切具有層次關係的問題都可用樹來描述。

 

樹(Tree)是元素的集合。我們先以比較直觀的方式介紹樹。下面的資料結構是一個樹:

樹有多個節點(node),用以儲存元素。某些節點之間存在一定的關係,用連線表示,連線稱為邊(edge)。邊的上端節點稱為父節點,下端稱為子節點。樹像是一個不斷分叉的樹根。

每個節點可以有多個子節點(children),而該節點是相應子節點的父節點(parent)。比如說,3,5是6的子節點,6是3,5的父節點;1,8,7是3的子節點, 3是1,8,7的父節點。樹有一個沒有父節點的節點,稱為根節點(root),如圖中的6。沒有子節點的節點稱為葉節點(leaf),比如圖中的1,8,9,5節點。從圖中還可以看到,上面的樹總共有4個層次,6位於第一層,9位於第四層。樹中節點的最大層次被稱為深度。也就是說,該樹的深度(depth)為4。

 

如果我們從節點3開始向下看,而忽略其它部分。那麼我們看到的是一個以節點3為根節點的樹:

三角形代表一棵樹

再進一步,如果我們定義孤立的一個節點也是一棵樹的話,原來的樹就可以表示為根節點和子樹(subtree)的關係:

 

上述觀察實際上給了我們一種嚴格的定義樹的方法:

1. 樹是元素的集合。

2. 該集合可以為空。這時樹中沒有元素,我們稱樹為空樹 (empty tree)。

3. 如果該集合不為空,那麼該集合有一個根節點,以及0個或者多個子樹。根節點與它的子樹的根節點用一個邊(edge)相連。

上面的第三點是以遞迴的方式來定義樹,也就是在定義樹的過程中使用了樹自身(子樹)。由於樹的遞迴特徵,許多樹相關的操作也可以方便的使用遞迴實現。我們將在後面看到。

 

樹的實現

樹的示意圖已經給出了樹的一種記憶體實現方式: 每個節點儲存元素和多個指向子節點的指標。然而,子節點數目是不確定的。一個父節點可能有大量的子節點,而另一個父節點可能只有一個子節點,而樹的增刪節點操作會讓子節點的數目發生進一步的變化。這種不確定性就可能帶來大量的記憶體相關操作,並且容易造成記憶體的浪費。

一種經典的實現方式如下:

樹的記憶體實現

擁有同一父節點的兩個節點互為兄弟節點(sibling)。上圖的實現方式中,每個節點包含有一個指標指向第一個子節點,並有另一個指標指向它的下一個兄弟節點。這樣,我們就可以用統一的、確定的結構來表示每個節點。

 

計算機的檔案系統是樹的結構,比如Linux檔案管理背景知識中所介紹的。在UNIX的檔案系統中,每個檔案(資料夾同樣是一種檔案),都可以看做是一個節點。非資料夾的檔案被儲存在葉節點。資料夾中有指向父節點和子節點的指標(在UNIX中,資料夾還包含一個指向自身的指標,這與我們上面見到的樹有所區別)。在git中,也有類似的樹狀結構,用以表達整個檔案系統的版本變化 (參考版本管理三國志)。

 二叉樹: 

二叉樹是由n(n≥0)個結點組成的有限集合、每個結點最多有兩個子樹的有序樹。它或者是空集,或者是由一個根和稱為左、右子樹的兩個不相交的二叉樹組成。

特點:

(1)二叉樹是有序樹,即使只有一個子樹,也必須區分左、右子樹;

(2)二叉樹的每個結點的度不能大於2,只能取0、1、2三者之一;

(3)二叉樹中所有結點的形態有5種:空結點、無左右子樹的結點、只有左子樹的結點、只有右子樹的結點和具有左右子樹的結點。

 

二叉樹(binary)是一種特殊的樹。二叉樹的每個節點最多隻能有2個子節點:

二叉樹

由於二叉樹的子節點數目確定,所以可以直接採用上圖方式在記憶體中實現。每個節點有一個左子節點(left children)和右子節點(right children)。左子節點是左子樹的根節點,右子節點是右子樹的根節點。

 

如果我們給二叉樹加一個額外的條件,就可以得到一種被稱作二叉搜尋樹(binary search tree)的特殊二叉樹。二叉搜尋樹要求:每個節點都不比它左子樹的任意元素小,而且不比它的右子樹的任意元素大。

(如果我們假設樹中沒有重複的元素,那麼上述要求可以寫成:每個節點比它左子樹的任意節點大,而且比它右子樹的任意節點小)

二叉搜尋樹,注意樹中元素的大小

二叉搜尋樹可以方便的實現搜尋演算法。在搜尋元素x的時候,我們可以將x和根節點比較:

1. 如果x等於根節點,那麼找到x,停止搜尋 (終止條件)

2. 如果x小於根節點,那麼搜尋左子樹

3. 如果x大於根節點,那麼搜尋右子樹

二叉搜尋樹所需要進行的操作次數最多與樹的深度相等。n個節點的二叉搜尋樹的深度最多為n,最少為log(n)。

 

二叉樹的遍歷

遍歷即將樹的所有結點訪問且僅訪問一次。按照根節點位置的不同分為前序遍歷,中序遍歷,後序遍歷。

前序遍歷:根節點->左子樹->右子樹

中序遍歷:左子樹->根節點->右子樹

後序遍歷:左子樹->右子樹->根節點

例如:求下面樹的三種遍歷

 

前序遍歷:abdefgc

中序遍歷:debgfac

後序遍歷:edgfbca

 

 二叉樹的型別

(1)完全二叉樹——若設二叉樹的高度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第h層有葉子結點,並且葉子結點都是從左到右依次排布,這就是完全二叉樹

(2)滿二叉樹——除了葉結點外每一個結點都有左右子葉且葉子結點都處在最底層的二叉樹。

(3)平衡二叉樹——平衡二叉樹又被稱為AVL樹(區別於AVL演算法),它是一棵二叉排序樹,且具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹

如何判斷一棵樹是完全二叉樹?按照定義,

教材上的說法:一個深度為k,節點個數為 2^k - 1 的二叉樹為滿二叉樹。這個概念很好理解,

就是一棵樹,深度為k,並且沒有空位。

首先對滿二叉樹按照廣度優先遍歷(從左到右)的順序進行編號。

一顆深度為k二叉樹,有n個節點,然後,也對這棵樹進行編號,如果所有的編號都和滿二叉樹對應,那麼這棵樹是完全二叉樹。

 

image