数据结构树(Tree)详解

树(tree)

  • 树(Tree)的基本概念
    • 定义
    • 树的结构
      • 二叉树
      • 二叉树的特点
      • 满二叉树
      • 完全二叉树
      • 二叉查找树(Binary Search Tree - BST,又称二叉排序树、二叉搜索树)
      • AVL树
  • 二叉树的存储结构
    • 二叉树的顺序存储:
    • 二叉树的链式存储结构
  • 遍历二叉树的算法
  • 层次遍历
  • 普通遍历

树(Tree)的基本概念

定义

树是由结点或顶点和边组成的(可能是非线性的)且不存在着任何环的一种数据结构。没有结点的树称为空(null或empty)树。一棵非空的树包括一个根结点,还(很可能)有多个附加结点,所有结点构成一个多级分层结构
例如:
数据结构树(Tree)详解_第1张图片
图 1(A) 是使用树结构存储的集合 {A,B,C,D,E,F,G,H,I,J,K,L,M} 的示意图。对于数据 A 来说,和数据 B、C、D 有关系;对于数据 B 来说,和 E、F 有关系。这就是“一对多”的关系。

将具有“一对多”关系的集合中的数据元素按照图 1(A)的形式进行存储,整个存储形状在逻辑结构上看,类似于实际生活中倒着的树(图 1(B)倒过来),所以称这种存储结构为“树型”存储结构。

树的结构

结点:使用树结构存储的每一个数据元素都被称为“结点”。例如,图 1(A)中,数据元素 A 就是一个结点;

父结点(双亲结点)、子结点和兄弟结点:对于图 1(A)中的结点 A、B、C、D 来说,A 是 B、C、D 结点的父结点(也称为“双亲结点”),而 B、C、D 都是 A 结点的子结点(也称“孩子结点”)。对于 B、C、D 来说,它们都有相同的父结点,所以它们互为兄弟结点。

树根结点(简称“根结点”):每一个非空树都有且只有一个被称为根的结点。图 1(A)中,结点A就是整棵树的根结点。
树根的判断依据为:如果一个结点没有父结点,那么这个结点就是整棵树的根结点。
叶子结点:如果结点没有任何子结点,那么此结点称为叶子结点(叶结点)。例如图 1(A)中,结点 K、L、F、G、M、I、J 都是这棵树的叶子结点。此外还有以下属性:

节点的度(degree):该节点子树的个数称为该节点的度。
树的度:所有节点中,度的最大值称为树的度。
非叶子节点:度不为零的节点。
高度(height):当前节点到最远叶子节点的路径长,所有树叶的高度为零。
深度(depth):对于任意节点n,n的深度为从根到n的唯一路径长。有些地方认为根深度为0,有些地方认为根深度为1。
兄弟节点:具有相同父节点的节点互相称为兄弟节点。
节点的层数(level):从根开始定义,根为第一层,根的子节点为第二层。以此类推。
堂兄弟节点:父节点在同一层的节点互为堂兄弟。
节点的祖先(ancestor):从根到该节点所经分支上的所有节点。
子孙(descendant):以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m >= 0)棵互不相交的树的集合称为森林。

一般来说数据结构如下(Java)


public class TreeNode {
    public T value;
    public TreeNode leftNode;
    public TreeNode rightNode;
    //public List> nodes;
}

二叉树

二叉树是每个节点最多有两个子树的树结构,左侧子树节点称为“左子树”(left subtree),右侧子树节点称为“右子树”(right subtree)。每个节点最多有2个子节点的树(即每个定点的度小于3)

二叉树的特点

至少有一个节点(根节点)

每个节点最多有两颗子树,即每个节点的度小于3。

左子树和右子树是有顺序的,次序不能任意颠倒。

即使树中某节点只有一棵子树,也要区分它是左子树还是右子树

满二叉树

除了叶子节点外每一个节点都有两个子节点,且所有叶子节点都在二叉树的同一高度上
数据结构树(Tree)详解_第2张图片

完全二叉树

如果二叉树中除去底层节点后为满二叉树,且底层节点依次从左到右分布,则此二叉树被称为完全二叉树。

数据结构树(Tree)详解_第3张图片

二叉查找树(Binary Search Tree - BST,又称二叉排序树、二叉搜索树)

二叉查找树根节点的值大于其左子树中任意一个节点的值,小于其右子树中任意一节点的值,且该规则适用于树中的每一个节点。

数据结构树(Tree)详解_第4张图片
二叉查找树的查询效率介于O(log n)~O(n)之间,理想的排序情况下查询效率为O(log n),极端情况下BST就是一个链表结构(如下图),此时元素查找的效率相等于链表查询O(n)。

