zoukankan      html  css  js  c++  java
  • 七、基本数据结构(树形结构)

    一、数的概念 Tree

    树形结构
    • 如上图所示,是一个树形机构,这里面每个元素叫作“节点”,用来连线相邻节点之间的关系,叫作“父子关系”。
    • A 节点是 B 节点的父节点, B 节点是 A 节点的子节点。
    • B、 C、 D 这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。
    • 没有父节点的节点叫作根节点,也就是图中的节点 E。
    • 我们把没有子节点的节点叫作叶子节点或者叶节点,比如图中的 G、 H、 I、 J、 K、 L 都是叶子节点。
    • 节点的高度:节点到叶子节点的最长路径(边数)。
    • 节点的深度:根节点到这个节点所经历的边的个数。
    • 节点的层数:节点的深度 + 1.
    • 树的高度:就是根节点的高度。

    二、二叉树

    2.1、二叉树介绍

    • 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。

    • 不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。

    • 二叉树
    • 编号2的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫作满二叉树

    • 编号3的二叉树中,叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫作完全二叉树

    • 完全二叉树

    2.2、二叉树的存储

    存储一棵二叉树,有两种方法,一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。

    1.链式存储法

    • 链式存储比较简单、直观。
    • 如下图所示,每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。
    • 只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。
    • 这种存储方式我们比较常用。大部分二叉树代码都是通过这种结构来实现的。
    • 链式存储

    2.基于数组的顺序存储

    • 如下图所示,把根节点存储在下标 i = 1 的位置,那左子节点存储在下标 2 * i = 2 的位置,右子节点存储在 2 * i + 1 = 3 的位置。

    • 以此类推, B 节点的左子节点存储在 2 * i = 2 * 2 = 4 的位置,右子节点存储在 2 * i + 1 = 2 * 2 + 1 = 5 的位置。

    • 如果节点 X 存储在数组中下标为 i 的位置,下标为 2 * i 的位置存储的就是左子节点,下标为 2 * i + 1 的位置存储的就是右子节点。

    • 反过来,下标为 i/2 的位置存储就是它的父节点。

    • 通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为1的位置),这样就可以通过下标计算,把整棵树都串起来。

    2.3。二叉树的遍历

    • 前序遍历:对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。

    • 中序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。

    • 后序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

    • 二叉树的遍历
    • 实际上,二叉树的前、中、后序遍历就是一个递归的过程。

    • 比如,前序遍历,其实就是先打印根节点,然后再递归地打印左子树,最后递归地打印右子树。

    • 前序遍历的递推公式:preOrder(r) = print r->preOrder(r->left)->preOrder(r->right)。

    • 中序遍历的递推公式:inOrder(r) = inOrder(r->left)->print r->inOrder(r->right)

    • 后序遍历的递推公式:postOrder(r) = postOrder(r->left)->postOrder(r->right)->print r

    • 从上面的前、中、后序遍历的顺序图,可以看出来,每个节点最多会被访问两次,所以遍历操作的时间复杂度,跟节点的个数 n 成正比,也就是说二叉树遍历的时间复杂度是 O(n)

    static class Node {
        Node rightNode;
        Node leftNode;
        String data;
    }
    
    /**
     * 先序遍历,先打印本身,在打印左节点,在打印右节点
     *
     * @param node
     */
    public static void preOrderTraverse(Node node) {
        if (node == null) {
            return;
        }
        System.out.print(node.data + " ");
        preOrderTraverse(node.leftNode);
        preOrderTraverse(node.rightNode);
    }
    
    /**
     * 中序遍历,先打印左节点,在打印本身,在打印右节点
     *
     * @param node
     */
    public static void inOrderTraverse(Node node) {
        if (node == null) {
            return;
        }
        inOrderTraverse(node.leftNode);
        System.out.print(node.data + " ");
        inOrderTraverse(node.rightNode);
    }
    
    /**
     * 后序遍历,先打印左节点,在打印右节点,在打印自己
     *
     * @param node
     */
    public static void postOrderTraverse(Node node) {
        if (node == null) {
            return;
        }
        preOrderTraverse(node.rightNode);
        preOrderTraverse(node.leftNode);
        System.out.print(node.data + " ");
    
    }
    
    /**
     * 数的层级遍历
     *
     * @param root
     */
    public static void levelTraverse(Node root) {
        if (root == null) {
            return;
        }
    
        LinkedList<Node> queue = new LinkedList<Node>();
        Node current = null;
        // 根节点入队
        queue.offer(root);
    
        // 左侧数的深度
        int leftNum = 0;
        // 右侧数的深度
        int rightNum = 0;
    
        // 只要队列中有元素,就可以一直执行,非常巧妙地利用了队列的特性
        while (!queue.isEmpty()) {
            // 出队队头元素
            current = queue.poll();
            System.out.print("-->" + current.data);
            // 左子树不为空,入队
            if (current.leftNode != null) {
                queue.offer(current.leftNode);
                leftNum++;
            }
    
            // 右子树不为空,入队
            if (current.rightNode != null) {
                queue.offer(current.rightNode);
                rightNum++;
            }
        }
        System.out.println(rightNum + "	" + leftNum);
    }
    

    三、二叉查找树(Binary Search Tree)

    • 二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。
    • 二叉查找树是为了实现快速查找而生的。
    • 它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。
    • 二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值
    • 二叉查找树最大的特点就是,支持动态数据集合的快速插入、删除、查找操作。
    • 中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度是 O(n),非常高效。
    • 二叉查找树

    3.1、二叉查找树的查找操作

    • 先取根节点,如果它等于要查找的数据,就返回。
    • 如果要查找的数据比根节点的值小,那就在左子树中递归查找;
    • 如果要查找的数据比根节点的值大,那就在右子树中递归查找。
    • 二叉树查找
    public class BinarySearchTree {
    
        private Node tree;
    
        public Node find(int data) {
            Node p = tree;
            while (p != null) {
                if (data < p.data) p = p.leftNode;
                else if (data > p.data) p = p.rightNode;
                else return p;
            }
            return null;
        }
    
    
        class Node {
            private int data;
            private Node leftNode;
            private Node rightNode;
    
            public Node(int data) {
                this.data = data;
            }
        }
    }
    

    3.2、二叉查找树的插入操作

    • 二叉查找树的插入过程有点类似查找操作。
    • 新插入的数据一般都是在叶子节点上,所以只需要从根节点开始,依次比较要插入的数据和节点的大小关系。
    • 如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;
    • 如果不为空,就再递归遍历右子树,查找插入位置。
    • 同理,如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置。
      二叉树插入
    /**
     * 插入操作
     *
     * @param data
     * @return
     */
    public Boolean insert(int data) {
        if (tree == null) {
            tree = new Node(data);
            return true;
        }
        Node p = tree;
        while (p != null) {
            if (data > p.data) {
                // 插入右节点
                if (p.rightNode == null) {
                    p.rightNode = new Node(data);
                    return true;
                }
                p = p.rightNode;
            } else {
                // 插入 左节点
                if (p.leftNode == null) {
                    p.leftNode = new Node(data);
                    return true;
                }
                p = p.leftNode;
            }
        }
        return false;
    }
    

    3.3、二叉查找树的删除操作

    • 针对要删除节点的子节点个数的不同,需要分三种情况来处理。
    • 第一种情况是,如果要删除的节点没有子节点
      • 只需要直接将父节点中,指向要删除节点的指针置为 null。
      • 比如图中的删除节点 55。
    • 第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点)
      • 只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。
      • 比如图中的删除节点 13。
    • 第三种情况是,如果要删除的节点有两个子节点
      • 需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。
      • 然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了)。
      • 所以,可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。
    • 删除
    /**
     * 删除
     * @param data
     */
    public void delete(int data) {
        // p指向要删除的节点,初始化指向根节点
        Node p = tree;
        // pp记录的是p的父节点
        Node pp = null;
    
        // 查找要删除的节点位置,及其父节点
        while (p != null && p.data != data) {
            pp = p;
            if (data > p.data) {
                p = p.rightNode;
            } else {
                p = p.leftNode;
            }
        }
        if (p == null) {
            return;// 没有找到
        }
        // 要删除的节点有两个子节点
        if (p.leftNode != null && p.rightNode != null) {
            // 查找右子树中最小节点
            Node minp = p.rightNode;
            Node minpp = p; // minPP表示minP的父节点
            while (minp.leftNode != null) {
                minpp = minp;
                minp = minp.leftNode;
            }
            // 将 minp 的数据替换到 p 中
            p.data = minp.data;
            // 下面就变成了删除 minp 了
            p = minp;
            pp = minpp;
        }
        // 删除节点是叶子节点或者仅有一个子节点
        Node child; // p 的子节点
        if (p.leftNode != null) {
            child = p.leftNode;
        } else if (p.rightNode != null) {
            child = p.rightNode;
        } else {
            child = null;
        }
        if (pp == null) {
            // 删除的是根节点
            tree = child;
        } else if (pp.leftNode == p) {
            pp.leftNode = child;
        } else {
            pp.rightNode = child;
        }
    }
    
    • 实际上,关于二叉查找树的删除操作,还有个非常简单、取巧的方法,就是单纯将要删除的节点标记为“已删除”,但是并不真正从树中将这个节点去掉
    • 这样原本删除的节点还需要存储在内存中,比较浪费内存空间,但是删除操作就变得简单了很多。
    • 而且,这种处理方法也并没有增加插入、查找操作代码实现的难度。

    3.4、支持重复数据的二叉查找树

    • 上文提到的二叉查找树,默认树中节点存储的都是数字。很多时候,在二叉查找树中存储的,是一个包含很多字段的对象。
    • 利用对象的某个字段作为键值(key)来构建二叉查找树。把对象中的其他字段叫作卫星数据。
    • 前面讲的二叉查找树的操作,针对的都是不存在键值相同的情况。
    • 那如果存储的两个对象键值相同,解决方法如下:
      • 第一种方法比较容易。二叉查找树中每一个节点不仅会存储一个数据,因此通过链表和支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节
        点上。

      • 第二种方法是,每个节点仍然只存储一个数据。

      • 在查找插入位置的过程中,如果碰到一个节点的值,与要插入数据的值相同,就将这个要插入的数据放到这个节点的右子树,也就是说,把这个新插入的数据当作大于这个节点的值来处理。

      • 当要查找数据的时候,遇到值相同的节点,并不停止查找操作,而是继续在右子树中查找,直到遇到叶子节点,才停止。这样就可以把键值等于要查找值的所有节点都找出来。

      • 对于删除操作,也需要先查找到每个要删除的节点,然后再按前面讲的删除操作的方法,依次删除。

    3.5、二叉查找树的时间复杂度分析

    • 二叉查找树的形态各式各样。如下图所示,对于同一组数据,构造了三种二叉查找树。它们的查找、插入、删除操作的执行效率都是不一样的。

    • 图中第一种二叉查找树,根节点的左右子树极度不平衡,已经退化成了链表,所以查找的时间复杂度就变成了 O(n)。

    • 从前面的例子、图,以及还有代码来看,不管操作是插入、删除还是查找, 时间复杂度其实都跟树的高度成正比,也就是 O(height)。

    • 既然这样,现在问题就转变成另外一个了,也就是,如何求一棵包含 n 个节点的完全二叉树的高度?

    • 树的高度就等于最大层数减一,为了方便计算,转换成层来表示。

    • 从图中可以看出,包含 n 个节点的完全二叉树中,第一层包含1个节点,第二层包含2个节点,第三层包含4个节点,依次类推,下面一层节点个数是上一层的 2 倍,第 K 层包含的节点个数就是 2^(K-1)。

    • 不过,对于完全二叉树来说,最后一层的节点个数有点儿不遵守上面的规律了。它包含的节点个数在 1 个到 2^(L-1) 个之间(假设最大层数是L)。

    • 如果我们把每一层的节点个数加起来就是总的节点个数 n。也就是说,如果节点的个数是 n,那么 n 满足这样一个关系:

    • n >= 1+2+4+8+...+2^(L-2)+1

    • n <= 1+2+4+8+...+2^(L-2)+2^(L-1)

    • 借助等比数列的求和公式,我们可以计算出,L 的范围是 [log2(n+1), log2n +1]。完全二叉树的层数小于等于 log2n +1,也就是说,完全二叉树的高度小于等于 log2n。

    • 显然,极度不平衡的二叉查找树,它的查找性能肯定不能满足需求。

    • 需要构建一种不管怎么删除、插入数据,在任何时候,都能保持任意节点左右子树都比较平衡的二叉查找树,这就是平衡二叉查找树。

    • 平衡二叉查找树的高度接近 logn,所以插入、删除、查找操作的时间复杂度也比较稳定,是 O(logn)。

    四、红黑树

    • 二叉查找树是最常用的一种二叉树,它支持快速插入、删除、查找操作,各个操作的时间复杂度跟树的高度成正比,理想情况下,时间复杂度是 O(logn)。
    • 不过,二叉查找树在频繁的动态更新过程中,可能会出现树的高度远大于 log2n 的情况,从而导致各个操作的效率下降。
    • 极端情况下,二叉树会退化为链表,时间复杂度会退化到 O(n)。
    • 要解决这个复杂度退化的问题,需要设计一种平衡二叉查找树,比如红黑树。

    4.1、平衡二叉查找树

    • 平衡二叉树的严格定义是这样的:二叉树中任意一个节点的左右子树的高度相差不能大于 1

    • 从这个定义来看,完全二叉树、满二叉树其实都是平衡二叉树,但是非完全二叉树也有可能是平衡二叉树。

    • 平衡二叉查找树
    • 平衡二叉查找树不仅满足上面平衡二叉树的定义,还满足二叉查找树的特点。

    • 最先被发明的平衡二叉查找树是 AVL 树,它严格符合刚讲到的平衡二叉查找树的定义,即任何节点的左右子树高度相差不超过 1,是一种高度平衡的二叉查找树。

    • 但是很多平衡二叉查找树其实并没有严格符合上面的定义(树中任意一个节点的左右子树的高度相差不能大于1),比如红黑树,它从根节点到各个叶子节点的最长路径,有可能会比最短路径大一倍。

    • 发明平衡二叉查找树这类数据结构的初衷是,解决普通二叉查找树在频繁的插入、删除等动态更新的情况下,出现时间复杂度退化的问题。

    • 所以, 平衡二叉查找树中“平衡”的意思,其实就是让整棵树左右看起来比较“对称”、比较“平衡”,不要出现左子树很高、右子树很矮的情况。

    • 这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些。

    • 所以,如果现在设计一个新的平衡二叉查找树,只要树的高度不比 log2n 大很多(比如树的高度仍然是对数量级的),尽管它不符合严格的平衡二叉查找树的定义,但仍然可以说,这是一个合格的平衡二叉查找树。

    4.2、红黑树

    • 平衡二叉查找树其实有很多,比如, Splay Tree(伸展树)、 Treap(树堆)等,但是提到平衡二叉查找树,听到的基本都是红黑树。
    • 他的出镜率甚至要高于“平衡二叉查找树”这几个字,有时候,甚至默认平衡二叉查找树就是红黑树。
    • 红黑树的英文是“Red-Black Tree”,简称R-B Tree。它是一种不严格的平衡二叉查找树,它的定义是不严格符合平衡二叉查找树的定义的。
    • 顾名思义,红黑树中的节点,一类被标记为黑色,一类被标记为红色。除此之外,一棵红黑树还需要满足这样几个要求:
      • 根节点是黑色的;
      • 每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据;
      • 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
      • 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;
      • 这里的第二点要求“叶子节点都是黑色的空节点”,它主要是为了简化红黑树的代码实现而设置的。
      • 下图中将黑色的、空的叶子节点都省略掉了。

    1.为什么说红黑树是“近似平衡”的?

    • 平衡二叉查找树的初衷,是为了解决二叉查找树因为动态更新导致的性能退化问题。所以,“平衡”的意思可以等价为性能不退化。

    • “近似平衡”就等价为性能不会退化的太严重。

    • 二叉查找树很多操作的性能都跟树的高度成正比。

    • 一棵极其平衡的二叉树(满二叉树或完全二叉树)的高度大约是 log2n,所以如果要证明红黑树是近似平衡的,只需要分析,红黑树的高度是否比较稳定地趋近 log2n 就好了。

    • 如果将红色节点从红黑树中去掉,那单纯包含黑色节点的红黑树的高度是多少呢?

    • 红色节点删除之后,有些节点就没有父节点了,它们会直接拿这些节点的祖父节点(父节点的父节点)作为父节点。所以,之前的二叉树就变成了四叉树。

    • 前面红黑树的定义里有这么一条:从任意节点到可达的叶子节点的每个路径包含相同数目的黑色节点。

    • 从四叉树中取出某些节点,放到叶节点位置,四叉树就变成了完全二叉树。所以,仅包含黑色节点的四叉树的高度,比包含相同节点个数的完全二叉树的高度还要小。

    • 完全二叉树的高度近似 log2n,这里的四叉“黑树”的高度要低于完全二叉树,所以去掉红色节点的“黑树”的高度也不会超过 log2n。

    • 现在知道只包含黑色节点的“黑树”的高度,那我们现在把红色节点加回去,高度会变成多少呢?

    • 从上面画的红黑树的例子和定义看,在红黑树中,红色节点不能相邻,也就是说,有一个红色节点就要至少有一个黑色节点,将它跟其他红色节点隔开。

    • 红黑树中包含最多黑色节点的路径不会超过l og2n,所以加入红色节点之后,最长路径不会超过 2log2n,也就是说,红黑树的高度近似 2log2n。

    • 所以,红黑树的高度只比高度平衡的AVL树的高度(log2n)仅仅大了一倍,在性能上,下降得并不多。

    • 这样推导出来的结果不够精确,实际上红黑树的性能更好。

    查看全文
  • 相关阅读:
    leetcode面试准备:Kth Largest Element in an Array
    leetcode面试准备:Minimum Size Subarray Sum
    leetcode面试准备:Valid Anagram
    leetcode面试准备:Divide Two Integers
    leetcode面试准备:Container With Most Water
    面试:归并排序和分治法
    leetcode面试准备:Lowest Common Ancestor of a Binary Search Tree & Binary Tree
    Leetcode解题思想总结篇:双指针
    leetcode面试准备: CountPrimes
    RF中BDD编写
  • 原文地址:https://www.cnblogs.com/xiexiandong/p/13060094.html
  • 最新文章
  • 20190923-08Linux压缩和解压类 000 016
    20190923-07Linux搜索查找类 000 015
    20190923-06Linux文件权限类 000 014
    20190923-05Linux用户组管理命令 000 013
    20190923-04Linux用户管理命令 000 012
    由system.currentTimeMillis() 获得当前的时间 .
    MySQL常用命令
    Gentoo安装配置过程与总结
    Centos6.5以下Samba服务配置
    端口映射的几种实现方法
  • 热门文章
  • Linux 下开放指定端口
    CentOS6.5 安装JDK1.7详细步骤参考
    GCC常用命令行一览表
    GCC笔记
    三张图看遍Linux 性能监控、测试、优化工具
    对中级Linux用户有用的20个命令
    详谈socket请求Web服务器过程
    Linux常用命令查看日志
    JQuery常用实用的事件[较容易忽略的方法]
    自定义右键菜单,禁用浏览器自带的右键菜单[右键菜单实现--Demo]
