【C++】二叉搜索树

一、二叉搜索树概念

二叉搜索树又称二叉排序树,可以简写成 BST,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

如下就为一颗简单的二叉搜索树:

在这里插入图片描述

二、二叉搜索树操作

  1. 二叉搜索树的查找
  • 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
  • 最多查找高度次,走到到空,还没找到,这个值不存在。
  1. 二叉搜索树的插入

插入的具体过程如下:

  • 树为空,则直接新增节点,赋值给 root 指针
  • 树不空,按二叉搜索树性质查找插入位置,插入新节点

例如有以下这个数组,依次按照数组的元素插入就如下图的二叉搜索树:

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

在这里插入图片描述

再分别插入 0 和 16 后,如下图所示:

在这里插入图片描述

  1. 二叉搜索树的删除

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

  • 要删除的结点无孩子结点(左右都为空)
  • 要删除的结点只有左孩子结点(右为空)
  • 要删除的结点只有右孩子结点(左为空)
  • 要删除的结点有左、右孩子结点(左右都不为空)

看起来有待删除节点有 4 种情况,实际情况第一种情况(左右都为空)可以与情况二或者三合并起来,因此真正的删除过程如下:

情况二:删除该结点且使被删除节点的父结点指向被删除节点的左孩子结点- - -直接删除

情况三:删除该结点且使被删除节点的父结点指向被删除结点的右孩子结点- - -直接删除

情况四:在它的右子树中寻找最小值,即右子树的最左节点,用它的值与被删除节点中的值交换,再来处理该结点的删除问题- - -替换法删除

更多的细节在代码实现中会介绍。

三、二叉搜索树的实现

需要实现二叉搜索树,首先我们得先定义一个树的节点的类,如下:

		template<class K>
		struct BSTreeNode
		{
			BSTreeNode(const K& key)
				:_left(nullptr)
				, _right(nullptr)
				, _key(key)
			{}
		
			BSTreeNode<K>* _left;
			BSTreeNode<K>* _right;
			K _key;
		};

然后就定义二叉搜索树的类:

		template<class K>
		class BSTree
		{
		public:
			typedef BSTreeNode<K> Node;
			
		private:
			Node* _root;
		};

接下来我们实现的功能放在二叉搜索树的类中,有如下功能:

1. BST的插入

  • 非递归插入

      		// 插入
      		bool Insert(const K& val)
      		{
      			// 如果根节点为空
      			if (_root == nullptr)
      			{
      				_root = new Node(val);
      				return true;
      			}
      	
      			// cur 找到插入的位置;prev 记录 cur 的前驱节点,即父节点
      			// 比当前节点大往右走;比当前节点小往左走
      			Node* cur = _root, * prev = nullptr;
      			while (cur)
      			{
      				prev = cur;
      				if (val > cur->_key)
      				{
      					cur = cur->_right;
      				}
      				else if (val < cur->_key)
      				{
      					cur = cur->_left;
      				}
      				else
      				{
      					return false;
      				}
      			}
      	
      			// 找到插入的位置连接起来
      			cur = new Node(val);
      			if (prev->_key > val)
      			{
      				prev->_left = cur;
      			}
      			else
      			{
      				prev->_right = cur;
      			}
      			return true;
      		}
    
  • 递归插入

      		// 递归 insert
      		bool InsertR(const K& val)
      		{
      			return _InsertR(_root, val);
      		}
    

由于类的根节点是私有的,外部无法访问,所以需要嵌套一层访问根节点,子函数递归如下:

			bool _InsertR(Node*& root, const K& val)
			{
				if (root == nullptr)
				{
					root = new Node(val);
					return true;
				}
		
				if (val > root->_key)
					return _InsertR(root->_right, val);
		
				else if (val < root->_key)
					return _InsertR(root->_left, val);
		
				else
					return false;
			}