二叉查找树需要注意的是删除节点操作时的不同情况,删除节点根据节点位置会有以下三种情况:

删除节点的度为0,则直接删除

删除节点的度为1,则该子节点替代删除节点

删除节点的度为2,则从左子树中寻找值最大的节点替代删除节点。对树结构改动最少、节点值最进行删除节点值的必然是左子树中的最大叶子节点值与右子树中的最小叶子节点值
平衡二叉搜索树 (Balanced binary search trees,又称AVL树、平衡二叉查找树)

AVL树

AVL树是最早被发明的自平衡二叉搜索树,树中任一节点的两个子树的高度差最大为1,所以它也被称为高度平衡树,其查找、插入和删除在平均和最坏情况下的时间复杂度都是O(log n)。

平衡二叉搜索树由Adelson-Velskii和Landis在1962年提出,因此又被命名为AVL树。平衡因子(平衡系数)是AVL树用于旋转平衡的判断因子,某节点的左子树与右子树的高度(深度)差值即为该节点的平衡因子。

AVL树的特点

具有二叉查找树的特点(左子树任一节点小于父节点,右子树任一节点大于父节点),任何一个节点的左子树与右子树都是平衡二叉树

任一节点的左右子树高度差小于1,即平衡因子为范围为[-1,1] 如上左图根节点平衡因子=1,为AVL树;右图根节点平衡因子=2,固非AVL树,只是BST。
为什么选择AVL树而不是BST?

大多数BST操作(如搜索、最大值、最小值、插入、删除等)的时间复杂度为O(h),其中h是BST的高度。对于极端情况下的二叉树,这些操作的成本可能变为O(n)。如果确保每次插入和删除后树的高度都保持O(log n),则可以保证所有这些操作的效率都是O(log n)。

二叉树的存储结构

二叉树的存储结构有两种,分别为顺序存储和链式存储。本节先介绍二叉树的顺序存储结构。

二叉树的顺序存储:

指的是使用顺序表(数组)存储二叉树。需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。
有读者会说,满二叉树也可以使用顺序存储。要知道,满二叉树也是完全二叉树,因为它满足完全二叉树的所有特征。

普通二叉树转完全二叉树的方法很简单,只需给二叉树额外添加一些节点,将其"拼凑"成完全二叉树即可。如图 1 所示:
数据结构树(Tree)详解_第5张图片
左侧是普通二叉树,右侧是转化后的完全(满)二叉树。
完全二叉树的顺序存储,仅需从根节点开始,按照层次依次将树中节点存储到数组即可

数据结构树(Tree)详解_第6张图片
上图的数组存储结构
存储由普通二叉树转化来的完全二叉树也是如此,例如上图普通二叉树,可以如此存储:
普通二叉树的顺序存储

二叉树的链式存储结构

数据结构树(Tree)详解_第7张图片
此为一棵普通的二叉树,若将其采用链式存储,则只需从树的根节点开始,将各个节点及其左右孩子使用链表存储即

数据结构树(Tree)详解_第8张图片

遍历二叉树的算法

数据结构树(Tree)详解_第9张图片

层次遍历

前面讲过,树是有层次的,拿图 1 来说,该二叉树的层次为 3。通过对树中各层的节点从左到右依次遍历,即可实现对正棵二叉树的遍历,此种方式称为层次遍历。
数据结构树(Tree)详解_第10张图片
代码实现:

 public static void leverErgodic(TreeNode root) {
        if (root == null) return;
        LinkedList<TreeNode<Integer>> list = new LinkedList<TreeNode<Integer>>();
        list.add(root);
        TreeNode currentNode;
        while (!list.isEmpty()) {
            currentNode = list.poll();
            System.out.println(((Integer) currentNode.value).intValue());
            if (currentNode.leftNode != null) {
                list.add(currentNode.leftNode);
            }
            if (currentNode.rightNode != null) {
                list.add(currentNode.rightNode);
            }
        }
    }

普通遍历

其实,还有一种更普通的遍历二叉树的思想,即按照 “从上到下,从左到右” 的顺序遍历整棵二叉树。
数据结构树(Tree)详解_第11张图片
普通遍历又可以分为:
中序遍历:即左-根-右遍历,对于给定的二叉树根,寻找其左子树;对于其左子树的根,再去寻找其左子树;递归遍历,直到寻找最左边的节点i,其必然为叶子,然后遍历i的父节点,再遍历i的兄弟节点。随着递归的逐渐出栈,最终完成遍历
先序遍历:即根-左-右遍历
后序遍历:即左-右-根遍历

