直接进入正题:暂时只讨论了节点的插入,节点删除还未纳入。
一、如何从数组生成一个二叉树
假设数组为:{ 30, 13, 7, 43, 23, 12, 9, 33, 42, 21, 18, 6, 3, 50 },我们不对数组排序,直接生成二叉树。
创建流程:
1.将第一数作为根节点:
2.插入13,13小于30,放在30的左边子节点。
3.插入7,7小于30,7小于13,放在13的左边子节点。
4.插入43,43大于30,放在30的右边子节点。
5.放入23,23小于30,23大于13,放入13的右边子节点。
6.放入12,12小于30,12小于13,12大于7,放入7的右边子节点。
7.放入9,9小于30,9小于13,9大于7,9小于12,放入12的左边子节点。
8.中间省略,最后生成的二叉树为如下:
9.由上图可以看出,普通二叉树是不平衡的,最坏的情况可能会形成以下情况:
在这种情况下,当我们需要查找1的时候,时间复杂度就是O(n)。
普通二叉树的查找时间复杂度为[O(log2n),O(n)]之间。如果让其始终保持为O(log2n)的时间复杂度呢,我们就要创建平衡二叉树。
2-3树和红黑树都是平衡二叉树,我们先看2-3树,然后再由2-3树引出红黑树的原理。
二、二叉树的遍历
二叉树的遍历分为以下几种方式:
如何理解先序、中序和后序?假设我们有一个一共三个节点的二叉树:
- 先序遍历:根节点作为第一位,然后左孩子、右孩子。遍历顺序是A->B->C。
- 中序遍历:先左孩子,根节点作为中间位,然后右孩子。遍历顺序是B->A->C。
- 后序遍历:先左孩子,再右孩子,最后根节点。遍历顺序是B->C->A。
总结:遍历的顺序是根据根节点所在的遍历顺序来定义的,除了根节点所处位置的变化,其他子节点遍历顺序是不变的。
我们以一个简单二叉树作为例子,如图:
我们根据遍历顺序不同,将各节点的值(圆圈里的字母)打印出来:
1.前序遍历的打印结果:A->B->D->G->C->E->F
前序遍历递归方法的代码如下:
//前序递归遍历 void PreOrder(Node * nd) { if (nd != NULL) { cout << nd->data << endl; PreOrder(nd->lchild); PreOrder(nd->rchild); } }
前序遍历非递归方法的代码如下:
//先序非递归遍历 void NonRecPreOrder(Node * nd) { //定义一个栈 stack<Node*> s; Node * p = nd; //当根节点不为空时,先打印其值(先序) while (p != NULL) { cout << p->data << endl; //该节点被压如栈 s.push(p); //遍历其左子节点(左子树) p = p->lchild; } //所有左子树的左节点都遍历完后,开始通过栈回退到根节点,并开始遍历右子树。 while (!s.empty()) { //取栈顶的节点指针 p = s.top(); //取他的右子树,然后重复两个while循环。 p = p->rchild; //弹出 s.pop(); } }
2.中序遍历的打印结果:D->G->B->A->E->C->F
中序遍历递归方法的代码如下:
//中序递归遍历 void MedOrder(Node * nd) { if (nd != NULL) { PreOrder(nd->lchild); cout << nd->data << endl; PreOrder(nd->rchild); } }
中序遍历非递归方法的代码如下:
//中序非递归遍历 void NonRecPreOrder(Node * nd) { //定义一个栈 stack<Node*> s; Node * p = nd; while(p || !s.empty()){ //当根节点不为空时,继续往左边走 while (p != NULL) { //该节点被压如栈 s.push(p); //遍历其左子节点(左子树) p = p->lchild; } //所有左子树的左节点都遍历完后,开始通过栈回退到根节点,并开始遍历右子树。 while (!s.empty()) { //取栈顶的节点指针 p = s.top(); //中序,打印完左节点值,回来的时候打印根节点值 cout << p->data << endl; //取他的右子树,然后重复两个while循环。 p = p->rchild; //弹出 s.pop(); } } }
3.后续遍历的打印结果:G->D->B->E->F->C->A
后序遍历递归方法的代码如下:
//后序递归遍历 void PostOrder(Node * nd) { if (nd != NULL) { PreOrder(nd->lchild); PreOrder(nd->rchild); cout << nd->data << endl; } }
后序遍历非递归方法的代码如下:
//后序非递归遍历 void NonRecPreOrder(Node * nd) { //定义一个栈 stack<Node*> s; Node * p = nd; //定义一个指针r,用来记录是否访问过右子节点 Node * r = NULL; while (p || !s.empty()) { if (p) { s.push(p); p = p->lchild; } else { //获取栈顶部元素,注意不是弹出 p = s.top(); //判断右子节点的状态,1.为NULL,2.已经被访问 if (p->rchild && p->rchild != r) { p = p->rchild; } else { //如果右子节点不存在,或已经被访问了(r已经记录),打印子树根节点值 cout << p->data << endl; s.pop(); r = p; //将辅助指针指向访问过的右子树根节点 p = NULL; //将p置为空,从而继续访问栈顶 } } } }
4.层次遍历
层次遍历也是非递归的一种遍历方法,遍历的结果是非有序的,顺序为A->B->C->D->E->F,即从上到下一层一层遍历。代码如下:
//层次遍历(非递归) void LevelOrder(Node * nd) { queue<Node *> q; Node * p = nd; //先将根节点入队列 q.push(p); while (!q.empty()) { //从队列中获取最先压如的节点 p = q.front(); //打印值 cout << p->data << endl; //弹出 q.pop(); //判断该节点是否存在左右子节点,如果存在,就压入队列 if (p->lchild != NULL) q.push(p->lchild); if (p->rchild != NULL) q.push(p->rchild); } }
总结:
前序遍历、中序遍历、后序遍历的递归写法很类似,唯一的不同就是遍历左子树、遍历右子树、打印当前节点值三者之间的顺序不同。
非递归遍历方法,特别需要注意后序遍历,需要依靠一个辅助指针记录右节点是否已经访问。
层次遍历需要借助队列来实现,即每一层在遍历的时候,就检查其下一层元素是否存在,存在的话压入队列,这样就能实现一层一层遍历的结果。
三、平衡二叉树 2-3树
2-3树的意思就是,某个节点有两种可能:
一是正常的2-节点,包含一个值(或键),包含左右两个子节点。二是3-节点,包含两个值,包含左中右三个子节点。
如图所示:
左边为2-节点,右边为3-节点。
向一个2-3树的节点中插入一个新的元素有以下几种基础操作:
1.插入一个比2-节点值小的元素,例如对值为30的2-节点插入20:
2.插入一个比2-节点值大的元素,例如对值为30的2-节点插入40:
3.插入一个比3-节点左边值更小的值,例如对值为20 30的3-节点插入15:
注意,这里对3-节点插入数据后,形成了一个4-节点,可以分解为最右边的二叉子树。
4.插入一个比3-节点左边值更大、比右边值更小的值,例如对值为20 30的3-节点插入25:
5.插入一个比3-节点右边值更大的值,例如对值为20 30的3-节点插入40:
6.当下面一层的元素形成了4-节点,将4-节点的中间数往上层升级(分左右方向):
有了上述6个基本操作,我们开始使用前面的数组来创建2-3树:
数组为{ 30, 13, 7, 43, 23, 12, 9, 33, 42, 21, 18, 6, 3, 50 }。
创建流程:
1.将30作为根节点。
2.插入13,13比30小,形成一个值为13 30的3-节点。
3.插入7,7比13小,形成一个值为7 13 30的4-节点,然后分解。
4.插入43,43大于13,43大于30,与30一起形成3-节点。
5.插入23,23大于13,23小于30,与30 43形成4-节点,然后分解。
6.插入12,12小于13,12大于7,与7组成3-节点
7.插入9,9小于13,9大于7,9小于12,与7 12组成4-节点,然后分解。分解后9升级到上一层,与13 30形成4-节点,再次分解。
8.插入33,33大于13,33大于30,33小于43,与43组成3-节点。
9.插入42,42大于13,42大于30,42大于33,42小于43,与33 43形成4-节点,然后分解。
10.省略后面过程,最终生成结果为:
至此,我们创建了一颗2-3树,可以看出2-3树的平衡性还是很好的。
得到2-3树以后,我们可以将其进行结构上的一些变化:
1.将其中的所有3-节点,变换为以下形状,以左边子树为例:
2.将所有的3-节点进行变换:
3.得到的就是一颗红黑树,所以2-3树和红黑树是可以一一对应的,但是需满足三个条件:
- 红链接均为左链接。
- 没有任何一个节点同时和两条红链接相连。
- 任意空链接到根节点路径上的黑色连接数目相同。
从图中可以看出,我们的红连接都是左链接,满足条件一。没有节点同时链接两条红线,满足条件二。每个叶子节点下得空链接到根节点30的路径中黑连接数量都是2,所以满足条件三。
四、红黑树
红黑树的红和黑主要指红黑连接,而不是指节点是红色和黑色,但是一般可以将红连接下面的子节点图为红色。
我们先将上面的2-3树转换为真正的红黑树:
红黑树插入操作的几个基本动作:
注意:新插入的节点都使用红线连接,我们将节点也表示为红色,方便观察。
1.向一个黑色节点插入一个元素比他小的元素:
2.向一个黑色节点插入一个元素比他大的元素,要进行一次左旋:
3.向一个已经有一个子节点(红色)的黑色节点插入一个大于他的元素:
在这种情况下,节点6同时存在左右两条红色连接,那么直接将其都变为黑色。
如果这个黑色节点有父节点,则需要将与父节点之间的连接也变为红色,自己变为红色:
4.向一个已经有一个子节点(红色)的黑色节点插入一个小于红色子节点的元素:
5.向一个已经有一个子节点(红色)的黑色节点插入一个大于红色子节点的元素:
有了以上5个基本动作,我们就可以开始构建一颗红黑树了:
数组为{ 30, 13, 7, 43, 23, 12, 9, 33, 42, 21, 18, 6, 3, 50 }。
创建流程:
1.将第一个数30作为根节点:
只有一个节点,直接变黑。
2.插入13,13小于30,放在左边子节点,颜色为红色,连接为红色:
3.插入7,7小于30,7小于13,放在13的左边子节点,红色。然后右旋,再进行变色:
4.插入43,43大于13,43大于30,放在30的右边子节点,然后局部左旋:
5.插入23,23大于13,23小于43,23小于30,放在30的左边子节点,红色,然后右旋,变色,再左旋:
6.插入12,12小于30,12小于13,12大于7,放在7的右边子节点,然后左旋:
7.插入9,9小于30,9小于13,9小于12,9大于7,放在7的右边,左旋,右旋,变色,右旋,变色:
8.插入33,33大于13,33大于30,33小于43,放在43的左边子节点:
9.插入42,42大于13,42大于30,42小于43,42大于33,放在33右边子节点,左旋,右旋,变色,左旋:
10.插入21,21大于13,21小于42,,21小于30,21小于23,放在23左边子节点:
11.插入18,18大于13,18小于42,18小于30,18小于23,18小于21,放在21左边子节点,右旋,变色,右旋,变色,左旋:
12.插入6,6小于30,6小于13,6小于9,6小于7,放在7的左边子节点:
13.插入3,3小于30,3小于13,3小于9,3小于7,3小于6,放在6的左边子节点,右旋,变色:
14.插入50,50大于30,,50大于42,50大于43,放在43的右边子节点,左旋:
15.红黑树生成完毕,检查一下几个点:
- 每个节点不存在同时链接两条红线。
- 所有红线都是左连接。
- 不存在两条连续的红连接。
- 每个叶节点的空连接到根节点经过的黑连接数量相同(这里都是2)。