2. BST的查找

  • 非递归查找

      		// 在 BST 中查找 val
      		bool Find(const K& val)
      		{
      			Node* cur = _root;
      			while (cur)
      			{
      				if (val > cur->_key)
      				{
      					cur = cur->_right;
      				}
      				else if (val < cur->_key)
      				{
      					cur = cur->_left;
      				}
      				else
      				{
      					return true;
      				}
      			}
      			return false;
      		}
    
  • 递归查找

      		// 递归查找 val
      		bool FindR(const K& val)
      		{
      			return _FindR(_root, val);
      		}
    
      		bool _FindR(Node* root, const K& val)
      		{
      			if (root == nullptr)
      				return false;
      	
      			if (root->_key > val)
      			{
      				_FindR(root->_left, val);
      			}
      			else if (root->_key < val)
      			{
      				_FindR(root->_right, val);
      			}
      			else
      			{
      				return true;
      			}
      		}
    

3. 按有序打印BST

因为二叉搜索树的中序遍历就是有序的,所以我们按照中序遍历打印二叉搜索树:

			// 按照中序遍历打印,即有序的顺序
			void InOrder()
			{
				_Inorder(_root);
				cout << endl;
			}

			void _Inorder(Node* root)
			{
				if (root == nullptr)
					return;
		
				_Inorder(root->_left);
				cout << root->_key << " ";
				_Inorder(root->_right);
			}

4. BST的删除

  • 非递归删除

      	// 删除节点
      	bool Erase(const K& val)
      	{
      		if (_root == nullptr)
      			return false;
      		
      		// prev 记录 cur 的父节点
      		Node* cur = _root, * prev = nullptr;
      		while (cur)
      		{
      			if (val > cur->_key)
      			{
      				prev = cur;
      				cur = cur->_right;
      			}
      			else if (val < cur->_key)
      			{
      				prev = cur;
      				cur = cur->_left;
      			}
      
      			// 找到了开始删除
      			else
      			{
      				// 左为空(把左右都为空归为这一类)
      				if (cur->_left == nullptr)
      				{
      					// 当删除的节点是根节点
      					if (cur == _root)
      					{
      						_root = cur->_right;
      					}
      
      					// 不是根节点
      					else
      					{
      						if (prev->_key > cur->_key)
      						{
      							prev->_left = cur->_right;
      						}
      						else
      						{
      							prev->_right = cur->_right;
      						}
      					}
      					delete cur;
      				}
      
      				// 右为空
      				else if (cur->_right == nullptr)
      				{
      					if (cur == _root)
      					{
      						_root = cur->_left;
      					}
      
      					else
      					{
      						if (prev->_key > cur->_key)
      						{
      							prev->_left = cur->_left;
      						}
      						else
      						{
      							prev->_right = cur->_left;
      						}
      					}
      					delete cur;
      				}
      				
      				// 左右都不为空
      				else
      				{
      					Node* subLeft = cur->_right, *parent = cur;
      					while (subLeft->_left)
      					{
      						parent = subLeft;
      						subLeft = subLeft->_left;
      					}
      
      					swap(cur->_key, subLeft->_key);
      
      					if (subLeft == parent->_left)
      					{
      						parent->_left = subLeft->_right;
      					}
      					else
      					{
      						parent->_right = subLeft->_right;
      					}
      					delete subLeft;
      				}
      				return true;
      			}
      		}
      		return false;
      	}
    

左为空的示意图如下(左右都为空归入这一类):

在这里插入图片描述

右为空的示意图如下:

在这里插入图片描述

左右都不为空的示意图如下:

在这里插入图片描述

  • 递归删除

      		// 递归 erase
      		bool EraseR(const K& val)
      		{
      			return _EraseR(_root, val);
      		}
    
    
      		bool _EraseR(Node*& root, const K& val)
      		{
      			if (root == nullptr)
      				return false;
      	
      			if (root->_key > val)
      				return _EraseR(root->_left, val);
      	
      			else if (root->_key < val)
      				return _EraseR(root->_right, val);
      	
      			else
      			{
      				// 左为空(把左右都为空归为这一类)
      				if (root->_left == nullptr)
      				{
      					Node* del = root;
      					root = root->_right;
      					delete del;
      	
      					return true;
      				}
      	
      				// 右为空
      				else if (root->_right == nullptr)
      				{
      					Node* del = root;
      					root = root->_left;
      					delete del;
      	
      					return true;
      				}
      	
      				// 左右都不为空
      				else
      				{
      					Node* subLeft = root->_right;
      					while (subLeft->_left)
      					{
      						subLeft = subLeft->_left;
      					}
      	
      					swap(root->_key, subLeft->_key);
      	
      					// 转换为在子树中递归删除
      					return _EraseR(root->_right, val);
      				}
      			}
      		}
    

