数据结构--二叉树进阶

2023-11-13

因为我们之前在学习数据结构的时候使用的是C语言,但是并不是所有的数据结构都适合使用C语言学习,如今我们了解了C++的基础语法,具备了学习这些稍微难一点的数据结构的前提,所以我们再次回顾数据结构,使用C++这一更加先进的武器,来解决更加复杂的问题

二叉搜索树

二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树 ,或者是具有以下性质的二叉树 :
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树

int a [] = {5,3,4,1,7,8,2,6,0,9};

 二叉搜索树操作

二叉搜索树的查找

代码实现:

bool Find(const K& key)//传入key值
{
	Node* cur = root;//初始化cur指针
	while (cur)//循环cur
	{
		if (cur->_key < key)//当节点上的key较小
		{
			cur = cur->_right;//向右查找
		}
		else if (cur->_key > key)//当节点上的值较大
		{
			cur = cur->_left;//向左查找
		}
		else
		{
			return true;//当相等时则找到
		}
	}
	return false;//当循环出树时未找到
}

 二叉搜索树的插入

插入的具体过程如下:
a. 树为空,则直接插入

 b. 树不空,按二叉搜索树性质查找插入位置,插入新节点

bool Insert(const K& key)//二叉树的插入
	{
		if (_root == nullptr)//当根为空,将要插入的节点赋给根
		{
			_root = new Node(key);
			return true;//返回插入成功
		}

		Node* parent = nullptr;//初始化父节点
		Node* cur = _root;//初始化cur结点
		while (cur)//开始循环cur,当cur走到末尾为空时停止
		{
			if (cur->_key < key)//当cur的_key小于要插入的key时
			{
				parent = cur;//双指针开始向右迭代移动
				cur = cur->_right;
			}
			else if (cur->_key > key)//当cur的_key大于要插入的key
			{
				parent = cur;//双指针开始向左移动
				cur = cur->_left;
			}
			else
			{
				return false;//否则返回失败
			}
		}
		//当我们找到要插入的位置,将cur置于此位置时
		cur = new Node(key);//将要插入的节点赋给cur
		//下面的操作是为了将cur与原来的树连接起来,因为我们cur此时为空,不知道要在parent哪边,所以进行比较
		if (parent->_key < key)//如果父节点的_key值小于插入的值
		{
			parent->_right = cur;//置于父亲的右子树
		}
		else
		{
			parent->_left = cur;//置于父亲的左子树
		}
		return true;//返回找到
	}

 二叉树的插入,我们先通过一个cur指针与树上的节点比较大小,小的话向左走,大的话向右走,直到走到待插入的位置,而后将结点赋给cur,利用双指针再将其连接到二叉树上即可

二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点

这种删除的话就直接用指针找到2并且删除即可 

b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点

 

这两种其实可以合在一起,都是仅有一个子结点进行删除,思路就是将待删节点的子节点与待删节点的父节点相连 ,比如删除我们的1和8

d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,
再来处理该结点的删除问题

 在我们需要删除一个拥有两个子节点的结点时,我们不能直接进行删除,需要找到一个叶子节点去替换它,然后删除,所以我们需要找到左子树的最大节点或者右子树的最右结点,只有这两个是可以替换它的

