《戀上資料結構與演算法》筆記(九):二叉搜尋樹 II
由於篇幅, 此篇和上篇文章無縫銜接 : 二叉搜尋樹 I
目錄
五、二叉搜尋樹練習
1、計算二叉搜尋樹的高度
思路:
- 遞迴方式: 因為我們發現
根節點
的高度就是整課二叉樹的高度;根節點的高度 = Math.max(其左右子樹的高度) + 1
; - 迭代方式: 通過
層序遍歷
我們可以發現, 當每一層遍歷完之後我們就對定義的height++, 這樣就可以計算出樹的高度了, 此時又出現一個問題, 怎麼判斷每一層節點遍歷完呢
, 可以發現queue.size()
就是下一層的節點數量
// 方式一: 遞迴的方式
public int height() {
return height(root);
}
public int height(Node<E> node) {
if (node == null) return 0;
// 左子節點或右子節點中高度最高的值 + 1
return 1 + Math.max(height(node.left), height(node.right));
}
// 方式二: 迭代的方式
/**
* 使用層序遍歷的方式, 計算樹的高度
*/
public int height2() {
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root); // 往佇列中放節點
// 當前佇列中節點個數
int elementCount = 1;
// 樹的高度
int height = 0;
while (!queue.isEmpty()) {
// 取出節點
Node<E> node = queue.poll();
// 佇列中節點個數-1
elementCount--;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
// 當 elementCount = 0, 說明當前層的節點遍歷完成
if (elementCount == 0) {
// 記錄下一層節點個數
elementCount = queue.size();
// 高度+1
height++;
}
}
return height;
}
2、判斷一棵二叉樹是否為完全二叉樹
// 首先給內部的Node節點增加兩個方法
private static class Node<E> {
E element;
Node<E> left;
Node<E> right;
Node<E> parent;
// 這裡不初始化左,右結點,因為不常用,比如所葉子結點,就滅有左右結點
// 父節點,除了根節點外,都有父節點
public Node(E element, Node<E> parent) {
this.element = element;
this.parent = parent;
}
/**
* 判斷是否為葉子節點
* @return
*/
public boolean isLeaf() {
return left == null && right == null;
}
/**
* 判斷是否為左右節點都不為空的節點
* @return
*/
public boolean hasTwoChildren() {
return left != null && right != null;
}
}
// 使用這種方式更加清晰
public boolean isCompleteTree2() {
if (root == null) return false;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root); // 將根節點先入隊
boolean leaf = false;
while (!queue.isEmpty()) {
Node<E> node = queue.poll(); // 取出隊頭節點
if (leaf && !node.isLeaf()) return false;
if (node.left != null) { // 如果該節點左節點不為空,則將該左節點入隊
queue.offer(node.left);
} else if (node.right != null) {
// node.left == null && node.right != null
return false;
}
if (node.right != null) { // 如果該節點右節點不為空,則將該右節點入隊
queue.offer(node.right);
} else {
// 能來到這裡, 說明後面的節點都是葉子節點
// node.left == null && node.right == null
// node.left != null && node.right == null
leaf = true;
}
}
return true;
}
// 判斷一棵二叉樹是否為完全二叉樹(方式一, 推薦方式二)
public boolean isCompleteTree() {
if (root == null) return false;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root); // 將根節點先入隊
boolean leaf = false;
while (!queue.isEmpty()) {
Node<E> node = queue.poll(); // 取出隊頭節點
// node不是葉子節點的情況
if (leaf && !node.isLeaf()) return false;
if (node.hasTwoChildren()) { // 左右節點都不為空
queue.offer(node.left);
queue.offer(node.right);
} else if (node.left == null && node.right != null) { // 不符合完全二叉樹的要求
return false;
} else { // 這裡的節點都是葉子節點
leaf = true;
if (node.left != null) {
queue.offer(node.left);
}
}
}
return true;
}
注意:
以後使用到二叉樹的層序遍歷
, 首先要保證可以遍歷到所有的節點
, 模板如下
// 層序遍歷
public void levelOrder(r) {
if (root == null) return;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root); // 將根節點先入隊
while (!queue.isEmpty()) {
Node<E> node = queue.poll(); // 取出隊頭節點
// TODO
if (node.left != null) { // 如果該節點左節點不為空,則將該左節點入隊
queue.offer(node.left);
}
if (node.right != null) { // 如果該節點右節點不為空,則將該右節點入隊
queue.offer(node.right);
}
}
}
3、翻轉一個二叉樹
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return root;
// 遞迴實現
// 前序遍歷, 交換結點
/* TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);*/
// 後序遍歷, 交換結點
/* invertTree(root.left);
invertTree(root.right);
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;*/
// 中序遍歷, 交換節點
invertTree(root.left);
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
//invertTree(root.right);
invertTree(root.left); // 之所以改為root.left, 在交換左右結點的時候已經更改了right
return root;
}
}
六、重構二叉樹
七、二叉搜尋樹的前驅和後繼
前提: 中序遍歷
1、前驅節點(predecessor)
中序遍歷
時的前
一個節點。- 如果是二叉搜尋樹,前驅節點就是前一個比它小的節點。
尋找前驅節點分三種情況:
1、左子樹不為空
- 舉例:
6,13,8
predecessor = node.left.right.right.right...
- 終結條件:
right = null
2、左子樹為空,父節點不為空
- 舉例:
7,11,9,1
predecessor = node.parent.parent.parent...
- 終結條件:
node
在parent
的右子樹
中。
3、左子樹為空且父節點為空
- 那就沒有前驅節點
- 舉例:沒有
左子樹
的根節點。
/**
* 根據傳入的節點, 返回該節點的前驅節點 (中序遍歷)
*
* @param node
* @return
*/
private Node<E> predecessor(Node<E> node) {
if (node == null) return node;
// (中序遍歷)前驅節點在左子樹當中(node.left.right.right.right...)
Node<E> p = node.left;
// 左子樹存在
if (p != null) {
while (p.right != null) {
p = p.right;
}
return p;
}
// 程式走到這裡說明左子樹不存在; 從父節點、祖父節點中尋找前驅節點
/*
* node的父節點不為空 && node是其父節點的左子樹時. 就一直往上尋找它的父節點
* 因為node==node.parent.right, 說明你在你父節點的右邊, 那麼node.parent就是其node的前驅節點
*/
while (node.parent != null && node == node.parent.left) {
node = node.parent;
}
// 能來到這裡表示: 兩種情況如下
// node.parent == null 表示沒有父節點(根節點),返回空 ==> return node.parent;
// node==node.parent.right 說明你在你父節點的右邊, 那麼node.parent就是其node的前驅節點 ==> return node.parent;
return node.parent;
}
2、後繼節點(successor)
中序遍歷
時的後
一個節點。- 如果是二叉搜尋樹,後繼節點就是前一個比它
大
的節點。
尋找後繼節點分三種情況:
1、右子樹不為空
- 舉例:
1,4,4
successor = node.right.left.left.left...
- 終結條件:
left = null
2、右子樹為空,父節點不為空
- 舉例:
7,6,3,11
successor = node.parent.parent.parent...
- 終結條件:
node在parent的左子樹
中。
3、右子樹為空且父節點為空
- 那就沒有前驅節點
- 舉例:沒有
右子樹
的根節點。
/**
* 根據傳入的節點, 返回該節點的後驅節點 (中序遍歷)
*
* @param node
* @return
*/
private Node<E> successor(Node<E> node) {
if (node == null) return node;
Node<E> p = node.right;
if (p != null) {
while (p.left != null) {
p = p.left;
}
return p;
}
// node.right為空
while (node.parent != null && node == node.parent.right) {
node = node.parent;
}
return node.parent;
}
八、二叉排序樹刪除節點
提示
:刪除元素會使用前驅
,後繼
的知識,可以先閱讀: 二叉搜尋樹(Binary Search Tree)的前驅和後繼
- 刪除節點分為
刪除葉子節點
,刪除度為1的節點
,刪除度為2的節點
。
刪除葉子結點
- 刪除
葉子節點
,直接刪除
即可。-
如果
node == node.parent.left
node.parent.left = null
-
如果
node == node.parent.right
node.parent.right = null
-
如果
node.parent == null
root = null
-
刪除度為1的結點
刪除度為1的節點
,用子節點替代原節點的位置。child = node.left或者child = node.right
-
用child替代node的位置。
-
如果node是左子節點:
child.parent = node.parent
node.parent.left = child
-
如果node是右子節點:
child.parent = node.parent
node.parent.right = child
-
如果node是根節點:
root = child
刪除度為2的結點
刪除度為2的節點
,先用前驅或後繼節點的值覆蓋原節點的值
,然後刪除相應的前驅或者後繼節點
。(如果這個節點的度為2,那麼它的前驅,後繼節點的度只能是1和0)
public void add(E element) {
elementNotNullCheck(element);
// 新增第一個節點
if (root == null) {
// 給根節點賦值,且根節點沒有父節點
root = new Node<>(element, null);
size++;
return;
}
// 新增的不是第一個節點
Node<E> parent = root; // 這個是第一次比較的父節點
Node<E> node = root;
int cmp = 0;
while (node != null) {
cmp = compare(element, node.element); // 兩者具體比較的方法
parent = node; // 記錄其每一次比較的父節點
if (cmp > 0) {
// 插入的元素大於根節點的元素,插入到根節點的右邊
node = node.right;
} else if (cmp < 0) {
// 插入的元素小於根節點的元素,插入到根節點的左邊
node = node.left;
} else { // 相等
node.element = element;
return;
}
}
// 看看插入到父節點的哪個位置
Node<E> newNode = new Node<>(element, parent);
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
size++;
}
public void remove(E element) {
remove(node(element));
}
/**
* 刪除結點的邏輯
*
* @param node
*/
private void remove(Node<E> node) {
if (node == null) return;
// node 不為空, 必然要刪除結點, 先size--;
size--;
// 刪除node是度為2的結點
if (node.hasTwoChildren()) {
//1 找到後繼(也可以找到前驅)
Node<E> successor = successor(node);
//2 用後繼結點的值覆蓋度為2結點的值
node.element = successor.element;
//3 刪除後繼節點
node = successor;
}
// 刪除node,即刪除後繼節點 (node節點必然是度為1或0)
// 因為node只有一個子節點/0個子節點, 如果其left!=null, 則用node.left來替代, node.left==null, 用node.right來替代,
// 若node為葉子節點, 說明, node.left==null, node.right也為null, 則replacement==null;
Node<E> replacement = node.left != null ? node.left : node.right;
// 刪除node是度為1的結點
if (replacement != null) {
// 更改parent
replacement.parent = node.parent;
// 更改parent的left、right的指向
if (node.parent == null) { // node是度為1且是根節點
root = replacement;
} else if (node == node.parent.left) {
node.parent.left = replacement;
} else if (node == node.parent.right) {
node.parent.right = replacement;
}
// 刪除node是葉子節點, 且是根節點
} else if (node.parent == null) {
root = null;
} else { // node是葉子結點, 且不是根節點
if (node == node.parent.left) {
node.parent.left = null;
} else { // node == node.parent.right
node.parent.right = null;
}
}
}
/**
* 傳入element找到對應element對應的結點
*
* @param element
* @return
*/
private Node<E> node(E element) {
Node<E> node = root;
while (node != null) {
int cmp = compare(element, node.element);
if (cmp == 0) return node;
if (cmp > 0) { // 說明element對應的結點, 比node的element大, 所以去它的右子樹找
node = node.right;
} else {
node = node.left;
}
}
return null; // 沒有找到element對應的結點
}
// 實現contains和clear方法
public boolean contains(E element) {
return node(element) != null;
}
public void clear() {
root = null;
size = 0;
}
相關文章
- 演算法與資料結構——AVL樹(平衡二叉搜尋樹)演算法資料結構
- 【資料結構】二叉搜尋樹!!!資料結構
- 資料結構-二叉搜尋樹資料結構
- 資料結構之「二叉搜尋樹」資料結構
- 資料結構☞二叉搜尋樹BST資料結構
- 【資料結構與演算法】二叉搜尋樹的簡單封裝資料結構演算法封裝
- 資料結構中的樹(二叉樹、二叉搜尋樹、AVL樹)資料結構二叉樹
- 資料結構和演算法-Go實現二叉搜尋樹資料結構演算法Go
- 資料結構-二叉搜尋樹的實現資料結構
- 【資料結構】搜尋樹資料結構
- 看圖輕鬆理解資料結構與演算法系列(二叉搜尋樹)資料結構演算法
- 二叉搜尋樹的結構
- 演算法與資料結構之二分搜尋樹演算法資料結構
- 資料結構之二叉搜尋樹—Java實現資料結構Java
- 資料結構與演算法 排序與搜尋資料結構演算法排序
- 資料結構:一文看懂二叉搜尋樹 (JavaScript)資料結構JavaScript
- 資料結構高階--二叉搜尋樹(原理+實現)資料結構
- 【資料結構與演算法】二叉樹資料結構演算法二叉樹
- 【資料結構】【二叉樹】四、二叉搜尋樹的特性(不斷補充)資料結構二叉樹
- 資料結構學習系列之二叉搜尋樹詳解!資料結構
- 資料結構與演算法:二叉排序樹資料結構演算法排序
- 資料結構-二分搜尋樹資料結構
- 資料結構和演算法學習筆記七:圖的搜尋資料結構演算法筆記
- (戀上資料結構筆記):歸併排序(Merge Sort)資料結構筆記排序
- [leetCode]95. 不同的二叉搜尋樹 IILeetCode
- LeetCode-095-不同的二叉搜尋樹 IILeetCode
- 資料結構之樹結構概述(含滿二叉樹、完全二叉樹、平衡二叉樹、二叉搜尋樹、紅黑樹、B-樹、B+樹、B*樹)資料結構二叉樹
- 資料結構與演算法-表示式二叉樹資料結構演算法二叉樹
- 資料結構與演算法-kd二叉樹(kNN)資料結構演算法二叉樹KNN
- 資料結構與演算法-二叉查詢樹資料結構演算法
- 資料結構與演算法-二叉樹性質資料結構演算法二叉樹
- 資料結構與演算法-二叉樹遍歷資料結構演算法二叉樹
- 搜尋中常見資料結構與演算法探究(二)資料結構演算法
- 演算法篇 - 二叉搜尋樹演算法
- 【亡羊補牢】挑戰資料結構與演算法 第39期 LeetCode 501. 二叉搜尋樹中的眾數(二叉樹)資料結構演算法LeetCode二叉樹
- 資料結構與演算法-kd二叉樹(基礎)資料結構演算法二叉樹
- 【演算法與資料結構 02】二叉樹的引入演算法資料結構二叉樹
- 【資料結構與演算法】手撕平衡二叉樹資料結構演算法二叉樹