使用遞歸底層實現二分搜索樹

基本方法的實現

同樣的使這個類支持泛型,但因爲二分搜索樹必須具有可比較性,讓它繼承Comparable
BST.java

// 支持泛型,這個類型必須擁有可比較性
public class BST<E extends Comparable<E>> {
    // 對應的節點類
    private class Node {
        public E e;
        public Node left;
        public Node right;

        public Node(E e) {
            this.e = e;
            left = null;
            right = null;
        }
    }

    // 需要一個根節點
    private Node root;
    // 記錄二分搜索樹存儲多少個元素
    private int size;

    public BST() {
        root = null;
        size = 0;
    }

    // 當前存儲了多少個元素
    public int getSize() {
        return size;
    }

    // 查看當前二分搜索樹是否爲空
    public boolean isEmpty() {
        return size == 0;
    }
}

添加新元素

1.使用遞歸添加新元素

如果在最開始的時候,二分搜索樹中一個節點也沒有,root爲NULL。假如現在添加一個新的元素33,那麼這個33這個節點將會成爲根。
此時在添加一個新的元素22,因爲22比根節點33小,所以它需要添加到33的左子樹。33的左子樹整體爲空,22將稱爲33的左子樹的根節點,即33的左孩子。
利用上述規律,每添加一個都從二分搜索樹的根開始,如果比根節點小,往左;反之爲右。不管是左子樹還是右子樹,同樣是一顆二分搜索樹,繼續判斷大小,添加的過程可以以此類推。

考慮特殊的情況:當添加的新元素與節點相同時,不添加新元素。因爲始終需要比較關係,所以實現的二分搜索樹不包含重複元素。如果需要重複,只需要改變定義:左子樹小於等於節點或者右子樹大於等於節點。

儘管二分搜索樹的非遞歸實現與鏈表很像,只需要在添加時進行一次大小的if判斷即可,但在這裏更加關注遞歸實現

  • 使用add(E e)方法向二分搜索樹中添加新的元素e,如果根節點本身爲空,只需要將根節點指向新建的元素就可以了,之後維護一下大小對size++;否則從根節點開始添加元素。
  • 定義一個私有的遞歸函數private void add(Node node, E e)它的區別在於需要傳入一個節點和添加的元素e。它是以node爲根的向二分搜索樹中添加新的元素,是一個遞歸算法。
  • 需要注意的是,在進行節點元素之間的比較時,由於它們不是基礎類型,不能直接使用大於小於號,因爲BST類實現了Compareble接口,所以使用compareTo()方法進行比較。
// 向二分搜索樹中添加一個新的元素
public void add(E e) {
    // 如果根節點本身爲null,那麼這個元素就是根節點
    if (root == null) {
        // 根節點直接指向一個新建的元素
        root = new Node(e);
        // 維護size
        size++;
    } else {
        // 如果不爲空,則嘗試從根節點開始插入元素e
        add(root, e);
    }

}

// 像以node爲根的二分搜素樹插入新的元素e
private void add(Node node, E e) {

    // 遞歸終止條件

    // 先檢查一下要插入的元素e是否等於node.e
    if (e.equals(node.e)) {
        // 如果相等,說明元素存在,直接return
        return;
    }
    // 這兩個元素之間的比較由於不是基礎類型,不能直接比較
    // 因爲E滿足Comparable接口,所以使用compareTo方法進行比較
    // 此時node的左子樹爲null,直接插入
    else if (e.compareTo(node.e) < 0 && node.left == null) {
        node.left = new Node(e);
        size++;
        return;
    }
    // 否則的話,比節點元素值大且爲null稱爲節點的右孩子
    else if (e.compareTo(node.e) > 0 && node.right == null) {
        node.right = new Node(e);
        size++;
        return;
    }

    // 判斷待插入元素與節點元素做比較的結果,如果不等並且節點待插入位置爲null,直接插入元素。否則進行遞歸調用
    if (e.compareTo(node.e) < 0) {
        // 如果待插入元素小於節點,需要遞歸的像左子樹插入元素
        add(node.left, e);
    }
    // 此時一定(e.compareTo(node.e) > 0)
    else {
        add(node.right, e);
    }
}