bool Erase(const K& key)//二叉树的删除
	{
		Node* parent = nullptr;//初始化父节点
		Node* cur = _root;//初始化cur

		while (cur)//循环cur,进行查找
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				// 找到了,开始分情况删除
				// 1、左为空
				if (cur->_left == nullptr)//当左子树为空时,可能有右子树,也可能没有
				{
					if (cur == _root)//防止删除的为最上面无法找到parent
					{
						_root = cur->_left;
					}
					else
					{
						if (parent->_right == cur)//如果cur在parent的右子树上也就是\结构
							parent->_right = cur->_right;//直接相连cur的右子树与其父节点
						else//< 结构
							parent->_left = cur->_right;
					}
					delete cur;//释放删除cur

				}
				// 2、右为空
				else if (cur->_right == nullptr)
				{
					if (cur == _root)//防止删除的为最上面无法找到parent
					{
						_root = cur->_left;
					}
					else
					{
						if (parent->_left == cur)//    /结构
							parent->_left = cur->_left;
						else// >结构
							parent->_right = cur->_left;
					}
					delete cur;
				}
				// 3、左右都不为空
				else
				{
					Node* rightMinParent = cur;//初始化遍历节点的父节点
					Node* rightMin = cur->_right;//初始化遍历结点,向右走
					while (rightMin->_left)//当节点的左子树不为空
					{
						rightMinParent = rightMin;//向左走
						rightMin = rightMin->_left;
					}
					cur->_key = rightMin->_key;//将最小的结点赋给目标节点
					//转换成删除rightMin(rightMin是左为空,父亲指向它的右)
					if (rightMin == rightMinParent->_left)
						rightMinParent->_left = rightMin->_right;//将最小节点的右子树赋给父节点
					else
						rightMinParent->_right = rightMin->_right;
					delete rightMin;//释放已经换过的最小节点
				}
				return true;//返回删除成功
			}
		}
		return false;//没找到节点则返回失败
	}

对于删除的关键就是要掌握删除时的3种情况,从而分别分析删除

二叉搜索树的基本实现

#pragma once

template<class K>
struct BSTreeNode // 二叉搜索树
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;

	K _key;

	BSTreeNode(const K& key)//初始化列表初始化
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
	{}
};

template<class K>
class BSTree // Binary Search Tree
{
	typedef BSTreeNode<K> Node;
public:
	bool Insert(const K& key)//二叉树的插入
	{
		if (_root == nullptr)//当根为空,将要插入的节点赋给根
		{
			_root = new Node(key);
			return true;//返回插入成功
		}

		Node* parent = nullptr;//初始化父节点
		Node* cur = _root;//初始化cur结点
		while (cur)//开始循环cur,当cur走到末尾为空时停止
		{
			if (cur->_key < key)//当cur的_key小于要插入的key时
			{
				parent = cur;//双指针开始向右迭代移动
				cur = cur->_right;
			}
			else if (cur->_key > key)//当cur的_key大于要插入的key
			{
				parent = cur;//双指针开始向左移动
				cur = cur->_left;
			}
			else
			{
				return false;//否则返回失败
			}
		}
		//当我们找到要插入的位置,将cur置于此位置时
		cur = new Node(key);//将要插入的节点赋给cur
		//下面的操作是为了将cur与原来的树连接起来,因为我们cur此时为空,不知道要在parent哪边,所以进行比较
		if (parent->_key < key)//如果父节点的_key值小于插入的值
		{
			parent->_right = cur;//置于父亲的右子树
		}
		else
		{
			parent->_left = cur;//置于父亲的左子树
		}
		return true;//返回找到
	}

	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else
			{
				return true;
			}
		}

		return false;
	}

	bool Erase(const K& key)//二叉树的删除
	{
		Node* parent = nullptr;//初始化父节点
		Node* cur = _root;//初始化cur

		while (cur)//循环cur,进行查找
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				// 找到了,开始分情况删除
				// 1、左为空
				if (cur->_left == nullptr)//当左子树为空时,可能有右子树,也可能没有
				{
					if (cur == _root)//防止删除的为最上面无法找到parent
					{
						_root = cur->_left;
					}
					else
					{ 
					if (parent->_right == cur)//如果cur在parent的右子树上也就是\结构
						parent->_right = cur->_right;//直接相连cur的右子树与其父节点
					else//< 结构
						parent->_left = cur->_right;
					}
					delete cur;//释放删除cur

				}
				// 2、右为空
				else if (cur->_right == nullptr)
				{
					if (cur == _root)//防止删除的为最上面无法找到parent
					{
						_root = cur->_left;
					}
					else
					{
						if (parent->_left == cur)//    /结构
							parent->_left = cur->_left;
						else// >结构
							parent->_right = cur->_left;
					}
					delete cur;
				}
				// 3、左右都不为空
				else
				{
					Node* rightMinParent = cur;//初始化遍历节点的父节点
					Node* rightMin = cur->_right;//初始化遍历结点,向右走
					while (rightMin->_left)//当节点的左子树不为空
					{
						rightMinParent = rightMin;//向左走
						rightMin = rightMin->_left;
					}
					cur->_key = rightMin->_key;//将最小的结点赋给目标节点
					//转换成删除rightMin(rightMin是左为空,父亲指向它的右)
					if (rightMin == rightMinParent->_left)
						rightMinParent->_left = rightMin->_right;//将最小节点的右子树赋给父节点
					else
						rightMinParent->_right = rightMin->_right;
					delete rightMin;//释放已经换过的最小节点
				}
				return true;//返回删除成功
			}
		}
		return false;//没找到节点则返回失败
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

