真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解

  此文章为从二叉树到红黑树系列文章的第一节,主要介绍写这系列文章的起因,致谢邓老师,解释二叉树节点类和二叉树的四种遍历写法(包括递归和迭代写法)


文章目录

  • 一、前言与致谢~(点击右边波浪线可以返回目录)
  • 二、本系列文章内容基本介绍~
    • 1.与邓老师原有代码对比~
    • 2.主要流程(重点)~
    • 3.要看懂文章内的代码你需要了解的内容~
  • 三、树的语义规定~
    • 1.树的高度与深度~
    • 2.树的遍历~
      • (1)树的先序遍历~
      • (2)树的中序遍历~
      • (3)树的后序遍历~
      • (4)树的层次遍历~
  • 四、基本二叉树节点类~
    • (一)定义变量和接口~
      • 1.需要的变量~
      • 2.需要的接口~
      • 3.其他辅助函数~
      • 4.BinNode.h~
    • (二)通用全局静态函数(不包含后面代码就运行不了哦)~
      • BInNode_Macro.h~
    • (三)树的四种遍历~
      • (重点)借助仿函数来实现遍历的高明方法~
      • 1.层次遍历的队列写法~
      • 2.先序遍历~
        • (1)先序遍历选择接口~
        • (2)先序遍历的递归写法~
        • (3)先序遍历借助栈的迭代写法1~
        • (4)先序遍历借助栈的迭代写法2--便于理解中序和后序的迭代写法~
      • 3.中序遍历~
        • (1)中序遍历选择接口~
        • (2)中序遍历的递归写法~
        • (3)中序遍历借助栈的迭代写法~
        • (4)中序遍历不借助栈的迭代写法--在理解了怎么求直接后继后再来看~
      • 4.后序遍历~
        • (1)后序遍历选择接口~
        • (2)后序遍历的递归写法~
        • (3)后序遍历借助栈的迭代写法~
          • a)找到当前子树后续遍历的第一个节点~
          • b) 主遍历函数~
          • c)后序遍历迭代算法完整版~
    • (四)求当前节点的直接后继(用于中序遍历)~
      • (1)若当前节点有右孩子,则其直接后继必然存在,且属于其右子树。~
      • (2)若当前节点没有右孩子,则其直接后继若存在,并为其祖先。~
      • (3)后继节点代码~
    • (五)BinNode.h完整代码~
  • 五.后序文章链接~

一、前言与致谢~(点击右边波浪线可以返回目录)

  c++是一门与时俱进的语言,纵观网上绝大多数c++的数据结构实现,都是c with class,不得不说,这样确实有利于理解和学习,但对于那些想借助数据结构来巩固c++的人来说,不由得有点遗憾(初学直接啃stl源码会直接自闭的)。

  在此,感谢清华的邓俊辉老师的c++数据结构教材和视频,多亏了邓老师的辛勤耕耘,我才有幸见到现代化的适用于初学者的c++数据结构代码。在邓老师教材的第五章,第七章和第八章,全部是介绍树的知识,看完和仿照敲完代码之后,获益匪浅,不止是对数据结构和算法的理解更深了一步,也对c++的特性掌握的更加牢固了一步。

  笔者将其代码中的一些特性进行了一些更新和修改,让其更符合现代风格。由于老师对堆区数据的回收都处理的很好,有的地方用智能指针的话,需要改比较多的东西,所以还是保持原来的new和delete。

  本系列文章为了方便,会引用教材和视频中的一些插图(侵删),虽然我对原代码进行了些许修改,但原代码所有权依然归邓老师所有

  在写此系列文章的时候,笔者也对文章进行了排版,让其更容易进行阅读,大家放心阅读,笔者在适当的地方都留了空行和分段。(md的排版真心没word好= =,排个版都要敲代码!)

  本系列文章的所有代码,均在vs2019版本下编译通过,如果你无法编译过,请把编译器的c++版本开启c++17以上。

二、本系列文章内容基本介绍~

1.与邓老师原有代码对比~

我主要对以下几点进行了更新和修改

  1. 将所有的宏定义换成了c++11的constexpr和inline。有的换成了全局静态函数,有的换成了类中使用的函数。
  2. 大概是处于教学需要,邓公用的都是自定义的stack和queue,笔者就偷懒,直接用stl现成的stack和queue了,实现上大致相同。
  3. 将原来的某些代码进行了增减,这样更便于呈现主要内容。
  4. 所有主要代码都有迭代形式,主要代码流程中没有递归,有的代码会分别给出递归和迭代的形式,以便于理解。

2.主要流程(重点)~

