zoukankan      html  css  js  c++  java
  • 数据结构-树

    一、回顾基本概念

    image-20211020095334628

    每个结点有零或多个子节点,没有父结点的结点后称为根结点,除了根结点每个子结点可以分为多个不相交的子树。

    结点层次:根结点为第一层,其子结点为第二层,依次递推

    结点深度:从根结点向下累加

    结点高度:从叶结点向上累加

    树的高度:为结点最大层数,图中为5

    森林:互不相交的树的集合(把根节点去掉就是森林了)

    二叉树

    每个结点最多有两棵子树,且结点有左右之分。二叉树的左子树和右子树也是二叉树。根据二叉树的状态又有几个特殊的子树。

    斜二叉树

    image-20211020100616872

    这也是后续要尽量避免的情形,失去了树的意义。

    满二叉树

    高度h,则结点为2h-1的树为满二叉树,也就是每一层都是满的。

    image-20211020100944283

    完全二叉树

    不一定满,只有最后一层可能不满,且不会有间隔。

    image-20211020101215620

    二叉查找树

    别名:二叉搜索树、二叉排序树、BST

    一个结点A,左边结点要么为空,要么小于结点A;右边结点要么为空,要么大于结点A。没有键值相等的结点。

    上图就是一个二叉查找树。

    平衡二叉树

    特殊的二叉搜索树,一个结点左右子树高度差不超过1。上图其实还是一个平衡二叉树。

    二、二叉树的遍历

    存储方式

    二叉树的存储有如下方法:

    数组:适合完全二叉树,普通的二叉树(可能是斜二叉树)空间浪费大。

    父结点指针子结点指针。本文选用子结点指针。

    class Node {
        int data;
        Node left;
        Node right;
    }
    

    前、中、后序遍历

    先序:考察到一个节点后,即刻输出该节点的值,并继续遍历其左右子树。(根左右)

    中序:考察到一个节点后,将其暂存,遍历完左子树后,再输出该节点的值,然后遍历右子树。(左根右)

    后序:考察到一个节点后,将其暂存,遍历完左右子树后,再输出该节点的值。(左右根)

    以二叉查找树为例。BST首先应该有根结点和插入方法。二叉查找树插入:小于当前结点插入左边,否则右边。

    public class BST {
        // 根结点
        Node root = null;
        // 插入二叉搜索树
        public boolean insert(int data) {
            if(root==null) {
                root = new Node(data);
                return true;
            }
            Node n = root;
            while (true) {
                if (data<n.data) {="" if(n.left="=null)" n.left="new" node(data);="" return="" true;="" }="" else="" n="n.left;" if(data="=" n.data)="" bst没有键值相等的="" false;="" if(n.right="=null)" n.right="new" ```="" 遍历接口,选择前中后序遍历。="" ```java="" public="" void="" traverse(string="" method)="" switch="" (method)="" case="" "preorder":="" preordertraverse(root);="" break;="" "postorder":="" postordertraverse(root);="" "inorder":="" inordertraverse(root);="" default:="" system.out.println("preorder、postorder、inorder,输入无效,按preorder非递归遍历");="" preordertraverse();="" 测试插入:="" static="" main(string[]="" args)="" bst="" bst();="" bst.insert(3);="" bst.insert(6);="" bst.insert(9);="" bst.insert(2);="" bst.insert(1);="" bst.insert(10);="" bst.insert(7);="" bst.traverse("inorder");="" 1="" 2="" 3="" 6="" 7="" 9="" 10="" 如图:="" ![image-20211020113408623](https:="" gitee.com="" hqinglau="" img="" raw="" master="" 20211020113408.png)="" ####="" 前序遍历="" 考察到一个节点后,即刻输出该节点的值,并继续遍历其左右子树。(根左右)="" 递归方法比较明了,如下:="" private="" preordertraverse(node="" n)="" if(n="=null)" return;="" system.out.println(n.data);="" preordertraverse(n.left);="" preordertraverse(n.right);="" 非递归方法:输出一个结点的值后,要遍历其左子树,完了之后还要回来遍历它的右子树,所以应该有个栈存放遍历过的结点。="" 非递归方法:="" preordertraverse()="" if(root="=null)" stack<node=""> stack = new Stack<>();
        System.out.println(root.data); // 输出
        stack.push(root); // 压栈,遍历左子树,完了弹出栈,遍历右子树
        Node cur = root;
        while(true) {
            if(cur.left!=null) {
                System.out.println(cur.left.data);
                stack.push(cur.left);
                cur = cur.left;
            }
            else {
                do {
                    if(stack.empty()) {
                        return;
                    }
                    cur = stack.pop().right;
                } while (cur==null);
                System.out.println(cur.data);
                stack.push(cur);
            }
        }
    }
    

    如图:

    image-20211020131428109

    中序遍历

    考察到一个节点后,将其暂存,遍历完左子树后,再输出该节点的值,然后遍历右子树。(左根右)

    递归方法:

    private void inOrderTraverse(Node n) {
        if(n==null) {
            return;
        }
        inOrderTraverse(n.left);
        System.out.println(n.data);
        inOrderTraverse(n.right);
    }
    

    非递归方法:

    private void inOrderTraverse() {
        if(root==null) {
            return;
        }
        Stack<node> stack = new Stack<>();
        stack.push(root); // 压栈,遍历左子树,打印当前结点,完了弹出栈,遍历右子树
        Node cur = root;
        while(true) {
            if(cur.left!=null) {
                stack.push(cur.left);
                cur = cur.left;
            }
            else {
                do {
                    if(stack.empty()) {
                        return;
                    }
                    Node n = stack.pop();
                    System.out.println(n.data);
                    cur = n.right;
                } while (cur==null);
                stack.push(cur);
            }
        }
    }
    

    后序遍历

    递归方法:

    private void postOrderTraverse(Node n) {
        if(n==null) {
            return;
        }
        postOrderTraverse(n.left);
        postOrderTraverse(n.right);
        System.out.println(n.data);
    }
    

    层序遍历

    用队列存一层的结点。读的时候从头依次读取,左右子结点依次存取。

    public void levelOrderTraverse() {
        if(root==null) {
            return;
        }
        Queue<node> stack = new LinkedList<>();
        stack.add(root); // 压栈,遍历左子树,完了弹出栈,遍历右子树
    
        int lastLayer = 1;
        int n = 0;
        while(!stack.isEmpty()) {
            for(int i=0;i<lastlayer;i++) {="" node="" system.out.println(node.data);="" if(node.left!="null)" n++;="" stack.add(node.left);="" }="" if(node.right!="null)" stack.add(node.right);="" lastlayer="n;" n="0;" ```="" ##="" 三、二叉搜索树="" ###="" 二叉搜索树="" ####="" 插入="" 见第二节的存储方式部分。="" 删除="" 如要删除6结点="" <img="" src="https://gitee.com/hqinglau/img/raw/master/img/20211020160635.png" alt="image-20211020160635608" style="zoom:80%;">
    
    按照递归思路比较容易,找到了就处理,找不到就分配给子函数处理。找到之后,如果是左树为空或右树为空,直接挂上,都不为空需要找到右子树的最小结点,赋值给根结点,然后删除最小结点。
    
    ```java
    private Node remove(Node r, int data) {
        if(r==null) return null;
        if(data>r.data) {
            r.right = remove(r.right,data);
        } else if(data<r.data) {="" r.left="remove(r.left,data);" }="" else="" root="" 的data="=" data="" if(r.left="=null)" r="r.right;" if="" (r.right="=" null)="" 把右子树最小结点提到根结点,然后删除这个最小结点="" node="" minnode="r.right;" while(minnode.left!="null)" r.data="minNode.data;" r.right="remove(r.right,minNode.data);" return="" r;="" ```="" ####="" 查找="" 小往左子树找,大往右子树找。最大查找时间为树的高度。="" ###="" 平衡二叉树="" 普通二叉搜索树问题:极端情况会退化成线性。="" ![image-20211020133555445](https:="" gitee.com="" hqinglau="" img="" raw="" master="" 20211020133555.png)="" 平衡二叉树在修改时借助**旋转操作**,左右子树高度差不超过1,避免了这一问题。如图所示,要在该树种插入9,会破坏平衡:="" ![image-20211020163051980](https:="" 20211020163052.png)="" 旋转="" ![image-20211020164728884](https:="" 20211020164728.png)="" 左旋:以**右子树结点为轴**,动左边的x结点,旋转过程接收y的左子树作为右结点。右旋同理。="" 上图中,左旋之后,a的深度+1,c的深度-1。所以:**可以通过合适的左旋和右旋操作,调整二叉树的深度。**="" ll插入="" 对的左儿子的左子树进行一次插入(左左):根结点右旋="" ![image-20211020165340908](https:="" 20211020165340.png)="" lr插入="" lr问题,对根结点左儿子右子树的插入问题:="" <img="" src="https://gitee.com/hqinglau/img/raw/master/img/20211020172401.png" alt="image-20211020172401514" style="zoom:80%;">
    
    #### RR插入
    
    LL插入的镜像问题,右边可能过深,对整体来一次左旋。
    
    #### RL插入
    
    LR问题的镜像问题,先对右子树右旋,再整体左旋。
    
    #### 代码实现
    
    按常规插入后调整。
    
    ```java
    public class AVL {
        Node root;
    
        public int getHeight(Node r) {
            if (r == null) return 0;
            return Math.max(getHeight(r.left), getHeight(r.right)) + 1;
        }
    
        public Node rotateLeft(Node r) {
            Node t = r.right;
            r.right = t.left;
            t.left = r;
            return t;
        }
    
        public Node rotateRight(Node r) {
            Node t = r.left;
            r.left = t.right;
            t.right = r;
            return t;
        }
    
        public void insert(int data) {
            root = insert(root,data);
            Node left = root.left;
            Node right = root.right;
            if(left!=null) {
                if(getHeight(left.left)-getHeight(left.right)>=2) {
                    root.left =  rotateRight(left);
                } else if(getHeight(left.right)-getHeight(left.left)>=2) {
                    root.left = rotateLeft(left);
                }
            }
            if(right!=null) {
                if(getHeight(right.left)-getHeight(right.right)>=2) {
                    root.right=rotateRight(right);
                } else if(getHeight(right.right)-getHeight(right.left)>=2) {
                    root.right=rotateLeft(right);
                }
            }
            if(getHeight(left)-getHeight(right)>=2) {
                root=rotateRight(root);
            }  else if(getHeight(right)-getHeight(left)>=2) {
                root=rotateLeft(root);
            }
        }
    
        public Node insert(Node r, int data) {
            if (r == null) {
                return new Node(data);
            }
            if (data > r.data) {
                r.right = insert(r.right, data);
            } else if (data < r.data) {
                r.left = insert(r.left, data);
            }
            return r;
        }
    }
    

    四、红黑树简述

    image-20211020201047683

    有了平衡二叉树,为何还要红黑树?

    AVL插入时,几乎都需要旋转操作维持平衡。红黑树牺牲严格的平衡,换取插入/删除时的性能。

    红黑树规则

    规则一:结点非黑即红

    规则二:根结点为黑色

    规则三:叶结点的子结点(null)为黑色

    规则四:红色结点的子结点为黑色

    规则五:每个结点到null结点的所有路径,包含相同数目的黑色结点(相同的黑色高度)

    如上图中17nil结点,黑色结点数都是2。

    这5条约束保证了:从根结点到叶子结点的最长路径,不会超过最短路径的两倍。由规则5具有相同的黑色高度,最短路径就全是黑色,最长路径黑红相间,而红色子结点为黑色,即不大于最短路径的两倍。

    三种变换

    插入后不破坏规则,不需要旋转变色,破坏规则需要调整。

    image-20211020203100266

    变色

    插入结点一般选择红色,因为黑色由于规则五:相同黑色高度问题,很难调整。红红相连问题可通过变色和旋转解决。

    左旋

    image-20211020203330311

    和之前的平衡二叉树左旋、右旋是一样的。

    右旋

    image-20211020203437203

    参考文献

    二叉树遍历(先序、中序、后序)

    数据结构:树(Tree)【详解】

    数据结构和算法可视化工具

    平衡二叉树(树的旋转)

    https://visualgo.net/en

    红黑树详解

  • 相关阅读:
    nvelocity的Foreach 中使用DataTable数据
    好的博客链接收集
    Uber Shaders
    DOF
    纹理过滤模式中的Bilinear、Trilinear以及Anistropic Filtering
    开通博客了
    代码中的隐式转换问题
    使用C++/CLI封装引用BOOST库的DLL造成的问题
    在Free Radius的PAP认证过程中使用MD5密码
    (转)测试CPU是大端Big endian 还是小端Little Endian的C代码
  • 原文地址:https://www.cnblogs.com/hqinglau/p/15430679.html
Copyright © 2011-2022 走看看