高級數據結構—二叉查找樹及其增刪改查實現

二叉樹查找樹:

二叉查找樹也叫二叉搜索樹,二叉排序樹。它也是一種特殊的二叉樹,

它具有以下特點

1.如果它的左子樹不為空,則左子樹上結點的值都小於根結點。

2.如果它的右子樹不為空,則右子樹上結點的值都大於根結點。

3.子樹的子樹同樣也要遵循以上兩點

 

為什麼又叫做二叉排序樹因為具有這種特殊特點的二叉樹,它的中序遍歷一定是有序的,如下:

 

 

 

中序遍歷的結果是0,1,3,4,5,7,8,9,10

 

推薦一個網站://www.cs.usfca.edu/~galles/visualization/Algorithms.html 這個上面有各種數據結構的操作,之前在mysql索引數據結構這篇文章中也推薦過。

 

插入的時候每次都是和根結點或者子樹的根結點比較,大於走右邊,小於走左邊,直到找到它應該插入的位置。新元素插入的位置肯定值在葉子結點。其實它的插入就是一次查找。每次判斷之後就會折半,所以說它的查詢效率是O(logn)。

查詢和插入的時候類似,修改就沒什麼好說的了,直接把數據覆蓋上去即可

重點說下二叉查找樹的刪除。

它的刪除分三種情況:

1.刪除的是葉子結點數據,可以看出來,直接刪除就可以了,比如上面0,4,7,10,將其雙親的對應子樹指向null即可

2.刪除的是度為1的結點,比如上面的1,9,只需要將它的子樹的根結點覆蓋當前刪除的這個節點即可,以1的刪除為例,也就是把其雙親3的右子樹指針改指向像它的孩子0即可。

3.刪除的結點度為2,也就有兩棵子樹的結點。這個的處理就稍微複雜一點了,因為二叉查找樹的特性,根大於左子樹小於右子樹的。所以刪除的需要去尋找前驅/後繼結點來補充它的位置。如果刪除的根左邊的結點(比根小的結點),那麼就是找前驅結點,前驅結點是其左子樹中最大的結點,前驅結點的右子樹一定為空,因為沒有比它大的了;如果刪除的根右邊的結點(比根大的結點),那麼就是找後繼結點,後繼結點是其右子樹中最小的結點,後繼結點的左子樹一定為空,因為沒有比它小的了。其實前驅後繼就是中序遍歷的時候在根前後的兩個結點。如此一來,可以看出找到的前驅/後繼結點的條件肯定是滿足1或者2的,找到這個節點之後,將其覆蓋當前要刪除的這個節點。各項指針都移動好了,只需要將這個前驅/後繼結點刪除就可以了,這個節點的刪除就和前面兩種情況一樣的了。

 

具體每一個注意點可以看下程式碼注釋說明,應該算比較詳細,裡面涉及的那些結點指針的指向變更,還是比較繞的。

 

package com.nijunyang.algorithm.tree;

import com.nijunyang.algorithm.util.RefObject;

/**
 * Description:
 * Created by nijunyang on 2020/4/19 20:03
 */
public class BinarySearchTree<T extends Comparable<T>> extends TreeNode<T> {
    private T data;
    private BinarySearchTree<T> leftChild;
    private BinarySearchTree<T> rightChild;

    public BinarySearchTree(T data) {
        this.data = data;
    }

    public static void main(String[] args) {
        BinarySearchTree<Integer> binarySearchTree = new BinarySearchTree(5);
        BinarySearchTree.insert(binarySearchTree, 3);
        BinarySearchTree.insert(binarySearchTree, 1);
        BinarySearchTree.insert(binarySearchTree, 4);
        BinarySearchTree.insert(binarySearchTree, 8);
        BinarySearchTree.insert(binarySearchTree, 7);
        BinarySearchTree.insert(binarySearchTree, 9);
        BinarySearchTree.insert(binarySearchTree, 10);
        BinarySearchTree.insert(binarySearchTree, 0);
        TreeUtil.inOrderTraversal(binarySearchTree);
        System.out.println();
        TreeUtil.levelOrder(binarySearchTree);
        System.out.println();
        BinarySearchTree<Integer> integerBinarySearchTree = BinarySearchTree.find(binarySearchTree, 9, new RefObject<>());
        delete(binarySearchTree, 8);
        delete(binarySearchTree, 7);
        TreeUtil.inOrderTraversal(binarySearchTree);
    }