笔者根据代码的内容不同,将分六部分来展现

  1. 基本二叉树节点,通用函数
  2. 基本二叉树类的定义和实现
  3. BST(二叉搜索树的实现)
  4. AVL(二叉平衡搜索树的实现)
  5. B树的实现(如果你只想了解B树,可以跳过所有章节,直接看B树)
  6. 红黑树的实现

  强烈建议按照流程进行浏览,只有这样,你才能真正了解红黑树!

  但若你没有时间,也没关系,我会在一些重点部分,涉及到之前知识点的地方弄一个提示,去指明你去哪才能看到相关的内容。

3.要看懂文章内的代码你需要了解的内容~

  1. 掌握c++的引用,指针,类的基本使用
  2. 知道什么是指针引用
  3. 了解命名空间,using声明,using别名。
  4. 了解c++的模板,仿函数的使用
  5. 熟悉stl中stack和queue容器的用法
  6. 对constexpr和inline不陌生
  7. 对树有一定的了解

  要完全看懂本系列文章,如果你是初次接触,不要妄想能在两三个小时内弄懂此系列文章所有内容,没人可以做的到,正确的方法是一天抽一两个小时研究其中一篇文章,对于红黑树可能需要更长的时间,你才能真正的看懂代码,如果你看过邓老师的教程的话,会更能体会到我这段话的意思。

  如果你嫌长,大可以看其他的文章,但我相信,弄懂本系列文章之后,不仅你的数据结构的功底,还是c++的功底,都可以更上一层楼。(针对c++初学者)。

三、树的语义规定~

1.树的高度与深度~

在此系列文章中,会大量用到树的高度和深度的概念,在此做一个强调

深度: 从根到该节点的唯一路径长,根的深度为0;从上到下数
高度 从该节点到叶节点的最长路径,所有树叶的高度为0;从下往上数

另外,规定空树的高度为-1

所以,若只有根节点存在,根节点的高度为0。

2.树的遍历~

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第1张图片

(1)树的先序遍历~

先访问根节点,再依次遍历左子树和右子树
即根左右(一直左到底再右)
3102546

(2)树的中序遍历~

先访问左子树,再依次遍历根和右子树
即左根右(即将各个节点映射到一条直线上)
0123456

(3)树的后序遍历~

先访问左子树,再依次遍历右子树和根
即左右根
0214653

(4)树的层次遍历~

从上而下,从左到右访问树中各个节点
3150246

四、基本二叉树节点类~

(一)定义变量和接口~

1.需要的变量~

T _data;//存放数据
BinNodePtr _parent;
BinNodePtr _lchild;
BinNodePtr _rchild;
int _height;//高度(通用)
RBColor _color;//红黑树专用

2.需要的接口~

四种遍历
取得当前节点的直接后继(BST,AVL,RedBlack用)

3.其他辅助函数~

判等器等操作符重载

4.BinNode.h~

	namespace {
     
		enum class RBColor {
      RED, BLACK };
	}

	//===二叉树节点类===//
	template<typename T = int>
	class BinNode {
     

	public:
		using BinNodePtr = BinNode<T>*;

	public:
		T _data;//存放数据
		BinNodePtr _parent;
		BinNodePtr _lchild;
		BinNodePtr _rchild;
		int _height;//高度(通用)
		RBColor _color;//红黑树专用

	public:
		BinNode() :_data(0), _parent(nullptr),_lchild(nullptr), _rchild(nullptr), _height(0), _color(RBColor::RED) {
     }

		BinNode(
			const T& data, 
			const BinNodePtr parent = nullptr, 
			const BinNodePtr lchild=nullptr, 
			const BinNodePtr rchild=nullptr,
			const int &height=0, 
			const RBColor& color = RBColor::RED)
			:_data(data), _parent(parent), _lchild(lchild), _rchild(rchild),_height(height),_color(color) {
     }

	public:
		constexpr bool operator==(const BinNode& bN)const {
     
			return this->_data == bN._data;
		}

		constexpr bool operator!=(const BinNode& bN)const {
     
			return !(this->_data == bN._data);
		}

		constexpr bool operator<(const BinNode& bN)const {
     
			return this->_data < bN._data;
		}

		constexpr bool operator>(const BinNode& bN)const {
     
			return this->_data > bN._data;
		}

		constexpr bool operator<=(const BinNode& bN)const {
     
			return !(this->_data > bN._data);
		}

		constexpr bool operator>=(const BinNode& bN)const {
     
			return !(this->_data < bN._data);
		}
	public:

		BinNodePtr succ();//取得中序遍历当前节点的直接后继,必然无左孩子

	public:
		template <typename VST> void travLevel(VST); //子树层次遍历

		template <typename VST> void travPre(VST,const int &method=1); //子树先序遍历

		template <typename VST> void travPost(VST,const int&method=1); //子树后序遍历

		template <typename VST> void travIn(VST,const int&method=1); //子树中序遍历

	};//class BinNode

(二)通用全局静态函数(不包含后面代码就运行不了哦)~

  邓老师书上的宏定义有很多个,我将用的多的封装成相应的constexpr和inline函数,将用的少的放入对应的树的类中,这样更有利于管理和应用。

