《数据结构与算法之美》专栏阅读笔记6——树

本文深入探讨了二叉树的基本概念、存储方式、遍历方法及二叉查找树的实现,特别介绍了红黑树的定义、性质、复杂度分析及其实现的基本思路,包括插入、删除操作的调整策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

因为从看到写笔记,中间隔了好久,木有激情啊啊啊,但是立flag了丫,你看flag对我这样的人多有用。

二叉树

树啥的,差不多是不管看什么书都很不想看的一个章节了。

树的基本概念

节点:树结构中的每个元素都称作节点。
节点的高度:节点到叶子节点的最长路径
节点的深度:根节点到该节点所经历的边的个数
节点的层数:节点深度+1
树的高度:根节点的高度

二叉树的基本概念

二叉树:最多两个子结点。左子节点、右子节点
满二叉树:除了叶子节点外,每个节点都有左右两个子结点。
完全二叉树:最后一层的叶子节点都靠左排列,除了最后一层,其他层的节点个数都要达到最大。

二叉树的存储
链式存储

基于链表进行存储。每个节点三个字段,存储数据、左右子结点的指针。

顺序存储

基于数组进行存储。根节点存储在i = 1的位置,根节点下标为i,则其左子节点存储在下标为2*i的位置,右子节点存储在下标为2*i+1的位置。

二叉树的遍历

下面的描述都是左子树、右子树,不是左子节点、右子节点。
前序遍历:对树中的任意节点来说,先打印当前节点,再打印左子树,最后打印右子树
中序遍历:对树中的任意节点来说,先打印左子树,再打印当前节点,最后打印右子树
后序遍历:对树中的任意节点来说,先打印右子树,再打印当前节点,最后打印左子树

二叉查找树

为了实现快速查找,支持快速查找、插入、删除数据。
要求:在树中的任意一个节点,其左子树的每个节点的值小于该节点,右子树节点的值大于该节点。

实现

二叉查找树的插入、查找、删除、前序遍历、中序遍历、后序遍历,按层遍历的实现如下

public class BinSearchTree<T extends Comparable<T>> {
    static class Node<T> {
        T val;
        Node<T> left;
        Node<T> right;
        Node(T value) {
            val = value;
        }

        public Node(T value, Node left, Node right) {
            this.val = value;
            this.left = left;
            this.right = right;
        }

        @Override
        public String toString() {
            return String.valueOf(val);
        }
    }

    private Node root;
    BinSearchTree(T value) {
        root = new Node(value);
    }

    public void insert(T value) {
        Node<T> node = new Node(value);

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

        Node<T> p = root;
        while(p != null) {
            if (value.compareTo(p.val) > 0) {
                if (p.right == null) {
                    p.right = node;
                    return;
                }
                p = p.right;
            } else {
                if (p.left == null) {
                    p.left = node;
                    return;
                }
                p = p.left;
            }
        }
    }

    public Node<T> find(T value) {
        Node<T> p = root;
        while(p != null) {
            if (value.compareTo(p.val) < 0) {
                p = p.left;
            } else if (value.compareTo(p.val) > 0) {
                p = p.right;
            } else {
                return p;
            }
        }
        return null;
    }

    public void delete(T value) {
        Node<T> p = root;
        Node<T> pp = null;
        while (p != null && p.val != value) {
            pp = p;
            if (value.compareTo(p.val) > 0) {
                p = p.right;
            } else {
                p = p.left;
            }
        }

        if (p == null) {
            return;
        }

        if (p.left != null && p.right != null) {
            Node<T> minP = p.right;
            Node<T> minPP = p;
            while (minP.left != null) {
                minPP = minP;
                minP = minP.left;
            }
            p.val = minP.val;
            p = minP;
            pp = minPP;
        }

        Node<T> child;
        if (p.left != null) {
            child = p.left;
        } else if (p.right != null) {
            child = p.right;
        } else {
            child = null;
        }

        if (pp == null) {
            root = child;
        } else if (pp.left == p) {
            pp.left = child;
        } else if (pp.right == p) {
            pp.right = child;
        }
    }

    Node<T> getRoot() {
        return root;
    }

    public void preOder(Node<T> root) {
        if (root == null)
            return;

        System.out.print(root + " ");
        preOder(root.left);
        preOder(root.right);
    }

    public  void inOder(Node<T> root) {
        if (root == null)
            return;

        inOder(root.left);
        System.out.print(root + " ");
        inOder(root.right);
    }

    public void postOder(Node<T> root) {
        if (root == null)
            return;

        postOder(root.right);
        postOder(root.left);
        System.out.print(root + " ");
    }

    public void layerOder(Node<T> root) {
        Queue<Node<T>> queue = new LinkedList<Node<T>>();
        queue.add(root);

        while (!queue.isEmpty()) {
            Node<T> curNode = queue.poll();
            System.out.print(curNode + " ");

            if (curNode.left != null) {
                queue.add(curNode.left);
            }
            if (curNode.right != null) {
                queue.add(curNode.right);
            }
        }
    }
}
复杂度分析

时间复杂度跟树的高度成正比。O(logn)。

红黑树

平衡查找二叉树是用来应对树在动态更新中,出现的复杂度退化的情况。

平衡查找二叉树

平衡二叉树定义:二叉树中任意一个节点的做右子树的高度差不能大于1。
平衡查找二叉树定义:平衡二叉树 + 二叉查找树

红黑树

红黑树中的节点,一类标记为黑色,一类标记为红色。此外还需满足:

  • 根节点是黑色
  • 每个叶子节点都是黑色的空节点(NIL)。叶子节点不存储数据
  • 任何相邻的节点不能同时为红色
  • 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数据的黑色节点。

平衡:树的高度稳定地趋近于log2n

【性能预推导】
我喜欢这样换个角度把问题简单化,至少在如临大敌前,打个乐观预防针。
根据据红黑树定义的最后一条,来分析最好情况下的复杂度。

去掉红色节点后的“红黑树”,看作是四叉树,比包含相同节点个数的完全二叉树的高度要小,所以复杂度不超过logn。
然后把红色节点加回去,根据红黑树定义第三条,红色节点的个数不超过黑色节点,所以最长路径不超过2logn。

红黑树相较于高度平衡的AVL树,是近似平衡。在维护平衡的成本上要低一点。

实现的基本思路

概念:

  • 左旋:围绕某个节点的左旋
  • 右旋:围绕某个节点的右旋

因为红黑树首先是一个二叉查找树,所以完之后要调整为正确的查找树。

插入

首先有个小规律记一下:二叉查找树新插入的节点都是放在叶子节点上。
红黑树规定:插入的节点必须是红色。
此时需要处理两种特殊情况:

  • 插入的是父节点,直接改颜色
  • 插入节点的父节点是黑色的,直接插入即可。

其他情况均违背红黑树的定义,需要进行调整:左右旋、改颜色。

调整策略就是这样了,不晓得为啥,但是背后肯定有一个计算公式丫,定理丫啥的在支持。

删除

分为两步:

  • 初步调整:保证树在节点删除后仍满足红黑树的最后一条规定。
  • 针对关注节点调整:保证满足红黑树的第三条定义。

初步调整

二次调整

大概就是记一下,不知道为啥要这样调之前我肯定是不会用到这些规则的,红黑树这么难撩,交给专业的人写就好啦。

递归树

用来求解时间复杂度。
(不要骂,我就看明白了这个……)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值