2.改進添加操作

當前的插入操作所存在的問題:

  1. 插入操作的算法過程,整體上是向以node爲根的二分搜索樹插入新的元素e,具體的過程其實是把新的元素e插入給node的左孩子或者是node的右孩子。對當前的node來說,如果想插入的位置不爲空的話,在進行遞歸調用將它插入到對應的左子樹或者右子樹當中。 對於遞歸的add()方法初始調用部分,即public void add()中,對根節點進行了特殊的處理,根爲空,根直接是一個新的節點,否則才調用遞歸函數。在遞歸函數中,都是新元素e作爲node的子節點插入進去的。儘管算法是正確的,但形成了邏輯的不統一
  2. 遞歸函數對於元素e和node.e這個元素進行了兩輪比較。第一輪比較,在比較它們的大小同時還需要看一下它們的左右是否爲空,如果爲空直接插入元素。如果不爲空,則進行了第二輪比較,不能作爲node的孩子直接插入元素e,只好遞歸的再去調用add()函數。
  3. 遞歸函數的終止條件顯得極爲臃腫,這是因爲需要考慮node的左孩子和右孩子是否爲空。空本身也是一顆二叉樹。如果這個函數走到了一個node爲空的地方,此時就一定需要新創建一個節點,在上述代碼的if判斷中,並沒有遞歸到底。

當插入的元素e小於node.e,不管node.left是否爲空,再遞歸一層。如果遞歸的這一層爲空,也就是此時新插入一個元素需要插入到空的位置上,此時的位置本身就應該是新元素節點。可以講遞歸終止條件改善,如果node==null,此時一定需要新插入節點new Node(e),還需要將這個節點return給函數調用的上一層,將這個節點與二叉樹掛接起來。

// 向二分搜索樹中添加一個新的元素
public void add(E e) {
    root = add(root,e);
}

// 像以node爲根的二分搜素樹插入新的元素e
// 返回插入新節點後二分搜索樹的根
private Node add(Node node, E e) {

    // 遞歸終止條件
    // node爲空時,直接返回node,即對於一個空的二叉樹來說,插入一個新節點,這顆二分搜素樹的根是這個節點本身
    if (node == null) {
        size++;
        return new Node(e);
    }

    // 遞歸調用層

    // 當前的插入元素比node元素小,向node的左子樹中插入元素e
    // 爲了讓整個二叉樹發生改變,在node的左子樹中插入元素e的結果可能是變化的,讓node的左子樹接住這個變化
    if (e.compareTo(node.e) < 0)
        // 如果node.left爲空,則這一次add操作就會返回一個新節點,然後node.left賦值新的節點
        node.left = add(node.left, e);
    else if (e.compareTo(node.e) > 0)
        // 上述同理,當右側爲空,先返回一個新節點,然後讓右子樹等於這個新節點。
        node.right = add(node.right, e);

    // 插入新節點以後,二分搜索樹的根還是node,根據函數定義的功能語意將其返回
    return node;
}

查詢操作

對於查詢操作,只需要比對每一個node裏的元素是否匹配。不牽扯像二分搜索樹添加元素那樣需要掛接節點。
使用遞歸實現,在遞歸查找元素e的過程中,需要從二分搜索樹的根開始逐漸轉移在新的二分搜索樹的子樹中縮小問題規模,即縮小查詢樹的規模直到找到元素e或者最終沒有找到元素e。
設置私有的遞歸算法private boolean contains(Node node,E e),首先考慮終止情況node爲空,不存在元素的情況直接返回false,其次在進行判斷。

// 二分搜索樹的查詢是否包含元素e
public boolean contains(E e) {
    return contains(root, e);
}

// 以node爲根節點的二分搜索樹中是否包含元素e,遞歸算法。
private boolean contains(Node node, E e) {
    // 終止情況,node爲空
    if (node == null)
        return false;

    // 找到返回true,未找到根據判斷去左右子樹尋找
    if (e.compareTo(node.e) == 0)
        return true;
    else if (e.compareTo(node.e) < 0)
        return contains(node.left, e);
    else
        return contains(node.right, e);
}