BInNode_Macro.h~

#pragma once

/******************************************************************************************
 * BinNode状态与性质的判断
 ******************************************************************************************/

#include
using std::max;

namespace mytree_marcro {
     
    template<typename BinNodePtr>
    static constexpr bool IsRoot(const BinNodePtr& x) {
     
        return !(x->_parent);
    }

    template<typename BinNodePtr>
    static constexpr bool IsLChild(const BinNodePtr& x) {
     
        return (!IsRoot(x) && (x == x->_parent->_lchild));
    }

    template<typename BinNodePtr>
    static constexpr bool IsRChild(const BinNodePtr& x) {
     
        return (!IsRoot(x) && (x == x->_parent->_rchild));//不是根节点,并且其地址跟其父亲的右孩子地址相同
    }


    template<typename BinNodePtr>
    static constexpr bool HasLChild(const BinNodePtr& x) {
     
        return (x == nullptr) ? false : x->_lchild;
	}

    template<typename BinNodePtr>
    static constexpr bool HasRChild(const BinNodePtr& x) {
     
        return (x == nullptr) ? false : x->_rchild;
	}

    template<typename BinNodePtr>
    static constexpr int stature(const BinNodePtr& x) {
     //获取高度
        return x ? x->_height : -1;//空指针高度为-1
    }

}// namespace mytree_marcro

(三)树的四种遍历~

(重点)借助仿函数来实现遍历的高明方法~

  对于老手和善用stl的人而言, 这种利用回调函数来进行访问对应节点的方式,再正常不过。在邓老师的遍历代码中,就高明的用了这种方式,通过这种高内聚,低耦合的方式,从而能对节点进行更多的操作,而并非仅仅是cout就完事。在之后的BST,AVL和RedBlack中,我都会提供一些测试用的仿函数,读者可根据自己的需要进行改进。

1.层次遍历的队列写法~

  根节点入队,然后弹出根节点,并将其左右节点入队。直至队列为空。
真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第2张图片

template<typename T>template<typename VST>
void BinNode<T>::travLevel(VST visit) {
     
	if (this == nullptr)//提前判断是否为空树
		return;
	queue<BinNodePtr> Q;
	Q.push(this);//根节点入队
	BinNodePtr current = nullptr;//防止多次调用构造析构,故在循环为创建临时变量
	while (!Q.empty()) {
     //如果队列不为空
		current = Q.front();
		visit(current);
		Q.pop();
		if (HasLChild(current))
			Q.push(current->_lchild);
		if (HasRChild(current))
			Q.push(current->_rchild);
	}
}

2.先序遍历~

(1)先序遍历选择接口~

由于有三种先序遍历的写法,因此创建一个接口函数,并且默认调用迭代版的先序遍历

template<typename T>template<typename VST>
void BinNode<T>::travPre(VST visit,const int& method) {
     

	using mytree_trav::travPre_1;
	using mytree_trav::travPre_2;
	using mytree_trav::travPre_R;

	if (this == nullptr)//提前判断是否为空树
		return;
	switch (method)
	{
     
	case 0:travPre_1(this, visit); break;
	case 1:travPre_2(this, visit); break;
	default:travPre_R(this, visit); break;
	}
}

(2)先序遍历的递归写法~

以根左右的顺序,进行递归先序遍历

/*先序遍历递归版*/
template<typename BinNodePtr, typename VST>
static void travPre_R(BinNodePtr x, VST visit) {
     
	if (x == nullptr)
		return;
	visit(x);
	travPre_R(x->_lchild, visit);
	travPre_R(x->_rchild, visit);
}

(3)先序遍历借助栈的迭代写法1~

  尾递归转化为循环。如果只有一个递归体,那么直接转循环就可以。因为此处有两个递归体,因此,还需要借助一个辅助栈来实现。

  递归时,分成3部分来看,第一部分基本语句,第二部分递归调用左孩子,第三部分递归调用右孩子。
  从递归时函数调用的堆栈分析来看,相当于先把最后一个语句,也就是遍历右孩子入栈,再把倒数第二个语句,也就是遍历左孩子入栈,下一步就是取出栈顶元素进行遍历。重复循环。理解了此点,你就能很快的理解为什么需要一个辅助栈!

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第3张图片

//-----------先序遍历--------------//
/*迭代版1*//*递归转迭代*//*此法与迭代2在效率上,没什么区别,但是此法只适用于先序
 *而迭代2的方式,可以适用于中序和后序遍历*/
template<typename BinNodePtr, typename VST>
static void travPre_1(BinNodePtr x, VST visit) {
     
	stack<BinNodePtr> S;
	S.push(x);
	while (!S.empty()) {
     
		x = S.top();
		S.pop();
		visit(x);
		if (HasRChild(x))//入栈次序为先右后左
			S.push(x->_rchild);
		if (HasLChild(x))
			S.push(x->_lchild);
	}
}

