- 讲起数据结构,就绝对离不开树。那么: 为什么要使用树?线性结构存在什么问题?如何从逻辑上实现树?如何存储树?如何遍历树?树有哪些应用实例? 这些都是需要考虑的问题。
引入树
- 自然界中本身就存在很多的层次结构,并且使用层次结构表示数据可以使查询更加有效,比如在文件夹
- 在数据有序时,使用二分查找法可以减小时间复杂度,但是使用数组存的话储难以进行插入和删除操作。
-
- 动态查找:经常发生增删操作
-
- 静态查找:只有查找操作比较频繁
实现树
- 树的表示一般使用链表的方法,但是传统的链表表示方法(一个子节点对应一个指针)会造成大量的空间浪费。
- 孩子兄弟表示法,所有节点有两个指针域,左指针指向第一个左孩子,右指针指向相邻的最近的兄弟节点(此处兄弟节点是同一个父节点的子节点集和)。
存储树
- 数组存储:只有完全二叉树才能使用数组存储,普通二叉树使用数组存储(构造成完全二叉树)会造成大量空间浪费。
- 链表存储:二叉树使用链表存储很方便。
遍历树
树一种二维结构,遍历树即是得到所有节点的某个序列,这样序列化的过程本质上是一个线性化的过程(二维到一维)。遍历的难点是:树的结构有点链表的味道,当你需要访问树的时候,你需要访问该节点和它的左子树和右子树,此时如果先访问了左子树而不记住父节点,那么将永远回不到父节点,那么永远都无法访问它的右子树,所以保存右子树或者说保留右子树的父节点是遍历树中根本要解决的问题:使用堆栈或者队列。
- 先序、中序、后序遍历:递归实现与非递归实现(堆栈)
- 层序遍历:队列实现
- 思考:
-
- 求二叉树的叶子节点?
-
- 求二叉树的高度?
-
- 二元表达式树及其遍历?
-
- 如何用两种遍历序列(先序、中序、后序)来确认二叉树?
树的典型应用
二叉搜索树/二叉查找树
回顾:动态查找和静态查找
- 可以使用递归查找的方法,但是此处递归使用的尾递归的方法(即在return出调用递归函数),原理上讲,尾递归都可以用循环实现。
- 使用非递归查找(循环实现),函数执行效率比递归高!
- 在建立二叉搜索树的时候,我们发现搜索次数取决于树的高度,最差的情况是所有节点组成一条链,由此引出二叉平衡树。
- 常用操作:普通查找、最大最小查找、插入、删除
- 二叉搜索树的查询
- 二叉搜索树的插入
- 二叉搜索树的删除:没有子节点或者只有一个子节点的情况是比较简单的,直接删除或者用子节点替代自己的位置,在有两个子节点的情况时,可以将复杂问题简单化:删除有两个子节点的情况转化为删除没有子节点或者只有一个子节点的情况,具体实现是这样的:在待删除的节点的左子树中寻找最大值或者右子树中寻找最小值(最值一定只有没有或者只有一个子节点),然后用该节点替代待删除结点,并且将该最值结点原来的位置删除,即转化为简单的情况
平衡二叉树
- 平衡二叉树定义为:空树或者任意节点的左右子树高度差不超过1
- 经过推导可以得知,给定n个结点的AVL树,它的最大高度不超过 O ( l o g n ) O(logn) O(logn)
- 平衡二叉树需要考虑到在进行插入、删除操作时,可能将平衡二叉树变得不平衡
- RR旋转,即破坏节点在发现节点的右子树的右子树
- LL旋转,即破坏节点在发现节点的左子树的左子树上
- LR旋转,即破坏节点在发现节点的左子树的右子树上
- RL旋转,即破坏节点在发现节点的右子树的左子树上
- RR旋转,即破坏节点在发现节点的右子树的右子树
思考
- 树的同构问题:
-
- 二叉树的表示
-
- 建树
-
- 同构判别
- 是否为同一颗二叉搜索树的问题:
- 使用某个序列插入二叉搜索树,那么二叉搜索树也就确定了,但是同一颗二叉搜索树却不能唯一确定插入序列。此处的问题是,给定几个插入序列,如何判断它们构成的二叉搜索树是否相同
- 方法一(建多棵树):先构造二叉搜索树,然后使用递归的方法判断:根?左子树?右子树?
- 方法二(不建树):先判断根节点,然后将剩余的序列按照顺序分为比根节点大的和根节点小的,然后继续判断
- 方法三(建一棵树):首先把标准的树建好,然后在后面的读入序列之后,在建好的树中间查找读入的序列数,如果在查找过程中发现有以前没遇到过的值,则表示不一致
参考
浙江大学数据结构课程——中国大学mooc