今天是一年一度的植樹節,說到樹,我就想到了《西遊記》中的古樹精,今年下半年。。。好了好了,不皮了,咱們直接開花。今天就趁着植樹節來種一棵咱們程序員的「樹」吧。node
在種樹以前,咱們先來了解下什麼是樹?看個例子:程序員
不對不對,放錯了,應該是下面這個:算法
維基百科對於樹的定義是:數據庫
在計算機科學中,樹(英語:tree)是一種抽象數據類型(ADT)或是實現這種抽象數據類型的數據結構,用來模擬具備樹狀結構性質的數據集合。它是由 n(n>0)個有限結點組成一個具備層次關係的集合。把它叫作「樹」是由於它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。數組
說白了,只要是形如上圖的數據結構就叫樹。bash
天然界中有「迎客鬆」、「軒轅柏」這樣出名的樹,咱們今天要種的樹也是咱們IT圈裏面的扛把子——「二叉樹」。數據結構
二叉樹是數據結構中一種重要的數據結構,也是樹表家族最爲基礎的結構之一。post
二叉樹的特色是每一個結點最多有兩個子樹,左邊的叫作左子樹,右邊的叫作右子樹,二叉樹要麼爲空,要麼由根結點、左子樹和右子樹組成,而左子樹和右子樹分別是一棵二叉樹。 下面這棵樹就是一棵二叉樹。學習
接下來的代碼均用 Java 展示,完整代碼可在公衆號「01二進制」後臺回覆「二叉樹」查看。
如今咱們就開始種一棵如上圖的樹吧。根據定義,咱們瞭解到,結點是一棵二叉樹最重要的元素,而做爲一個結點,必須知足如下條件:
所以咱們能夠建立一個結點類(TreeNode):
class TreeNode {
String data;
TreeNode left;
TreeNode right;
TreeNode(String data) {
this.data = data;
}
}
複製代碼
有了這個結點類以後咱們就能夠建立出一個如上圖的樹了:
// 建立二叉樹
private TreeNode createTree() {
TreeNode root = new TreeNode("A");
root.left = new TreeNode("B");
root.right = new TreeNode("C");
root.left.left = new TreeNode("D");
root.left.right = new TreeNode("E");
root.right.left = new TreeNode("F");
root.right.right = new TreeNode("G");
return root;
}
複製代碼
你看,一棵樹不就種好了嗎?
做爲一個新時代的好青年,咱們不能把樹種好了就無論不顧了,得負起責任啊。最起碼你要知道本身的樹長啥樣吧。因此接下來咱們就來看看如何獲取樹的特徵。
咱們描述一我的的特徵每每都是從他的外形、長相、身材入手的,描述一顆樹也是如此,咱們接下來將會從下面幾個角度去獲取樹的特徵:
// 判斷是否爲空
public boolean isEmpty(TreeNode root) {
return root == null;
}
複製代碼
這裏咱們採用遞歸的方式,由於樹的高度是由其子樹決定的,因此咱們只須要比較左、右子樹的高度而後取最大值便可,代碼以下:
// 獲取樹的高度
private int height(TreeNode root) {
if (root == null)
return 0;//遞歸結束:空樹高度爲0
else {
int i = height(root.left);
int j = height(root.right);
return (i < j) ? (j + 1) : (i + 1);
}
}
複製代碼
一個樹的結點個數一定爲其左子樹的結點個數 + 右子樹的結點個數 +1,所以咱們一樣能夠用遞歸很是簡單的將其表示出來:
// 獲取結點大小
private int size(TreeNode root) {
if (root == null) {
return 0;
} else {
return 1 + size(root.left) + size(root.right);
}
}
複製代碼
上一節中咱們獲取到了樹的特徵,但這遠遠不夠,特徵只能粗略的描述一個二叉樹,想要詳細的瞭解一個二叉樹,咱們必須對其進行「遍歷」。
遍歷二叉樹是指以必定的次序訪問二叉樹中的每一個結點。所謂訪問結點是指對結點進行各類操做的簡稱(最簡單的就是訪問該結點的值)。
而訪問結點無非就 3 個操做:
咱們以根結點爲核心,若是先訪問根節點在訪問左右結點成爲前序遍歷;若是先訪問左結點而後根結點最後右結點則爲中序遍歷;若最後訪問根節點則爲後序遍歷。
所以上圖的遍歷結果爲:
前序:A B D E C F G
中序:D B E A F C G
後序:D E B F G C A
複製代碼
固然這只是咱們本身根據定義手寫出來的,該如何用代碼表示出來呢?
根據定義咱們知道,前序遍歷就是先訪問根結點,而後是左結點和右結點,所以用遞歸能夠很簡單的展示這一過程:
// 前序遍歷二叉樹
private void preOrder(TreeNode root) {
if (root != null) {
System.out.print(root.data + " ");
preOrder(root.left);
preOrder(root.right);
}
}
複製代碼
同理咱們也能夠很快的知道中序和後序遍歷了:
// 中序遍歷二叉樹
private void inOrder(TreeNode root) {
if (root != null) {
inOrder(root.left);
System.out.print(root.data + " ");
inOrder(root.right);
}
}
複製代碼
// 後序遍歷二叉樹
private void postOrder(TreeNode root) {
if (root != null) {
postOrder(root.left);
postOrder(root.right);
System.out.print(root.data + " ");
}
}
複製代碼
這裏遍歷二叉樹的代碼均是以遞歸方法完成的,非遞歸遍歷二叉樹的過程較爲麻煩,因爲篇幅限制,這裏就不放出來了。若想查看非遞歸版本的代碼可在公衆號「01二進制」後臺回覆「二叉樹」查看。
事實上,以人來看一個樹的話大多都是一層一層的看,這種遍歷方式稱爲層序遍歷。具體思路:用隊列實現,先將根節點入隊列,只要隊列不爲空,而後出隊列,並訪問,接着講訪問節點的左右子樹依次入隊列。
// 層序遍歷
private void levelTravel(TreeNode root) {
if (root == null) return;
Queue<TreeNode> q = new LinkedList<TreeNode>();
q.add(root);
while (!q.isEmpty()) {
TreeNode temp = q.poll();
System.out.print(temp.data + " ");
if (temp.left != null) q.add(temp.left);
if (temp.right != null) q.add(temp.right);
}
}
複製代碼
二叉樹是種很是強大的數據結構,那她到底強大在哪裏呢?咱們來看下面這個簡單的例子:
該例來自於Aditya Bhargava 的《算法圖解》
假如說,你想從微博中找到一我的,最快的方法通常是二分查找。但當有新用戶增長時,都得將新用戶插入組別內再排序,由於二分查找法只會有序的組別纔有用。
因此就有人想了,若是能夠將新增的用戶插入到數組的正確位置就行了,這樣就不須要在插入後在排序了。
因而就有人設計了一種二叉樹:對於每一個結點,左子節點的值都比它小,右子節點值都比它大。以下圖所示:
Maggie
排在David
後面,所以向右找Maggie
,排在Manning
前面,所以向左找。
這個運行時間,用大O表示法,平均運行時間是O(log2 N),最差運行時間是O(N)
在有序數組查找時,與二分查找法運行時間相同。
二叉樹對比二分查找法優點在於<以下圖>:
能夠看出插入和刪除速度都快。二叉樹的缺點也很明顯,就是不能隨機訪問。
上述例子其實就是一個二叉查找樹的簡易使用,那麼除此以外二叉樹還有什麼常見的應用呢?下面列出四個,有興趣的小夥伴能夠本身搜索相關的文檔閱讀學習。
種一棵樹最好的時間是十年前,然後是如今。咱們經常去後悔過去的事情。遺憾本身犯的錯誤,遺憾本身錯過的機會。雖然現實很讓人感到惋惜,但其實不少事情早就該作了,再懊惱又有什麼用呢?與其無故抱怨還不如行動起來。當你感到遺憾時,纔是行動的最好時機!