常用树结构

  • 二分搜索树
  • 平衡二叉树:AVL、红黑树
  • 堆;并查集
  • 线段树;Trie(字典树、前缀树)

二叉树基础

和链表一样,二叉树是一种动态数据结构,数据存储在“节点”(Node)中,left指向左孩子,right指向右孩子

二叉树具有唯一根节点,每个节点最多有两个孩子,没有孩子的节点称为叶子节点

二叉树具有天然的递归结构,每个节点的左右子树也是二叉树

二分搜索树01:实现二分搜索树_子树

二分搜索树基础

二分搜索树是二叉树,其每个子树也是二分搜索树

二分搜索树的根节点的值满足:大于左子树的所有节点值,小于右子树的所有节点值,不能重复

也就是根节点相当于二分查找法中的mid,只需比较target和根节点的大小,就可以快速的判断出其在左子树还是右子树,然后用递归的方式直到查找到目标

二分搜索树01:实现二分搜索树_子树_02

递归实现二分搜索树

增、删、改、查,前序、中序、后序遍历

public class Algorithm {

    public static void main(String[] args) {

        BinarySearchTree<Integer> bst = new BinarySearchTree<>();
        int[] nums = {5,3,6,8,4,2};

        for (int i : nums) {
            bst.add(i);
        }

        bst.preOrder();
        System.out.println();
        bst.inOrder();
        System.out.println();
        bst.postOrder();
        System.out.println();

        System.out.println(bst.removeMin());
        System.out.println();
        System.out.println(bst.removeMax());
        System.out.println();
        bst.remove(5);
        System.out.println();

        bst.preOrder();
    }
}

class BinarySearchTree<E extends Comparable<E>> {
    /**
     * 节点类Node
     */
    private class Node{

        public E e;
        public Node leftNext;
        public Node rightNext;

        private Node(E e){

            this.e = e;
            leftNext = null;
            rightNext = null;
        }
    }

    private Node root;
    private int size;

    public BinarySearchTree(){

        root = null;
        size = 0;
    }

    public int size(){

        return size;
    }

    public boolean isEmpty(){

        return size == 0;
    }

    /**
     * 添加节点
     * 将根节点和要添加的节点值传入可递归的私有add()方法
     * 从根节点挨个进行递归比较,值相同则抛弃,左右孩子为空则添加
     * 最后返回添加节点后新的根节点
     */
    public void add(E e) {

        root = add(root, e);
    }

    private Node add(Node root, E e){

        if (root == null){

            size++;

            return new Node(e);
        }

        if (root.e.compareTo(e) < 0){
            root.rightNext = add(root.rightNext, e);
        }
        else if (root.e.compareTo(e) > 0) {
            root.leftNext = add(root.leftNext, e);
        }

        return root;
    }

    /**
     * 删除最小的节点
     * Min()方法在树为空时已经做出判断了,故无需再次判断
     * 传入根节点进行递归查找,如果左孩子为空,说明该节点就是最小节点
     * 删除最小节点以后新的根节点就是右孩子,最后返回新的根节点
     */
    public E removeMin(){

        E value = Min();
        root = removeMin(root);

        return value;
    }

    private Node removeMin(Node root){

        if (root.leftNext == null){

            Node newRoot = root.rightNext;
            root.rightNext = null;
            size--;

            return newRoot;
        }

        root.leftNext = removeMin(root.leftNext);

        return root;
    }

    /**
     * 删除最大的节点
     */
    public E removeMax(){

        E value = Max();
        root = removeMax(root);

        return value;
    }

    private Node removeMax(Node root){

        if (root.rightNext == null){

            Node newRoot = root.leftNext;
            root.leftNext = null;
            size--;

            return newRoot;
        }

        root.rightNext = removeMax(root.rightNext);

        return root;
    }

    /**
     * 删除任意节点(Hubbard Deletion)
     * 先找到待删除节点,如果该节点有左右孩子,那就让右子树的最小节点或者左子树的最大节点顶替该节点的位置
     * 先获取右子树的最小节点,其就是新的根节点,让其指向待删除节点的左右孩子,注意右孩子是删除最小节点后新的右孩子,最后安全删除节点,返回新的根节点
     * 注意不能直接把最大/小节点值直接赋值给该节点,因为其可能也有左右子树
     */
    public void remove(E e){

        root = remove(root, e);
    }

    private Node remove(Node root, E e){

        if (root == null){
            return null;
        }

        if (root.e.compareTo(e) < 0){

            root.rightNext = remove(root.rightNext, e);
            return root;
        }
        else if (root.e.compareTo(e) > 0){

            root.leftNext = remove(root.leftNext, e);
            return root;
        }
        else {

            if (root.leftNext == null) {

                Node newRoot = root.rightNext;
                root.rightNext = null;
                size--;

                return newRoot;
            } else if (root.rightNext == null) {

                Node newRoot = root.leftNext;
                root.leftNext = null;
                size--;

                return newRoot;
            } else {

                /**
                 * removeMin()方法中已经进行过size--
                 */
                Node newRoot = Min(root.rightNext);
                newRoot.rightNext = removeMin(root.rightNext);
                newRoot.leftNext = root.leftNext;
                root.leftNext = null;
                root.rightNext = null;

                return newRoot;
            }
        }
    }