(4)先序遍历借助栈的迭代写法2–便于理解中序和后序的迭代写法~

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第4张图片

  从书上虚线的的路径,不难发现遍历过程中的一个套路,即遍历可以等价的理解成,从当前节点从上到下访问左子树并visit对应的节点,直到访问到左子树为空为止,同时在途中将右孩子均存入栈中。当左子树访问到根节点时,就取出栈顶元素,继续做一次从上到下的向左遍历。

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第5张图片

/*迭代版2*/
template<typename BinNodePtr, typename VST>
static void visitLeft_Pre(BinNodePtr x, VST visit, stack<BinNodePtr>& S) {
     
	while (x) {
     
		visit(x);//访问当前节点
		if (HasRChild(x))//右孩子入栈暂存
			S.push(x->_rchild);
		x = x->_lchild;
	}
}

template<typename BinNodePtr, typename VST>
static void travPre_2(BinNodePtr x, VST visit) {
     
	stack<BinNodePtr> S;
	while (true) {
     
		visitLeft_Pre(x, visit, S);//从当前节点出发,从上到下,向左访问。
		if (S.empty())//直到栈空
			break;
		x = S.top();//若栈不空,则取出栈顶节点继续循环
		S.pop();
	}
}

3.中序遍历~

(1)中序遍历选择接口~

由于有三种中序遍历的写法,因此创建一个接口函数,并且默认调用迭代版的中序遍历

template<typename T>template<typename VST>
void BinNode<T>::travIn(VST visit, const int& method) {
     

	using mytree_trav::travIn_1;
	using mytree_trav::travIn_2;
	using mytree_trav::travIn_R;

	if (this == nullptr)//提前判断是否为空树
		return;
	switch (method)
	{
     
	case 1:travIn_1(this, visit); break;//1相对普通树最快
	case 2:travIn_2(this, visit); break;
	default:travIn_R(this, visit); break;
	}
}

(2)中序遍历的递归写法~

以左根右的顺序,进行递归中序遍历

/*递归版*/
template<typename BinNodePtr,typename VST>
static void travIn_R(BinNodePtr x, VST visit) {
     
	if (x == nullptr)
		return;
	travIn_R(x->_lchild,visit);
	visit(x);
	travIn_R(x->_rchild,visit);
}

(3)中序遍历借助栈的迭代写法~

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第6张图片
  从上图可以看出,我们遍历时第一个访问的必然是最左边的节点a,但我们的传入的节点一般为根节点,即都是从i开始进行中序遍历的。因此,首先也必须要到最左边的节点a,即要做一次从上到下的向左访问左子树,而沿途的节点都不妨存入栈中,方便之后使用。

  每从栈中弹出一个节点,就对其进行visit,都需要对其有没有右孩子进行检查,如果有右孩子,则右孩子入栈,并且对右孩子也做一次从上到下的向左遍历,将沿途的节点全部存入栈中。

  这样一直到栈为空为止。
真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第7张图片

//---------中序遍历---------//
template<typename BinNodePtr>
static void visitLeft_In(BinNodePtr x, stack<BinNodePtr>& S) {
     //将所有左孩子插入栈中
	while (x) {
     
		S.push(x);
		x = x->_lchild;
	}
}

/*迭代版1*/
template<typename BinNodePtr, typename VST>
static void travIn_1(BinNodePtr x, VST visit) {
     
	stack<BinNodePtr> S;
	while (true) {
     
		visitLeft_In(x, S);//将所有左孩子插入栈中
		if (S.empty())
			break;
		x = S.top();

		visit(x);
		S.pop();
		x = x->_rchild;//如果栈顶元素有右孩子,则对右孩子的左子树进行visitLeft_In,若右孩子为空,则直接结束visitLeft_In
	}
}

(4)中序遍历不借助栈的迭代写法–在理解了怎么求直接后继后再来看~

  本方法实际上本人不推荐使用,因为其每一次遍历都需要去求后继,无疑相对前面一种迭代方法而言,增加了不必要的代价。

  因此,此法有兴趣者可以看看。此法最重要的是一种利用后继来遍历的思想,这点对于线索二叉树而言非常有效。有兴趣的读者可以看看本人按照大话数据结构改编的线索二叉树的c++代码https://blog.csdn.net/bioinformatique/article/details/106082423

/*迭代版2*/
template<typename BinNodePtr, typename VST>
static void travIn_2(BinNodePtr x, VST visit) {
     
	while (true) {
     
		if (HasLChild(x)) {
     //若有左子树,则
			x = x->_lchild;//深入左子树
		}
		else {
     
			visit(x);//访问当前节点,并
			while (!HasRChild(x)) {
     //不断地在无右分支处
				if (x = x->succ())//回溯至直接后继,注意此处为赋值
					visit(x);//如果有后继,则访问
				else
					return; //(在没有后继的末节点处,直接退出)
			}
			x = x->_rchild;//(直至有右分支处)转向非空的右子树
		}
	}
}