二叉搜索树的应用

1. K 模型: K 模型即只有 key 作为关键码,结构中只需要存储 Key 即可,关键码即为需要搜索到的值
比如: 给一个单词 word ,判断该单词是否拼写正确 ,具体方式如下:
以单词集合中的每个单词作为 key ,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

其实我们的k模型就是我们一般的二叉树模型,去查找k是否在二叉树中

2. KV 模型:每一个关键码 key ,都有与之对应的值 Value ,即 <Key, Value> 的键值对 。该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系 ,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese> 就构成一种键值对;再比如 统计单词次数 ,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是 <word, count> 就构成一种键值对
比如:实现一个简单的英汉词典 dict ,可以通过英文找到与其对应的中文,具体实现方式如: <单词,中文含义 > 为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较 Key
查询英文单词时,只需给出英文单词,就可快速找到与其对应的 key
#pragma once

template<class K,class V>
struct BSTreeNode // 二叉搜索树
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;

	K _key;
	V _value;

	BSTreeNode(const K& key)//初始化列表初始化
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
		, _value(value)
	{}
};

template<class K,class V>
class BSTree // Binary Search Tree
{
	typedef BSTreeNode<K, V> Node;
public:
	bool Insert(const K& key,const V& value)//二叉树的插入
	{
		if (_root == nullptr)//当根为空,将要插入的节点赋给根
		{
			_root = new Node(key);
			return true;//返回插入成功
		}

		Node* parent = nullptr;//初始化父节点
		Node* cur = _root;//初始化cur结点
		while (cur)//开始循环cur,当cur走到末尾为空时停止
		{
			if (cur->_key < key)//当cur的_key小于要插入的key时
			{
				parent = cur;//双指针开始向右迭代移动
				cur = cur->_right;
			}
			else if (cur->_key > key)//当cur的_key大于要插入的key
			{
				parent = cur;//双指针开始向左移动
				cur = cur->_left;
			}
			else
			{
				return false;//否则返回失败
			}
		}
		//当我们找到要插入的位置,将cur置于此位置时
		cur = new Node(key, value);//将要插入的节点赋给cur
		//下面的操作是为了将cur与原来的树连接起来,因为我们cur此时为空,不知道要在parent哪边,所以进行比较
		if (parent->_key < key)//如果父节点的_key值小于插入的值
		{
			parent->_right = cur;//置于父亲的右子树
		}
		else
		{
			parent->_left = cur;//置于父亲的左子树
		}
		return true;//返回找到
	}

	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}

		return nullptr;
	}

	bool Erase(const K& key)//二叉树的删除
	{
		Node* parent = nullptr;//初始化父节点
		Node* cur = _root;//初始化cur

		while (cur)//循环cur,进行查找
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				// 找到了,开始分情况删除
				// 1、左为空
				if (cur->_left == nullptr)//当左子树为空时,可能有右子树,也可能没有
				{
					if (cur == _root)//防止删除的为最上面无法找到parent
					{
						_root = cur->_left;
					}
					else
					{ 
					if (parent->_right == cur)//如果cur在parent的右子树上也就是\结构
						parent->_right = cur->_right;//直接相连cur的右子树与其父节点
					else//< 结构
						parent->_left = cur->_right;
					}
					delete cur;//释放删除cur

				}
				// 2、右为空
				else if (cur->_right == nullptr)
				{
					if (cur == _root)//防止删除的为最上面无法找到parent
					{
						_root = cur->_left;
					}
					else
					{
						if (parent->_left == cur)//    /结构
							parent->_left = cur->_left;
						else// >结构
							parent->_right = cur->_left;
					}
					delete cur;
				}
				// 3、左右都不为空
				else
				{
					Node* rightMinParent = cur;//初始化遍历节点的父节点
					Node* rightMin = cur->_right;//初始化遍历结点,向右走
					while (rightMin->_left)//当节点的左子树不为空
					{
						rightMinParent = rightMin;//向左走
						rightMin = rightMin->_left;
					}
					cur->_key = rightMin->_key;//将最小的结点赋给目标节点
					//转换成删除rightMin(rightMin是左为空,父亲指向它的右)
					if (rightMin == rightMinParent->_left)
						rightMinParent->_left = rightMin->_right;//将最小节点的右子树赋给父节点
					else
						rightMinParent->_right = rightMin->_right;
					delete rightMin;//释放已经换过的最小节点
				}
				return true;//返回删除成功
			}
		}
		return false;//没找到节点则返回失败
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << " " << root->_value << " ";
		_InOrder(root->_right);
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