因爲二分搜索樹中不存在索引的概念,暫時不設置通過索引查詢或者修改元素的方法。

遍歷操作

對於數據結構的遍歷,就是把數據結構中所存儲的所有的元素都訪問一遍。相應的對於二分搜索樹來說就是將所有的節點都訪問一遍。
在線性數據結構中,對於遍歷操作是極其容易的。無論樹數組還是鏈表,從頭到尾做一層循環即可。但在樹結構下,並非如此。

1.前序遍歷

二分搜索樹的遞歸操作,無論是添加元素還是查詢元素,在遞歸的過程中,每次只選擇一個子樹進行下去,直到達到遞歸的終止條件。
但對於遍歷操作是不同的,由於需要訪問二叉樹中所有的節點,兩個子樹讀需要考慮。訪問完根節點之後,既要訪問左子樹中所有節點,也要訪問右子樹中所有節點,進行兩次遞歸的調用。整體簡單結構如下:

function traverse(node):
    if (node == null)
        return;
        
    訪問該節點
    traverse(node.left);
    traverse(node.right);

上述遍歷通常稱爲二叉樹的前序遍歷,是因爲先訪問節點,再訪問左右子樹。

// 二分搜索樹的前序遍歷 用戶調用的不需要傳參數
public void preOrder(){
    // 用戶的初始調用只需要對root進行調用
    preOrder(root);
}

// 前序遍歷以node爲根的二分搜索樹,遞歸算法
private void preOrder(Node node){
    // 首先遞歸終止條件 如果node爲空沒得遍歷
    if(node == null)
        return;

    // 將node中存儲的元素打印輸出出來
    System.out.println(node.e);
    preOrder(node.left);
    preOrder(node.right);
}

編寫測試代碼Main.java,將數組{5, 3, 6, 8, 4, 2};存放進二叉搜索樹進行遍歷打印。

public class Main {
    public static void main(String[] args) {
        BST<Integer> bst = new BST<>();
        int[] nums = {5, 3, 6, 8, 4, 2};
        for(int n :nums){
            bst.add(n);
        }
        //         5
        //        / \
        //       3   6
        //      / \   \
        //     2   4   8
        bst.preOrder();
    }
}

測試結果
從根節點開始,先遍歷左子樹,在遍歷右子樹

5
3
2
4
6
8

前序遍歷的簡單應用
基於前序遍歷遞歸的生成當前二叉樹對應的字符串

@Override
public String toString() {
    StringBuilder res = new StringBuilder();
    // 根節點 - 左子樹 - 右子樹
    generateBSTString(root, 0, res);
    return res.toString();
}

// 遞歸函數傳入三個參數,遍歷的根節點、遍歷的當前深度、字符串對象
// 生成以node爲根節點,深度爲depth的描述二叉樹的字符串
private void generateBSTString(Node node, int depth, StringBuilder res) {
    if (node == null) {
        res.append(generateDepthString(depth) + "null\n");
        return;
    }
    // 如果當前節點不爲空,直接訪問
    res.append(generateDepthString(depth) + node.e + "\n");
    // 遞歸的訪問節點的左右子樹
    generateBSTString(node.left, depth + 1, res);
    generateBSTString(node.right, depth + 1, res);


}

// 表達深度的字符串 用"--"來表達深度
private String generateDepthString(int depth) {
    StringBuilder res = new StringBuilder();
    for (int i = 0; i < depth; i++) {
        res.append("--");
    }
    return res.toString();
}

在Main中打印bst對象,System.out.println(bst);輸出結果如下:

5
--3
----2
------null
------null
----4
------null
------null
--6
----null
----8
------null
------null

通過結果進行簡單的分析:

  • 根節點是5,前面沒有“–”,它的深度是0,
  • 5的下面有兩個子節點,分別是3和6,他們前面有一組“–”,則深度爲1
  • 3下面有兩個子節點分別是2和4,深度爲2
  • 由於2和4都是葉子節點,所以它們下面都是兩個空節點null
  • 對於6,他只有右孩子,左孩子爲空
  • 同理8是葉子節點,下面兩個爲空節點