先序遍历:

 //先序遍历
    public static void preTraver(TreeNode root) {
        if (null != root) {
            System.out.print(root.value.toString());
            
            preTraver(root.leftNode);
            
            preTraver(root.rightNode);
        }
    }

中序遍历:

  //中序遍历
    public static void midTraver(TreeNode root) {
        if (null != root) {
            midTraver(root.leftNode);
            System.out.print(root.value.toString());
            midTraver(root.rightNode);
        }
    }

后序遍历

 //后序遍历
    public static void lastTraver(TreeNode root) {
        if (null != root) {
            lastTraver(root.leftNode);
            lastTraver(root.rightNode);
            System.out.print(root.value.toString());
        }
    }

二叉查找树-插入

  //二叉 查找树 插入
    public static void insertNode(TreeNode<Integer> insert, TreeNode<Integer> root) {
        if (insert.value.intValue() == root.value.intValue()) {
            return;
        } else if (insert.value.intValue() > root.value.intValue()) { //大于根结点 ,右侧
            if (insert.rightNode == null) {
                insert.rightNode = insert;
                return;
            } else {
                insertNode(insert, root.rightNode);
            }
        } else {
            if (insert.leftNode == null) {
                insert.leftNode = insert;
                return;
            } else {
                insertNode(insert, root.leftNode);
            }
        }
    }

删除节点存在 3 种情况,分别如下:

  1. 没有左右子节点,可以直接删除
  2. 存在左节点或者右节点,删除后需要对子节点移动
  3. 同时存在左右子节点,不能简单的删除,但是可以通过和后继节点交换后转换为前两种情况
    实现代码如下:
 /**
     * 获取后继节点
     *
     * @param node
     * @return
     */
    public TreeNode getNode(TreeNode node) {
        if (!node.hR()) {
            return null; //无后继
        }

        TreeNode temp = node.rightNode;
        while (temp.hL()) {
            temp = temp.leftNode;
        }
        return temp;
    }

    TreeNode root;

    /**
     * 非相邻节点的替换逻辑(非相邻加粗!)
     *
     * @param node    被替换节点
     * @param replace 替换的节点
     */
    public void replaceNode(TreeNode node, TreeNode replace) {
        if (node.isLeft) {
            node.fNode.leftNode = replace;
        } else if (node.isRight) {
            node.fNode.rightNode = replace;
        } else {//根结点
            root = replace;
        }
        node.leftNode.fNode = node.rightNode.fNode = replace;
        replace.fNode = node.fNode;
        replace.leftNode = node.leftNode;
        replace.rightNode = node.rightNode;
    }

    public void deleteNode(TreeNode node, TreeNode root) {


        if (node.hL() && !node.hR()) {// 只有左节点
            if (node.isLeft) {
                node.fNode.leftNode = node.leftNode;
            } else if (node.isRight) {
                node.fNode.rightNode = node.leftNode;
            } else// 待删除节点是根节点
                root = node.leftNode;
            node.leftNode.fNode = node.fNode;
        } else if (node.hR() && !node.hL()) {// 只有右节点
            if (node.isLeft) {
                node.fNode.leftNode = node.rightNode;

            } else if (node.isRight) {
                node.fNode.rightNode = node.rightNode;
            } else// 待删除节点是根节点
                root = node.rightNode;
            node.rightNode.fNode = node.rightNode;
        } else if (node.hL() && node.hR()) {// 有左右子节点
            TreeNode sNode = getNode(node);
            if (sNode == node.rightNode) {// 后继节点是右子节点
                sNode.fNode = node.fNode;
                if (node.isLeft)
                    node.fNode.leftNode = sNode;
                else if (node.isRight)
                    node.fNode.rightNode = sNode;
                else {// 是根节点
                    sNode = root;
                }

                sNode.fNode = node.fNode;
                sNode.leftNode = node.leftNode;
                node.leftNode.fNode = sNode;

            } else {// 后继节点是右子节点的最左子节点
                if (sNode.hR()) {// 左子节点有右子树
                    sNode.fNode.leftNode = sNode.rightNode;
                    sNode.rightNode.fNode = sNode.fNode;
                    replaceNode(node, sNode);
                } else {// 左子节点没有右子树
                    // 叶节点,直接删除
                    sNode.fNode.leftNode = null;
                    replaceNode(node, sNode);
                }
            }

        } else {// 没有子节点
            if (node.isLeft) {
                node.fNode.leftNode = null;
            } else if (node.isRight) {
                node.fNode.rightNode = null;
            }

        }
        node = null;

    }

你可能感兴趣的:(算法,二叉树,数据结构,算法,java)