二叉树的遍历
- 先序遍历(NLR):先访问根节点,再访问左子树,最后访问右子树。
- 中序遍历(LNR):先访问左子树,再访问根节点,最后访问右子树。
- 后序遍历(LRN):先访问左子树,再访问右子树,最后访问根节点。
注:要进行二叉树重建时,中序遍历是必需要知道的,先序和后序只需知道其中一种。html
//先序遍历--递归
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
ios
AVL树(自平衡二叉查找树,Balanced Binary Tree)
为了改善二叉查找树的平均查找效率,从而提出了AVL树。
定义:具备以下特性的二叉树
- 是一棵二叉查找树
- 任意节点的左右两个子树的高度差的绝对值不超过1
- 任意节点的左右子树均为AVL树
AVL树的查找:
同二叉查找树是一致的。
AVL树的节点的平衡因子:
节点的平衡因子是它的左子树的高度减去它的右子树的高度。带有平衡因子 一、0 或 -1 的节点被认为是平衡的。
AVL树的节点的旋转:
树的旋转操做是为了改变树的结构,使其达到平衡。旋转总共分为左旋和右旋两类。
给出记号:节点p,节点p的左孩子pL,节点p的右孩子pR;
以p为轴右旋:p变为pL的右孩子,pL的原右孩子变为p的左孩子。
以p为轴左旋:p变为pR的左孩子,pR的原左孩子变为p的右孩子。
插入一个节点,必定能够经过1~2次旋转(多是左右组合旋转)达到平衡。
往AVL树中插入节点:向AVL树插入能够经过如同它是未平衡的二叉查找树同样把给定的值插入树中,接着自底向上向根节点折回,于在插入期间成为不平衡的全部节点上进行旋转来完成。
1、若是路径上节点平衡因子是0,则插入后不会打破这个节点的平衡性。
2、若是路径上的节点的平衡因子是1或-1,则可能会打破平衡性,在这种状况下若是此节点的新的平衡因子是0,则恰好将其补的更加平衡,平衡性未打破;不然平衡因子变成2或-2,则须要进行调整。
3、咱们在对树进行调整后恢复子树相对于插入前的高度,不改变子子树的平衡因子。
删除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要小。git
总结:
无序容器时候用unordered_map,有序容器时候用map;须要频繁查找元素用unordered_map,查询无需很快但须要稳定查找效率则首选map。
参考博客:
伸展树(Splay Tree)
也是一种自平衡二叉查找树,它能在O(n log n)内完成插入、查找和删除操做,提出的缘由也是为了提升AVL树在最坏状况下的查找效率。
还有许许多多种类的树....