天天看點

資料結構之樹-第二篇

資料結構之樹-第一篇

1、此時,将元素30從隊首拿出來,進行通路,之後将30的左孩子29、右孩子42入隊,那麼此時隊首元素就是13了。

資料結構之樹-第二篇
此時,将将元素13從隊首拿出來,進行通路,之後将13的左孩子、右孩子入隊,由于13是葉子節點沒有左右孩子,是以沒有元素入隊了。
資料結構之樹-第二篇
此時,看隊首元素是22,将元素22從隊首拿出來,進行通路,之後将22的左孩子、右孩子入隊,由于22是葉子節點沒有左右孩子,是以沒有元素入隊了。
資料結構之樹-第二篇
此時,看隊首元素是29,将元素29從隊首拿出來,進行通路,之後将29的左孩子、右孩子入隊,由于29是葉子節點沒有左右孩子,是以沒有元素入隊了。
資料結構之樹-第二篇
此時,看隊首元素是42,将元素42從隊首拿出來,進行通路,之後将42的左孩子、右孩子入隊,由于42是葉子節點沒有左右孩子,是以沒有元素入隊了。
資料結構之樹-第二篇
最後,檢視隊列中隊首是誰,現在整個隊列都為空了,沒有隊首元素,說明沒有任何元素排隊了,那麼,廣度優先周遊也就是我們的層序周遊就結束了。
資料結構之樹-第二篇
2、删除二分搜尋樹的最小值,對于下面這個二分搜尋樹,最小值就是一個葉子節點13,在這種情況下,非常的簡單,直接将這個葉子節點删除即可,對于整棵二分搜尋樹,不需要改變任何結構。
資料結構之樹-第二篇
同理,此時這個二分搜尋樹的最小值也是一個葉子節點15,在這種情況下,非常的簡單,直接将這個葉子節點删除掉即可,對于整棵二分搜尋樹,不需要改變任何結構。
資料結構之樹-第二篇
但是,複雜的是,在這種情況下,此時在這種二分搜尋樹中最小值是22,但是此時22這個節點不是葉子節點,22這個節點向左走再也走不動了,但是22這個節點有右子樹,在這種情況下,隻需要将22這個節點删除,将它的整個右子樹都變成是41的左子樹,就完成了這個删除操作。
資料結構之樹-第二篇
同理,删除二分搜尋樹的最大值,如果是葉子節點直接删除掉即可,對于整棵二分搜尋樹,不需要改變任何結構,同理,但是如果不是葉子節點,它向右走再也走不動了,但是58這個節點有左子樹,在這種情況下,隻需要删除58這個節點,然後将它的整個左子樹都變成是41的右子樹,就完成了這個删除操作。 
資料結構之樹-第二篇
3、如果删除的節點隻有左孩子的節點,在邏輯上和删除最大節點的邏輯是一緻的,注意,隻有左孩子的節點不一定是最大值所在的節點,删除滿足這種特性的節點,删除方式就是将該節點删除之後,将這個節點的左孩子所在的這棵二叉樹,也就是左子樹取代被删除的節點的位置,連到原來這個節點的父親節點的右孩子節點上。比如58這個節點,将58節點删除之後,将58的左子樹挂到58這個位置。
資料結構之樹-第二篇
如果删除的隻有右孩子的節點,比如58這個節點,隻有右孩子,在邏輯上和删除最小值的邏輯是一緻的,注意,隻有右孩子的節點不一定是最小值所在的節點,删除滿足這種特性的節點,删除方式就是将該節點删除之後,将這個節點的右孩子所在的這棵二叉樹,連接配接到被删除節點的這個位置上。
資料結構之樹-第二篇
二分搜尋樹中删除節點真正難的地方是删除左右還有孩子的節點,比如删除58這個節點如何删除。删除左右都有孩子的節點d,将58這個節點暫時起名為d,這個d節點既有左孩子又有右孩子,在這種情況下,如何删除呢,此時需要将d節點的左右子樹融合起來,那麼如何進行融合呢,對于這個d節點,既有左子樹,又有右子樹,根據二分搜尋樹的定義,d節點的左子樹的所有節點元素都小于d節點元素,d節點的右子樹的所有節點元素都大于d節點元素,此時,将d節點删除掉了,需要找到一個節點替代58這個節點的位置,如何進行找呢,此時,找到的58這個節點的後繼,也就是,在58這個節點的左右子樹中離58最近餓,比58還要大的這個節點,其實就是59,根據58的右子樹中所有對應的那個最小值的節點,這也很好了解,58的右子樹中所有的元素最小值的那個節點,因為根據二分搜尋樹,58右子樹中所有元素都比58大,其中最小的那個元素就是比58大,離58最近的那個元素,這裡面的最近,不是位置近,是大小的意思。
資料結構之樹-第二篇