private:
	Node* _root = nullptr;
};

void TestBSTree1()//查单词
{
	BSTree<string, string>dict;
	dict.Insert("sort", "排序");
	dict.Insert("string", "字符串");
	dict.Insert("tree", "树");
	dict.Insert("insert", "插入");

	string str;
	while (cin >> str)
	{
		BSTreeNode<string, string>*ret = dict.Find(str);
		if (ret)
		{
			cout << ret->_value << endl;
		}
		else
		{
			cout << "无此单词" << endl;
		}
	}

}
void TestBSTree2()//数数
{
	string strs[] = { "苹果","苹果","香蕉","香蕉","苹果","香蕉","苹果""橘子","苹果","橘子","苹果","苹果","香蕉" }
	BSTree<string, int>countTree;
		for (auto str : strArr)
	{
		BTSreeNode<string, int>* ret = countTree.Find(str);
		if (ret == nullptr)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			ret->_value++;
		}
	}
}

上面两个例子就是我们的K -Valu模型的例子,总的来说还是很常用的

二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的
深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

如上图我们可以知道,二叉搜索树搜索的效率是根据树的结构而决定的,如果树是顺序插入时,效率就不那么高了
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:logN
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2

 二叉树的非递归遍历

我们在之前的学习中,对于二叉树的许多操作都是基于递归而实现的,但事实上递归的效率却没有想得那么高,所以我们还需要来了解二叉树的非递归遍历

前序遍历

我们先来完成一下我们熟知的递归前序遍历

void _preorderTraversal(TreeNode* root; vector<int>& v)//子函数
{
	if (root == NULL)
		return;

	v.push_back(root->val);
	_preorderTraversal(root->left, v);
	_preorderTraversal(root->right, v);
}
vector<int> preorderTraversal(TreeNode* root)
{
	vector<int>ret;
	_preorderTraversal(root, ret);
	return ret;
}

迭代

我们的二叉树遍历方式其实思想上还是去利用递归的方式,只是递归的步骤我们进行了模拟,我们利用栈去模拟递归地访问顺序

 我们这样一棵树的前序遍历顺序为:1 2 4 5 3 6 7 顺序为根 左 右

 这其实就是我们的访问路径,我们可以看到,我们在在访问时,对于同一个节点可能有多次的经过,但是只会有一次的访问,所以我们需要利用栈这个数据结构来进行操作

1.我们先访问,后入栈

我们的cur初始在根节点,在首次接触到一个新节点时进行访问,并且入栈,我们先按照前序遍历的方式,根-左-右,先对这个树根访问,而后向左移动,有节点就入栈,不断向左移动,直到访问到NULL,此时我们的cur就到了栈顶结点的左子树上,而且此时我们就剩下栈中结点的右子树未访问,剩下的我们只需要取结点,访问右子树就可以了

