1. 预备知识
一棵树是一些结点的集合。集合可以使空集;若不是空集,则树由称作根(root)的结点r以及零个或多个非空的子树组成,子树的根都被来自根r的一条有向的边所连接。
每棵子树的根叫做根r的儿子,而r是每个子树根的父亲。
没有儿子的结点称为叶结点,上图的叶结点是BCHIPQKLMN。有相同父亲的结点称为兄弟。
从结点n1到nk的路径定义为结点n1,n2,…,nk的一个序列,ni是ni+1的父亲,路径的长为路径上的边的条数,即为k-1。
对任意结点ni,ni的深度(depth)为从根到ni的唯一路径的长,根的深度为0。ni的高度(height)是从ni到一片树叶的最长路径的长,因此,所有树叶的高为0。一棵树的高等于它的根的高。上图中,E的深度为1而高度为2;F的深度为1而高也是1;树的高为3。一个树的深度等于它的最深的树叶的深度;该深度总是等于这棵树的高。
1.1.树的实现
将每个结点的所有儿子都放在树结点的链表中。
1.2.树的遍历及应用
用于包括UNIX和DOS在内的许多常用操作系统中的目录结构。下图是UNIX文件系统中的一个典型目录。
列出目录中所有文件的名字,输出格式:深度为di的文件将被di次tab缩进后打印其名。算法伪代码:
输出:
这种遍历的策略称为前序遍历,对结点的处理在它的儿子结点被处理之前进行。
另一种遍历树的方法是后序遍历,一个结点的工作在它的儿子结点被计算后进行。
2. 二叉树
二叉树是每个结点都不能有多于两个儿子的树。
子树TL和TR都可能为空。
二叉查找树的深度的平均值为O(logN)。
2.1.实现
一个结点由element(元素)的信息加上两个到其他结点的引用组成的结构。
2.2.一个例子——表达式树
表达式树的树叶是操作数,其他结点为操作符。下图的操作都是二元的,因此正好是二叉树。
中序遍历得到中缀表达式:左à结点à右
后序遍历得到后缀表达式:左à右à结点
前序遍历得到前缀表达式:结点à左à右
构造一棵表达式树:
将后缀表达式转变为表达式树。(已经有了将中缀表达式转变成后缀表达式的算法)
例子:设输入为ab+cde+**
将a、b创建两棵单结点树,并将指向它们的指针压入栈:
“+”被读入,形成一棵新的树,将指针压入栈:
C、d、e读入,创建单结点树,将指针压入栈:
读入“+”,将d、e合并
读入“*”号
最后,读入一个“*”号
3. 查找树ADT——二叉查找树
树中的每个结点X,它的左子树中所有项的值小于X中的项,右子树中所有项的值大于X中的项。二叉查找树的平均深度为O(longN)。
二叉查找树模板的接口:
数据成员是指向树根结点的指针,该指针对空树为NULL。Public成员函数使用调用private递归函数的常规技术。
下面描述private方法。
3.1.contains
首先要对是否是空树进行测试。
3.2.findMax和finaMin
返回指向树中包含最小元和最大元的结点的指针。
下面用递归编写findMin,用非递归编写findMax。
3.3.insert
3.4.remove
如果结点是一片树叶,那么可以立即删除。如果结点有一个儿子,则该结点在其父节点调整它的链以绕过该结点后被删除。
复杂的情况是处理有两个儿子的结点。删除策略是用其右子树的最小的数据代替该节点的数据并递归地删除那个结点。因为右子树最小的结点不可能有左儿子,所以第二次remove就很容易。
3.5.析构函数和复制赋值操作符
析构函数调用makeEmpty。在递归地处理t的子树之后,对t调用delete,这样,所有的结点就都递归地回收了。在最后,t(此时为root)改为指向NULL。
复制赋值操作首先调用makeEmpty来回收内存,然后进行rhs的复制。
4. AVL树
AVL树是每个结点的左子树和右子树的高度最多差1的二叉查找树(空树的高度定义为-1)。
插入一个结点可能破坏AVL树的特性,旋转操作可以恢复平衡。
必须平衡的结点叫做a,a的两棵子树的高度差为2,有4种情况:
1、 对a的左儿子的左子树进行一次插入
2、 对a的左儿子的右子树进行一次插入
3、 对a的右儿子的左子树进行一次插入
4、 对a的右儿子的右子树进行一次插入
第一种情况是插入发生在外边(即左-左或右-右的情况),该情况通过一次单旋转完成调整。第二种情况是插入发生在内部的情形(即左-右或右-左的情况),该情况通过双旋转处理。
4.1.单旋转
子树X“长”出一层,使得比Z深处2层。
4.2.双旋转
上图中B或C中有一棵比D深两层。
5.伸展树
连续M次操作花费O(MlogN)时间。
6.树的遍历
中序遍历:左à结点à右,总的运行时间是O(N)
通过将一些元素插入到查找树然后执行一次中序遍历,得到的是排过顺序的元素,这给出了排序的一种O(NlogN)算法。
后序遍历:左à右à结点,总的运行时间是O(N)
前序遍历:结点à左à右,可用来利用结点深度标志每个结点
层序遍历:所有深度为d的结点要在深度为d+1的结点之前进行处理,它不是递归地实施的,用到队列,而不使用递归所默认的栈。
7.B树
二叉树不适合磁盘上的查找树。
B树是平衡M路数,能更好地适应有磁盘操作的情况。
8.标准库中的set和map
vector和list对于查找来说是不够用的,STL提供了两个附加的容器set和map,保证了基本操作(如插入、删除和查找)的对数时间开销。
8.1.set
Set是一个排序后的容器,不允许重复。
Set特有的操作是高效的插入、删除和执行基本查找。
Set有insert、erase、find例程。
8.2.map
Map用来存储排序后的由键和值组成的项的集合,键必须唯一,但多个键可对于同一个值。Map中键保持逻辑排序后的顺序。
Map也支持insert、find、erase。Map还有对数组索引操作符的重载,可以像访问数组一样得到相应的值。
Operator[]的语法:如果map中存在key,就返回指向相应值得引用;如果不存在key,就在map中插入一个默认的值,然后返回指向这个插入的默认值的引用,默认值通过零参数构造函数获得。
8.3.set和map的实现
Set和map对基本操作insert、erase、find仅消耗对数时间。低层实现是平衡二叉查找树,典型的用法不是AVL树,而常常使用自顶向下红黑树。