1 package com.tree;
  2 
  3 
  4 import java.util.*;
  5 
  6 /**
  7  * 1、二分搜尋樹,二分搜尋樹存儲的内容支援泛型,但是不是支援所有的類型,應該對類型有一個限制,
  8  * 這個限制是這個類型必須擁有可比較性,對泛型E進行限制,這個限制就是繼承Comparable<E>接口,
  9  * 這個類型E必須具有可比較性。
 10  * <p>
 11  * <p>
 12  * 2、對于前序、中序、後序周遊來說,不管是遞歸寫法還是非遞歸寫法,對于我們這棵二分搜尋樹來說,
 13  * 我們都是在周遊的過程中一紮到底,這樣的一種周遊方式,其實還有另外一個名字,叫做,深度優先周遊。
 14  * 對于這棵樹來說,都會先來到這棵樹最深的地方,直到不能再深了,才開始傳回回去,
 15  * 這種周遊叫做深度優先周遊。和深度優先周遊相對的是另外一種周遊方式,叫做廣度優先周遊,
 16  * 廣度優先周遊周遊出來的結果,這個順序其實是整個二分搜尋樹的層序周遊的順序。
 17  */
 18 public class BinarySearchTree<E extends Comparable<E>> {
 19 
 20     // 二分搜尋樹的節點類,私有内部類。
 21     private class Node {
 22         private E e;// 存儲元素e;
 23         private Node left;// 指向左子樹,指向左孩子。
 24         private Node right;// 指向右子樹,指向右孩子。
 25 
 26         /**
 27          * 含參構造函數
 28          *
 29          * @param e
 30          */
 31         public Node(E e) {
 32             this.e = e;// 使用者存儲的元素e。
 33             left = null;// 左孩子初始化為空。
 34             right = null;// 右孩子初始化為空。
 35         }
 36     }
 37 
 38     private Node root;// 根節點
 39     private int size;// 二分搜尋樹存儲了多少個元素
 40 
 41     /**
 42      * 無參構造函數,和預設構造函數做的事情一樣的。
 43      */
 44     public BinarySearchTree() {
 45         // 初始化的時候,二分搜尋樹一個元素都沒有存儲
 46         root = null;
 47         size = 0;// 大小初始化為0
 48     }
 49 
 50     /**
 51      * 傳回二分搜尋樹的含有多少個元素
 52      *
 53      * @return
 54      */
 55     public int size() {
 56         // 二分搜尋樹的大小
 57         return size;
 58     }
 59 
 60     /**
 61      * 判斷二分搜尋樹是否為空。
 62      *
 63      * @return
 64      */
 65     public boolean isEmpty() {
 66         return size == 0;
 67     }
 68 
 69 //    /**
 70 //     * 向二分搜尋樹中添加新的元素e.
 71 //     * <p>
 72 //     * 在公開的二分搜尋樹的調用的add方法。在将遞歸調用中,  是将新的元素e作為node子節點插入進去的。
 73 //     * 這個時候,形成了一個邏輯上的不統一,遞歸函數中,e元素和node.e元素進行了兩輪比較,第一輪比較,
 74 //     * 在比較完他們的大小的同時,還要看一下node的左右兩邊是不是為空,如果為空直接插入,
 75 //     * 對于第二輪比較,我們知道我們不能直接作為Node的孩子插入這個元素e,隻要再次遞歸的調用add的函數。
 76 //     *
 77 //     * @param e
 78 //     */
 79 //    public void add(E e) {
 80 //        // 判斷,如果根節點是空的時候,直接指向新建立的節點即可
 81 //        if (root == null) {
 82 //            // 對根節點進行特殊的處理,如果根為空的時候,就直接建立一個新的節點。
 83 //            root = new Node(e);
 84 //            // 維護size
 85 //            size++;
 86 //        } else {
 87 //            // 否則,根節點不是空的時候。從根節點開始添加一個元素。
 88 //            // 注意,由于需要遞歸的調用,對于每一個節點的左子樹也是一個更小的二分搜尋樹的根節點,
 89 //            // 對于每一個節點的右子樹也是一個更小的二分搜尋樹的根節點。
 90 //            // 是以在遞歸的過程中,建立一個新的遞歸的函數。
 91 //
 92 //            // 如果根不為空,嘗試從根節點開始插入元素e。
 93 //            add(root, e);
 94 //        }
 95 //
 96 //    }
 97 //
 98 //    /**
 99 //     * 私有的遞歸添加方法
100 //     * <p>
101 //     * 整體上是,向以node為根的二分搜尋樹中插入元素E,遞歸算法。
102 //     * <p>
103 //     * 之是以設定Node,就是因為在插入的過程中,我們要不斷地轉換到新的更小的二分搜尋樹中,
104 //     * 去找到這個新的元素e真正應該插入的位置,那麼相應的,我們在遞歸調用的過程中,
105 //     * 相當于二分搜尋樹的根在逐漸變化的,是以,我們需要靠這個參數來展現這個變化。
106 //     *
107 //     * @param node Node結構本身都是對使用者屏蔽的,使用者不需要了解二分搜尋樹中節點的結構。
108 //     * @param e
109 //     */
110 //    private void add(Node node, E e) {
111 //        // 精髓,将新的元素e插入給node的左孩子還是node的右孩子。
112 //        // 對目前的node來說,如果想插入到左邊,但是左孩子不為空的話,再遞歸的将新的元素e插入到add(node.left, e);node的左子樹。
113 //        // 如果想插入到右邊,但是右孩子不為空的話,再遞歸的将新的元素e插入到add(node.right, e);node的右子樹。
114 //
115 //
116 //        // 第一部分,遞歸終止的條件。
117 //        // 先檢查一下元素e,是不是等于node的元素e。
118 //        if (e.equals(node.e)) {
119 //            // 如果相等,說明要插入的元素已經存在于二分搜尋樹中了。
120 //            return;
121 //            // 主要的插入代碼,就是下面的判斷,然後将元素e進行插入到左子樹還是右子樹的操作。
122 //        } else if (e.compareTo(node.e) < 0 && node.left == null) {
123 //            // 如果待插入元素e小于node的元素e,則把元素e插入到node的左子樹上面。
124 //            // 由于元素E滿足Comparable<E>類型的,是以應該是使用compareTo方法比較,不是基礎資料類型,不能使用大于号小于号比較。
125 //
126 //            // 如果此時node的左子樹又等于空,直接建立一個Node,将元素存儲到Node裡面即可。
127 //            // 此時元素e就變成了node的左孩子了。
128 //            node.left = new Node(e);
129 //            // 維護size的大小。
130 //            size++;
131 //            // return表示此次插入操作也完成了。
132 //            return;
133 //        } else if (e.compareTo(node.e) > 0 && node.right == null) {
134 //            // 如果待插入元素e大于node節點的元素e,則把元素e插入到node的右子樹上面。
135 //            // 此時,如果node的右孩子又等于空,直接建立一個Node,将元素存儲到Node即可。
136 //            // 此時,元素就變成了node的右孩子了。
137 //            node.right = new Node(e);
138 //            // 維護size的大小。
139 //            size++;
140 //            // return表示此次插入操作也完成了。
141 //            return;
142 //        }
143 //
144 //
145 //        // 遞歸的第二部分。遞歸調用的邏輯。
146 //        if (e.compareTo(node.e) < 0) {
147 //            // 如果待插入元素e小于node的元素e,遞歸調用add方法,參數一是向左子樹添加左孩子。
148 //            // 向左子樹添加元素e。
149 //            add(node.left, e);
150 //        } else if (e.compareTo(node.e) > 0) {
151 //            // 如果待插入元素e大于node的元素e,遞歸調用add方法,參數一是向右子樹添加右孩子。
152 //            // 向右子樹添加元素e。
153 //            add(node.right, e);
154 //        }
155 //    }
156 
157 
158     /**
159      * 向二分搜尋樹中添加新的元素e。
160      *
161      * @param e
162      */
163     public void add(E e) {
164         // 此時,不需要對root為空進行特殊判斷。
165         // 向root中插入元素e。如果root為空的話,直接傳回一個新的節點,将元素存儲到該新的節點裡面。
166         root = add(root, e);
167     }
168 
169     /**
170      * 深入了解遞歸終止條件。
171      * <p>
172      * 傳回插入新節點後二叉搜尋樹的根。
173      * <p>
174      * 向以node為根的二分搜尋樹中插入元素e,遞歸算法。
175      * <p>
176      * <p>
177      * 對于新的add遞歸函數,将向以node為根的二分搜尋樹中插入元素e,并且傳回在做完這個操作以後這個二分搜尋樹的根節點,
178      * 如果我們傳入的這個node為空的話,插入新的元素以後,就産生新的根節點,就是建立了一個Node e,
179      * 否則的話,我們來比較我們待插入的元素e和目前的node e的大小關系,如果小于零的話,向左子樹添加一個左孩子,
180      * 如果大于零的話,向右子樹添加一個右孩子。如果等于零的話,什麼都不操作。
181      * 不管向左子樹添加元素還是向右子樹添加元素,插入完成之後,都将目前node的左孩子和右孩子進行重新指派,
182      * 在重新指派以後,這個node依賴是這個以node為根的二分搜尋樹。相應的根節點将他傳回回去。
183      * <p>
184      * 連結清單的遞歸插入和二分搜尋樹的添加具有高度相似性的,在二分搜尋樹中需要判斷一下,是要插入到左子樹還是右子樹。
185      *
186      * @param node
187      * @param e
188      * @return
189      */
190     private Node add(Node node, E e) {
191         // 空本身也是一種二分搜尋樹,空本身也是一顆二叉樹,換句話說,如果走遞歸函數,走到node為空的時候
192         // 一定要建立一個Node節點。上面的add方法實作,其實還沒有遞歸到底部。
193 
194         // 精髓,如果待插入元素e小于node的元素e,此時不管node.left是不是為空,就再遞歸一層,
195         // 如果遞歸的這一層,node等于空的話,也就是,現在要新插入一個元素,插入到哪裡,插入到空這顆
196         // 二叉樹上,那麼,很顯然,這個位置本身就應該是這個節點。
197 
198 
199         // 如果按照上面的思想,如果此時node等于空的話,此時,肯定要插入一個節點。
200         if (node == null) {
201             // 維護size的大小。
202             size++;
203             // 如果此時,直接建立一個Node的話,沒有和二叉樹挂接起來。
204             // 如何讓此節點挂接到二叉樹上呢,直接将建立的節點return傳回回去即可,傳回給調用的上層。
205             return new Node(e);
206         }
207 
208         // 遞歸的第二部分。遞歸調用的邏輯。
209         if (e.compareTo(node.e) < 0) {
210             // 如果待插入元素e小于node的元素e,遞歸調用add方法,參數一是向左子樹添加左孩子。
211             // 向左子樹添加元素e。
212             // 向左子樹添加元素e的時候,為了讓整顆二叉樹發生改變,在node的左子樹中插入元素e,
213             // 插入的結果,有可能是變化的,是以就要讓node的左子樹連接配接住這個變化。
214 
215             // 注意,如果此時,node.left是空的話,這次add操作相應的就會傳回一個新的Node節點,
216             // 對于這個新的節點,我們的node.left被指派這個新的節點,相當于我們就改變了整棵二叉樹。
217             node.left = add(node.left, e);
218         } else if (e.compareTo(node.e) > 0) {
219             // 如果待插入元素e大于node的元素e,遞歸調用add方法,參數一是向右子樹添加右孩子。
220             // 向右子樹添加元素e。
221             node.right = add(node.right, e);
222         }
223 
224         // 注意,如果如果待插入元素e等于node的元素e,不進行任何操作。
225 
226         // 最後将以node為根的二叉樹傳回回去。
227         // 不管在第二部分産生了什麼變化,如果我們的node不為空的話,我們進去了第二部分,
228         // 向以node為根的二分搜尋樹中插入元素e之後,最終,插入了這個節點以後,
229         // 二分搜尋樹的根呢,還是這個node。
230         return node;
231     }
232 
233 
234     /**
235      * 檢視二分搜尋樹中是否包含元素e
236      *
237      * @param e
238      * @return
239      */
240     public boolean contains(E e) {
241         // 遞歸實作,遞歸的過程中,就要從這個二分搜尋樹的根節點開始,逐漸的轉移在新的二分搜尋樹的子樹中,
242         // 縮小問題規模,縮小查詢的樹的規模,直到發現找到這個元素e或者找不到這個元素e。
243 
244         // 整體是看以我們這整棵二分搜尋樹為根的二分搜尋樹中是否包含元素e。
245         return contains(root, e);
246     }
247 
248     /**
249      * 看以node為根的二分搜尋樹中是否包含元素e,遞歸算法
250      * <p>
251      * 二分搜尋樹查詢元素,對于查詢元素來說,我們隻要不停的看節點node裡面儲存的元素就好了。
252      * 不牽扯到二分搜尋樹,添加一個元素之後,又如何把它挂接到整個二分搜尋樹中。
253      *
254      * @param node 節點Node
255      * @param e    帶查詢的元素e
256      * @return
257      */
258     private boolean contains(Node node, E e) {
259         // 遞歸的第一部分
260         if (node == null) {
261             // 如果node節點為空,是不包含元素e的,此時直接傳回false
262             return false;
263         }
264 
265         // 遞歸的第二部分,開始邏輯判斷
266         if (e.compareTo(node.e) == 0) {
267             // 如果待查詢元素e和node的元素e相等
268             return true;
269         } else if (e.compareTo(node.e) < 0) {
270             // 如果待查詢元素e小于node的元素e,如果此二分搜尋樹中還包含元素e的話,那麼隻可能在node的左子樹
271             return contains(node.left, e);
272         } else {
273             // 如果待查詢元素e大于node的元素e,如果此二分搜尋樹中還包含元素e的話,那麼隻可能在node的右子樹
274             return contains(node.right, e);
275         }
276     }
277 
278 
279     /**
280      * 二分搜尋樹的前序周遊。二分搜尋樹的周遊操作,就是把所有節點都通路一遍。
281      * 對于二分搜尋樹的周遊操作來說,兩棵子樹都要顧及,差別于添加或者查詢是否包含,
282      * 隻走左子樹或者右子樹。
283      * <p>
284      * <p>
285      * 二分搜尋樹的前序周遊。為什麼這種周遊稱為前序周遊呢,是因為先通路這個節點,
286      * 再通路左右子樹,也就是說,通路這個節點放在了通路左右子樹的前面,是以叫做前序周遊。
287      * 基于此,也就有了二叉樹的中序周遊和後序周遊。
288      */
289     public void preOrder() {
290         // 需要指定遞歸的調用,指定是那一棵二叉樹進行的前序周遊。
291 
292         // 使用者的初始的調用,隻需要對root根節點調用遞歸的preOrder就行了。
293         preOrder(root);
294     }
295 
296     /**
297      * 前序周遊以node為根的二分搜尋樹,遞歸算法。
298      * <p>
299      * 前序周遊思路。
300      * 首先先周遊這個節點,再周遊這個節點的左子樹,之後周遊這個節點的右子樹。
301      * <p>
302      * 前序周遊是最自然的周遊方式,同時也是最常用的周遊方式。如果沒有特殊情況下,在大多數情況下都是使用前序周遊。
303      *
304      * @param node
305      */
306     private void preOrder(Node node) {
307         // 遞歸的第一部分
308         if (node == null) {
309             // 如果node為空的話,直接傳回
310             return;
311         }
312 
313         // 遞歸的第二部分,開始周遊
314         System.out.println(node.e);
315         // 遞歸的調用根節點root的左子樹。
316         preOrder(node.left);   // 此處是循環周遊左孩子,直到左孩子為空,直接傳回。
317         // 遞歸的調用根節點root的右子樹。
318         preOrder(node.right);  // 執行完上面循環周遊左孩子,執行下面的循環周遊右孩子,直到右孩子是空,直接傳回。
319 //        // 此時,完成了二分搜尋樹的前序周遊。
320 
321 
322         // 更新版遞歸過程
323 //        if (node != null) {
324 //            // 遞歸的第二部分,開始周遊
325 //            System.out.println(node.e);
326 //            // 遞歸的調用根節點root的左子樹。
327 //            preOrder(node.left);
328 //            // 遞歸的調用根節點root的右子樹。
329 //            preOrder(node.right);
330 //            // 此時,完成了二分搜尋樹的前序周遊。
331 //        }
332     }
333 
334 
335     /**
336      * 二分搜尋樹的的前序周遊的非遞歸方式實作。
337      */
338     public void preOrderNonRecursive() {
339         // 棧的作用是記錄下面要到底要通路那些節點,記錄下一次依次要通路那些節點。
340         // 泛型是Node類型的,存儲的二叉樹節點類型的對象。
341         Stack<Node> stack = new Stack<Node>();
342         // 初始的時候将root根節點push進去
343         stack.push(root);
344 
345         // 進行循環操作,隻要stack.isEmpty()為false,說明了棧裡面有元素。
346         // !stack.isEmpty()說明了記錄了下面要通路那個節點。
347         while (!stack.isEmpty()) {
348             // 隻要棧不為空,說明記錄了下面要通路那個節點。
349             // 就要進行通路這個節點。
350 
351             // 目前通路的節點。
352             Node current = stack.pop();//将目前的棧頂元素拿出來。
353             // 此時current就是目前要通路的節點。目前要通路的節點直接列印即可。
354             System.out.println(current.e);
355 
356 
357             // 通路了目前節點之後,就要依次通路目前節點的左子樹,右子樹。
358             // 由于棧是後入先出的,是以先将目前節點的右子樹push進去。
359             // 此時應該判斷目前節點的右子樹是否為空,如果為空,就不要壓入棧了。
360             if (current.right != null) {
361                 stack.push(current.right);// 右子樹壓入棧
362             }
363             // 此時應該判斷目前節點的左子樹是否為空,如果為空,就不要壓入棧了。
364             if (current.left != null) {
365                 stack.push(current.left);//左子樹壓入棧
366             }
367         }
368 
369     }
370 
371 
372     /**
373      * 二分搜尋樹的中序周遊。
374      * <p>
375      * 二分搜尋樹的的中序周遊,先通路這個節點的左子樹,再通路這個節點,再通路這個節點的右子樹。
376      * 中序周遊的中就展現在通路該節點放在周遊這個節點的左子樹和右子樹的中間。
377      */
378     public void inOrder() {
379         inOrder(root);
380     }
381 
382     /**
383      * 中序周遊以node為根的二分搜尋樹,遞歸算法。
384      * <p>
385      * <p>
386      * 二分搜尋樹的中序周遊結果是順序的。有時候,因為這個功能,二分搜尋樹也叫做排序樹。
387      *
388      * @param node
389      */
390     private void inOrder(Node node) {
391         // 遞歸的第一部分,遞歸終止的條件
392         if (node == null) {
393             // 如果node為空的話,直接傳回
394             return;
395         }
396 
397         // 遞歸的第二部分,開始周遊
398         // 遞歸的調用根節點root的左子樹。
399         inOrder(node.left);   // 此處是循環周遊左孩子,直到左孩子為空,直接傳回。
400         // 此程式就是簡單的列印node節點的元素。就是通路這個節點元素的過程。
401         System.out.println(node.e);
402         // 遞歸的調用根節點root的右子樹。
403         inOrder(node.right);  // 執行完上面循環周遊左孩子,執行下面的循環周遊右孩子,直到右孩子是空,直接傳回。
404         // 此時,完成了二分搜尋樹的中序周遊。
405     }
406 
407 
408     /**
409      * 後續周遊,二分搜尋樹的後序周遊。
410      */
411     public void postOrder() {
412         postOrder(root);
413     }
414 
415     /**
416      * 二分搜尋樹的的後序周遊。
417      * <p>
418      * 二分搜尋樹的的後序周遊,先通路這個節點的左子樹,再通路這個節點的右子樹,最後再通路這個節點。
419      * <p>
420      * 後續周遊必須先處理完左子樹,再處理完右子樹,最後處理這個節點。
421      * <p>
422      * 後續周遊的一個應用,為二分搜尋樹釋放記憶體。先釋放左右孩子的記憶體,最後釋放這個節點的記憶體。
423      *
424      * @param node
425      */
426     private void postOrder(Node node) {
427         // 遞歸的第一部分,遞歸終止的條件
428         if (node == null) {
429             // 如果node為空的話,直接傳回
430             return;
431         }
432 
433         // 遞歸的第二部分,開始周遊
434         // 遞歸的調用根節點root的左子樹。
435         postOrder(node.left);   // 此處是循環周遊左孩子,直到左孩子為空,直接傳回。
436         // 遞歸的調用根節點root的右子樹。
437         postOrder(node.right);  // 執行完上面循環周遊左孩子,執行下面的循環周遊右孩子,直到右孩子是空,直接傳回。
438         // 此程式就是簡單的列印node節點的元素。就是通路這個節點元素的過程。
439         System.out.println(node.e);
440         // 此時,完成了二分搜尋樹的後序周遊。
441     }
442 
443 
444     /**
445      * 二分搜尋樹的層序周遊
446      * <p>
447      * <p>
448      * 相對于深度優先周遊,廣度優先周遊最大的有點,可以更快的找到你想要查詢的那個元素,
449      * 這樣的差別,主要用于搜尋政策上,而不是用于周遊這種操作上,
450      * 因為周遊需要将所有的元素都通路一遍,在這種情況下,深度優先周遊,
451      * 廣度優先周遊是沒有差別的。但是如果想要在一棵樹中找到某個問題的解的話,
452      * 對于深度優先周遊來說,将從根節點一下子通路到樹的最深的地方,
453      * 但是問題的解很可能在樹的上面,是以深度優先周遊先估計左子樹,
454      * 需要很長的時間才能通路到最上面,在這種情況下,廣度優先周遊就很有意義了,
455      * 這種問題的模型,最常用于算法設計中的最短路徑的。
456      */
457     public void levelOrder() {
458         // 由于Queue是接口,這裡使用連結清單的方式實作該Queue。
459         Queue<Node> queue = new LinkedList<Node>();
460         // 将根節點添加到隊列中
461         queue.add(root);
462         // 循環周遊,當隊列不為空的時候,queue.isEmpty()結果為false表示隊列不為空,
463         // 當!queue.isEmpty()表示隊列不為空的時候,繼續循環周遊。
464         while (!queue.isEmpty()) {
465             // 聲明一個目前的節點current,也就是我們隊列中的元素出隊之後的那個元素,
466             // 就是我們目前要通路的這個元素。
467             Node current = queue.remove();
468             // 對于目前要通路的這個元素,可以進行列印輸出
469             System.out.println(current.e);
470 
471             // 之後,根據目前通路的元素來看目前的這個節點左右孩子,如果有左孩子或者右孩子進行入隊。
472             if (current.left != null) {
473                 queue.add(current.left);
474             }
475             // 如果目前節點有右孩子,就将目前節點的右孩子進行入隊操作。
476             if (current.right != null) {
477                 queue.add(current.right);
478             }
479         }
480 
481     }
482 
483 
484     /**
485      * 尋找二分搜尋樹的最小元素。
486      * <p>
487      * <p>
488      * 此時,使用遞歸的算法比寫非遞歸的算法,要麻煩一點,因為這裡相當于我們不停的隻想左走,
489      * 根本不考慮每一個節點的它的右子樹,或者是右孩子,那麼此時呢,我們跟操作一個連結清單是沒有差別的,
490      * 相當于我們在操作對于每一個節點的next就是node.left這樣的一個連結清單,找它的尾節點而已。
491      *
492      * @return
493      */
494     public E minimum() {
495         // 首先,判斷二分搜尋樹中元素個數為零,說明二分搜尋樹中沒有元素,就抛出異常。
496         if (size == 0) {
497             // 首先,判斷二分搜尋樹中元素個數為零,就抛出異常。
498             throw new IllegalArgumentException("BinarySearchTree is Empty.");
499         }
500 
501         // 調用遞歸的方式進行查詢最小值
502         Node minimum = minimum(root);
503         // 此時将傳回的最小節點的元素值e傳回即可。
504         return minimum.e;
505     }
506 
507     /**
508      * 傳回以node為根的二分搜尋樹的最小值所在的節點。
509      *
510      * @param node
511      */
512     private Node minimum(Node node) {
513         // 遞歸算法,第一個部分,遞歸終止的條件
514         // 如果向左走,走不動了,這個就是最左節點了,即最小值
515         if (node.left == null) {
516             return node;
517         } else {
518             // 遞歸算法,第二部分,即如果節點最左節點不為空
519             // 将這個節點的左孩子作為node傳遞進去,依次檢視最左節點的值。
520             return minimum(node.left);
521         }
522     }
523 
524 
525     /**
526      * 尋找二分搜尋樹的最大元素。
527      *
528      * @return
529      */
530     public E maximum() {
531         // 首先,判斷二分搜尋樹中元素個數為零,說明二分搜尋樹中沒有元素,就抛出異常。
532         if (size == 0) {
533             // 首先,判斷二分搜尋樹中元素個數為零,就抛出異常。
534             throw new IllegalArgumentException("BinarySearchTree is Empty.");
535         }
536 
537         // 調用遞歸的方式進行查詢最大值
538         Node maximum = maximum(root);
539         // 此時将傳回的最大節點的元素值e傳回即可。
540         return maximum.e;
541     }
542 
543     /**
544      * 傳回以node為根的二分搜尋樹的最大值所在的節點。
545      *
546      * @param node
547      */
548     private Node maximum(Node node) {
549         // 遞歸算法,第一個部分,遞歸終止的條件
550         // 如果向右走,走不動了,這個就是最右節點了,即最大值
551         if (node.right == null) {
552             return node;
553         } else {
554             // 遞歸算法,第二部分,即如果節點最右節點不為空
555             // 将這個節點的右孩子作為node傳遞進去,依次檢視最右節點的值。
556             return maximum(node.right);
557         }
558     }
559 
560 
561     /**
562      * 從二分搜尋樹中删除最小值所在節點,傳回最小值。
563      *
564      * @return
565      */
566     public E removeMin() {
567         // 将删除的元素删除掉
568         E ret = minimum();
569 
570         // 設計删除最小值的邏輯。最小值所在的節點從二分搜尋樹中删除掉。
571         // 從根節點開始,嘗試從root節點開始,删除最小值所在的節點。
572 
573         // 将删除最小值以後的節點傳回給root根節點。
574         // 換句話說,我們從root為根的二分搜尋樹删除掉了最小值,
575         // 之後又傳回了删除掉最小值之後的那棵二分搜尋樹對應的根節點。
576         // 這個根節點就是新的root。
577         root = removeMin(root);
578 
579         // 最後,将待删除元素删除掉就行了。
580         return ret;
581     }
582 
583     /**
584      * 删除掉以node為根的二分搜尋樹中的最小節點。
585      * 傳回删除節點後新的二分搜尋樹的根。
586      * <p>
587      * <p>
588      * 這個邏輯就是在這種情況下,删除node節點的左子樹對應的最小值,删除掉之後,對于我們目前
589      * 這個以node為根的二分搜尋樹,在做完這些操作之後,它的根節點依然是node,将它傳回回去。
590      *
591      * @param node
592      */
593     private Node removeMin(Node node) {
594         // 遞歸調用,遞歸到底,終止條件。
595         // 就是節點左子樹為空的時候,就是向左走,直到不能再走的時候,就終止條件
596         // 就是目前這個節點不能再向左走了,即目前這個節點就是最小值。
597         // 所在的節點,就是這個待删除的節點。
598         if (node.left == null) {
599             // 我們要删除目前這個節點,但是目前這個節點可能有右子樹的,則右子樹的部分不可以丢失掉。
600             // reightNode儲存目前節點的右子樹。
601             Node reightNode = node.right;
602             // 儲存目前節點的右子樹之後,就将目前節點的右子樹置空就行了。
603             // 将目前節點的右子樹和這個二分搜尋樹脫離關系。
604             node.right = null;
605             // 傳回儲存的右子樹,為什麼傳回這個呢,因為這個函數做的事情就是删除掉以node為根的二分搜尋樹中的最小節點,
606             // 傳回删除節點後新的二分搜尋樹的根,此時,最小節點就是node節點本身,将node删除掉之後,
607             // 新的二分搜尋樹的根就是node.right,就是node的右孩子,它就是node的右子樹對應的那個根節點。
608             // 如果node的右孩子為空,沒有關系,就說明node的右子樹為空,一棵空樹,它的根節點還是空。
609 
610             // 維護size的大小。因為删除掉一個元素,是以要維護size的大小。
611             size--;
612             return reightNode;
613             // 這就是删除最小節點這個算法來說,遞歸到底的情況。
614         }
615 
616 
617         // 如果此時,沒有遞歸到底,就說明還有左孩子。
618         // 就是進行遞歸操作的第二部分就行了。去删除掉這個目前節點node的左子樹所對應的最小值。
619         // Node removeMin = removeMin(node.left);
620         // 删除掉以node為根的二分搜尋樹中的最小節點。
621         // 傳回删除節點後新的二分搜尋樹的根。
622         // 這樣才可以真正改變二分搜尋樹的結構。
623         // node.left = removeMin;
624         node.left = removeMin(node.left);
625 
626         // 傳回目前節點
627         return node;
628     }
629 
630 
631     /**
632      * 從二分搜尋樹中删除最大值所在節點,傳回最大值。
633      * <p>
634      * 删除最大值,類比删除最小值的思路即可。
635      *
636      * @return
637      */
638     public E removeMax() {
639         // 将删除的元素删除掉
640         E ret = maximum();
641 
642         root = removeMax(root);
643 
644         // 最後,将待删除元素删除掉就行了。
645         return ret;
646     }
647 
648 
649     /**
650      * 删除掉以node為根的二分搜尋樹中的最大節點。
651      * 傳回删除節點後新的二分搜尋樹的根。
652      *
653      * @param node
654      */
655     private Node removeMax(Node node) {
656         // 當走到該節點最右孩子的時候,即最右孩子是空的時候,就走到了最底。
657         if (node.right == null) {
658             // 儲存目前節點的左子樹。因為對于目前節點,右子樹已經為空了。
659             // 删除掉這個節點之後,這個節點的左子樹的根節點就是新的根節點,
660             Node leftNode = node.left;
661             // 儲存目前節點的左子樹之後,就将目前節點的左子樹置空就行了。
662             // 将目前節點的左子樹和這個二分搜尋樹脫離關系。
663             node.left = null;
664             // 删除節點,維護size的大小
665             size--;
666             // 删除掉這個節點之後,那麼,這個節點的左子樹的根節點,就是新的根節點。
667             return leftNode;
668         }
669 
670         // 如果遞歸沒有到到底的話,我們做的事情就是去删除目前這個節點右子樹中的最大值,
671         // 将最終的結果傳回給目前的這個節點的右孩子。然後傳回node節點。
672         node.right = removeMax(node.right);
673 
674         // 傳回目前節點
675         return node;
676     }
677 
678 
679     /**
680      * 從二分搜尋樹中删除元素為e的節點。
681      *
682      * @param e
683      */
684     public void remove(E e) {
685         // 參數一是根節點,參數二是待删除元素
686 
687         // 将删除掉元素e之後d得到的新的二分搜尋樹的根節點傳回回來
688         root = remove(root, e);
689     }
690 
691     /**
692      * 删除掉以node為根的二分搜尋樹中值為e的節點,遞歸算法。
693      * 傳回删除節點後新的二分搜尋樹的根。
694      * <p>
695      * <p>
696      * 删除左右都有孩子的節點d,找到p = max(d -> node.left)。p是d的前驅。
697      * 此删除方法是找到待删除節點的後繼,也可以找到目前節點的前驅。
698      *
699      * @param node
700      * @param e
701      */
702     private Node remove(Node node, E e) {
703         // 如果node節點為空,直接傳回空即可。
704         // 另外一層含義,在二分搜尋樹中找這個元素為e的節點,根本沒有找到,最後找到node為空的位置了,
705         // 直接傳回空就行了。
706         if (node == null) {
707             return null;
708         }
709 
710         // 遞歸函數,開始近邏輯
711         // 如果待删除元素e和目前節點的元素e進行比較,如果待删除元素e小于該節點的元素e
712         if (e.compareTo(node.e) < 0) {
713             // 此時,去該節點的左子樹,去找到待删除元素節點
714             // 遞歸調用,去node的左子樹,去删除這個元素e。
715             // 最後将删除的結果賦給該節點左子樹。
716             node.left = remove(node.left, e);
717             return node;
718         } else if (e.compareTo(node.e) > 0) {
719             // 如果待删除元素e大于該節點的元素e
720             // 去目前節點的右子樹去尋找待删除元素節點
721             // 将删除後的結果傳回給目前節點的右孩子
722             node.right = remove(node.right, e);
723             return node;
724         } else {
725             // 目前節點元素e等于待删除節點元素e,即e == node.e,
726             // 相等的時候,此時就是要删除這個節點的。
727 
728             // 如果目前節點node的左子樹為空的時候,待删除節點左子樹為空的情況
729             if (node.left == null) {
730                 // 儲存該節點的右子樹
731                 Node rightNode = node.right;
732                 // 将node和這棵樹斷開關系
733                 node.right = null;
734                 // 維護size的大小
735                 size--;
736                 // 傳回原來那個node的右孩子。也就是右子樹的根節點,此時就将node删除掉了
737                 return rightNode;
738             }
739 
740             // 如果目前節點的右子樹為空,待删除節點右子樹為空的情況。
741             if (node.right == null) {
742                 // 儲存該節點的左子樹
743                 Node leftNode = node.left;
744                 // 将node節點和這棵樹斷開關系
745                 node.left = null;
746                 // 維護size的大小
747                 size--;
748                 //傳回原來那個節點node的左孩子,也就是左子樹的根節點,此時就将node删除掉了。
749                 return leftNode;
750             }
751 
752             // 待删除節點左右子樹均為不為空的情況。
753             // 核心思路,找到比待删除節點大的最小節點,即待删除節點右子樹的最小節點
754             // 用這個節點頂替待删除節點的位置。
755 
756             // 找到目前節點node的右子樹中的最小節點,找到比待删除節點大的最小節點。
757             // 此時的successor就是node的後繼。
758             Node successor = minimum(node.right);
759             // 此時将目前節點node的右子樹中的最小節點删除掉,并将二分搜尋樹的根節點傳回。
760             // 将新的二分搜尋樹的根節點指派給後繼節點的右子樹。
761             successor.right = removeMin(node.left);
762 
763             // 因為removeMin操作,删除了一個節點,但是此時目前節點的右子樹的最小值還未被删除
764             // 被successor後繼者指向了。是以這裡做一些size加加操作,
765             size++;
766 
767             // 将目前節點的左子樹指派給後繼節點的左子樹上。
768             successor.left = node.left;
769             // 将node節點沒有用了,将node節點的左孩子和右孩子置空。讓node節點和二分搜尋樹脫離關系
770             node.left = node.right = null;
771 
772             // 由于此時,将目前節點node删除掉了,是以這裡做一些size減減操作。
773             size--;
774 
775             // 傳回後繼節點
776             return successor;
777         }
778 
779     }
780 
781 
782     @Override
783     public String toString() {
784         StringBuilder stringBuilder = new StringBuilder();
785         // 使用一種形式展示整個二分搜尋樹,可以先展現根節點,再展現左子樹,再展現右子樹。
786         // 上述這種過程就是一個前序周遊的過程。
787         // 參數一,目前周遊的二分搜尋樹的根節點,初始調用的時候就是root。
788         // 參數二,目前周遊的這棵二分搜尋樹的它的深度是多少,根節點的深度是0。
789         // 參數三,将字元串傳入進去,為了友善生成字元串。
790         generateBSTString(root, 0, stringBuilder);
791 
792         return stringBuilder.toString();
793     }
794 
795     /**
796      * 生成以node為根節點,深度為depth的描述二叉樹的字元串。
797      *
798      * @param node          節點
799      * @param depth         深度
800      * @param stringBuilder 字元串
801      */
802     private void generateBSTString(Node node, int depth, StringBuilder stringBuilder) {
803         // 遞歸的第一部分
804         if (node == null) {
805             // 顯示的,将在字元串中追加一個空字元串null。
806             // 為了表現出目前的空節點對應的二分搜尋樹的層次,封裝了一個方法。
807             stringBuilder.append(generateDepthString(depth) + "null\n");
808             return;
809         }
810 
811 
812         // 遞歸的第二部分
813         // 當目前節點不為空的時候,就可以直接通路目前的node節點了。
814         // 将目前節點資訊放入到字元串了
815         stringBuilder.append(generateDepthString(depth) + node.e + "\n");
816 
817         // 遞歸進行調用
818         generateBSTString(node.left, depth + 1, stringBuilder);
819         generateBSTString(node.right, depth + 1, stringBuilder);
820     }
821 
822     /**
823      * 為了表現出二分搜尋樹的深度
824      *
825      * @param depth
826      * @return
827      */
828     private String generateDepthString(int depth) {
829         StringBuilder stringBuilder = new StringBuilder();
830         for (int i = 0; i < depth; i++) {
831             stringBuilder.append("--");
832         }
833         return stringBuilder.toString();
834     }
835 
836 
837     public static void main(String[] args) {
838         BinarySearchTree<Integer> binarySearchTree = new BinarySearchTree<Integer>();
839         int[] nums = new int[]{1, 5, 3, 6, 8, 4, 2};
840 //        for (int i = 0; i < nums.length; i++) {
841 //            // 二分搜尋樹的新增
842 //            binarySearchTree.add(nums[i]);
843 //        }
844         for (int num : nums) {
845             binarySearchTree.add(num);
846         }
847 
848 
849         // 二分搜尋樹的前序周遊
850         binarySearchTree.preOrder();
851         System.out.println();
852 
853 
854         // 二分搜尋樹的中序周遊
855         binarySearchTree.inOrder();
856         System.out.println();
857 
858         // 二分搜尋樹的後序周遊
859         binarySearchTree.postOrder();
860         System.out.println();
861 
862         // 二分搜尋樹的非遞歸的前序周遊。
863         binarySearchTree.preOrderNonRecursive();
864         System.out.println();
865 
866         // 二分搜尋樹的非遞歸的層序周遊。
867         binarySearchTree.levelOrder();
868         System.out.println();
869 
870         // 二分搜尋樹的遞歸的删除任意元素
871         System.out.println("二分搜尋樹的遞歸的删除任意元素.");
872         binarySearchTree.remove(5);
873         System.out.println(binarySearchTree);
874 
875 
876 //        System.out.println(binarySearchTree.toString());
877 //        5
878 //        --3
879 //        ----2
880 //        ------null
881 //        ------null
882 //        ----4
883 //        ------null
884 //        ------null
885 //        --6
886 //        ----null
887 //        ----8
888 //        ------null
889 //        ------null
890 
891 
892         // 二分搜尋樹的遞歸的删除最小值
893 //        BinarySearchTree<Integer> binarySearchTree = new BinarySearchTree<Integer>();
894 //        Random random = new Random();
895 //        int n = 1000;
896 //        for (int i = 0; i < n; i++) {
897 //            // 随機添加1000個0-10000之間的數字。有可能重複,總數可能小于1000
898 //            binarySearchTree.add(random.nextInt(10000));
899 //        }
900 //
901 //        ArrayList<Integer> nums = new ArrayList<Integer>();
902 //        while (!binarySearchTree.isEmpty()) {
903 //            nums.add(binarySearchTree.removeMin());
904 //        }
905 //        System.out.println(nums);
906 //
907 //        // 判斷是否是從小到大排序的
908 //        for (int i = 1; i < nums.size(); i++) {
909 //            if (nums.get(i - 1) > nums.get(i)) {
910 //                throw new IllegalArgumentException("Error.");
911 //            }
912 //        }
913 //        System.out.println("removeMin test completed.");
914 
915 
916         // 二分搜尋樹的遞歸的删除最大值
917 //        BinarySearchTree<Integer> binarySearchTree2 = new BinarySearchTree<Integer>();
918 //        Random random2 = new Random();
919 //        int n2 = 1000;
920 //        for (int i = 0; i < n2; i++) {
921 //            // 随機添加1000個0-10000之間的數字。有可能重複,總數可能小于1000
922 //            binarySearchTree2.add(random2.nextInt(10000));
923 //        }
924 //
925 //        ArrayList<Integer> nums2 = new ArrayList<Integer>();
926 //        while (!binarySearchTree2.isEmpty()) {
927 //            nums2.add(binarySearchTree2.removeMax());
928 //        }
929 //        System.out.println(nums2);
930 //
931 //        // 判斷是否是從小到大排序的
932 //        for (int i = 1; i < nums2.size(); i++) {
933 //            if (nums2.get(i - 1) < nums2.get(i)) {
934 //                throw new IllegalArgumentException("Error.");
935 //            }
936 //        }
937 //        System.out.println("removeMax test completed.");
938 
939     }
940 
941 }           

繼續閱讀