数据结构---树---总结
一,一些定义
树的深度定义:对于树中的节点n(i),n(i)的深度定义为,从根到n(i)的唯一路径的长度。
树的高度定义:对于树中的节点n(i),n(i)的高度定义为,从n(i)到树中叶子节点的最长路径的长度。因为树中可能有多个叶子结点,n(i)到每个叶子结点都会有路径,路径最长的即为n(i)的高度。
二,表达式树
表达式树的树叶是操作数,非叶结点是操作符。
表达式树可用来进行表达式求值。对表达式树中序遍历,就会得到一个符合人类习惯的表达式,这是一个中缀表达式。关于中缀表达式,参考:
对表达式树后序遍历,就得到一个后缀表达式,关于后缀表达式,参考:
那,如何构造表达式树呢?
即:给定一个后缀表达式,如何将之转换成中缀表达式?---后缀表达式转换成中缀表达式的过程,即为构造表达式树的过程。
幸运的是:后缀表达式转换成中缀表达式的过程 与 后缀表达式求值的过程非常相似。
算法如下:
输入:后缀表达式
输出:(表达式树)中缀表达式
扫描后缀表达式,一次读入一个符号
①若它是操作数,则建立一个单节点树并将它推入栈中
②若它是操作符,则从栈中弹出两颗树T1(先弹出) 和 T2。该树的树根就是操作符,左、右儿子分别是T2 和 T1,然后再将该树压入栈中。
代码如下:
另外一篇博客--链接
三,查找树---二叉查找树
二叉查找树:对于树中每个节点X,它的左子树所有项的值小于X,它的右子树所有项的值大于X。中序遍历二叉查找树 生成一个有序序列。
二叉查找树的特点:让很多操作都可以在O(logN)的时间内完成。注意,我这里说的是 “可以”,因为:二叉查找树并不是平衡二叉树。
插入操作:
比如,当序列是有序的时,二叉查找树就退化成链表。
例如:1,2,3,4,5作为输入,构造二叉查找树
对于1,作为树的根结点。对于2,作为根结点的右孩子。对于3,作为2的右孩子。对于4,作为3的右孩子,对于5,作为4的右孩子。
这样的二叉查找树的高度为O(N)。 那么相关的操作也降为O(N)
删除操作:
当删除二叉查找树中的某个节点时,不通改变二叉查找树的特点。故,一般采用如下方式删除:
不直接删除该节点,而是删除以该节点为根的左子树中的最右下节点, 或者删除以该节点为根的,右子树中的最左下节点。
由于删除操作可能会改变二叉查找树的高度,如果总是删除”最右下节点“ 或者 ”最左下节点“ 会导致节点严重偏向一边,故可采用随机删除法,即随机地选择”最右下节点“ 或 ”最左下节点“删除。
现在考虑以下几种操作,及它们采用不同的数据结构实现时的时间复杂度情况:
findMin 二叉查找树--O(logN); 线性表--O(N); HashSet--O(N)
HashSet可以遍历每个Entry,然后找出最小的,故时间复杂度为O(N)
findMax 二叉查找树--O(logN); 线性表--O(N); HashSet--O(N)
如果数组有序,findMax操作可为O(1)。但是对于链表而言,findMax只能为O(N)
insert 二叉查找树--O(logN); 线性表--O(N); HashSet--O(1)
在数组中插入时,需要移动元素,平均时间复杂度为O(N)。但对于链表,插入操作可以为O(1)
delete 二叉查找树--O(logN); 线性表--O(N); HashSet--O(1)
在数组中删除时,需要移动元素,平均时间复杂度为O(N)。但对于链表,删除操作可以为O(1)
contains 二叉查找树--O(logN); 数组--O(N); HashSet--O(1)
需要遍历数组一次,才能判断是否包含该元素。当是有序序列时,数组可以采用二分查找,复杂度降为O(logN)
三,AVL树--平衡二叉树
正是由于二叉查找树的高度随序列而变化。如,当输入序列有序时,构造出来的二叉查找树退化为链表,高度为O(N)。那么相关的操作,如findMax、findMin的时间复杂度也降为O(N)
AVL树要求每棵树的左右子树的高度相关不能超过1,从而保证操作不会退化为O(N)的复杂度。但是AVL树的实现比较复杂。
四,伸展树
相对于AVL的复杂,伸展树要容易实现一些。
伸展树保证对树的连续M次操作的最坏时间复杂度为O(MlogN)。根据摊还分析,摊还到每一次操作的时间复杂度为 MlogN / M = O(logN)