注意,这里传入的 root 需要加 &,因为当前的 root 就是父节点的左或者右,可能需要删除当前的 root,传入引用即可在当前作用域改变上一个作用域中的 root

当左右都不为空的时候,我们找到了替换的节点,交换了值后,可以转换为在 root 的右子树中递归删除,因为 root 的右子树还是二叉搜索树,并且需要删除的节点必是左为空,因为我们在找替代节点的时候找的是 root 右子树中的最左节点。

5. BST的构造函数

构造函数只需要将根节点初始化为空即可:

			BSTree()
				:_root(nullptr)
			{}

6. BST的析构函数

析构函数需要将这颗 BST 的节点的空间全部释放,所以我们需要走后序遍历:

			~BSTree()
			{
				Destroy(_root);
			}
			
			void Destroy(Node*& root)
			{
				if (root == nullptr)
					return;
		
				Destroy(root->_left);
				Destroy(root->_right);
				delete root;
				root = nullptr;
			}

7. BST的拷贝构造

			// BSTree<int> bt1(bt2);
			BSTree(const BSTree<K>& t)
			{
				_root = Copy(t._root);
			}
			
			Node* Copy(Node* root)
			{
				if (root == nullptr)
					return nullptr;
		
				Node* newnode = new Node(root->_key);
				newnode->_left = Copy(root->_left);
				newnode->_right = Copy(root->_right);
		
				return newnode;
			}

8. BST的赋值运算符重载

赋值运算符重载可以复用拷贝构造,即在传参的时候不使用引用接收:

			// BSTree<int> bt2;
			// bt2 = bt1;
			BSTree<K>& operator=(BSTree<K> t)
			{
				swap(_root, t._root);
				return *this;
			}

9. 测试BST

我们准备了以下这组数组,分别包括左为空,右为空,左右都不为空的情况,所以如果我们以下这组数组没有出错,那么我们的BST就没有问题;数据如下:

	vector<int> arr = {8, 3, 1, 10, 6, 4, 7, 14, 13};

图形结构如下:

在这里插入图片描述

  1. 先测试非递归的插入与删除

     	void Test1()
     	{
     		BSTree<int> bt1;
     		vector<int> arr = { 8, 3, 1, 10, 6, 4, 7, 14, 13, 2 };
     		for (auto e : arr)
     		{
     			bt1.Insert(e);
     		}
     		bt1.InOrder();
     	
     		for (auto e : arr)
     		{
     			bt1.Erase(e);
     			bt1.InOrder();
     		}
     	}
    

结果如下:

在这里插入图片描述

  1. 测试递归的插入与删除

     	void Test2()
     	{
     		BSTree<int> bt1;
     		vector<int> arr = { 8, 3, 1, 10, 6, 4, 7, 14, 13, 2 };
     		for (auto e : arr)
     		{
     			bt1.InsertR(e);
     		}
     		bt1.InOrder();
     	
     		for (auto e : arr)
     		{
     			bt1.EraseR(e);
     			bt1.InOrder();
     		}
     	}
    

结果如下:

在这里插入图片描述

  1. 测试拷贝构造与赋值运算符重载

     	void Test3()
     	{
     		BSTree<int> bt1;
     		vector<int> arr = { 8, 3, 1, 10, 6, 4, 7, 14, 13, 2 };
     		for (auto e : arr)
     		{
     			bt1.InsertR(e);
     		}
     		bt1.InOrder();
     	
     		BSTree<int> bt2(bt1);
     		bt2.InOrder();
     	
     		BSTree<int> bt3;
     		bt3 = bt2;
     		bt3.InOrder();
     	}
    

结果如下:

在这里插入图片描述

四、二叉搜索树的应用

1. K模型

K模型即只有 key 作为关键字,结构中只需要存储 key 即可,关键字即为需要搜索到的值。

比如:给一个单词 word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为 key,构建一棵二叉搜索树,在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

2. KV模型

每一个关键码 key,都有与之对应的值 Value,即 <Key, Value> 的键值对。该种方式在现实生活中非常常见。

比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文 <word, chinese> 就构成一种键值对;