    /**
     * 修改节点
     * 前序遍历,先访问根节点,再递归访问左子树和右子树
     */
    public void preOrder(){

        preOrder(root);
    }

    private void preOrder(Node root){

        if (root == null) {
            return;
        }

        System.out.println(root.e);
        preOrder(root.leftNext);
        preOrder(root.rightNext);
    }

    /**
     * 中序遍历,先递归访问左子树,再访问根节点,最后递归访问右子树
     * 该结果就是所有节点的升序排列
     */
    public void inOrder(){

        inOrder(root);
    }

    private void inOrder(Node root){

        if (root == null){
            return;
        }

        inOrder(root.leftNext);
        System.out.println(root.e);
        inOrder(root.rightNext);
    }

    /**
     * 后序遍历,先递归访问左子树,再递归访问右子树,最后访问根节点
     */
    public void postOrder(){

        postOrder(root);
    }

    private void postOrder(Node root){

        if (root == null){
            return;
        }

        postOrder(root.leftNext);
        postOrder(root.rightNext);
        System.out.println(root.e);
    }

    /**
     * 查询节点
     */
    public boolean contains(E e){

        return contains(root, e);
    }

    private boolean contains(Node root, E e){

        if (root == null) {
            return false;
        }

        if (root.e.compareTo(e) == 0){
            return true;
        }
        else if (root.e.compareTo(e) > 0){
            return contains(root.leftNext, e);
        }
        else {
            return contains(root.rightNext, e);
        }
    }

    /**
     * 查找最小的节点
     * 如果左孩子为空,则当前节点就是最小节点
     */
    public E Min(){

        return Min(root).e;
    }

    private Node Min(Node root){

        if (root == null){
            return null;
        }

        if (root.leftNext == null){
            return root;
        }

        return Min(root.leftNext);
    }

    /**
     * 查找最大的节点
     */
    public E Max(){

        return Max(root).e;
    }

    private Node Max(Node root){

        if (root == null){
            return null;
        }

        if (root.rightNext == null){
            return root;
        }

        return Max(root.rightNext);
    }

    @Override
    public String toString(){

        StringBuilder str = new StringBuilder();

        generateString(root, 0, str);

        return str.toString();
    }

    public void generateString(Node root, int depth, StringBuilder str){

        if (root == null) {

            str.append(generateDepth(depth) + "null\n");

            return;
        }

        str.append(generateDepth(depth) + root.e + "\n");

        generateString(root.leftNext, depth + 1, str);
        generateString(root.rightNext, depth + 1, str);
    }

    public String generateDepth(int depth){

        StringBuilder str = new StringBuilder();

        for (int i = 0; i < depth; i++) {
            str.append("--");
        }

        return str.toString();
    }
}

深入理解前中后序遍历(深度优先遍历DFS)

前中后指的是访问根节点的顺序,前序遍历第一次遇到该节点就打印,中序遍历第二次遇见该节点才打印,后序遍历则是第三次遇见该节点才打印

中序遍历相当于排序,可以打印出按顺序排列的所有节点

后序遍历的应用:为二分搜索树释放内存,只有先释放了子树,才能释放根节点

拓展:使用栈实现前序遍历的非递归写法

public void preOrder(){

    preOrder(root);
}

public void preOrder(Node root) {

    /**
     * 前序遍历,在每次弹出节点后,先后压入自己的右子节点和左子节点
     * 然后对栈顶节点,也就是左子节点执行相同的操作,直到所有的左子树都遍历完了,再遍历右子树
     * 栈空了说明所有节点都遍历并且弹出了
     */
    Stack<Node> stack = new Stack<>();
    stack.push(root);

    while (!stack.isEmpty()) {

        Node cur = stack.pop();
        System.out.println(cur.e);

        if (cur.right != null) {
            stack.push(cur.right);
        }

        if (cur.left != null) {
            stack.push(cur.left);
        }
    }
}

中序遍历和后序遍历的非递归实现更复杂,实际应用也不广

层序遍历(广度优先遍历BFS)

根节点为第0层

使用队列实现广度优先遍历,采用非递归方式

public void levelOrder(){

    levelOrder(root);
}

public void levelOrder(Node root) {

    /**
     * 层序遍历,每次节点出队后,先后将自己的左右子节点入队
     * 然后对队首节点,也就是左子节点执行相同的操作,再让左子节点的左右子节点入队
     * 队列空了说明所有节点都遍历并且出队了
     */
    Queue<Node> queue = new LinkedList<>();
    queue.add(root);

    while (!queue.isEmpty()) {

        Node cur = queue.remove();
        System.out.println(cur.e);

        if (cur.left != null) {
            queue.add(cur.left);
        }

        if (cur.right != null) {
            queue.add(cur.right);
        }
    }
}

常用于算法设计中——最短路径