树、二叉树、二叉搜索树的实现和特性

点赞再看,养成习惯,微信搜一搜【一角钱小助手】关注更多原创技术文章。
本文 GitHub github.com/JavaStudy 已收录,有我的系列文章。

前言

本篇先回顾树、二叉树、二叉搜索树的实现和特性,《AVL 树和红黑树的实现和特性》可以阅读这篇。另外,树的面试题解法一般都是递归 为什么? 我们在后面总结。

一维结构:数组、链表、跳表、栈、队列等,这些结构都是 线性存储结构

二维结构:树、图,是一种非线性存储结构,存储的是具有 “一对多”关系的数据元素的集合。

树 Tree

树的结点

结点:使用树结构存储的每一个数据元素都被称为“结点”。

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

树根结点(简称“根结点”):每个非空树都有且只有一个被称为根的结点。

叶子结点:如果结点没有任何子结点,那么此结点称为叶子结点(叶结点)。

子树和空树

子树:单个结点也是一棵树,只不过根结点就是它本身。知道了子树的概念后,树也可以这样定义:树是由根结点和若干个子树构成的。

空树:如果集合本身为空,那么构成的树就被称为空树。空树中没有结点。

结点的度和层次

对于一个结点,拥有的子树数(结点有多少分支)称为结点的度(Degree)

结点的层次:从一颗树根开始,树根所在层为第一层,根的孩子结点所在的层为第二层,以此类推。

有序树和无序数

如果树中结点的子树从左到右看,谁在左边,谁在右边,是有规定的,这颗树称为 有序树,反之称为 无序树

在有序树中,一个结点最左边的子树称为 “第一个孩子”,最右边的称为 “最后一个孩子”。

森林

由 m(m >= 0)个互不相交的树组成的集合被称为 森林

小结

树型存储结构类似于家族的族谱,各个结点之间也同样可能具有父子、兄弟、表兄弟的关系。需要重点理解树的根结点和子树的定义,同时要会计算树中各个结点的度和层次,以及树的深度。

二叉树 Binary Tree

简单地理解,满足以下两个条件的树就是二叉树:

  1. 本身是有序树;
  2. 树中包含的各个结点的度不能超过2,即只能是0、1或者2.

二叉树的性质

二叉树具有以下几个性质:

  1. 二叉树中,第 层最多有 个结点。
  2. 如果二叉树的深度为 ,那么此二叉树最多有 个结点。
  3. 二叉树中,终端结点数(叶子结点数)为 ,度为2的结点数为 ,则 。

二叉树还可以继续分类,衍生出 满二叉树完全二叉树

满二叉树

如果二叉树中除了叶子结点,每个结点的度都为2,则此二叉树称为 满二叉数

满二叉树除了满足普通二叉树的性质,还具有以下性质:

  1. 满二叉树中,第 层最多有 个结点。
  2. 深度为 k 的满二叉树 个结点,叶子数为 。
  3. 满二叉树中不存在度为1 的结点,每一个分支点中都有两颗深度相同的子树,且叶子结点都在对底层。
  4. 具有 n 个结点的满二叉树的深度为

完全二叉树

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

完全二叉树除了具有普通二叉树的性质,它自身也具有一些独特的性质,比如说,n 个结点的完全二叉树的深度为 。

对于任意一个完全二叉树来说,如果将含有的结点按照层次从左到右依次标号,对于任意一个结点 i ,完全二叉树还有以下几个结论成立:

  1. 当 i > 1 时,父亲结点为结点 [i/2] 。(i = 1 时,表示的是根结点,无父亲结点)。
  2. 如果 2 * i>n (总结点的个数) ,则结点 i 肯定没有左孩子(为叶子结点),否则其左孩子是结点 2 * i 。
  3. 如果 2 * i+1>n , 则结点 i 肯定没有右孩子,否则右孩子是结点 2 * i+1 。

二叉树遍历

  1. 前序(Pre-order):根-左-右
  2. 中序(In-order):左-根-右
  3. 后序(Post-order):左-右-根

示例代码:

def preorder(self, root):
  if root:
    self.traverse_path.append(root.val)
    self.preorder(root.left)
    self.preorder(root.right)

def inorder(self, root):
  if root:
    self.inorder(root.left)
    self.traverse_path.append(root.val)
    self.inorder(root.right)

def postorder(self, root):
  if root:
    self.postorder(root.left)
    self.postorder(root.right)
    self.traverse_path.append(root.val)

二叉搜索树 Binary Search Tree

二叉搜索树,也称二叉搜索树、有序二叉树(Ordered Binary Tree)、排序二叉树(Sorted Binary Tree),是指一颗空树或者具有下列性质的二叉树:

  • 左子树上所有结点的值均小于它的根结点的值;
  • 右子树上所有结点的值均大于它的根结点的值;
  • 以此类推,左、右子树也分别为二叉查找树(这就是重复性!)

中序遍历:升序排列

树和链表没有本质上的区别,当一个链表的话,当它分出两个 next 的话,我们就把它称为树,所以它的数据结构就是从一维空间扩散到二维空间了,这种扩散的好处就是引入了二叉搜索树。当它本身是有序的话,那么每一次的话就可以根据它和当前结点的大小关系分出来它只走左分支还是只走右分支,这样的话查询插入和搜索的效率就从 O(n)的变成了 log2n 的时间复杂度。

二叉搜索树常见操作

  1. 查询
  2. 插入新结点(创建)
  3. 删除

Demo: https://visualgo.net/zh/bst

如何查找结点

假设我们要查找 10,首先来和根相比,如果是小于 9 的话,只要去左子树就行了,10 是大于 9 的,那就去右子树,10 是小于 13 的就去左子树,10 是小于 11 的再去左子树就找到 10 了,而找不到的话也就是找不到。这时候你会发现查找的时间复杂度就是这个树的深度。如何这个树本身有 n 层的话,那么它的时间复杂度是 n,但是一般我们定义 n 为树的总的结点个数,这个时候它的时间复杂度平均来说就是 log2n,这里的 n 表示树里面总的结点个数。


极端情况

基于上面介绍可能会出现一种极端情况,这种极端情况的话就是在你维护二叉搜索树的时候,没有特殊处理的话,在极端情况下它会变成一根棍子,这根棍子的话就是你在插入的时候始终插在左边,这个时候二叉搜索树就退化成一个链表了。

如何优化可以阅读《AVL 树和红黑树的实现和特性》这篇。

复杂度分析

image.png

图片来源:http://www.bigocheatsheet.com/

实战题目

  1. https://leetcode-cn.com/problems/binary-tree-inorder-traversal/
  2. https://leetcode-cn.com/problems/binary-tree-preorder-traversal/
  3. https://leetcode-cn.com/problems/n-ary-tree-postorder-traversal/
  4. https://leetcode-cn.com/problems/n-ary-tree-preorder-traversal/
  5. https://leetcode-cn.com/problems/n-ary-tree-level-order-traversal/

总结

树的数据结构决定了递归是最好的解法,这是因为

  1. 树的结构与递归类似,递归的自调用就相当于让树的子树们再进行一次同样的操作;
  2. 递归的代码结构更加简单明了,也是树这么一个高维数据结构所带来的优势之一;
  3. 对于一种数据结构来说,无非就是查找插入和删除,在进行遍历这个反复循环的动作时,对于树的结构来说递归非常实用。
树、二叉树、二叉搜索树
部分图片来源于网络,版权归原作者,侵删。

你可能感兴趣的:(树、二叉树、二叉搜索树的实现和特性)