2.中序遍歷

對於二分搜索樹,前序遍歷是最自然的一種遍歷方式,同時也是最常用的遍歷方式,在大多數情況下使用前序遍歷。

前序遍歷是先訪問節點,然後訪問左子樹,右子樹。相應的,順序可以改變,先訪問節點的左子樹,其次訪問節點,再訪問節點的右子樹。如此稱爲中序遍歷

function traverse(node):
    if (node == null)
        return;
   
    traverse(node.left);
    訪問該節點
    traverse(node.right);

中體現在訪問節點放在了左子樹和右子樹的中間。中序遍歷的實現有了前面實現前序遍歷的方法,也變得更加簡單,只需要調換遍歷左子樹與打印節點的位置即可。

// 中序遍歷
public void inOrder(){
    inOrder(root);
}

// 中序遍歷以node爲根的二分搜索樹,遞歸算法
private void inOrder(Node node) {
    // 首先遞歸終止條件 如果node爲空沒得遍歷
    if (node == null)
        return;

    // 先訪問左子樹
    inOrder(node.left);
    // 將node中存儲的元素打印輸出出來
    System.out.println(node.e);
    inOrder(node.right);
}

同樣在Main函數中調用inOder方法bst.inOrder();進行結果輸出

2
3
4
5
6
8

根據中序遍歷的結果明顯的發現就是在二分搜索樹中所有元素排序的結果。對於二分搜索樹,任何一個節點x,它的左子樹的所有節點都比x小,右子樹的所有節點都比x大,而中序遍歷恰好是先遍歷左子樹,即先把比節點小的元素都遍歷,在遍歷節點,最後遍歷比節點大的所有元素。得到的結果是一個由小及大的順序結果。所以二分搜索樹又被稱爲排序樹,這是它額外的效能。由於使用數組或者鏈表對元素進行排序的話,還需要額外的工作,不能保證一次遍歷就能得到排序的結果,而使用二分搜索樹只需要遵從定義,最終使用中序遍歷就能得到所有元素順序排列的結果。

3.後序遍歷

同理,有了前序遍歷和中序遍歷,可想而知,後序遍歷的結構就很明顯了。

function traverse(node):
    if (node == null)
        return;
   
    traverse(node.left);
    traverse(node.right);
    訪問該節點

先訪問節點的左子樹,在訪問右子樹,最後訪問節點。

// 後序遍歷
public void posOrder(){
    posOrder(root);
}

// 後序遍歷以node爲根的二分搜索樹,遞歸算法
private void posOrder(Node node){
    if(node==null)
        return;

    posOrder(node.left);
    posOrder(node.right);
    System.out.println(node.e);
}

在Main中調用bst.posOrder();進行結果輸出

2
4
3
8
6
5

觀察結果,發現後序遍歷和前序遍歷不像中序遍歷一樣擁有規律。
後序遍歷的應用場景,一個典型的應用就是二分搜索樹內存的釋放。需要先把孩子節點都是釋放完再釋放節點本身。對於Java語言來說,由於有自動的垃圾回收機制,不需要對內存管理進行手動的控制。如果寫C++語言等需要手動的控制內存,在二分搜索樹釋放這方面就需要使用後序遍歷。

深入理解前中後序遍歷

雖然這三種遍歷的方式在程序的實現上是非常容易的,如果有一顆樹結構,如何快速的寫出它的遍歷結果。

對於二分搜索樹都可以看作是如下圖所示的一種遞歸結構

          節點
          /  \
       左子樹 右子樹

對於每一個節點,都連接着左子樹和右子樹。在具體遍歷的時候,對每一個節點都有三次的都訪問機會:

  1. 在遍歷左子樹之前會訪問一次節點
  2. 遍歷完左子樹之後,會回到當前節點,接着遍歷右子樹
  3. 遍歷完右子樹之後,又回到了這個節點