4.后序遍历~

(1)后序遍历选择接口~

由于有两种后序遍历的写法,因此创建一个接口函数,并且默认调用迭代版的后序遍历

template<typename T>template<typename VST>
void BinNode<T>::travPost(VST visit, const int& method) {
     

	using mytree_trav::travPost_1;
	using mytree_trav::travPost_R;

	if (this == nullptr)//提前判断是否为空树
		return;
	switch (method)
	{
     
	case 1:travPost_1(this, visit); break;
	default:travPost_R(this, visit); break;
	}
}

(2)后序遍历的递归写法~

以左右根的顺序,进行递归后序遍历

/*递归版*/
template<typename BinNodePtr, typename VST>
static void travPost_R(BinNodePtr x, VST visit) {
     
	if (x == nullptr)
		return;
	travPost_R(x->_lchild, visit);
	travPost_R(x->_rchild, visit);
	visit(x);
}

(3)后序遍历借助栈的迭代写法~

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第8张图片
  后序遍历与中序和先序遍历不同的是,后序遍历的第一个被访问的节点稍有不同,其没有先序(必然为根节点)和中序(必然为最左孩子)这样的规律性。

  1. 若最左边孩子没有右子树(最左边孩子显然没有左子树),则最左边节点是第一个被访问的节点。
  2. 若最左边孩子有右子树,则是最左边的节点的右子树的最左边的节点。

  首先,无论如何,都需要找到第一个节点。并且沿途的节点也要像中序和前序遍历一样很好地利用起来。幸运的是,邓老师已经帮我们解决了这个难题。

a)找到当前子树后续遍历的第一个节点~

  方法就是从当前节点开始,先看其有没有右孩子,如果有右孩子,则将右孩子入栈,然后再看有没有左孩子,如果有左孩子,则将左孩子再入栈,并且将左孩子当做“当前节点”继续循环。

  如果当前节点没有左孩子,则看这个节点的右孩子有没有孩子,如果这个节点的右孩子有孩子,则把这个节点作为“当前节点”,继续进行循环。

  直到“当前节点”没有孩子为止。那么,此时的栈顶元素,必将为当前树(子树)的后序遍历应该访问的第一个节点。

如下图,第一个节点显然是a

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第9张图片
在这里插入图片描述
代码实现为

template<typename BinNodePtr>
static void visitLeft_Post(stack<BinNodePtr>& S) {
     
	BinNodePtr x = S.top();//将栈顶节点作为当前节点
	while (true) {
     //判断是否有左右孩子,并且先插入右孩子
		if (HasRChild(x)) {
     
			S.push(x->_rchild);
		}
		if (HasLChild(x)) {
     
			S.push(x->_lchild);
		}
		if (x == S.top())//说明栈没有插入任何元素,即抵达此子树应该被访问的第一个节点
			break;
		x = S.top();//否则就将栈顶元素给当前的x
	}
}
b) 主遍历函数~

  在解决了找到当前子树的相对后继遍历的第一个节点后,就解决了后继遍历的绝大多数问题。并且,并非在遍历过程中,所有的节点都需要去执行visitLeft_Post函数。

再来观看此图,以及此时的栈结构

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第10张图片
在这里插入图片描述
我们需要对b进行一次visitLeft_Post函数么,显然不需要,因为其没有左子树。
因此直接访问b。
在这里插入图片描述
下一个被访问的元素为G么?,显然不是G,其左右子树也都没有在栈中,因此,我们需要以G为子树进行一次visitLeft_Post函数,从而找到c。
在这里插入图片描述
从上面3个图可以看出,访问b前不需要进行visitLeft_Post函数,访问c时需要进行visitLeft_Post函数。那么,到底什么时候需要进行visitLeft_Post函数?

  不妨看一下父子关系。b是a的父亲,而G不是b的父亲,反而,G是b的兄弟节点!

  不妨可以得出结论,只有当此时栈顶元素不是刚刚弹出的栈顶元素的父亲时(必然为刚弹出元素的兄弟),才会进行visitLeft_Post函数,将新的节点插入栈中。

下面给出后序遍历迭代版的算法