    /**
     * 插入數據
     * @param root
     * @param data
     * @param <T>
     */
    public static <T extends Comparable<T>> void insert(BinarySearchTree<T> root, T data) {
        if (root.data.compareTo(data) < 0) {
            if (root.rightChild == null) {
                root.rightChild = new BinarySearchTree(data);
            } else {
                insert(root.rightChild, data);
            }
        } else {
            if (root.leftChild == null) {
                root.leftChild = new BinarySearchTree(data);
            } else {
                insert(root.leftChild, data);
            }
        }
    }


    /**
     * 查詢數據
     * @param root
     * @param data
     * @param parent   用於帶出父節點
     * @param <T>
     * @return
     */
    public static <T extends Comparable<T>> BinarySearchTree<T> find(
            BinarySearchTree<T> root, T data, RefObject<BinarySearchTree<T>> parent) {
        if (root.data.compareTo(data) == 0) {
            return root;
        }
        parent.setValue(root);
        if (root.data.compareTo(data) < 0) {
            if (root.rightChild != null) {
                return find(root.rightChild, data, parent);
            }
        } else {
            if (root.leftChild != null) {
                return find(root.leftChild, data, parent);
            }
        }
        return null;
    }

    /**
     * 查詢最大數據
     * @param root
     * @param parentRef  返回結果的父結點包裝
     * @param <T>
     * @return
     */
    public static <T extends Comparable<T>> BinarySearchTree<T> findMax(
            BinarySearchTree<T> root, RefObject<BinarySearchTree<T>> parentRef) {
        if (root.rightChild != null) {
            parentRef.setValue(root);
            return findMax(root.rightChild, parentRef);
        }
        return root;
    }

    /**
     * 查詢最小數據
     * @param root
     * @param parentRef 返回結果的父結點包裝
     * @param <T>
     * @return
     */
    public static <T extends Comparable<T>> BinarySearchTree<T> findMin(
            BinarySearchTree<T> root, RefObject<BinarySearchTree<T>> parentRef) {
        if (root.leftChild != null) {
            parentRef.setValue(root);
            return findMin(root.leftChild, parentRef);
        }
        return root;
    }

