数据结构与算法:树及二叉树

树(Tree)

什么是“树”?再完备的定义,都没有图直观。所以我在图中画了几棵“树”。你来看看,这些“树”都有什么特征?

在这里插入图片描述

树满足递归定义的特性。也就是说,如果一个数据结构是树结构,那么剔除掉根结点后,得到的若干个子结构也是树,通常称作子树。

在一棵树中,根据结点之间层次关系的不同,对结点的称呼也有所不同。我们来看下面这棵树,如下图所示:

A 结点是 B 结点和 C 结点的上级,则 A 就是 B 和 C 的父结点,B 和 C 是 A 的子结点。

B 和 C 同时是 A 的“孩子”,则可以称 B 和 C 互为兄弟结点。

A 没有父结点,则可以称 A 为根结点。

G、H、I、F 结点都没有子结点,则称 G、H、I、F 为叶子结点。

在这里插入图片描述

当有了一棵树之后,还需要用深度、层来描述这棵树中结点的位置。结点的层次从根结点算起,根为第一层,根的“孩子”为第二层,根的“孩子”的“孩子”为第三层,依此类推。树中结点的最大层次数,就是这棵树的树深(称为深度,也称为高度)。如下图所示,就是一棵深度为 4 的树。

在这里插入图片描述

二叉树

在树的大家族中,有一种被高频使用的特殊树,它就是二叉树。在二叉树中,每个结点最多有两个分支,即每个结点最多有两个子结点,分别称作左子结点和右子结点。

在二叉树中,有下面两个特殊的类型,如下图所示:

  • 满二叉树,定义为除了叶子结点外,所有结点都有 2 个子结点。

  • 完全二叉树,定义为除了最后一层以外,其他层的结点个数都达到最大,并且最后一层的叶子结点都靠左排列。

在这里插入图片描述

你可能会困惑,完全二叉树看上去并不完全,但为什么这样称呼它呢?这其实和二叉树的存储有关系。存储二叉树有两种办法,一种是基于指针的链式存储法,另一种是基于数组的顺序存储法。

链式存储法,也就是像链表一样,每个结点有三个字段,一个存储数据,另外两个分别存放指向左右子结点的指针,如下图所示:

在这里插入图片描述

顺序存储法,就是按照规律把结点存放在数组里,如下图所示,为了方便计算,我们会约定把根结点放在下标为 1 的位置。随后,B 结点存放在下标为 2 的位置,C 结点存放在下标为 3 的位置,依次类推。

在这里插入图片描述

根据这种存储方法,可以发现如果结点 X 的下标为 i,那么 X 的左子结点总是存放在 2 * i 的位置,X 的右子结点总是存放在 2 * i + 1 的位置。

之所以称为完全二叉树,是从存储空间利用效率的视角来看的。对于一棵完全二叉树而言,仅仅浪费了下标为 0 的存储位置。而如果是一棵非完全二叉树,则会浪费大量的存储空间。

我们来看如下图所示的非完全二叉树,它既需要保留出 5 和 6 的位置。同时,还需要保留 5 的两个子结点 10 和 11 的位置,以及 6 的两个子结点 12 和 13 的位置。这样的二叉树,没有完全利用好数组的存储空间。

在这里插入图片描述

树的基本操作

在数据结构中,查找具有某个数值特性的数据需要遍历每一条数据。这在“一对一”的结构中,直接按顺序访问就好了。可是,树是“一对多”的关系,那么我们该如何进行数据的遍历,才能保证每条数据都会被访问一次且没有遗漏呢?我们只有解决了遍历问题,才能通过树来进行数据的增删查操作。

其实,遍历一棵树,有非常经典的三种方法,分别是前序遍历、中序遍历、后序遍历。这里的序指的是父结点的遍历顺序,前序就是先遍历父结点,中序就是中间遍历父结点,后序就是最后遍历父结点。不管哪种遍历,都是通过递归调用完成的。如下图所示:

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

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

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

在这里插入图片描述

/**
 * 二叉树
 */
public class BinaryTree {

    Node<String> root;

    public BinaryTree(String data) {
        //初始化根结点
        this.root = new Node<>(data, null, null);
    }


    /**
     * 结点
     */
    public class Node<T>{
        T data;//数据
        Node<T> leftChild;//左子结点
        Node<T> rightChild;//右子结点

        public Node(T data, Node<T> leftChild, Node<T> rightChild) {
            this.data = data;
            this.leftChild = leftChild;
            this.rightChild = rightChild;
        }
    }

    /**
     * 创建一棵二叉树
     */
    public void createTree() {
        Node<String> nodeB = new Node<String>("B", null, null);
        Node<String> nodeC = new Node<String>("C", null, null);
        Node<String> nodeD = new Node<String>("D", null, null);
        Node<String> nodeE = new Node<String>("E", null, null);
        Node<String> nodeF = new Node<String>("F", null, null);
        Node<String> nodeG = new Node<String>("G", null, null);
        root.leftChild = nodeB;
        root.rightChild = nodeC;
        nodeB.leftChild = nodeD;
        nodeC.leftChild = nodeE;
        nodeC.rightChild = nodeF;
    }

    /**
     * 中序遍历
     */
    public void midOrderTraverse(Node root) {
        if (root == null) {
            return;
        }
        midOrderTraverse(root.leftChild);
        System.out.println("mid: "+root.data);
        midOrderTraverse(root.rightChild);
    }


    /**
     * 前序遍历
     */
    public void preOrderTraverse(Node root) {
        if (root == null) {
            return;
        }
        System.out.println("pre: "+root.data);
        preOrderTraverse(root.leftChild);
        preOrderTraverse(root.rightChild);
    }

    /**
     * 后序遍历
     */
    public void postOrderTraverse(Node root) {
        if (root == null) {
            return;
        }
        postOrderTraverse(root.leftChild);
        postOrderTraverse(root.rightChild);
        System.out.println("post: "+root.data);
    }
}

测试类代码如下:

public class Client {
    public void test() {
        BinaryTree binaryTree = new BinaryTree("A");
        binaryTree.createTree();
        binaryTree.midOrderTraverse(binaryTree.root);
        binaryTree.preOrderTraverse(binaryTree.root);
        binaryTree.postOrderTraverse(binaryTree.root);
    }
}

不难发现,二叉树遍历过程中,每个结点都被访问了一次,其时间复杂度是 O(n)。接着,在找到位置后,执行增加和删除数据的操作时,我们只需要通过指针建立连接关系就可以了。对于没有任何特殊性质的二叉树而言,抛开遍历的时间复杂度以外,真正执行增加和删除操作的时间复杂度是 O(1)。树数据的查找操作和链表一样,都需要遍历每一个数据去判断,所以时间复杂度是 O(n)

二叉树遍历过程中,每个结点都被访问了一次,其时间复杂度是 O(n)。接着,在找到位置后,执行增加和删除数据的操作时,我们只需要通过指针建立连接关系就可以了。对于没有任何特殊性质的二叉树而言,抛开遍历的时间复杂度以外,真正执行增加和删除操作的时间复杂度是 O(1)。树数据的查找操作和链表一样,都需要遍历每一个数据去判断,所以时间复杂度是 O(n)

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值