所以對於每一個節點使用遞歸遍歷的時候會訪問它三次,由此,對二分搜索樹的前、中、後三種順序的遍歷,其實就是對節點在哪裏進行真正的訪問操作,也就是對應代碼中在哪裏打印輸出節點相應的值。

function traverse(node):
    if (node == null)
        return;
        
    第一次訪問該節點
    traverse(node.left);
    第二次訪問該節點
    traverse(node.right);
    第三次訪問該節點

現有如下圖一顆二叉樹。向二叉樹存入元素:15,6,17,3,9,16,23,7,12,20,25,8,11,10,14

              15
           /      \
          6       17
        /  \      /  \
       3    9    16   23
           / \        / \
          7   12     20  25
           \  / \
           8 11 14
             / 
            10

1.理論分析結果

前序遍歷也就是在第一次訪問節點的時候打印node.e,根據圖示分析:

  • 首先訪問節點15,打印15接下來訪問15的左子樹,先訪問節點6,打印6
  • 接着訪問6的左子樹,打印葉子節點3,接下來訪問6的右子樹9,打印9
  • 開始訪問9的左子樹,打印7,7沒有左子樹,訪問7的右子樹,打印葉子節點8;接着訪問9的右子樹,打印12
  • 訪問12的左子樹,打印11,訪問11的左子樹,打印葉子節點10,11沒有右子樹,訪問12的右子樹,打印葉子節點14至此,已訪問完根節點15的左子樹的所有節點
  • 開始訪問15的右子樹先訪問節點17,打印17
  • 訪問17的左子樹,打印葉子節點16
  • 訪問17的右子樹,打印23,訪問23的左子樹,打印葉子節點20,訪問23的右子樹,打印葉子節點25至此,已第一次訪問完根節點右子樹的所有節點

需要注意的是,前序遍歷是在第一次訪問使打印節點,並不代表此時的遞歸已經完全訪問結束。根據上述的步驟分析,可以得出當前二叉樹的前序遍歷結果爲:15,6,3,9,7,8,12,11,10,14,17,16,23,20,25

同樣的中序遍歷就是在第二次訪問節點的時候打印node.e,根據二叉搜索樹的圖示分析如下:

  • 第一次訪問根節點15,接下來訪問15的左子樹, 第一次訪問6,開始訪問6的左子樹
  • 第一次訪問3,接下來訪問3的左子樹,由於3沒有左子樹, 第二次訪問到3,這時打印3,由於3沒有右子樹,第三次訪問回3。接着遞歸繼續往回訪問,這是第二次訪問到6,打印6,接下來訪問6的右子樹。
  • 同理,第一次訪問9,接下來第一次訪問7,接着訪問7的左子樹,由於沒有左子樹,再一次訪問回到7,打印7,接下來訪問7的右子樹。
  • 第一次訪問8,開始訪問左子樹,沒有左子樹,第二次訪問8,打印8,訪問右子樹,沒有右子樹,第三次訪問8,往上返回第三次訪問到7,再往上第二次訪問回到到9,打印9
  • 開始訪問9的右子樹,第一次訪問12,接着訪問11,訪問11的左子樹,因爲沒有左子樹,第二次訪問11,打印11,向上返回,第二次訪問12,打印12。12的右子樹,同理第二次訪問14時進行打印14
  • 特別注意,打印14以後接下來訪問14的右子樹,14沒有右子樹,這時第三次訪問14,接着向上返回,第三次訪問12,9,6。再向上返回,第二次訪問15,這時打印15至此,已訪問完根節點15的左子樹的所有節點
  • 接下來訪問15的右子樹,根據上述同樣的道理可以分別打印出,16,17,20,23,25。至此,已第二次訪問完所有右子樹的節點,繼續遞歸往上訪問直到第三次訪問15纔算結束。

最終遍歷結束得到的打印結果爲:3,6,7,8,9,10,11,12,14,15,16,17,20,23,25,也就是由小及大的排序結果。

