数据结构-树

3,084 阅读11分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

树是一种数据结构,它是由n(n≥1)个有限节点组成一个具有层次关系的集合。
把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的

结构

1、结点: 是数据结构中的基础,是构成复杂数据结构的基本组成单位。

2、树(Tree): 是n(n>=0)个结点的有限集。n=0时称为空树。在任意一颗非空树中:

1)有且仅有一个特定的称为根(Root)的结点; 2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、......、Tn,其中每一个集合本身又是一棵树,并且称为根的子树。

此外,树的定义还需要强调以下两点: 1)n>0时根结点是唯一的,不可能存在多个根结点,数据结构中的树只能有一个根结点。 2)m>0时,子树的个数没有限制,但它们一定是互不相交的。

3、结点的度: 结点拥有的子树数目称为结点的

4、结点关系: 结点子树的根结点为该结点的孩子结点。相应该结点称为孩子结点的双亲结点。 同一个双亲结点的孩子结点之间互称兄弟结点

5、结点层次: 从根开始定义起,根为第一层,根的孩子为第二层,以此类推。

6、树的深度: 树中结点的最大层次数称为树的深度或高度

7、平衡因子: 此节点往下 左子树深度 - 右子树深度=平衡因子

二叉树

n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),
或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。

特点

  1. 每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
  2. 左子树和右子树是有顺序的,次序不能任意颠倒。
  3. 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

性质

  1. 在二叉树的第i层上最多有2^(i-1) 个节点 。(i>=1)
  2. 二叉树中如果深度为k,那么最多有2^k-1个节点。(k>=1)
  3. n0=n2+1 n0表示度数为0的节点数,n2表示度数为2的节点数。
  4. 在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]是向下取整。
  5. 若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性:
    1)、 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点; 
    2)、 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点; 
    3)、 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。

斜树

所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。

存储结构

顺序存储结构

使用一维数组存储二叉树中的结点,并且结点的存储位置,就是数组的下标索引。

当二叉树为完全二叉树时,结点数刚好填满数组

二叉树不为完全二叉树时,其中浅色结点表示结点不存在

二叉树.png

数组表示如下:∧表示数组中此位置没有存储结点。 顺序存储结构中已经出现了空间浪费的情况。

二叉树顺序存储结构.png

二叉链表

采用链式存储。结点数据结构定义为一个数据和两个指针域

二叉树遍历

指从二叉树的根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次,且仅被访问一次

二叉树是一种递归定义的结构,故采用递归方式遍历二叉树的代码十分简单。

二叉树的访问次序可以分为四种: 前序遍历、中序遍历、后序遍历、层序遍历

前序遍历序列和中序遍历序列,确定一棵二叉树。

后序遍历序列和中序遍历序列,确定一棵二叉树。

已知前序遍历序列和后序遍历序列,不可以唯一确定一棵二叉树。

1.png

先初始化这颗树,遍历需要

public class TreeNode<T> {
    private T data; // 节点数据
    private int level; // 层级
    private TreeNode<T> left; // 左子树
    private TreeNode<T> right; // 右子树
    public TreeNode(T data, int level) {
        this.data = data;
        this.level = level;
    }
}
public class NewTree<T> {
    private TreeNode<T> rootNode; // 根节点
    private int depth = 1; // 树深度
    private Map<String, Integer> map; // 遍历时计数用
    public static void main(String[] args)  {
        NewTree<Integer> tree = new NewTree<Integer>();
        tree.rootNode = new TreeNode<Integer>(7, 1);
        tree.add(tree.rootNode, 4);
        tree.add(tree.rootNode, 9);
        tree.add(tree.rootNode, 2);
        tree.add(tree.rootNode, 6);
        tree.add(tree.rootNode, 8);
        tree.add(tree.rootNode, 10);
        tree.add(tree.rootNode, 1);
        tree.add(tree.rootNode, 3);
        tree.add(tree.rootNode, 5);
        System.out.println("初始化链表成功!");
    }
    public void add(TreeNode<Integer> treeNode, int x) {
        int data = treeNode.getData();
        int level =  treeNode.getLevel() + 1;
        if (x >= data) {
            if (treeNode.getRight() != null) {
                this.add(treeNode.getRight(), x);
            } else {
                treeNode.setRight(new TreeNode<Integer>(x, level));
                this.depth = level > this.depth ? level : this.depth;
            }
        }
        if (x < data) {
            if (treeNode.getLeft() != null) {
                this.add(treeNode.getLeft(), x);
            } else {
                treeNode.setLeft(new TreeNode<Integer>(x, level));
                this.depth = level > this.depth ? level : this.depth;
            }
        }
    }
}

前序遍历

从二叉树的根结点出发,当第一次到达结点时就输出结点数据,按照先向左在向右的方向访问。

注: 从根结点出发,则第一次到达结点7,故输出7; 继续向左访问,第一次访问结点4,故输出4;

如上图访问如下:7 4 2 1 3 6 5 9 8 10

// 打印结果: 7 4 2 1 3 6 5 9 8 10
public void prologue(TreeNode<Integer> node) {	// 前序
    Integer x = node.getData();
    if (map.get(x) == null) {
        map.put(x, 1);
        System.out.print(x + " ");
    }
    if (node.getLeft() != null) {
        this.prologue(node.getLeft());
    }
    if (node.getRight() != null) {
        this.prologue(node.getRight());
    }
}

中序遍历

从二叉树的根结点出发,当第二次到达结点时就输出结点数据,按照先向左在向右的方向访问。