Copyright © 2011-2022 走看看

代做工资流水公司肇庆流水单制作绍兴工资银行流水查询芜湖办车贷流水济南个人工资流水 公司沧州银行对公流水多少钱保定日常消费流水费用石家庄自存银行流水代做宁德购房银行流水代做黄冈薪资流水单查询衡阳开流水单海口银行流水账制作临沂银行流水账单办理菏泽办公司流水鞍山背调银行流水图片蚌埠代做银行流水修改临沂个人流水公司郑州公司银行流水样本开封代开签证工资流水遵义薪资流水报价绵阳日常消费流水代办台州代开薪资流水单宁德查签证工资流水沧州对公账户流水报价惠州自存银行流水代办铜陵工作收入证明办理深圳代开贷款流水泉州车贷工资流水 代开东莞个人工资流水 制作北京代办薪资流水咸阳贷款工资流水 图片香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声卫健委通报少年有偿捐血浆16次猝死汪小菲曝离婚始末何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言男子被猫抓伤后确诊“猫抓病”周杰伦一审败诉网易中国拥有亿元资产的家庭达13.3万户315晚会后胖东来又人满为患了高校汽车撞人致3死16伤 司机系学生张家界的山上“长”满了韩国人?张立群任西安交通大学校长手机成瘾是影响睡眠质量重要因素网友洛杉矶偶遇贾玲“重生之我在北大当嫡校长”单亲妈妈陷入热恋 14岁儿子报警倪萍分享减重40斤方法杨倩无缘巴黎奥运考生莫言也上北大硕士复试名单了许家印被限制高消费奥巴马现身唐宁街 黑色着装引猜测专访95后高颜值猪保姆男孩8年未见母亲被告知被遗忘七年后宇文玥被薅头发捞上岸郑州一火锅店爆改成麻辣烫店西双版纳热带植物园回应蜉蝣大爆发沉迷短剧的人就像掉进了杀猪盘当地回应沈阳致3死车祸车主疑毒驾开除党籍5年后 原水城县长再被查凯特王妃现身!外出购物视频曝光初中生遭15人围殴自卫刺伤3人判无罪事业单位女子向同事水杯投不明物质男子被流浪猫绊倒 投喂者赔24万外国人感慨凌晨的中国很安全路边卖淀粉肠阿姨主动出示声明书胖东来员工每周单休无小长假王树国卸任西安交大校长 师生送别小米汽车超级工厂正式揭幕黑马情侣提车了妈妈回应孩子在校撞护栏坠楼校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变老人退休金被冒领16年 金额超20万西藏招商引资投资者子女可当地高考特朗普无法缴纳4.54亿美元罚金浙江一高校内汽车冲撞行人 多人受伤

代做工资流水公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化