    /**
     * 刪除數據
     * @param root
     * @param data
     * @param <T>
     * @return
     */
    public static <T extends Comparable<T>> void delete(BinarySearchTree<T> root, T data) {

        RefObject<BinarySearchTree<T>> parentRef = new RefObject<>();
        BinarySearchTree<T> delBinarySearchTree = find(root, data, parentRef);
        if (delBinarySearchTree == null) {
            return;
        }
        /**
         * 二叉搜索樹結點的刪除分三種情況:
         * 1.葉子結點,可以直接刪除
         * 2.度為1的結點,可以直接刪除(只有一個子樹的結點)
         * 3.兩棵子樹的結點刪除,找前驅結點/後繼結點。就是刪除了該結點,前驅/後繼結點可以直接補位
         * 如果刪除的根左邊的結點,那麼就是找前驅結點,前驅結點是其左子樹中最大的結點,前驅結點的右子樹一定為空,因為沒有比它大的了
         * 如果刪除的根右邊的結點,那麼就是找後繼結點,後繼結點是其右子樹中最小的結點,後繼結點的左子樹一定為空,因為沒有比它小的了
         * 如此一來,可以看出找到的前驅/後繼結點的條件肯定是滿足1或者2的
         */
        BinarySearchTree<T> parent = parentRef.getValue();
        //葉子結點直接將父結點的孩子置空
        if (delBinarySearchTree.leftChild == null && delBinarySearchTree.rightChild == null) {
            if (parent.rightChild == delBinarySearchTree) {
                parent.rightChild = null;
            } else {
                parent.leftChild = null;
            }
        }
        //度為2的結點刪除
        else if (delBinarySearchTree.leftChild != null && delBinarySearchTree.rightChild != null) {
            //刪除比根大的,刪除的結點在根的右邊,需要找後繼結點
            if (root.data.compareTo(data) < 0) {
                RefObject<BinarySearchTree<T>> postParentRef = new RefObject<>(); //後繼結點的父結點
                BinarySearchTree<T> postNode = findMin(root.rightChild, postParentRef);
                //判斷要刪除的結點是它父結點的左結點還是右結點,修改對應指針指向後繼結點
                if (parent.data.compareTo(delBinarySearchTree.data) < 0) {
                    parent.rightChild = postNode;
                } else {
                    parent.leftChild = postNode;
                }
                postParentRef.getValue().leftChild = null; //後繼結點因為要移走,所以置空其父結點的左孩子(後繼必定是其父的左孩子)
                if (postNode.rightChild != null) {//後繼結點的左子樹一定為空, 將其父結點的左孩子指向後繼結點的右孩子
                    postParentRef.getValue().leftChild = postNode.rightChild;
                    postNode.rightChild = null; //置空相關引用,便於垃圾回收
                }
                //將刪除的這個結點的左右子樹的指針給到後繼結點
                postNode.rightChild = delBinarySearchTree.rightChild;
                delBinarySearchTree.rightChild = null; //置空相關引用,便於垃圾回收
                postNode.leftChild = delBinarySearchTree.leftChild;
                delBinarySearchTree.leftChild = null; //置空相關引用,便於垃圾回收
            }
            //刪除根或者比根小的,刪除的結點在根的左邊,需要找前驅結點
            else{
                RefObject<BinarySearchTree<T>> preParentRef = new RefObject<>(); //前驅結點的父結點
                BinarySearchTree<T> preNode = findMax(root.leftChild, preParentRef);

                if (parent != null) { //如果刪除的是根結點 沒有父結點
                    //判斷要刪除的結點是它父結點的左結點還是右結點,修改對應指針指向前驅結點
                    if (parent.data.compareTo(delBinarySearchTree.data) < 0) {
                        parent.rightChild = preNode;
                    } else {
                        parent.leftChild = preNode;
                    }
                }
                preParentRef.getValue().rightChild = null; //前驅結點因為要移走,所以置空其父結點的右孩子(前驅必定是其父的右孩子)
                if (preNode.leftChild != null) {//前驅結點的右子樹一定為空, 將其父結點的右孩子指向前驅結點的左孩子
                    preParentRef.getValue().rightChild = preNode.leftChild;
                    preNode.leftChild = null; //置空相關引用,便於垃圾回收
                }

                if (delBinarySearchTree == root) {
                    //刪除的是根 直接將交換值
                    delBinarySearchTree.data = preNode.data;
                }
                else {
                    //將刪除的這個結點的左右孩子的指針給到前驅結點
                    preNode.rightChild = delBinarySearchTree.rightChild;
                    delBinarySearchTree.rightChild = null; //置空相關引用,便於垃圾回收
                    preNode.leftChild = delBinarySearchTree.leftChild;
                    delBinarySearchTree.leftChild = null; //置空相關引用,便於垃圾回收
                }
            }
        }
        //度為1的結點刪除 將其父結點的孩子指向它的孩子
        else {
            BinarySearchTree<T> leftChild = delBinarySearchTree.leftChild;
            BinarySearchTree<T> child =  leftChild == null ? delBinarySearchTree.rightChild : leftChild;
            delBinarySearchTree.leftChild = null;  //置空相關引用,便於垃圾回收
            delBinarySearchTree.rightChild = null; //置空相關引用,便於垃圾回收
            if (parent.data.compareTo(child.data) < 0) {
                parent.rightChild = child;
            } else {
                parent.leftChild = child;
            }
        }
    }


    @Override
    public T getData() {
        return data;
    }

    @Override
    public void setData(T data) {
        this.data = data;
    }