:从根结点出发,则第一次到达结点7,不输出7,继续向左访问,第一次访问结点4,不输出4;继续到达2,1; 到达1,1左子树为空,则返回到1,此时第二次访问1,故输出1;

如上图访问如下:1 2 3 4 5 6 7 8 9 10

// 打印结果:1 2 3 4 5 6 7 8 9 10
public void middleOrder(TreeNode<Integer> node) { // 中序
    Integer x = node.getData();
    Integer size = map.get(x);
    if (size == null) {
        map.put(x, 1);
        if (node.getLeft() != null) {
            this.middleOrder(node.getLeft());
        }
        map.put(x, 2);
        System.out.print(x + " ");
        if (node.getRight() != null) {
            this.middleOrder(node.getRight());
        }
    }
}

后序遍历

从二叉树的根结点出发,当第三次到达结点时就输出结点数据,按照先向左在向右的方向访问。

:从根结点出发,则第一次到达结点7,不输出7,继续向左访问,第一次访问结点4,不输出4;继续到达2,1;到达1,1左子树为空,则返回到1,此时第二次访问1,不输出1;1右子树为空,则返回至1,此时第三次到达1,故输出1;

如上图访问如下:1 3 2 5 6 4 8 10 9 7

// 最终结果:1 3 2 5 6 4 8 10 9 7 
public void postSequence(TreeNode<Integer> node) { // 后序
    Integer x = node.getData();
    Integer size = map.get(x);
    if (size == null) {
        map.put(x, 1);
        if (node.getLeft() != null) {
            this.postSequence(node.getLeft());
        }
        map.put(x, 2);
        if (node.getRight() != null) {
            this.postSequence(node.getRight());
        }
        map.put(x, 3);
        System.out.print(x + " ");
    }	
}

层次遍历

按照树的层次自上而下,自左到右的遍历二叉树

如上图访问如下:7 4 9 2 6 8 10 1 3 5

// 最终结果:7 4 9 2 6 8 10 1 3 5 
public void level(NewTree<Integer> tree) {	// 层次
    tree.order(tree.rootNode);
    for (int i = 1; i <= tree.depth ; i++) {
        List<Integer> list = tree.levelMap.get(i);
        list.stream().forEach(x -> System.out.print(x + " "));
    }
}
public void order(TreeNode<Integer> node) {
    Integer x = node.getData();
    int level = node.getLevel();
    List<Integer> list = this.levelMap.get(level);
    if (list == null) {
        list = new ArrayList<Integer>();
    }
    list.add(x);
    this.levelMap.put(level, list);
    if (node.getLeft() != null) {
        this.order(node.getLeft());
    }
    if (node.getRight() != null) {
        this.order(node.getRight());
    }
}

二叉树分类

满二叉树、完全二叉树、二叉查找树、平衡二叉树、红黑树、B+Tree、B-Tree...

满二叉树

在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。

特点

1、叶子只能出现在最下一层。出现在其它层就不可能达成平衡。

2、非叶子结点的度一定是2。

3、在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。

完全二叉树

对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。

特点

1、叶子结点只能出现在最下层和次下层。

2、最下层的叶子结点集中在树的左部。

3、倒数第二层若存在叶子结点,一定在右部连续位置。

4、如果结点度为1,则该结点只有左孩子,即没有右子树。

5、同样结点数目的二叉树,完全二叉树深度最小。

:满二叉树一定是完全二叉树,但反过来不一定成立。

二叉查找树

又:二叉搜索树,二叉排序树;或是一棵空树

特点

1、所有子树上面的左节点的值都比根结点要小,右节点的值都比根结点要大

2、任意结点的左右子树也都是二叉查找树

3、通过中序遍历,将得到的是一个有序的数列

对其操作的最优的时间复杂度为O(log2n),相当于对数列进行二分查找法。最坏的时间复杂度为O(n),相当于线性查找。

平衡二叉树

又被称为AVL树(有别于AVL算法);

1、是二叉查找树最优的情况 ;

2、它很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。

3、但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

特点

1、它是一个二叉查找树

2、它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树

3、当删除、新增、修改节点上的值时,它会通过左旋或右旋的操作使二叉树保持平衡。

4、最坏的时间复杂度为O(log2n)

调整措施:单旋/双旋

在平衡搜索树中进行插入结点时,有可能会破坏整棵树的平衡。为了保证平衡不被破坏,就要根据平衡因子对一些节点进行旋转,从而来降低树的高度,这样也能保证树的平衡。

平衡因子计算在:树-结构 中有描述
单旋

1、左左情况

指根节点的平衡因子为2,根节点的左子节点平衡因子为0或1;则需要将靠近根节点且平衡因子为1的节点设为根节点进行右旋调整平衡

image.png

1、右右情况

指根节点的平衡因子为-2,根节点的右子节点平衡因子为0或-1;则需要将靠近根节点平衡因子为-1的节点设为根节点进行左旋调整平衡

双旋

1、左右情况(LR)

指根节点的平衡因子为2,左子节点平衡因子为-1;

则需要对平衡因子为-1的左子节点进行左旋调整平衡,再对根节点进行右旋

image.png

2、右左情况

指根节点的平衡因子为-2,右子节点平衡因子为1;

则需要对平衡因子为1的右子节点进行右旋调整平衡,再对根节点进行左旋

红黑树

一种特殊的二叉查找树

红黑树.jpg

特点

1、每个节点要么是红色,要么是黑色;

2、根节点永远是黑色的;

3、所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点);

4、每个红色节点的两个子节点一定都是黑色;

5、从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;(通过旋转确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。 )