/*迭代版*/
template<typename BinNodePtr, typename VST>
static void travPost_1(BinNodePtr x, VST visit) {
     
	stack<BinNodePtr> S;
	S.push(x);
	while (!S.empty()) {
     
		//若栈顶节点不是父亲节点,即必为其兄弟节点(除了根节点以外),
		//若为其兄弟节点,则对兄弟节点进行visitLeft_Post的操作。
		if (S.top() != x->_parent)
			visitLeft_Post(S);
		visit(S.top());
		x = S.top();//将x设为栈顶元素
		S.pop();
	}
}
c)后序遍历迭代算法完整版~
//--------------后序遍历--------------//
template<typename BinNodePtr>
static void visitLeft_Post(stack<BinNodePtr>& S) {
     
	BinNodePtr x = S.top();//将栈顶节点作为当前节点
	while (true) {
     //判断是否有左右孩子,并且先插入右孩子
		if (HasRChild(x)) {
     
			S.push(x->_rchild);
		}
		if (HasLChild(x)) {
     
			S.push(x->_lchild);
		}
		if (x == S.top())//说明栈没有插入任何元素,即抵达此子树应该被访问的第一个节点
			break;
		x = S.top();//否则就将栈顶元素给当前的x
	}
}

/*迭代版*/
template<typename BinNodePtr, typename VST>
static void travPost_1(BinNodePtr x, VST visit) {
     
	stack<BinNodePtr> S;
	S.push(x);
	while (!S.empty()) {
     
		//若栈顶节点不是父亲节点,即必为其兄弟节点(除了根节点以外),
		//若为其兄弟节点,则对兄弟节点进行visitLeft_Post的操作。
		if (S.top() != x->_parent)
			visitLeft_Post(S);
		visit(S.top());
		x = S.top();//将x设为栈顶元素
		S.pop();
	}
}

(四)求当前节点的直接后继(用于中序遍历)~

succ()取得当前节点的后继节点

  与所有遍历一样,中序遍历的实质功能也可理解为,为所有节点赋予一个次序,从而将半线性的二叉树转化为线性结构。于是一旦指定了遍历策略,即可与向量和列表一样,在二叉树的节点之间定义后继关系。其中没有后继)的节点称作末节点。

  对于后面将要介绍的BST,AVL,RedBlack,中序遍历的作用至关重要。相关算法必需的一项基本操作,就是定位任一节点在中序遍历序列中的直接后继。

  分析一个节点的后继,不妨分做两种情况分别进行考虑。

(1)若当前节点有右孩子,则其直接后继必然存在,且属于其右子树。~

  在这种情况下,当前节点的直接后继,必然没有左孩子。

如下图a的右孩子b存在,b没有左孩子,即b为a的直接后继

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第11张图片

如下图d的右孩子h存在,h有左孩子,即e为d的直接后继

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第12张图片

(2)若当前节点没有右孩子,则其直接后继若存在,并为其祖先。~

如下图b的右孩子不存在,则其直接后继若存在,必为其某一祖先,即c为b的直接后继

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第13张图片

如下图p的右孩子不存在,但其已经为全树的最右节点,因此其没有后继

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第14张图片

读者们可以根据中序遍历来进一步体会节点的后继

真c++ 从二叉树到红黑树(1)之二叉树节点类及遍历详解_第15张图片

(3)后继节点代码~

//-------取得当前节点的直接后继----------//
template<typename T>
BinNode<T>* BinNode<T>::succ() {
     
	using BinNodePtr = BinNode<T>*;
	BinNodePtr s = this;
	if (HasRChild(s)) {
     //当前节点有右孩子
		s = s->_rchild;
		while (HasLChild(s))//这个右孩子有没有左孩子,直到没有左孩子为止
			s = s->_lchild;
	}
	else {
     
		//必须分是否是右孩子来判断,因为这对应不同的情况,如果这个节点不为右孩子,则返回其父亲
		//如果这个节点是右孩子,则需要不断向上找其父亲,直到不为右孩子为止(即为左孩子或根节点)
		while (IsRChild(s))
			s = s->_parent;
		s = s->_parent;//再往上找一格,就能找到后继。//当然,如果已经到了根节点,根节点的父亲当然为空。所以返回空。同时也说明这个节点是最后一个节点。
	}
	return s;
}

(五)BinNode.h完整代码~

#pragma once

#include
#include
#include "BinNode_Macro.h"

using std::queue;
using std::stack;

namespace mytree {
     

	using namespace mytree_marcro;

	namespace {
     
		enum class RBColor {
      RED, BLACK };
	}

	//===二叉树节点类===//
	template<typename T = int>
	class BinNode {
     

	public:
		using BinNodePtr = BinNode<T>*;

	public:
		T _data;//存放数据
		BinNodePtr _parent;
		BinNodePtr _lchild;
		BinNodePtr _rchild;
		int _height;//高度(通用)
		RBColor _color;//红黑树专用

	public:
		BinNode() :_data(0), _parent(nullptr),_lchild(nullptr), _rchild(nullptr), _height(0), _color(RBColor::RED) {
     }

		BinNode(
			const T& data, 
			const BinNodePtr parent = nullptr, 
			const BinNodePtr lchild=nullptr, 
			const BinNodePtr rchild=nullptr,
			const int &height=0, 
			const RBColor& color = RBColor::RED)
			:_data(data), _parent(parent), _lchild(lchild), _rchild(rchild),_height(height),_color(color) {
     }