2.访问栈顶元素的右子树,同理重复上述步骤,将右子树再分为左路结点与右子树来访问,那么此时是如何操作的呢?首先,我们取出栈顶元素,将cur指向栈顶结点的右子树,因为这里4的右子树为空,所以默认为访问过,所以我们再次重复,取出2,cur指向其右子树

 3.此时又被分为了左路节点与右树,所以我们将5入栈,向左访问,未NULL,向右访问,也为NULL,发现不管是左树还是右数都没有节点需要入栈了,重复了访问4的过程,所以我们将5出栈,cur指向栈顶的右子树

 4.此时我们的左树都访问完毕了,将cur指向了栈顶的右子树,栈顶元素出栈,再次重复上述过程,访问+入栈,并向左移动,所以我们将3,6依次入栈,并且将cur指向了6的左子树

 再次重复之前的步骤,6左子树为空,认为访问过,转到右子树,也为空,认为访问过,出栈,cur指向栈顶元素的右子树,3出栈,右子树不为空,7入栈,再次向左向右访问,为空认为访问过

此时7的左右树访问完成,出栈,栈空,完成遍历

 注意:是当我们将cur指向栈顶的右子树时栈顶元素出栈

代码实现:

vector<int>preorderTraversal(TreeNode* root)
	{
		vector<int> ret;//定义vector接收
		stack<TreeNode*> st;//定义栈
		TreeNode* cur = root;//初始化cur指针
		while (cur || !st.empty())//当cur不为空且栈中也不为空时
			//这个大循环是当有节点未被访问则会先去探索左结点,再去探索右树
		{
			while (cur)//当cur不为空
				//这个循环的作用是,我们一路向左走,碰到节点就访问+入栈,直到为空
			{
				ret.push_back(cur->val);//cur指向的值入vector(访问)
				st.push(cur);//入栈
				cur = cur->left;//访问左子树,向左走
			}
			//取栈中左路节点的右子树
			TreeNode* top = st.top();//取栈顶的元素赋给top指针
			st.pop();//出栈
			cur = top->right;//cur指向其右子树
		}
       return ret;
	}

其实我们看到,代码并不是很复杂,但是我们的分析过程却是比较详细的,最核心的就是将二叉树分为两部分,左路节点与右子树,访问右子树时将右子树再看作左路节点+右子树即可

中序遍历

我们的中序遍历其实也与前序遍历类似,用的是同一种方法,都是用栈去模拟递归过程,那么我们再来看看中序遍历是如何来实现的吧,在这里我们就不用递归的方式来进行演示了

我们依旧在用画图的方式来进行分析

初始时后依旧是cur在根节点

 1.我们进行遍历,同前序遍历一样,先将cur向左走,遇到节点,入栈,直到走到4的左子节点,为空

 2当我们访问完4的左子节点时,开始访问4,出栈,访问4的右树,为空,访问完成,此时2的左子节点访问完毕,访问2,出栈,cur指向2的右子树,入栈,并重复访问4的过程

3.我们访问完成5的左子节点之后,出栈,访问5,指针指向5的右子节点,右子节点为空,此时1的左子树的所有节点也完成了访问,所以,访问1,出栈,并将cur指向栈顶元素(1)的右子树上

 4.此时我们就可以又像访问之前左子树的方式去访问右子树,向左走,入栈,再向左走,为空访问完成,出栈,向右走,为空,访问完成,此时3的左子树访问完毕,访问3,出栈,cur指向7,入栈,再向左走,为空,访问完成,出栈,指向7的右子节点,为空,完成遍历

 下面我们对其进行代码实现

vector<int>inorderTraversal(TreeNode* root)
	{
		vector<int>ret;//定义vector接收
		stack<TreeNode*>st;//定义栈
		TreeNode* cur = root;//初始化cur指针
		while (cur || !st.empty())//当cur不为空且栈不为空时
		{
			while (cur)//循环cur
			{
				st.push(cur);//入栈
				cur = cur->left;//向左走
			}
			TreeNode* top = st.top();//将栈顶元素赋给top指针
			st.pop();//出栈
			ret.push_back(top->val);//访问栈顶元素
			cur = top->right;//cur指向右子树
		}
		return ret;
	}

中序遍历与前序遍历的关键其实是访问顺序的差别,中序遍历是在访问完左子树之后访问根,前序遍历是直接访问根

后序遍历

