你天天都那麼努力,忍受了那麼多的寂寞和痛苦。可我也沒見你有多優秀。java
當我仍是一個年輕男孩的時候畫的一張關於樹的畫。node
當你第一次學習編碼時,大部分人都是將數組做爲主要數據結構來學習。git
以後,你將會學習到哈希表。若是你是計算機專業的,你確定須要選修一門數據結構的課程。上課時,你又會學習到鏈表,隊列和棧等數據結構。這些都被統稱爲線性的數據結構,由於它們在邏輯上都有起點和終點。github
當你開始學習樹和圖的數據結構時,你會以爲它是如此的混亂。由於它的存儲方式不是線性的,它們都有本身特定的方式存儲數據。算法
這篇文章幫助你更好的理解樹形數據結構並儘量的幫你解決你的疑問。本章咱們將學到編程
當學習編程時,咱們更容易理解線性的數據結構而不是樹和圖的數據結構。小程序
樹是衆所周知的非線性數據結構。它們不以線性方式存儲數據。他們按層次組織數據。微信小程序
咱們所說的層次組織究竟是是什麼呢?數組
想象一下咱們的家譜:祖父母,父母,子女,兄弟姐妹等等,咱們一般按層次結構組織家譜。微信
個人家庭族譜
上圖是個人家譜。tossico,akikazu,hitomi和takemi是個人祖父母。
Toshiaki 和 Juliana 是個人父母。
TK 、Yuji 、Bruno 和 Kaio 是我父母的孩子(我和個人兄弟們)。
另外一個層次結構的例子是企業的組織結構。
公司的結構也是是一個層次結構的例子
在 HTML 中,文檔對象模型(DOM)是樹形結構的。
文檔對象模型(dom)
HTML 標籤包含其餘的標籤。咱們有一個 head 標籤和 body 標籤。這些標籤包含特色的元素。head 標籤中有 meta 和 title 標籤。body 標籤中有在用戶界面展現的標籤,如 h1 、a 、li 等等。
樹(tree
)是被稱爲結點(node
)的實體的集合。結點經過邊(edge
)鏈接。每一個結點都包含值或數據(value/date
),而且每結節點可能有也可能沒有子結點。
樹的首結點叫根結點(即root
結點)。若是這個根結點和其餘結點所鏈接,那麼根結點是父結點(parent node
,與根結點鏈接的是子結點(child node
)。
全部的結點都經過邊(edge
)鏈接。它是樹中很重要得一個概念,由於它負責管理節點之間的關係。
葉子結點(leaves
)是樹末端,它們沒有子結點。像真正的大樹同樣,咱們能夠看到樹上有根、枝幹和樹葉。
樹的高度(height
)和深度(depth
)
如今咱們來討論一個特殊的樹類型。咱們把它叫做二叉樹。
「在計算機科學領域,二叉樹是一種樹形數據結構,它的每一個節點最多有兩個孩子,被叫做左孩子和右孩」 — Wikipedia
當咱們要實現二叉樹時,咱們須要牢記的第一件事是它是一個結點集合。每一個結點都有三個屬性:value
,left_child``和right_child
。
那麼咱們怎麼才能實現一個有這三個屬性的簡單二叉樹呢?
咱們來實現一個二叉樹的例子
/** * Created on 2018/4/16. * * @author zlf * @since 1.0 */
public class BinaryTree {
public BinaryTree left; //左節點
public BinaryTree right; //右節點
public String data; //樹的內容
public BinaryTree() {
}
/** * 構造方法 * * @param data * @param left * @param right */
public BinaryTree(String data, BinaryTree left, BinaryTree right) {
this.left = left;
this.right = right;
this.data = data;
}
/** * 構造方法 * * @param data */
public BinaryTree(String data) {
this(data, null, null);
}
複製代碼
好,這就是咱們的二叉樹類
當咱們實例化一個對象時,咱們把值(點的相關數據)做爲參數傳遞給類。看上面類的左孩子結點和右孩子結點。兩個都被賦值爲null。
爲何?
由於當咱們建立節點時,它尚未孩子,只有結點數據。
代碼測試
/** * 構建樹 */
public static void testCreate() {
BinaryTree node = new BinaryTree("a");
System.out.println("【node data】:" + node.getData());
System.out.println("【node left data】:" + (node.left==null?"null":node.left.getData()));
System.out.println("【node right data】:" + (node.right==null?"null":node.right.getData()));
}
複製代碼
輸出:
【node data】:a
【node left data】:null
【node right data】:null
複製代碼
咱們能夠將字符串'a'做爲參數傳給二叉樹結點。若是將值、左孩子結點、右孩子結節點輸出的話,咱們就能夠看到這個值了。
下面開始插入部分的操做。那麼咱們須要作些什麼工做呢?
有兩個要求:
若是當前的結點沒有左孩子結點,咱們就建立一個新結點,而後將其設置爲當前結點的左結點。
若是已經有了左結點,咱們就建立一個新結點,並將其放在當前左結點的位置。而後再將原左結點值爲新左結點的左結點。
圖形以下:
下面是插入的代碼:
/** * 插入節點 ,若是當前的節點沒有左節點,咱們就建立一個新節點,而後將其設置爲當前節點的左節點。 * * @param node * @param value */
public static void insertLeft(BinaryTree node, String value) {
if (node != null) {
if (node.left == null) {
node.setLeft(new BinaryTree(value));
} else {
BinaryTree newNode = new BinaryTree(value);
newNode.left = node.left;
node.left = newNode;
}
}
}
複製代碼
再次強調,若是當前結點沒有左結點,咱們就建立一個新結點,並將其置爲當前結點的左結點。不然,就將新結點放在左結點的位置,再將原左結點置爲新左結點的左結點。
一樣,咱們編寫插入右結點的代碼
/** * 同插入左結點 * @param node * @param value */
public static void insertRight(BinaryTree node, String value) {
if (node != null) {
if (node.right == null) {
node.setRight(new BinaryTree(value));
} else {
BinaryTree newNode = new BinaryTree(value);
newNode.right = node.right;
node.right = newNode;
}
}
}
複製代碼
可是這還不算完成。咱們得測試一下。
咱們來構造一個像下面這樣的樹:
下面是這棵樹的實現代碼:
/** * 測試插入結點 */
public static void testInsert() {
BinaryTree node_a = new BinaryTree("a");
node_a.insertLeft(node_a, "b");
node_a.insertRight(node_a, "c");
BinaryTree node_b = node_a.left;
node_b.insertRight(node_b, "d");
BinaryTree node_c = node_a.right;
node_c.insertLeft(node_c, "e");
node_c.insertRight(node_c, "f");
BinaryTree node_d = node_b.right;
BinaryTree node_e = node_c.left;
BinaryTree node_f = node_c.right;
System.out.println("【node_a data】:" + node_a.getData());
System.out.println("【node_b data】:" + node_b.getData());
System.out.println("【node_c data】:" + node_c.getData());
System.out.println("【node_d data】:" + node_d.getData());
System.out.println("【node_e data】:" + node_e.getData());
System.out.println("【node_f data】:" + node_f.getData());
}
複製代碼
輸出:
【node_a data】:a
【node_b data】:b
【node_c data】:c
【node_d data】:d
【node_e data】:e
【node_f data】:f
複製代碼
插入已經結束
如今,咱們來考慮一下樹的遍歷。
樹的遍歷有兩種選擇,深度優先搜索(DFS)和廣度優先搜索(BFS)。
DFS是用來遍歷或搜索樹數據結構的算法。從根節點開始,在回溯以前沿着每個分支儘量遠的探索。 — Wikipedia
BFS是用來遍歷或搜索樹數據結構的算法。從根節點開始,在探索下一層鄰居節點前,首先探索同一層的鄰居節點。 — Wikipedia
下面,咱們來深刻了解每一種遍歷算法。
DFS 在回溯和搜索其餘路徑以前找到一條到葉節點的路徑。讓咱們看看這種類型的遍歷的示例。
輸出結果爲: 1–2–3–4–5–6–7
爲何?
讓咱們分解一下:
當咱們深刻到葉結點時回溯,這就被稱爲 DFS 算法。
既然咱們對這種遍歷算法已經熟悉了,咱們將討論下 DFS 的類型:前序、中序和後序。
這和咱們在上述示例中的做法基本相似。
/** * 前序遍歷 * * @param node */
public static void preOrder(BinaryTree node) {
if (node != null) {
System.out.println(node.data);
if (node.left != null) {
node.left.preOrder(node.left);
}
if (node.right != null) {
node.right.preOrder(node.right);
}
}
}
複製代碼
示例中此樹的中序算法的結果是3–2–4–1–6–5–7。
左結點優先,以後是中間,最後是右結點。
代碼實現:
/** * 中序遍歷 * * @param node */
public static void inOrder(BinaryTree node) {
if (node != null) {
if (node.left != null) {
node.left.inOrder(node.left);
}
System.out.println(node.data);
if (node.right != null) {
node.right.inOrder(node.right);
}
}
}
複製代碼
以此樹爲例的後序算法的結果爲 3–4–2–6–7–5–1 。
左結點優先,以後是右結點,根結點的最後。
代碼實現:
/** * 後序遍歷 * * @param node */
public static void postOrder(BinaryTree node) {
if (node != null) {
if (node.left != null) {
node.left.postOrder(node.left);
}
if (node.right != null) {
node.right.postOrder(node.right);
}
System.out.println(node.data);
}
}
複製代碼
BFS是一層層逐漸深刻的遍歷算法
下面這個例子是用來幫咱們更好的解釋該算法。
咱們來一層一層的遍歷這棵樹。本例中,就是1-2-5-3-4-6-7.
代碼實現:
/** * 廣度排序 * * @param node */
public static void bfsOrder(BinaryTree node) {
if (node != null) {
Queue<BinaryTree> queue = new ArrayDeque<BinaryTree>();
queue.add(node);
while (!queue.isEmpty()) {
BinaryTree current_node = queue.poll();
System.out.println(current_node.data);
if (current_node.left != null) {
queue.add(current_node.left);
}
if (current_node.right != null) {
queue.add(current_node.right);
}
}
}
}
複製代碼
爲了實現BFS算法,咱們須要用到一個數據結構,那就是隊列。
隊列具體是用來幹什麼的呢?
請看下面解釋。
二叉搜索樹有時候被稱爲二叉有序樹或二叉排序樹,二叉搜索樹的值存儲在有序的順序中,所以,查找表和其餘的操做可使用折半查找原理。——Wikipedia
二叉搜索樹中的一個重要性質是,二叉搜索樹中一個節點的值大於其左結點,可是小於其右結點
代碼實現二叉樹搜索
如今想像一下咱們有一棵空樹,咱們想將幾個節點添加到這棵空樹中,這幾個結點的值爲:50、7六、2一、四、3二、100、6四、52。
首先咱們須要知道的是,50是否是這棵樹的根結點。
如今咱們開始一個一個的插入結點
你注意到這裏的模式了嗎?
讓咱們把它分解。
代碼實現:
/** * 插入樹 * * @param node * @param value */
public void insertNode(BinaryTree node, Integer value) {
if (node != null) {
if (value <= Integer.valueOf(node.data) && node.left != null) {
node.left.insertNode(node.left, value);
} else if (value <= Integer.valueOf(node.data)) {
node.left = new BinaryTree(String.valueOf(value));
} else if (value > Integer.valueOf(node.data) && node.right != null) {
node.right.insertNode(node.right, value);
} else {
node.right = new BinaryTree(String.valueOf(value));
}
}
}
複製代碼
看起來很簡單。
該算法的強大之處是其遞歸部分,即第9行和第13行。這兩行代碼均調用 insertNode 方法,並分別爲其左結點和右結點使用它。第11行和第15行則在子結點處插入新結點。
咱們如今要構建的算法是關於搜索的。對於給定的值(整數),咱們會搜索出咱們的二叉查找樹有或者沒有這個值。
須要注意的一個重要事項是咱們如何定義樹的插入算法。 首先咱們有根結點。全部左子的節點值都比根結點小。全部右子樹的節點值都比根結點大。
讓咱們看一個例子。
假設咱們有這棵樹。
如今咱們想知道是否有一個結點的值爲52。
讓咱們把它分解。
代碼實現:
/** * 查找節點是否存在 * * @param node * @param value * @return */
public boolean findNode(BinaryTree node, Integer value) {
if (node != null) {
if (value < Integer.valueOf(node.data) && node.left != null) {
return node.left.findNode(node.left, value);
}
if (value > Integer.valueOf(node.data) && node.right != null) {
return node.right.findNode(node.right, value);
}
return value == Integer.valueOf(node.data);
}
return false;
}
複製代碼
代碼分析:
刪除是一個更復雜的算法,由於咱們須要處理不一樣的狀況。對於給定值,咱們須要刪除具備此值的結點。想象一下這個節點的如下場景:它沒有孩子,有一個孩子,或者有兩個孩子。
# |50| |50|
# / \ / \
# |30| |70| (DELETE 20) ---> |30| |70|
# / \ \
# |20| |40| |40|
複製代碼
若是要刪除的結點沒有子結點,咱們簡單地刪除它。該算法不須要重組樹。
# |50| |50|
# / \ / \
# |30| |70| (DELETE 30) ---> |20| |70|
# /
# |20|
複製代碼
在這種狀況下,咱們的算法須要使節點的父節點指向子結點。若是節點是左孩子,則使其父結點指向其子結點。若是結點是右孩子,則使其父結點指向其子結點。
# |50| |50|
# / \ / \
# |30| |70| (DELETE 30) ---> |40| |70|
# / \ /
# |20| |40|
複製代碼
當節點有兩個孩子,則須要從該節點的右孩子開始,找到具備最小值的結點。咱們將把具備最小值的這個節點置於被刪除的節點的位置。
代碼實現:
/** * 刪除節點 * @param node * @param value * @param parent * @return */
public boolean removeNode(BinaryTree node, Integer value, BinaryTree parent) {
if (node != null) {
if (value < Integer.valueOf(node.data) && node.left != null) {
return node.left.removeNode(node.left, value, node);
} else if (value < Integer.valueOf(node.data)) {
return false;
} else if (value > Integer.valueOf(node.data) && node.right != null) {
return node.right.removeNode(node.right, value, node);
} else if (value > Integer.valueOf(node.data)) {
return false;
} else {
if (node.left == null && node.right == null && node == parent.left) {
parent.left = null;
node.clearNode(node);
} else if (node.left == null && node.right == null && node == parent.right) {
parent.right = null;
node.clearNode(node);
} else if (node.left != null && node.right == null && node == parent.left) {
parent.left = node.left;
node.clearNode(node);
} else if (node.left != null && node.right == null && node == parent.right) {
parent.right = node.left;
node.clearNode(node);
} else if (node.right != null && node.left == null && node == parent.left) {
parent.left = node.right;
node.clearNode(node);
} else if (node.right != null && node.left == null && node == parent.right) {
parent.right = node.right;
node.clearNode(node);
} else {
node.data=String.valueOf(node.right.findMinValue(node.right));
node.right.removeNode(node.right,Integer.valueOf(node.right.data),node);
}
return true;
}
}
return false;
}
複製代碼
value
和 parent
。咱們想找到值等於該 value
的 node
,而且該 node
的父節點對於刪除該 node
是相當重要的。true
。不然返回 false
。value
的 node
。若是該 value
小於 current node
值,咱們進入左子樹,遞歸處理(當且僅當,current node
有左孩子)。若是該值大於,則進入右子樹。遞歸處理。true
。從第11行到第31行,咱們處理了這些狀況。因此直接返回 true
,這就夠了。/** * 清空n節點 * * @param node */
public void clearNode(BinaryTree node) {
node.data = null;
node.left = null;
node.right = null;
}
複製代碼
/** * 查找樹中最小值 */
public Integer findMinValue(BinaryTree node) {
if (node != null) {
if (node.left != null) {
return node.left.findMinValue(node.left);
} else {
return Integer.valueOf(node.data);
}
}
return null;
}
複製代碼
原文連接:Everything you need to know about tree data structures
從個人 github 中下載,【譯】數據結構中關於樹的一切(java版)
🙂🙂🙂關注微信小程序java架構師歷程 上下班的路上無聊嗎?還在看小說、新聞嗎?不知道怎樣提升本身的技術嗎?來吧這裏有你須要的java架構文章,1.5w+的java工程師都在看,你還在等什麼?