接下來的後序遍歷就是在第三次訪問節點的時候打印node.e,根據二分搜索樹的圖示分析如下(有了上面前序和中序的分析,這裏我進行簡寫):

  • 第一次訪問15,6,3,訪問3的左子樹,因爲沒有左子樹,第二次訪問3,開始訪問3的右子樹,由於沒有右子樹,第三次訪問3,這時打印3
  • 第二次訪問6,第一次訪問9,7,第二次訪問7,第一次訪問8,同3一樣,在沒有右子樹返回第三次訪問8進行打印8,往上第三次訪問7,打印7,第二次訪問9
  • 第一次訪問12,11,10,同3一樣,第三次訪問10打印10,向上返回第二次訪問11,無右子樹第三次訪問11,打印11.
  • 第二次訪問12,第一次訪問14,同3一樣,因爲無右子樹,第三次訪問打印14後,向上返回,第三次訪問12,9,6,進行打印,繼續向上返回第二次訪問15,至此,完成了根節點15所有左子樹節點的訪問
  • 接下來第一次訪問17,16,同3一樣,第三次訪問16,打印16後第二次訪問17
  • 接着第一次訪問23,20,同3一樣,打印20後,第二次訪問23,第一次訪問25
  • 還是同3一樣,第三次訪問25,打印25之後,開始向上返回,依次打印第三次訪問到的23,17,15至此,完成了所有節點的遍歷過程

最終根據上述分析,可以得到後序遍歷的結果:3,8,7,10,11,14,12,9,6,16,20,25,23,17,15

2.代碼測試對比

以上是所有分析的結果,根據理論可以快速得出一顆二分搜索樹的前、中、後序遍歷的結果。接下來用代碼進行測試驗證理論結果。

在Main中將15,6,17,3,9,16,23,7,12,20,25,8,11,10,14定義爲一個num2的數組分別存進二叉樹bst2中,調用preOrder(),inOrder(),posOrder()進行打印輸出。前面實現的代碼是換行逐一換行輸出,爲了觀察方便,相應的BST類的前、中、後序遍歷遞歸算法的函數中,將打印函數改寫成單行輸出 System.out.print(node.e + " ");

BST<Integer> bst2 = new BST<>();
int[] nums2 = {15,6,17,3,9,16,23,7,12,20,25,8,11,10,14};
for (int n : nums2) {
    bst2.add(n);
}
bst2.preOrder();
System.out.println();
bst2.inOrder();
System.out.println();
bst2.posOrder();

測試結果如下:

15 6 3 9 7 8 12 11 10 14 17 16 23 20 25 
3 6 7 8 9 10 11 12 14 15 16 17 20 23 25 
3 8 7 10 11 14 12 9 6 16 20 25 23 17 15 

通過觀察發現測試輸出結果與理論結果完全一致。

前序遍歷的非遞歸實現

1.藉助棧實現原理

對於中序、後序遍歷也可以使用非遞歸的方法實現,一方面代碼複雜,另一方面實際的應用並不多,所以主要實現前序遍歷的非遞歸寫法。

         5                      |   | 棧頂
        / \                     |   |
       3   6                    |   |
      / \   \                   |   |
     2   4   8                  |___| 棧底

以根節點爲5的二叉樹爲例子,右側是一個棧。
在遍歷的過程中首先訪問的是根節點5,所以一上來就先將根節點5壓入棧,壓入棧的意思就是接下來要訪問5這個根節點了。相當於使用自己的棧來模擬系統棧,指導下一次具體要訪問誰,初始的時候就把根節點壓入棧。
程序開始運行,首先將棧底元素5拿出來,這是記錄的下面要訪問的元素,對它相應的進行出棧操作,5這個節點就訪問結束了。

   |   |                 |   |  5
   |   |                 |   |
   |   |                 |   |
   |   |      ---->      |   |
   |   |                 |   |
   |_5_|                 |___|

