《戀上資料結構與演算法》筆記(九):二叉搜尋樹 II

白駱駝_發表於2020-11-17

由於篇幅, 此篇和上篇文章無縫銜接 : 二叉搜尋樹 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...
  • 終結條件:nodeparent右子樹中。

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;
}

相關文章