數據結構之樹-第二篇

數據結構之樹-第一篇

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最近的那個元素,這裏面的最近,不是位置近,是大小的意思。

此時,讓s的左子樹等於d的左子樹,s -> node.left = d -> node.left;最後,刪除d節點,s是新的子樹的根節點。
4、數據結構之樹,代碼實現,如下所示。

 

  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 }

 

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