    @Override
    public BinarySearchTree<T> getLeftChild() {
        return leftChild;
    }

    public void setLeftChild(BinarySearchTree<T> leftChild) {
        this.leftChild = leftChild;
    }

    @Override
    public BinarySearchTree<T> getRightChild() {
        return rightChild;
    }

    public void setRightChild(BinarySearchTree<T> rightChild) {
        this.rightChild = rightChild;
    }
}

 

 

遍歷程式碼:

package com.nijunyang.algorithm.tree;

import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

/**
 * @author: create by nijunyang
 * @date:2019/7/28
 */
public final class TreeUtil {

    private TreeUtil() {
    }


    /**
     * 構造二叉樹
     * @param dataList
     * @param <T>
     * @return
     */
    public static <T> TreeNode<T> createBinaryTree(LinkedList<T> dataList) {
        TreeNode<T> node = null;
        if (dataList == null || dataList.isEmpty()) {
            return null;
        }
        T data = dataList.removeFirst();
        if (data != null) {
            node = new TreeNode(data);
            node.setLeftChild((createBinaryTree(dataList)));
            node.setRightChild((createBinaryTree(dataList)));
        }
        return node;
    }

    /**
     * 前序遍歷 根 左子樹 右子樹
     * @param node
     */
    public static<N extends TreeNode<T>, T> void preOrderTraversal(N node) {
        if(node == null){
            return;
        }
        //遇根先輸出,再去找左右
        System.out.print(node.getData());
        preOrderTraversal(node.getLeftChild());
        preOrderTraversal(node.getRightChild());
    }

    /**
     * 二叉樹中序遍歷 左子樹 根 右子樹
     * @param node   二叉樹節點
     */
    public static<N extends TreeNode<T>, T> void inOrderTraversal(N node){
        if(node == null){
            return;
        }
        //先找左再輸出根,再去找右
        inOrderTraversal(node.getLeftChild());
        System.out.print(node.getData());
        inOrderTraversal(node.getRightChild());
    }

    /**
     * 二叉樹後序遍歷  左子樹 右子樹 根
     * @param node   二叉樹節點
     */
    public static<N extends TreeNode<T>, T> void postOrderTraversal(N node){
        if(node == null){
            return;
        }
        //先找左右,最後輸出根
        postOrderTraversal(node.getLeftChild());
        postOrderTraversal(node.getRightChild());
        System.out.print(node.getData());
    }

    /**
     * 利用棧前序遍歷二叉樹
     * @param root
     */
    public static <N extends TreeNode<T>, T> void preOrderTraversalByStack(N root) {
        Stack<TreeNode<T>> stack = new Stack<>();
        TreeNode<T> node = root;
        while(node != null || !stack.isEmpty()) {
            //節點不為空,遍歷節點,併入棧用於回溯
            while(node != null) {
                System.out.print(node.getData());
                stack.push(node);
                node = node.getLeftChild();
            }
            //沒有左節點,彈出該棧頂節點(回溯),訪問右節點
            if(!stack.isEmpty()) {
                node = stack.pop();
                node = node.getRightChild();
            }
        }
    }

    /**
     * 層次遍歷
     * @param root
     * @param <T>
     */
    public static <N extends TreeNode<T>, T> void levelOrder(N root) {
        if (root == null) {
            return;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);  //入隊
        while (!queue.isEmpty()) {
            TreeNode<T> node = queue.poll(); //取出
            if (node != null) {
                System.out.print(node.getData());
                queue.offer(node.getLeftChild());   //左孩子入隊
                queue.offer(node.getRightChild());  //右孩子入隊
            }
        }
    }
}

 

RefObject:

package com.nijunyang.algorithm.util;

/**
 * Description: 引用包裝,用於去一個方法裡面除開返回值之外,將其他額外需要的數據帶出來
 * Created by nijunyang on 2020/4/20 10:26
 */
public class RefObject<E> {

    public RefObject() {
    }

    public RefObject(E value) {
        this.value = value;
    }


    private E value;

    public E getValue() {
        return value;
    }

    public void setValue(E value) {
        this.value = value;
    }
}