二叉樹學習記
二叉樹
前言
通過閱讀本篇博文,我們可以大致掌握哪些知識(學到就是賺到)?
- 二叉樹的概念
- 二叉樹的種類
- 二叉樹的性質
- 二叉樹的三種遍歷方式(遞迴、非遞迴)
- 先序遍歷
- 中序遍歷
- 後序遍歷
- 二叉搜尋樹
- 二叉搜尋樹的API實現(遞迴與非遞迴)
- 增
- 刪
- 查
- 遍歷
- 獲取集合屬性
- 獲取節點的元素個數
- 獲取樹的深度
- 二叉樹的兩種構建方式
- 已知先序和中序
- 已知中序和後序
引入
樹是一種非線性的資料結構,相對於線性的資料結構(連結串列、陣列)⽽⾔,樹的平均運⾏時間更短(往往與樹相關的排序時間複雜度都不會⾼)
⼀般的樹是有很多很多個分⽀的,分⽀下⼜有很多很多個分⽀,如果在程式中研究這個會⾮常麻煩。因為本來樹就是⾮線性的,⽽我們計算機的記憶體是線性儲存的,太過複雜的話我們⽆法設計出來的。
因此,我們先來研究簡單⼜經常⽤的---> ⼆叉樹
一、概念:
二叉樹就是指每一個節點的子節點個數都不超過2個的樹
一顆樹至少會有一個節點(root)
樹由節點組成,每個節點都包括該節點的key和指向兩個子節點的節點引用(若子節點為空,則節點引用指向null)
class TreeNode<E> { TreeNode LChild; E key; TreeNode RChild; }
我們定義樹的時候往往是定義節點,節點連線起來就成了樹,而節點的定義就是:一個數據、兩個指標。
二、二叉樹的種類
滿二叉樹:最後一層的葉子結點是滿的
完全二叉樹:葉子節點只會出現在最後兩層,且最後一層的葉子節點都是靠左對齊
注:完全二叉樹從根節點到倒數第二層,組成了一棵滿二叉樹。(滿二叉樹一定是一棵完全二叉樹,但完全二叉樹不一定是滿二叉樹)
若完全二叉樹的總節點樹為n,則葉子節點的個數為 (n+1)>>1;
- 國外說的完滿二叉樹(Full Binary Tree)就是國內的真二叉樹。
- 國外說的完美二叉樹(Perfect Binary Tree)就是國內說的滿二叉樹。
- 完全二叉樹的定義,國內外一樣。
三、二叉樹的性質
在二叉樹的第 i 層至多有 2^(i -1)個結點。(i>=1)
深度為 k 的二叉樹至多有 2^(k-1)個結點(k >=1)
對任何一棵二叉樹T, 如果其葉結點數為n0, 度為2的結點數為 n2,則n0=n2+1
具有 n (n>=0) 個結點的完全二叉樹的深度為+1
如將一棵有n個結點的完全二叉樹自頂向下,同層自左向右連續為結點編號0,1, …, n-1,則有:
1)若i = 0, 則 i 無雙親, 若i > 0, 則 i 的雙親為」(i -1)/2」 2)若2*i+1 < n, 則i 的左子女為 2*i+1,若2*i+2 < n, 則 i 的右子女為2*i+2 3)若結點編號i為偶數,且i != 0,則左兄弟結點i-1. 4)若結點編號i為奇數,且i != n-1,則右兄弟結點為i+1. 5)結點i 所在層次為」log2(i+1) 」
四、二叉樹遍歷有三種方式
先序遍歷:
先訪問根節點,然後訪問左節點,最後訪問右節點(根->左->右)
中序遍歷:
先訪問左節點,然後訪問根節點,最後訪問右節點(左->根->右)
後序遍歷:
先訪問左節點,然後訪問右節點,最後訪問根節點(左->右->根)
注:
- 先、中、後代表的是根節點的位置
如上二叉樹的三種遍歷方式分別是:
先序(根->左->右):1 2 4 5 7 3 6
中序(左->根->右):4 2 7 5 1 6 3
後序(左->右->根):4 7 5 2 6 3 1
一句話總結:先序(根->左->右),中序(左->根->右),後序(左->右->根)。如果訪問有孩⼦的節點,先處理孩⼦的,隨後返回。
無論先中後遍歷,每個節點的遍歷如果訪問有孩⼦的節點,先處理孩⼦的(邏輯是⼀樣的)
- 因此我們很容易想到遞迴
- 遞迴出口就是:沒有子節點了,就返回
1、遞迴實現
// 利用遞迴,先序遍歷 // 建立一個列表,將遍歷過的節點儲存到列表中 public List<> preOrder(TreeNode root) { List<> list = new ArrayList<>(); preOrder(root, list); return list; } public void preOrder(TreeNode root, List<> list) { // 遞迴出口 if (root == null) return ; // 先序:根->左->右 list.add(root.key); preOrder(root.left, list); preOrder(root.right, list); }
// 利用遞迴,中序遍歷 // 建立一個列表,將遍歷過的節點儲存到列表中 public List<> inOrder(TreeNode root) { List<> list = new ArrayList<>(); inOrder(root, list); return list; } public void inOrder(TreeNode root, List<> list) { // 遞迴出口 if (root == null) return ; // 中序:左->根->右 inOrder(root.left, list); list.add(root.key); inOrder(root.right, list); }
// 利用遞迴,後序遍歷 // 建立一個列表,將遍歷過的節點儲存到列表中 public List<> postOrder(TreeNode root) { List<> list = new ArrayList<>(); inOrder(root, list); return list; } public void postOrder(TreeNode root, List<> list) { // 遞迴出口 if (root == null) return ; // 序:左->右->根 postOrder(root.left, list); inOrder(root.right, list); list.add(root.key); }
2、非遞迴實現
本質上還是模仿遞迴實現的過程
先序遍歷
// 非遞迴實現先序遍歷,藉助棧來實現(後進先出) public List<E> preOrder(TreeNode root) { List<E> list = new ArrayList<>(); if (root == null) return list; // 用棧實現 Deque<TreeNode> stack = new LinkedList<>(); stack.push(root); // 將根節點入棧 // 判斷棧是否為空 while (!stack.isEmpty()) { TreeNode x = stack.pop(); list.add(x.value); // 出棧並將根節點的值儲存到list中 // 判斷是否有右孩子,有則入棧 if (x.right != null) stack.push(x.right); if (x.left != null) stack.push(x.left); } return list; }
中序遍歷
// 非遞迴實現中序遍歷,藉助棧來實現(後進先出) public List<E> inOrder() { List<E> list = new ArrayList<>(); if (root == null) return list; Deque<TreeNode> stack = new LinkedList<>(); TreeNode x = root; while(!stack.isEmpty() || x != null) { while(x != null) { stack.push(x); x = x.left; // 一直往左走,直到左子樹的最左節點才退出迴圈 } // 此時x指向了左子樹的最左節點 x = stack.pop(); list.add(x.value); x = x.right; // 往右走 } return list; }
後序遍歷
// 非遞迴實現中序遍歷,藉助棧來實現(後進先出) // 通過LinkedList的addFirst()方法實現頭插 public List<E> postOrder() { LinkedList<E> list = new LinkedList<>(); if (root == null) return list; Deque<TreeNode> stack = new LinkedList<>(); // 將根節點入棧 stack.push(root); while (!stack.isEmpty()) { TreeNode x = stack.pop(); list.addFirst(x.value); // LinkedList的特有方法 // 判斷是否有左孩子 if (x.left != null) stack.push(x.left); // 判斷是否有右孩子 if (x.right != null) stack.push(x.right); } return list; } // 注:此處思考,我們用來儲存節點的集合list,為什麼選擇LinkedList而不選擇ArrayList來實現?
靜態建立二叉樹
class TreeNode {
TreeNode lift;
E key;
TreeNode right;
// 構造方法
public TreeNode(E e) {
this.key = e;
}
}
public class Test {
public static void main(String[] args) {
TreeNode tree = new TreeNode(1); // 跟節點
//左子樹
tree.lift = new TreeNode(2);
tree.lift.lift = new TreeNode(4);
tree.lift.right = new TreeNode(5);
tree.lift.right.lift = new TreeNode(7);
// 右子樹
tree.right = new TreeNode(3);
tree.right.lift = new TreeNode(6);
// 遍歷
preOrderTraver(root); // 先序
inOrderTraver(root); // 中序
postOrderTraver(root); // 後序
}
}
五、二叉搜尋樹
1、滿足條件:
- 左子樹上的所有節點值均小於根節點值
- 右子樹上的所有節點值均不小於根節點值
- 左右子樹也滿足上述兩個條件
public class BinarySearchTree<E extends Comparable<? super E>> { // 屬性 private TreeNode root; private int size; private class TreeNode { TreeNode left; E value; TreeNode right; public TreeNode(E value) { this.value = value; } } // 預設構造方法 // API的實現: / * 增: boolean add(E e) 刪: void clear() boolean remove(Object obj) 查: boolean contains(Object obj) E min() E max() 遍歷: List<E> preOrder() List<E> inOrder() List<E> postOrder() List<E> levelOrder() 獲取集合屬性: int size() boolean isEmpty() 建樹: */ }
2、二叉搜尋樹的API實現
(1)增加節點:
思路:
由於二叉搜尋樹的特殊性質確定了二叉搜尋樹中每個元素只可能出現一次,所以在插入的過程中如果發現這個元素已經存在於二叉搜尋樹中,就不進行插入,否則就查詢合適的位置進行插入。因此,在進行插入之前我們需要先找到插入的位置。
- 第一種情況:根節點為空 —— 直接插入,return true;
- 第二種情況:要插入的元素已經存在,如上面所說,如果在二叉搜尋樹中已經存在該元素,則不再進行插入,直接return false;
- 第三種情況:能夠找到合適位置進行插入
// 非遞迴方式實現 public boolean add(E e) { if (e == null) throw new IllegalArgumentException("Key cannot be null"); // 如果根節點為null if (root == null) { root = new TreeNode(e); size++; return true; } // 如果根節點不為null int cmp = e.compareTo(root.value); // 此處先行判斷一下,e 是否 等於root.value,排除cmp==0的情況 // 因為最後找到要新增到上面的節點時,遍歷的那個變數是指向null的,所以要用p保留它的父節點引用 TreeNode p = root; TreeNode x = cmp > 0 ?p.right:p.left; while(x != null) { p = x; cmp = e.compareTo(x.value); if (cmp > 0) x = x.right; else if (cmp < 0) x = x.left; else return false; } TreeNode node = new TreeNode(e); // 退出迴圈後,p指向了e要新增到的那個節點上 if(e.compareTo(p.value) > 0) p.right = node; else p.left = node; size++; return true; }
// 遞迴實現 public boolean add(E e) { if (e == null) throw new IllegalArgumentException("Key cannot be null"); int oldSize = size; root = add(root, e); return size > oldSize; } private TreeNode add(TreeNode node, E e) { // 遞迴出口 if (node == null) { // 找到了新增結點的位置 size++; return new TreeNode(e); } int cmp = e.compareTo(node.value); if (cmp < 0) node.left = add(node.left, e); else if (cmp > 0) node.right = add(node.right, e); return node; }
(2)刪除節點:
思路:
// 遞迴實現
// 遞迴實現
刪除節點
public TreeNode delNode(TreeNode root, E key) {
if (root == null) return null;
int cmp = key.compareTo(root.value);
// 先找到要刪除的節點
// 在左子樹上
if (cmp < 0) delNode(root.left, key);
// 在用子樹上
else if (cmp > 0) delNode(root.right, key);
// 找到要刪除的節點
else {
// 如果左子樹為空,返回右子樹,反之同理
if (root.left == null) return root.right;
else if (root.right == null) return root.left;
// 左右子樹都不為空,找到後繼節點或,前驅節點都可
else {
// 找前驅節點
TreeNode MaxNode = successor(root);
// 將前驅節點賦值給root,並呼叫遞迴刪除該前驅節點
root.value = MaxNode.value;
root.left = delNode(root.left, root.value);
}
return root;
}
}
/*
找前驅節點,即左子樹中最大的節點
*/
public TreeNode successor (TreeNode root) {
root = root.left;
while(root != null) root = root.right;
return root;
}
Task:
-
二叉樹新增節點
- 遞迴
- 非遞迴
-
二叉樹刪除節點
- 遞迴
- 非遞迴
-
層次遍歷
- BFS
-
先序、中序、後序遍歷
- 遞迴
- 非遞迴
-
構建二叉搜尋樹
- 先序 + 中序
- 中序 + 後序