	public:
		constexpr bool operator==(const BinNode& bN)const {
     
			return this->_data == bN._data;
		}

		constexpr bool operator!=(const BinNode& bN)const {
     
			return !(this->_data == bN._data);
		}

		constexpr bool operator<(const BinNode& bN)const {
     
			return this->_data < bN._data;
		}

		constexpr bool operator>(const BinNode& bN)const {
     
			return this->_data > bN._data;
		}

		constexpr bool operator<=(const BinNode& bN)const {
     
			return !(this->_data > bN._data);
		}

		constexpr bool operator>=(const BinNode& bN)const {
     
			return !(this->_data < bN._data);
		}
	public:

		BinNodePtr succ();//取得中序遍历当前节点的直接后继,必然无左孩子

	public:
		template <typename VST> void travLevel(VST); //子树层次遍历

		template <typename VST> void travPre(VST,const int &method=1); //子树先序遍历

		template <typename VST> void travPost(VST,const int&method=1); //子树后序遍历

		template <typename VST> void travIn(VST,const int&method=1); //子树中序遍历

	};//class BinNode

	//--------------遍历------------------//
	namespace mytree_trav {
     
		//-----------先序遍历--------------//
		/*迭代版1*//*递归转迭代*//*此法与迭代2在效率上,没什么区别,但是此法只适用于先序
		 *而迭代2的方式,可以适用于中序和后序遍历*/
		template<typename BinNodePtr, typename VST>
		static void travPre_1(BinNodePtr x, VST visit) {
     
			stack<BinNodePtr> S;
			S.push(x);
			while (!S.empty()) {
     
				x = S.top();
				S.pop();
				visit(x);
				if (HasRChild(x))//入栈次序为先右后左
					S.push(x->_rchild);
				if (HasLChild(x))
					S.push(x->_lchild);
			}
		}

		/*迭代版2*/
		template<typename BinNodePtr, typename VST>
		static void visitLeft_Pre(BinNodePtr x, VST visit, stack<BinNodePtr>& S) {
     
			while (x) {
     
				visit(x);//访问当前节点
				if (HasRChild(x))//右孩子入栈暂存
					S.push(x->_rchild);
				x = x->_lchild;
			}
		}

		template<typename BinNodePtr, typename VST>
		static void travPre_2(BinNodePtr x, VST visit) {
     
			stack<BinNodePtr> S;
			while (true) {
     
				visitLeft_Pre(x, visit, S);//从当前节点出发,从上到下,向左访问。
				if (S.empty())//直到栈空
					break;
				x = S.top();//若栈不空,则取出栈顶节点继续循环
				S.pop();
			}
		}

		/*先序遍历递归版*/
		template<typename BinNodePtr, typename VST>
		static void travPre_R(BinNodePtr x, VST visit) {
     
			if (x == nullptr)
				return;
			visit(x);
			travPre_R(x->_lchild, visit);
			travPre_R(x->_rchild, visit);
		}

		//--------------后序遍历--------------//
		template<typename BinNodePtr>
		static void visitLeft_Post(stack<BinNodePtr>& S) {
     
			BinNodePtr x = S.top();//将栈顶节点作为当前节点
			while (true) {
     //判断是否有左右孩子,并且先插入右孩子
				if (HasRChild(x)) {
     
					S.push(x->_rchild);
				}
				if (HasLChild(x)) {
     
					S.push(x->_lchild);
				}
				if (x == S.top())//说明栈没有插入任何元素,即抵达此子树应该被访问的第一个节点
					break;
				x = S.top();//否则就将栈顶元素给当前的x
			}
		}

		/*迭代版*/
		template<typename BinNodePtr, typename VST>
		static void travPost_1(BinNodePtr x, VST visit) {
     
			stack<BinNodePtr> S;
			S.push(x);
			while (!S.empty()) {
     
				//若栈顶节点不是父亲节点,即必为其兄弟节点(除了根节点以外),若为其兄弟节点,则对兄弟节点进行visitLeft_Post的操作。
				if (S.top() != x->_parent)
					visitLeft_Post(S);
				visit(S.top());
				x = S.top();//将x设为栈顶元素
				S.pop();
			}
		}

		/*递归版*/
		template<typename BinNodePtr, typename VST>
		static void travPost_R(BinNodePtr x, VST visit) {
     
			if (x == nullptr)
				return;
			travPost_R(x->_lchild, visit);
			travPost_R(x->_rchild, visit);
			visit(x);
		}

		//---------中序遍历---------//
		template<typename BinNodePtr>
		static void visitLeft_In(BinNodePtr x, stack<BinNodePtr>& S) {
     //将所有左孩子插入栈中
			while (x) {
     
				S.push(x);
				x = x->_lchild;
			}
		}

