二叉树的遍历
- 先序遍历(NLR):先访问根节点,再访问左子树,最后访问右子树。
- 中序遍历(LNR):先访问左子树,再访问根节点,最后访问右子树。
- 后序遍历(LRN):先访问左子树,再访问右子树,最后访问根节点。
注:要进行二叉树重建时,中序遍历是必须要知道的,先序和后序只需知道其中一种。
- 递归方式实现三种遍历方式
//先序遍历--递归 int traverseBiTreePreOrder(BiTreeNode *ptree,int (*visit)(int)) { if(ptree) { if(visit(ptree->c)) if(traverseBiTreePreOrder(ptree->left,visit)) if(traverseBiTreePreOrder(ptree->right,visit)) return 1; //正常返回 return 0; //错误返回 }else return 1; //正常返回 } //中序遍历--递归 int traverseBiTreeInOrder(BiTreeNode *ptree,int (*visit)(int)) { if(ptree) { if(traverseBiTreeInOrder(ptree->left,visit)) if(visit(ptree->c)) if(traverseBiTreeInOrder(ptree->right,visit)) return 1; return 0; }else return 1; } //后序遍历--递归 int traverseBiTreePostOrder(BiTreeNode *ptree,int (*visit)(int)) { if(ptree) { if(traverseBiTreePostOrder(ptree->left,visit)) if(traverseBiTreePostOrder(ptree->right,visit)) if(visit(ptree->c)) return 1; return 0; }else return 1; }
- 非递归方式实现
先序遍历:首先考虑非递归先序遍历(NLR)。在遍历某一个二叉(子)树时,以一当前指针记录当前要处理的二叉(左子)树,以一个栈保存当前树之后处理的右子树。首先访问当前树的根结点数据,接下来应该依次遍历其左子树和右子树,然而程序的控制流只能处理其一,所以考虑将右子树的根保存在栈里面,当前指针则指向需先处理的左子树,为下次循环做准备;若当前指针指向的树为空,说明当前树为空树,不需要做任何处理,直接弹出栈顶的子树,为下次循环做准备。
//先序遍历--非递归 int traverseBiTreePreOrder2(BiTreeNode *ptree,int (*visit)(int)) { Stack *qs=NULL; BiTreeNode *pt=NULL; qs=initStack(); pt=ptree; while(pt || !isEmpty(qs)) { if(pt) { //遍历根节点 if(!visit(pt->c)) return 0; //错误返回 push(qs,pt->right); //右子树入栈 pt=pt->left; //开始访问左子树 } else pt=pop(qs); //否则依次出栈访问右子树 } return 1; //正常返回 }
中序遍历:对于非递归中序遍历,若当前树不为空树,则访问其根结点之前应先访问其左子树,因而先将当前根节点入栈,然后考虑其左子树,不断将非空的根节点入栈,直到左子树为一空树;当左子树为空时,不需要做任何处理,弹出并访问栈顶结点,然后指向其右子树,为下次循环做准备。
//中序遍历--非递归 int traverseBiTreeInOrder2(BiTreeNode *ptree,int (*visit)(int)) { Stack *qs=NULL; BiTreeNode *pt=NULL; qs=initStack(); pt=ptree; while(pt || !isEmpty(qs)) { if(pt) { push(qs,pt); //根节点入栈 pt=pt->left; //开始访问左子树 } else { pt=pop(qs); if(!visit(pt->c)) return 0; pt=pt->right; } } return 1; }
后序遍历:由于在访问当前树的根结点时,应先访问其左、右子树,因而先将根结点入栈,接着将右子树也入栈,然后考虑左子树,重复这一过程直到某一左子树为空;如果当前考虑的子树为空,若栈顶不为空,说明第二栈顶对应的树的右子树未处理,则弹出栈顶,下次循环处理,并将一空指针入栈以表示其另一子树已做处理;若栈顶也为空树,说明第二栈顶对应的树的左右子树或者为空,或者均已做处理,直接访问第二栈顶的结点,访问完结点后,若栈仍为非空,说明整棵树尚未遍历完,则弹出栈顶,并入栈一空指针表示第二栈顶的子树之一已被处理。
//后序遍历--非递归 int traverseBiTreePostOrder2(BiTreeNode *ptree,int (*visit)(int)) { Stack *qs=NULL; BiTreeNode *pt=NULL; qs=initStack(); pt=ptree; while(1) //循环条件恒“真” { if(pt) { push(qs,pt); //根节点先入栈 push(qs,pt->right); //右子树再入栈 pt=pt->left; //开始遍历左子树 } else if(!pt) { pt=pop(qs); //右子树出栈 //如果右子树为空,即没有孩子 if(!pt) { pt=pop(qs); //根节点出栈 if(!visit(pt->c)) return 0; if(isEmpty(qs)) return 1; pt=pop(qs); } push(qs,NULL); } } return 1; }
满二叉树:高度为h,并且由2^h –1个结点的二叉树,被称为满二叉树。
完全二叉树:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下一层的叶结点集中在靠左的若干位置上。这样的二叉树称为完全二叉树。特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。
二叉查找树(Binary Search Tree)
定义:
- 每个节点都不比它左子树的任意节点小,而且不比它的右子树的任意节点大。
- 任意节点,其左右子树也分别是二叉查找树。
- 没有相等键值的节点。
查找:
二叉查找树可以方便的实现查找算法。在查找元素x的时候,我们可以将x和根节点比较:
1. 如果x等于根节点,那么找到x,停止查找 (终止条件)
2. 如果x小于根节点,那么查找左子树
3. 如果x大于根节点,那么查找右子树
二叉查找树所需要进行的操作次数最多与树的深度相等。n个节点的二叉查找树的深度最多为n,平均查找复杂度为O(log(n))。
插入节点:
- 如果树为空,直接插入作为根节点,然后返回。
- 如果树不为空,插入的节点小于根节点,则插入至左子树,如此递归下去。
- 如果树不为空,插入的节点大于根节点,则插入至右子树,如此递归下去。
注:新插入的节点一定是叶子节点。
删除节点:
删除节点相对比较复杂。删除节点后,有时需要进行一定的调整,以恢复二叉查找树的性质(每个节点都不比它左子树的任意元素小,而且不比它的右子树的任意元素大)。
- 叶节点可以直接删除。
- 当节点只有右子树或者左子树时,直接删除这个节点然后将其右孩子或左孩子替代其位置。
- 删除非叶节点时。比如下图中的节点8,我们可以删除左子树中最大的元素(或者右子树中最大的元素),用删除的节点来补充元素8产生的空缺。但该元素可能也不是叶节点,所以它所产生的空缺需要其他元素补充…… 直到最后删除一个叶节点。上述过程可以递归实现。
to
AVL树(自平衡二叉查找树,Balanced Binary Tree)
为了改善二叉查找树的平均查找效率,从而提出了AVL树。
定义:具有如下特性的二叉树
- 是一棵二叉查找树
- 任意节点的左右两个子树的高度差的绝对值不超过1
- 任意节点的左右子树均为AVL树
AVL树的查找:
同二叉查找树是一致的。
AVL树的节点的平衡因子:
节点的平衡因子是它的左子树的高度减去它的右子树的高度。带有平衡因子 1、0 或 -1 的节点被认为是平衡的。
AVL树的节点的旋转:
树的旋转操作是为了改变树的结构,使其达到平衡。旋转总共分为左旋和右旋两类。
给出记号:节点p,节点p的左孩子pL,节点p的右孩子pR;
以p为轴右旋:p变为pL的右孩子,pL的原右孩子变为p的左孩子。
以p为轴左旋:p变为pR的左孩子,pR的原左孩子变为p的右孩子。
插入一个节点,一定可以通过1~2次旋转(可能是左右组合旋转)达到平衡。
往AVL树中插入节点:向AVL树插入可以通过如同它是未平衡的二叉查找树一样把给定的值插入树中,接着自底向上向根节点折回,于在插入期间成为不平衡的所有节点上进行旋转来完成。
一、如果路径上节点平衡因子是0,则插入后不会打破这个节点的平衡性。
二、如果路径上的节点的平衡因子是1或-1,则可能会打破平衡性,在这种情况下如果此节点的新的平衡因子是0,则刚好将其补的更加平衡,平衡性未打破;否则平衡因子变成2或-2,则需要进行调整。
三、我们在对树进行调整后恢复子树相对于插入前的高度,不改变子子树的平衡因子。
删除AVL树中的节点:
方法1:将要删除的节点向下旋转成叶子节点,然后直接删除即可,向下旋转的过程中树可能会不满足二叉查找树的性质,但删除结束后一定仍为AVL树。
方法2:如同普通二叉查找树一样删除节点,若删除后未达到平衡,再通过旋转使树达到平衡。
红黑树(Red Black Tree)
定义:红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。
有如下性质:
性质1. 任意节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶子节点(指的是NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
以上性质可以推出关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长
特点:红黑树的平衡性不如AVL树,可能只是局部平衡,但只要满足上面几个性质即可。虽然是局部平衡,但是它的平均查找效率与AVL树相当(O(log(n))),统计性能要好于一般的AVL树。
往红黑树中插入节点:
整个过程十分复杂,简单说来是首先根据二叉查找树一样,将节点插入树中,若插入后不违反红黑树的各个性质,那么无需改变红黑树的结构;相反地,如果违反了性质,则要先通过类似于AVL树的左旋和右旋使得红黑树局部平衡,然后再根据性质对节点进行着色。
删除红黑树的节点:
删除的结点的方法与常规二叉搜索树中删除结点的方法是一样的,如果它的子结点是没有左孩子或者右孩子,那就用直接删除它,用NIL来顶替它的位置;如果被删除的结点只有一个左孩子或者右孩子,则直接删除这个结点,用它的唯一子结点顶替它的位置;如果该节点即有左孩子又有右孩子,我们就把它的直接后继结点内容复制到它的位置,之后以同样的方式删除它的后继结点,它的后继结点不可能是双子非空,因此此传递过程最多只进行一次。最后删除结束后可能违反了红黑树的性质,再通过改变着色来修复该树的红黑树性质。
插入和删除的参考资料:
红黑树的一个应用:C++中的set、map、multiset和multimap
针对set和map提出四个问题:
1. 为何map和set的插入删除效率比用其他序列容器高?
2. 为何每次insert之后,以前保存的iterator不会失效?
3. 为何map和set不能像vector一样有个reserve函数来预分配数据?
4. 当数据元素增多时(10000到20000个比较),map和set的插入和搜索速度变化如何?
(从他们的数据结构、储存方式、排序、查找、插入、删除的特性来考虑这几个问题)
哈希表的一个应用:C++11中的unordered_set、unordered_map、unordered_multiset和unordered_multimap
unordered容器的内部数据结构是基于hash table实现的,因此它其中储存的键值(key)是无序储存的,但是它的查找效率确实接近常数级的!unordered容器使用“桶”来存储元素,散列值相同的被存储在一个桶里。当散列容器中有大量数据时,同一个桶里的数据也会增多,造成访问冲突,降低性能。为了提高散列容器的性能,unordered库会在插入元素是自动增加桶的数量,不需要用户指定。
来看一个示例程序:
//test map & unordered_map #include <iostream> #include <map> #include <unordered_map> #include "time.h" using std::cout; using std::endl; using std::map; using std::unordered_map; using std::pair; int main() { //首先测试unordered_map unordered_map<int, int> hash; //测试插入效率 time_t first_time = time(0); //记录当前时间 for(int i = 0; i < 20000000; ++i) { hash[i] = 0; } cout << hash.size() << endl; time_t second_time = time(0); //测试查找效率 for(int i = 0; i < 20000001; ++i) { unordered_map<int, int>::iterator it = hash.find(i); if(it == hash.end()) { cout << "false" << endl; } } time_t third_time = time(0); cout << "second - first = " << second_time - first_time << endl; cout << "third - second = " << third_time - second_time << endl; //然后测试map map<int, int> rb_tree; //测试插入效率 first_time = time(0); //记录当前时间 for(int i = 0; i < 20000000; ++i) { rb_tree[i] = 0; } cout << rb_tree.size() << endl; second_time = time(0); //测试查找效率 for(int i = 0; i < 20000001; ++i) { map<int, int>::iterator it = rb_tree.find(i); if(it == rb_tree.end()) { cout << "false" << endl; } } third_time = time(0); cout << "second - first = " << second_time - first_time << endl; cout << "third - second = " << third_time - second_time << endl; return 0; }
测试输出:
可以看出无论是插入还是查找小,unordered_map的时间都比map要小。
总结:
无序容器时候用unordered_map,有序容器时候用map;需要频繁查找元素用unordered_map,查询无需很快但需要稳定查找效率则首选map。
参考博客:
伸展树(Splay Tree)
也是一种自平衡二叉查找树,它能在O(n log n)内完成插入、查找和删除操作,提出的原因也是为了提高AVL树在最坏情况下的查找效率。
还有许许多多种类的树....