后序遍历在我们三种遍历方式中是最复杂的一种,原因是其出栈之前需要访问完其所有的子节点,而又因为栈中只能出栈顶的缘故,所以我们对后序遍历进行了额外处理,让我们一起来了解下后序遍历吧

初始化

 1.同我们的前序与中序一样,先向左走,遇到节点入栈

 2.此时我们访问4的左子节点,访问完成,访问4的右子节点,为空

3.出栈,cur指向2的右子树,入栈,同样像刚才遍历访问一样访问5

 4.访问完成5了之后,出栈,并将5赋给lastNode,因为2的右子树为lastNode,说明2的右子树已经访问过了, 然后开始访问2

 5.访问完2后,出栈,此时1的左子树访问完毕,cur指向1的右子树,右子树3入栈 

6.此时像之前访问左子树一样访问右子树,先左移,入栈,访问左子树,因为右为空,访问右子树,而后访问6,出栈,置为lastNode,cur置为7,开始访问7的左子树,右为空,访问右子树,此时访问7,并将7置为lastNode,因为3的右子树为lastNode,代表其右子树已经访问过,此时再访问3,出栈,并将其置为lastNode,这时1的右子树也为lastNode,访问1,出栈,完成遍历

vector<int>postTraversal(TreeNode* root)
	{
		vector<int>ret;//定义vector接收
		stack<TreeNode*>st;//定义栈
		TreeNode* lastNode = NULL;//初始化上一个访问的节点
		TreeNode* cur = root;//初始化cur指针
		while (cur || !st.empty())//当cur不为空且栈不为空时
		{
			while (cur)//循环cur
			{
				st.push(cur);//入栈
				cur = cur->left;//向左走
			}
			TreeNode* top = st.top();//将栈顶元素赋给top指针
			if (top->right == NULL||lastNode==top->right)//当top的右子节点为空或者右子节点上一个访问过了
			{
				ret.push_back(top->val);//访问top
                lastNode = top;//将top赋给lastNode
				st.pop();//出栈
			}
			else
			{
				cur = top->right;//向右走
			}
			
		}
		return ret;
	}

后序遍历的关键其实就是去判断其右子树是否访问过,访问过了再进行访问,需要加个限定条件

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

数据结构--二叉树进阶 的相关文章