		/*迭代版1*/
		template<typename BinNodePtr, typename VST>
		static void travIn_1(BinNodePtr x, VST visit) {
     
			stack<BinNodePtr> S;
			while (true) {
     
				visitLeft_In(x, S);//将所有左孩子插入栈中
				if (S.empty())
					break;
				x = S.top();

				visit(x);
				S.pop();
				x = x->_rchild;//如果栈顶元素有右孩子,则对右孩子的左子树进行visitLeft_In,若右孩子为空,则直接结束visitLeft_In
			}
		}

		/*迭代版2*/
		template<typename BinNodePtr, typename VST>
		static void travIn_2(BinNodePtr x, VST visit) {
     
			while (true) {
     
				if (HasLChild(x)) {
     //若有左子树,则
					x = x->_lchild;//深入左子树
				}
				else {
     
					visit(x);//访问当前节点,并
					while (!HasRChild(x)) {
     //不断地在无右分支处
						if (x = x->succ())//回溯至直接后继,注意此处为赋值
							visit(x);//如果有后继,则访问
						else
							return; //(在没有后继的末节点处,直接退出)
					}
					x = x->_rchild;//(直至有右分支处)转向非空的右子树
				}
			}
		}

		/*递归版*/
		template<typename BinNodePtr,typename VST>
		static void travIn_R(BinNodePtr x, VST visit) {
     
			if (x == nullptr)
				return;
			travIn_R(x->_lchild,visit);
			visit(x);
			travIn_R(x->_rchild,visit);
		}

	}//namespace mytree_trav

	template<typename T>template<typename VST>
	void BinNode<T>::travLevel(VST visit) {
     
		if (this == nullptr)//提前判断是否为空树
			return;
		queue<BinNodePtr> Q;
		Q.push(this);//根节点入队
		BinNodePtr current = nullptr;//防止多次调用构造析构,故在循环为创建临时变量
		while (!Q.empty()) {
     //如果队列不为空
			current = Q.front();
			visit(current);
			Q.pop();
			if (HasLChild(current))
				Q.push(current->_lchild);
			if (HasRChild(current))
				Q.push(current->_rchild);
		}
	}

	template<typename T>template<typename VST>
	void BinNode<T>::travPre(VST visit,const int& method) {
     

		using mytree_trav::travPre_1;
		using mytree_trav::travPre_2;
		using mytree_trav::travPre_R;

		if (this == nullptr)//提前判断是否为空树
			return;
		switch (method)
		{
     
		case 0:travPre_1(this, visit); break;
		case 1:travPre_2(this, visit); break;
		default:travPre_R(this, visit); break;
		}
	}

	template<typename T>template<typename VST>
	void BinNode<T>::travPost(VST visit, const int& method) {
     

		using mytree_trav::travPost_1;
		using mytree_trav::travPost_R;

		if (this == nullptr)//提前判断是否为空树
			return;
		switch (method)
		{
     
		case 1:travPost_1(this, visit); break;
		default:travPost_R(this, visit); break;
		}
	}

	template<typename T>template<typename VST>
	void BinNode<T>::travIn(VST visit, const int& method) {
     

		using mytree_trav::travIn_1;
		using mytree_trav::travIn_2;
		using mytree_trav::travIn_R;

		if (this == nullptr)//提前判断是否为空树
			return;
		switch (method)
		{
     
		case 1:travIn_1(this, visit); break;//1相对普通树最快
		case 2:travIn_2(this, visit); break;
		default:travIn_R(this, visit); break;
		}
	}

	//-------取得当前节点的直接后继----------//
	template<typename T>
	BinNode<T>* BinNode<T>::succ() {
     
		using BinNodePtr = BinNode<T>*;
		BinNodePtr s = this;
		if (HasRChild(s)) {
     //当前节点有右孩子
			s = s->_rchild;
			while (HasLChild(s))//这个右孩子有没有左孩子,直到没有左孩子为止
				s = s->_lchild;
		}
		else {
     
			//必须分是否是右孩子来判断,因为这对应不同的情况,如果这个节点不为右孩子,则返回其父亲
			//如果这个节点是右孩子,则需要不断向上找其父亲,直到不为右孩子为止(即为左孩子或根节点)
			while (IsRChild(s))
				s = s->_parent;
			s = s->_parent;//再往上找一格,就能找到后继。//当然,如果已经到了根节点,根节点的父亲当然为空。所以返回空。同时也说明这个节点是最后一个节点。
		}
		return s;
	}
}//namespace mytree

五.后序文章链接~

  1. 基本二叉树节点,通用函数 二叉树节点
  2. 基本二叉树类的定义和实现 二叉树基类
  3. BST(二叉搜索树的实现) BST
  4. AVL(二叉平衡搜索树的实现)AVL
  5. B树的实现(如果你只想了解B树,可以跳过所有章节,直接看B树)B树
  6. 红黑树的实现 RedBlack

学一个东西,不知道其道理,不高明!

你可能感兴趣的:(C++数据结构与算法,c++,数据结构)