再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是 <word, count> 就构成一种键值对。

五、二叉搜索树的性能分析

首先插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其 Find() 的效率为 O(logN);如下图:

在这里插入图片描述

最差情况下,二叉搜索树退化为单支树(或者类似单支),其 Find() 的效率为:O(N);如下图:

在这里插入图片描述

如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键字,二叉搜索树的性能都能达到最优?我们后面学习的AVL树红黑树就可以达到最优。

六、二叉树的练习题

1. 根据二叉树创建字符串

题目链接 -> Leetcode -606.根据二叉树创建字符串

Leetcode -606.根据二叉树创建字符串

题目:给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。

空节点使用一对空括号对 “()” 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。

示例 1:
输入:root = [1, 2, 3, 4]
输出:“1(2(4))(3)”
解释:初步转化后得到 “1(2(4)())(3()())” ,但省略所有不必要的空括号对后,字符串应该是"1(2(4))(3)" 。

示例 2:
输入:root = [1, 2, 3, null, 4]
输出:“1(2()(4))(3)”
解释:和第一个示例类似,但是无法省略第一个空括号对,否则会破坏输入与输出一一映射的关系。

提示:
树中节点的数目范围是[1, 10^4]

  • 1000 <= Node.val <= 1000

思路就是走前序遍历,只要是左不为空或者右不为空都要括起来,代码如下:

		/**
		 * Definition for a binary tree node.
		 * struct TreeNode {
		 *     int val;
		 *     TreeNode *left;
		 *     TreeNode *right;
		 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
		 * };
		 */
		
		class Solution {
		public:
		    void toString(TreeNode* root, string& str)
		    {
		        if (root == nullptr)
		            return;
		
		        str += to_string(root->val);
		
		        // 只要左不为空或者右不为空都要括起来
		        if (root->left || root->right)
		        {
		            str += '(';
		            toString(root->left, str);
		            str += ')';
		        }
		
		        // 右子树不为空就要括起来
		        if (root->right)
		        {
		            str += '(';
		            toString(root->right, str);
		            str += ')';
		        }
		    }
		
		    string tree2str(TreeNode* root)
		    {
		        string str;
		        toString(root, str);
		
		        return str;
		    }
		};

2. 二叉树的最近公共祖先

题目链接 -> Leetcode -236.二叉树的最近公共祖先

Leetcode -236.二叉树的最近公共祖先

题目:给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

示例 1:
在这里插入图片描述