随机推荐

  • hdoj 题目分类

    1001 整数求和 水题 1002 C语言实验题 两个数比较 水题 1003 1 2 3 4 5 简单题 1004 渊子赛马 排序 贪心的方法归并 1005 Hero In Maze 广度搜索 1006 Redraiment猜想 数论 容斥
  • 论文笔记——CVPR 2017 Annotating Object Instances with a Polygon-RNN

    文章主页 http www cs toronto edu polyrnn 1 简介 文章作者基于深度学习提出一种半自动目标事例标注 semi automatic annotation of object instances 的算法 大多数前
  • @property基本概念

    1 什么是 property property是编译器的指令 什么是编译器的指令 编译器指令就是用来告诉编译器要做什么 property会让编译器做什么呢 property 用在声明文件中告诉编译器声明成员变量的的访问器 getter se
  • maven自定义archetype

    在开发过程中我们经常会创建一系列结构类似的新项目 这些项目结构和基础配置基本或完全一致 maven就提供了archetype类型来规定新建项目的结构及基础配置 利用archetype就可以快速简单的搭建新项目 一 创建Maven项目的一般步
  • 网络安全笔记7——防火墙技术

    网络安全笔记7 防火墙技术 参考课程 中国大学MOOC 网络安全 北京航空航天大学 文章目录 网络安全笔记7 防火墙技术 防火墙概述 防火墙的类型及结构 防火墙的发展史 防火墙的分类 OSI模型与防火墙的关系 静态包过滤防火墙 操作 工作原
  • es的配置文件(elasticsearch.yml)

    config目录下有2个配置文件 es的配置文件 elasticsearch yml 和日志配置文件 logging yml cluster name elasticsearch 配置es的集群名称 默认是elasticsearch es会
  • arduino IDE搭建ESP8266开发环境和简单使用

    arduino IDE搭建ESP8266开发环境和简单使用 文章目录 arduino IDE搭建ESP8266开发环境和简单使用 安装 下载IDE 在Arduino IDE上安装esp8266库 下载安装esp8266库 使用 选择开发板
  • Java进阶3 - 易错知识点整理(待更新)

    Java进阶3 易错知识点整理 待更新 该章节是Java进阶2 易错知识点整理的续篇 在前一章节中介绍了 ORM框架 中间件相关的面试题 而在该章节中主要记录关于项目部署中间件 监控与性能优化等常见面试题 文章目录 Java进阶3 易错知识
  • 使用mybatis-plus的insert方法遇到的坑(添加时id值不存在异常)

    在使用mybatis plus的insert方法的时候 报错 java sql SQLException Field id doesn t have a default value 后来了解到使用mybatis plus的insert方法
  • 05-2_Qt 5.9 C++开发指南_Model/View结构实例(QFileSystemModel、QStringListModel、QStandardItemModel;编程实例)

    接上篇 本篇主要介绍Model View框架下的模型类 QFileSystemModel QStringListModel QStandardItemModel的使用方法和编程实例 文章目录 1 QFileSystemModel 1 1 Q
  • 「拓数派(OpenPie)2022 发布会实录 」PieCloudDB Database 优化器

    10 月 24 日程序员节 拓数派 Openpie 发布了云原生数据库 PieCloudDB PieCloudDB 以云计算架构为设计基础 实现云上存算分离 打造了 元数据 计算 存储 分离三层架构 在计算层 PieCloudDB 设计了高
  • Microsoft Store 微软商店无法加载在页面解决

    导致此原因主要是因为你的电脑开启过vpn 开启了系统代理 导致微软商店无法加载 解决方法 1 第一步 开始 设置 2 第二步 网络和internet 3 第三步 点击代理 关闭手动代理服务器这一项 然后再打开微软商店 就可以了
  • Dvwa页面标红问题的逐步攻破(二)

    提示 第二个问题花了很长时间 试了很多种办法 都没成功 但是经过后续的操作我发现第二个问题并没有太大的影响 那就说一下在此过程中遇到的问题及解决吧 解决PHP module gd MIssing Only an issue if you w
  • HTML超链接标签

    一 超链接标签 a 的语法 a href 链接路径 target self 链接文本或图像 a 1 a 标签中href属性值是链接的路径 当href 时表示一个空连接 2 a 标签中target属性值表示的是链接在哪个窗口打开 它的常用值是
  • rlwrap

    rlwrap for Command Line History and Editing in SQL Plus and RMAN on Linux linux下安装后 用着确实方便 AIX下能用吗 more http www oracle
  • 【语义分割】13、SegNeXt

    文章目录 一 背景 二 方法 2 1 Convolutional Encoder 2 2 Decoder 三 效果 论文 SegNeXt Rethinking Convolutional Attention Design for Seman
  • 详解多线程中的互斥量;mutex头文件,lock与unlock ,lock_guard,unique_lock

    互斥量 假如你有一张水卡 要放在卡槽才能出水 现在你和小明都要热水 于是你接一下热水 用自己的水卡 他又接一下热水 巧了 两人都接到泡面的热水 互斥量是在Mutex的头文件中 并发的优点 可以极的减少时间 并且能够多个进程的运行东西 并发的
  • pca处理后建模 sklearn_汽油辛烷值建模

    题目来源 2020年研究生数学建模竞赛B题 小编第一次做研究生的竞赛题目 我的整体感受 首当其冲的是 关于题的描述很多 每一个题的页数都有好几页 说下关于B题 汽油辛烷值建模 的思考 就B题的难易程度来说 这个题太容易了 不论从数据量 还是
  • 26.kotlin的get和set方法

    1 kotlin类中的get和set方法 fun main args Array
  • 数据结构--二叉树进阶

    因为我们之前在学习数据结构的时候使用的是C语言 但是并不是所有的数据结构都适合使用C语言学习 如今我们了解了C 的基础语法 具备了学习这些稍微难一点的数据结构的前提 所以我们再次回顾数据结构 使用C 这一更加先进的武器 来解决更加复杂的问题