訪問了5,下面接着訪問5的子樹,在這裏將5的兩個孩子以先右孩子,後左孩子的順序壓入棧,遵循棧後入先出的原則。現在棧頂的元素是3,就是下一次要訪問的節點,棧頂元素出棧,對3進行相應的操作,3節點就訪問結束了。

   |   |  5              |   |  5              |   |  5     
   |   |                 |   |                 |   |  3
   |   |                 |   |                 |   |  
   |   |      ---->      |   |      ---->      |   |
   |   |                 | 3 |                 |   |
   |___|                 |_6_|                 |_6_|

之後要對3節點的左孩子右孩子進行操作,先壓入右孩子4,在壓入左孩子2,接下來棧頂元素2就是下面要訪問的節點,將2出棧,之後要壓入2的左孩子與右孩子,但因爲2是葉子節點,就不需要任何壓入操作,繼續來看棧頂元素,此時的棧頂元素爲4,將4出棧。

   |   |  5              |   |  5              |   |  5     
   |   |  3              |   |  3              |   |  3
   |   |                 |   |  2              |   |  2
   | 2 |      ---->      |   |      ---->      |   |  4
   | 4 |                 | 4 |                 |   |  
   |_6_|                 |_6_|                 |_6_|

此時因爲4是葉子節點,什麼都不需要壓入繼續看棧頂元素,這時的棧頂元素是根節點的右孩子6,6出棧以後,先壓入6的右孩子8,因爲6沒有左孩子,所以不做操作。接着訪問棧頂元素,8出棧。

   |   |  5              |   |  5              |   |  5     
   |   |  3              |   |  3              |   |  3
   |   |  2              |   |  2              |   |  2
   |   |  4   ---->      |   |  4   ---->      |   |  4
   |   |  6              |   |  6              |   |  6
   |___|                 |_8_|                 |___|  8

因爲8是葉子節點,所以什麼都不操作,但這時還要繼續拿出棧頂元素,此時的棧已經爲空,說明棧已經沒有記錄下面要訪問的任何節點。至此,整棵二分搜索樹遍歷結束。

2.代碼實現

// 藉助棧實現非遞歸
public void preOrderNR() {
    Stack<Node> stack = new Stack<>();
    // 初始的時候,將根節點壓入棧
    stack.push(root);
    // 在循環中,每一次stack不爲空時候,需要相應的訪問節點
    while (!stack.isEmpty()) {
        // 聲明當前訪問的節點cur 把棧頂元素拿出來放進cur裏
        Node cur = stack.pop();
        // 對於當前要訪問的節點進行打印
        System.out.print(cur.e+" ");

        // 訪問cur之後要依次訪問cur的左子樹和右子樹
        // 由於整個棧是後入先出,所以壓入先壓入右子樹
        // 壓入之前需要判斷是否爲空,爲空不需呀壓入棧中
        if (cur.right != null)
            stack.push(cur.right);
        if (cur.left != null)
            stack.push(cur.left);
    }
}

3.遞歸與非遞歸結果比較分析

在Main中調用遞歸方法和非遞歸方法進行比較

BST<Integer> bst = new BST<>();
int[] nums = {5, 3, 6, 8, 4, 2};
for (int n : nums) {
    bst.add(n);
}

//         5
//        / \
//       3   6
//      / \   \
//     2   4   8

bst.preOrder();
System.out.println();
bst.preOrderNR();

打印結果如下:

5 3 2 4 6 8 
5 3 2 4 6 8

通過對比,兩種遍歷的結果是一致的。
二分搜索樹的遍歷,非遞歸的實現要比遞歸實現複雜的多,因爲它必須使用一個輔助的數據結構才能完成這一過程,而且在算法語意的解讀上,也遠比遞歸實現的語意難很多。

層序遍歷

對於前序遍歷,不管是遞歸算法還是非遞歸算法,在這顆二分搜索樹在遍歷的過程中是一紮到底,最終的遍歷結果都會先到達整棵樹最深的地方,直到不能更深,纔開始返回。這樣的方式叫做深度優先遍歷。與深度優先遍歷相對應的即是廣度優先遍歷,它遍歷的結果對於整棵樹其實是按照一層一層的順序遍歷,即層序遍歷的結果。

不小心手誤在手機點了發佈,晚上在寫。。。

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