输入:root = [3, 5, 1, 6, 2, 0, 8, null, null, 7, 4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。

示例 2:

在这里插入图片描述

输入:root = [3, 5, 1, 6, 2, 0, 8, null, null, 7, 4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。

示例 3:
输入:root = [1, 2], p = 1, q = 2
输出:1

提示:
树中节点数目在范围[2, 10^5] 内。

  • 10^9 <= Node.val <= 10^9
  • 所有 Node.val 互不相同 。
  • p != q
  • p 和 q 均存在于给定的二叉树中。

思路是定义两个栈,分别存放p、q节点的路径,当找到两个节点的路径后,就转换为两个链表相交的问题了;先让路径长的出栈,当两个路径长度相等后,依次出栈,当两个节点相同时即为最近公共祖先。 代码如下:

		/**
		 * Definition for a binary tree node.
		 * struct TreeNode {
		 *     int val;
		 *     TreeNode *left;
		 *     TreeNode *right;
		 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
		 * };
		 */
		class Solution {
		public:
		    bool FindPath(TreeNode* root, TreeNode* aim, stack<TreeNode*>& path)
		    {
		        // 直接返回 false,因为节点不在
		        if (root == nullptr)
		            return false;
		
		        // 先将根节点放在路径中
		        path.push(root);
		
		        // 如果根节点和 aim 相等,路径找到,返回 true
		        if (root == aim)
		            return true;
		
		        // 如果根节点和 aim 不相等,先递归在左子树中找,找到则退出
		        if (FindPath(root->left, aim, path))
		            return true;
		
		        // 未找到时,再到根节点的右子树中找
		        if (FindPath(root->right, aim, path))
		            return true;
		
		        // 如果左右子树中都没有找到,说明该节点不在路径中,出栈,并返回 false
		        path.pop();
		        return false;
		    }
		
		    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
		    {
		        stack<TreeNode*> pathq, pathp;
		
		        // 找到两个节点的路径入栈
		        FindPath(root, p, pathp);
		        FindPath(root, q, pathq);
		
		        // 保证两个节点的路径长度相等
		        while (pathp.size() > pathq.size())
		            pathp.pop();
		
		        while (pathq.size() > pathp.size())
		            pathq.pop();
		
		        // 路径长度相等后出栈,直到两个节点相同则是最近公共祖先
		        while (pathp.top() != pathq.top())
		            pathp.pop(), pathq.pop();
		
		        return pathp.top();
		    }
		};

3. 二叉树的层序遍历Ⅰ

题目链接 -> Leetcode -102.二叉树的层序遍历Ⅰ

Leetcode -102.二叉树的层序遍历Ⅰ

题目:给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:
在这里插入图片描述

输入:root = [3, 9, 20, null, null, 15, 7]
输出: [[3], [9, 20], [15, 7]]

示例 2:
输入:root = [1]
输出: [[1]]

示例 3:
输入:root = []
输出:[]

提示:
树中节点数目在范围[0, 2000] 内

  • 1000 <= Node.val <= 1000

思路是用一个队列模拟实现,一开始先将根节点入队列,此时根节点在第一层,只有一个,用 levelSize 记录每一层的节点数;当入队列的时候,将这个节点的左右节点入队列,当 levelSize 为 0 时代表当前层已经出完,此时队列中剩余节点数就是下一层的节点数,更新 levelSize 即可,直到队列为空结束。 代码如下:

		/**
		 * Definition for a binary tree node.
		 * struct TreeNode {
		 *     int val;
		 *     TreeNode *left;
		 *     TreeNode *right;
		 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
		 * };
		 */
		class Solution {
		public:
		    vector<vector<int>> levelOrder(TreeNode* root)
		    {
		        queue<TreeNode*> q;
		        vector<vector<int>> ret;
		
		        if (root) q.push(root);
		        int levelSize = 1;
		        while (!q.empty())
		        {
		            vector<int> tp;
		
		            // 出当前层的节点
		            while (levelSize--)
		            {
		                if (q.front()->left) q.push(q.front()->left);
		                if (q.front()->right) q.push(q.front()->right);
		                tp.push_back(q.front()->val);
		                q.pop();
		            }
		
		            // 当前层节点出完,将将当前层的 val 尾插到 ret 中
		            ret.push_back(tp);
		
		            // 更新下一层的节点数
		            levelSize = q.size();
		        }
		        return ret;
		    }
		};

4. 二叉树的层序遍历Ⅱ

题目链接 -> Leetcode -107.二叉树的层序遍历Ⅱ

Leetcode -107.二叉树的层序遍历Ⅱ

题目:给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

示例 1:
输入:root = [3, 9, 20, null, null, 15, 7]
输出: [[15, 7], [9, 20], [3]]

示例 2:
输入:root = [1]
输出: [[1]]

示例 3:
输入:root = []
输出:[]

提示:
树中节点数目在范围[0, 2000] 内

  • 1000 <= Node.val <= 1000

思路是将 二叉树的层序遍历Ⅰ 的顺序反过来即可,即最后用一个 reverse 反转 ret 即可;因为它们的顺序正好是相反的。 代码如下:

		/**
		 * Definition for a binary tree node.
		 * struct TreeNode {
		 *     int val;
		 *     TreeNode *left;
		 *     TreeNode *right;
		 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
		 * };
		 */
		class Solution {
		public:
		    vector<vector<int>> levelOrderBottom(TreeNode* root) 
		    {
		        // 先按照自上向下的层序遍历方式走,最后再 reverse ret 即可
		        queue<TreeNode*> q;
		        vector<vector<int>> ret;
		
		        if(root) q.push(root);
		        int levelSize = 1;
		        while(!q.empty())
		        {
		            vector<int> tp;
		
		            // 出当前层的节点
		            while(levelSize--)
		            {
		                if(q.front()->left) q.push(q.front()->left);
		                if(q.front()->right) q.push(q.front()->right);
		                tp.push_back(q.front()->val);
		                q.pop();
		            }
		
		            // 当前层节点出完,将将当前层的 val 尾插到 ret 中
		            ret.push_back(tp);
		
		            // 更新下一层的节点数
		            levelSize = q.size();
		        }
		        // 反转 ret 即可
		        reverse(ret.begin(), ret.end());
		        return ret;
		
		    }
		    
		};

5. 二叉树的前序遍历 - - - 迭代实现

题目链接 -> Leetcode -144.二叉树的前序遍历

Leetcode -144.二叉树的前序遍历

题目:给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

示例 1:
输入:root = [1, null, 2, 3]
输出:[1, 2, 3]

示例 2:
输入:root = []
输出:[]

示例 3:
输入:root = [1]
输出:[1]

示例 4:
输入:root = [1, 2]
输出:[1, 2]

示例 5:
输入:root = [1, null, 2]
输出:[1, 2]

提示:
树中节点数目在范围[0, 100] 内

  • 100 <= Node.val <= 100

进阶:递归算法很简单,你可以通过迭代算法完成吗?

思路是使用迭代算法,需要用一个栈存放节点,每次入栈前先将值放入返回的数组中,即是前序遍历。 代码如下:

		/**
		 * Definition for a binary tree node.
		 * struct TreeNode {
		 *     int val;
		 *     TreeNode *left;
		 *     TreeNode *right;
		 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
		 * };
		 */
		class Solution {
		public:
		    vector<int> preorderTraversal(TreeNode* root) 
		    {
		        stack<TreeNode*> st;
		        vector<int> ret;
		
		        TreeNode* cur = root;
		
		        while(cur || !st.empty())
		        {
		            // 先入完该节点的左节点
		            while(cur)
		            {
		                st.push(cur);
		                ret.push_back(cur->val);
		                cur = cur->left;
		            }
		            // 更新 cur 为栈顶的右子树节点,然后出栈
		            cur = st.top()->right;
		            st.pop();
		        }
		        return ret;
		    }
		};

6. 二叉树的中序遍历 - - - 迭代实现

题目链接 -> Leetcode -94.二叉树的中序遍历

Leetcode -94.二叉树的中序遍历

题目:给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。

示例 1:
输入:root = [1, null, 2, 3]
输出:[1, 3, 2]

示例 2:
输入:root = []
输出:[]

示例 3:
输入:root = [1]
输出:[1]

提示:
树中节点数目在范围[0, 100] 内

  • 100 <= Node.val <= 100

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

思路与非递归的前序遍历类似,改变节点的值入 ret 的顺序即可。 代码如下:

		/**
		 * Definition for a binary tree node.
		 * struct TreeNode {
		 *     int val;
		 *     TreeNode *left;
		 *     TreeNode *right;
		 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
		 * };
		 */
		class Solution 
		{
		public:
		    vector<int> inorderTraversal(TreeNode* root) 
		    {
		        stack<TreeNode*> st;
		        vector<int> ret;
		
		        TreeNode* cur = root;
		        while(cur || !st.empty())
		        {
		            // 入完左节点再将值放入 ret
		            while(cur)
		            {
		                st.push(cur);
		                cur = cur->left;
		            }
		            ret.push_back(st.top()->val);
		
		            // 更新 cur
		            cur = st.top()->right;
		            st.pop();
		        }
		        return ret;
		    }
		};

7. 二叉树的后序遍历 - - - 迭代实现

题目链接 -> Leetcode -145.二叉树的后序遍历

Leetcode -145.二叉树的后序遍历 — 迭代实现

题目:给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。

示例 1:
输入:root = [1, null, 2, 3]
输出:[3, 2, 1]

示例 2:
输入:root = []
输出:[]

示例 3:
输入:root = [1]
输出:[1]

提示:
树中节点的数目在范围[0, 100] 内

  • 100 <= Node.val <= 100

进阶:递归算法很简单,你可以通过迭代算法完成吗?

思路与前序和中序不同的是,后序需要用一个 prev 指针记录上一次访问的节点,如果 prev 是当前节点的右子树或者右子树为空,说明当前根可以访问了,否则就要访问当前根的右子树。 代码如下:

		/**
		 * Definition for a binary tree node.
		 * struct TreeNode {
		 *     int val;
		 *     TreeNode *left;
		 *     TreeNode *right;
		 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
		 * };
		 */
		class Solution {
		public:
		    vector<int> postorderTraversal(TreeNode* root) 
		    {
		        stack<TreeNode*> st;
		        vector<int> ret;
		
		        // prev 记录上一个访问的节点
		        TreeNode* prev = nullptr;
		        TreeNode* cur = root;
		        while(cur || !st.empty())
		        {
		            // 先入左节点
		            while(cur)
		            {
		                st.push(cur);
		                cur = cur->left;
		            }
		
		            // 如果右节点为空 或者 右节点是上一个访问的节点,就不用入该右节点;cur 也就不用更新,依旧是空
		            TreeNode* top = st.top();
		            if(top->right == nullptr || top->right == prev)
		            {
		                prev = top;
		                ret.push_back(top->val);
		                st.pop();
		            }
		
		            // 否则更新 cur 为右节点
		            else
		            {
		                cur = top->right;
		            }
		        }
		        return ret;
		    }
		};

8. 二叉搜索树与双向链表

题目链接 -> Nowcoder -JZ36.二叉搜索树与双向链表

Nowcoder -JZ36.二叉搜索树与双向链表

题目:输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。

数据范围:输入二叉树的节点数:0 ≤ n ≤ 1000
二叉树中每个节点的值:0 ≤ val ≤ 1000

要求:空间复杂度 O(1)(即在原树上操作)
时间复杂度 O(n)

注意:
1.要求不能创建任何新的结点,只能调整树中结点指针的指向。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继
2.返回链表中的第一个节点的指针
3.函数返回的TreeNode,有左右指针,其实可以看成一个双向链表的数据结构
4.你不用输出双向链表,程序会根据你的返回值自动打印输出

输入描述:
二叉树的根节点

返回值描述:
双向链表的其中一个头节点。

思路是先按照正常的中序遍历顺序走,在访问完左子树后进行操作,需要有一个 prev 指针记录上一个访问的节点,然后到当前节点再改变上一个节点的右指针,指向当前,即上一个节点的后继;在当前节点就改变当前节点的左指针,指向前驱。 代码如下:

		/*// 树节点
		struct TreeNode {
			int val;
			struct TreeNode *left;
			struct TreeNode *right;
			TreeNode(int x) :
					val(x), left(NULL), right(NULL) {
			}
		};
		*/
		
		class Solution {
		public:
		
			// pRoot 为当前处理的子树节点,pPrev 为上一个处理的节点(因为要改变上一个节点的右指针指向,所以要用引用)
			void prevOrder(TreeNode* pRoot, TreeNode*& pPrev)
			{
				// 空就返回
				if (pRoot == nullptr)
					return;
		
				// 先处理左子树
				prevOrder(pRoot->left, pPrev);
		
				// 因为当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继
				// 所以 pRoot 的 left 指向 pPrev;right 到下一个节点再处理
				pRoot->left = pPrev;
		
				// 处理上一个节点的右指针
				// 如果不为空,就让它的右指针指向当前的节点
				if (pPrev)
					pPrev->right = pRoot;
		
				// 当前节点变成前驱节点,然后递归右子树
				pPrev = pRoot;
				prevOrder(pRoot->right, pPrev);
			}
		
			TreeNode* Convert(TreeNode* pRootOfTree)
			{
				if (pRootOfTree == nullptr)
					return nullptr;
		
				TreeNode* prev = nullptr;
				prevOrder(pRootOfTree, prev);
		
				// 找头节点,然后返回头节点
				TreeNode* cur = pRootOfTree;
				while (cur->left)
					cur = cur->left;
		
				return cur;
			}
		};

9. 从前序与中序遍历序列构造二叉树

题目链接 -> Leetcode -105.从前序与中序遍历序列构造二叉树

Leetcode -105.从前序与中序遍历序列构造二叉树

题目:给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例 1:
输入: preorder = [3, 9, 20, 15, 7], inorder = [9, 3, 15, 20, 7]
输出 : [3, 9, 20, null, null, 15, 7]

示例 2 :
输入 : preorder = [-1], inorder = [-1]
输出 : [-1]

提示 :

  • 1 <= preorder.length <= 3000
  • inorder.length == preorder.length
  • 3000 <= preorder[i], inorder[i] <= 3000
  • preorder 和 inorder 均 无重复 元素
  • inorder 均出现在 preorder
  • preorder 保证 为二叉树的前序遍历序列
  • inorder 保证 为二叉树的中序遍历序列

思路是前序确定根,中序分割左右子树,将左右子树继续进行递归处理。 代码如下:

		/**
		 * Definition for a binary tree node.
		 * struct TreeNode {
		 *     int val;
		 *     TreeNode *left;
		 *     TreeNode *right;
		 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
		 * };
		 */
		class Solution {
		public:
		    TreeNode* _build(vector<int>& preorder, vector<int>& inorder, int& previ, int inbegin, int inend)
		    {
		        // 非法区间返回空
		        if(inbegin > inend)
		            return nullptr;
		        
		        // rooti 从 inbegin 开始找到与前序对应的中序节点
		        int rooti = inbegin;
		        while(rooti <= inend)
		        {
		            if(preorder[previ] == inorder[rooti])
		            {
		                break;
		            }
		            rooti++;
		        }
		
		        // new 一个节点
		        TreeNode* root = new TreeNode(preorder[previ++]);
		
		        // 开始划分区间
		        // [inbegin, rooti - 1] rooti [rooti + 1, inend]
		        // 其中 [inbegin, rooti - 1] 是 rooti 的左子树
		        // 其中 [rooti + 1, inend] 是 rooti 的右子树
		        root->left = _build(preorder, inorder, previ, inbegin, rooti - 1);
		        root->right = _build(preorder, inorder, previ, rooti + 1, inend);
		
		        return root;
		    }
		
		    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) 
		    {
		        int i = 0;
		        return _build(preorder, inorder, i, 0, preorder.size() - 1);
		    }
		};

10. 从中序与后序遍历序列构造二叉树

题目链接 -> Leetcode -106.从中序与后序遍历序列构造二叉树

Leetcode -106.从中序与后序遍历序列构造二叉树

题目:给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

示例 1:
输入:inorder = [9, 3, 15, 20, 7], postorder = [9, 15, 7, 20, 3]
输出:[3, 9, 20, null, null, 15, 7]

示例 2 :
输入:inorder = [-1], postorder = [-1]
输出:[-1]

提示 :

  • 1 <= inorder.length <= 3000
  • postorder.length == inorder.length
  • 3000 <= inorder[i], postorder[i] <= 3000
  • inorder 和 postorder 都由 不同 的值组成
  • postorder 中每一个值都在 inorder 中
  • inorder 保证是树的中序遍历
  • postorder 保证是树的后序遍历

思路与上一题差不多,要注意的是后序是从后往前遍历 postorder 数组,所以倒着走的时候先连接右子树,再左子树。 代码如下:

		/**
		 * Definition for a binary tree node.
		 * struct TreeNode {
		 *     int val;
		 *     TreeNode *left;
		 *     TreeNode *right;
		 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
		 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
		 * };
		 */
		class Solution 
		{
		public:
		    TreeNode* _build(vector<int>& postorder, vector<int>& inorder, int& posti, int inbegin, int inend)
		    {
		        // 非法区间返回空
		        if(inbegin > inend)
		            return nullptr;
		        
		        // rooti 从 inbegin 开始找到与后序对应的中序节点
		        int rooti = inbegin;
		        while(rooti <= inend)
		        {
		            if(postorder[posti] == inorder[rooti])
		            {
		                break;
		            }
		            rooti++;
		        }
		
		        // new 一个节点
		        TreeNode* root = new TreeNode(postorder[posti--]);
		
		        // 开始划分区间
		        // 注意后序是 左子树->右子树->根,所以倒着走的时候先连接右子树,再左子树
		        // [inbegin, rooti - 1] rooti [rooti + 1, inend]
		        // 其中 [inbegin, rooti - 1] 是 rooti 的左子树
		        // 其中 [rooti + 1, inend] 是 rooti 的右子树
		        root->right = _build(postorder, inorder, posti, rooti + 1, inend);
		        root->left = _build(postorder, inorder, posti, inbegin, rooti - 1);
		    
		        return root;
		    }
		
		    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) 
		    {
		        int i = inorder.size() - 1;
		        return _build(postorder, inorder, i, 0, i);
		    }
		};
  • 35
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 47
    评论
评论 47
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值