23 二叉搜索树

本节目标

1.内容安排说明

2.二叉搜索树实现

3.应用分析

4.进阶题

1. 内容安排说明

二叉树在c数据结构已经说过了,本节内容是因为:

  1. map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
  2. 二叉搜索树的特性了解,有助于更好的理解map和set的特性
  3. 二叉树中有部分题有难度,前面不容易接受,且容易遗忘
  4. oj题用c语言实现麻烦,有些地方要返回动态开辟的二维数组,非常麻烦

因此本节借二叉搜索树,对二叉树部分进行收尾总结

2. 二叉搜索树

2.1 二叉搜索树概念

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

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

二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质:

1.非空左子树的所有键值小于其根节点的键值

2.非空右子树的所有键值都大于其根节点的值

3.左、右子树都是二叉搜索树

2.2 二叉搜索树操作

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

1.二叉搜索树的查找

a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找

b、最多查找高度次,走到到空,还没找到,这个值不存在

2.二叉搜索树的插入

插入的具体过程如下:

a、树为空,则直接新增节点,赋值给root指针

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

插入9和16的过程

3.二叉搜索树的删除

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

a、要删除的结点无孩子节点

b、要删除的节点只有左孩子节点

c、要删除的结点只有右孩子节点

d、要删除的结点有左、右孩子节点

看起来有待删除节点有4种情况,实际情况a可以和情况b或者c结合起来,因此真正的删除过程如下:

情况b:删除该及诶点且被删除节点的双亲结点指向被删除节点的左孩子节点--直接删除

情况c:删除该节点且使被删除节点的双亲结点指向被删除节点的有孩子结点--直接删除

情况d:在它的右子树中寻找中序下的第一个节点(关键码最小),用它的值填补到被删除节点中,再来处理该节点的删除问题--替换法删除

删除节点左右孩子有一个为空,只需要将它的父节点指向不为空的这个节点。如果删除节点的左右两边都有节点,则需要找一个可以替代这个删除节点的,也就是左子树里的最大值或右子树里的最小值,这个最值一定是叶子结点或者只有一个子节点的情况,交换删除节点和它的值后,就可以按上面的方法删除这个节点

2.3 二叉搜索树的实现

节点的结构 ,左右节点和值和构造

二叉树结构 ,保存根节点

插入

cpp 复制代码
bool insert(const K& key)
{
	if (_root == nullptr)
	{
		node* newnode = new node(key);
		_root = newnode;
		return true;
	}

	node* cur = _root;
	node* parent = nullptr;
	while (cur)
	{
		if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}

	node* newnode = new node(key);
	if (key < parent->_key)
	{
		parent->_left = newnode;
	}
	else
	{
		parent->_right = newnode;
	}

	return true;

}

首先判断是不是空树,空树先创建根节点。当前节点用来寻找插入位置,父节点变量连接,小往左走,大往右走,值相等插入失败。空节点就是插入的位置,判断连接的是左还是右节点

中序遍历

cpp 复制代码
void inorder()
{
	_inorder(_root);
	std::cout << std::endl;
}
void _inorder(node* root)
{
	if (root == nullptr)
	{
		return;
	}

	_inorder(root->_left);
	std::cout << root->_key << " ";
	_inorder(root->_right);
}

递归需要一个初始参数,,根节点是私有的,实例化不能访问,所以套一层接口传入根节点。中序先左再根再右

删除

cpp 复制代码
bool earse(const K& key)
{

	node* del = _root;
	node* parent = nullptr;
	while (del)
	{
		if (key < del->_key)
		{
			parent = del;
			del = del->_left;
		}
		else if (key > del->_key)
		{
			parent = del;
			del = del->_right;
		}
		else
		{
			//删除
			//左结点为空或右节点为空,父节点领养子节点
			if (del->_left == nullptr)
			{
				//删除节点是根节点,父节点为空,子节点成为根节点
				if (parent == nullptr)
				{
					_root = del->_right;
				}
				else
				{
					//删除节点是父节点的左还是右
					if (parent->_left == del)
					{
						parent->_left = del->_right;
					}
					else
					{
						parent->_right = del->_right;
					}
				}

				delete del;
				del = nullptr;
			}
			else if (del->_right == nullptr)
			{
				if (parent == nullptr)
				{
					_root = del->_left;
				}
				else
				{
					//删除节点是父节点的左还是右
					if (parent->_left == del)
					{
						parent->_left = del->_left;
					}
					else
					{
						parent->_right = del->_left;
					}
				}

				delete del;
				del = nullptr;
			}
			else
			{
				//两个节点都不为空,从左子树找最大的替换
				node* max = del->_left;
				//parent设置为空,如果删除根节点会出错,所以赋初始值
				node* parent = del;
				while (max->_right)
				{
					parent = max;
					max = max->_right;
				}

				std::swap(max->_key, del->_key);
				//不能再调用一遍函数删除,因为不是二叉搜索树了,找不到del
				if (parent == del)
				{
					parent->_left = max->_left;
				}
				else
				{
					//一般情况,右节点断开连接
					parent->_right = max->_left;
				}

				delete max;

			}

			return true;
		}
	}

	return false;
}

和插入一样,先寻找删除的位置,相等就是需要删除了。

分两种情况:

1.左右只有一个子节点。先判断是不是删除根节点,删除根节点就要改变_root的指向。不是根节点就判断左右哪个不为空,父节点连接到不为空的结点

2.左右都有节点。从左子树中找最大结点,记录父节点用来删除。交换max节点和删除节点的值,之后需要删除的就变成了max节点

父节点初始值给成删除节点,方便后面删除,无需更多判断

如果父节点就是删除节点,max节点就是删除节点的左节点,父节点连接到max的左节点,就删除了max。这里右节点一定为空,不然父节点就不会和删除节点一样。下图内只需要8和5交换,parent的左连接到max的左

如果父节点不是删除节点,那么max节点一定是右节点,且它的右节点肯定为空,因为没有比它大的了。只需要parent的右连接到max的左。下图只需要8和7交换,parnt的右连接到max的左
查找

cpp 复制代码
bool find(const K& key)
{
	if (_root == nullptr)
	{
		return false;
	}

	node* cur = _root;
	while (cur)
	{
		if (key < cur->_key)
		{
			cur = cur->_left;
		}
		else if (key > cur->_key)
		{
			cur = cur->_right;
		}
		else
		{
			return true;
		}
	}

	return false;
}

相等返回找到

构造

default是c++11的特性,会生成没有定义的默认的构造函数

拷贝构造调用copy函数,copy函数递归复制二叉树每个节点,需要前序遍历,最后返回根节点

cpp 复制代码
BinaryTree(const BinaryTree<K>& x)
{
	_root = copy(x._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;
}

赋值构造只需要交换临时对象的根节点,函数调用完自动释放临时对象

cpp 复制代码
BinaryTree<K>& operator=(BinaryTree<K> x)
{
	std::swap(_root, x._root);
	return *this;
}

析构

cpp 复制代码
~BinaryTree()
{
	destory(_root);
}

void destory(node*& root)
{
	if (root == nullptr)
	{
		return;
	}

	destory(root->_left);
	destory(root->_right);
	delete root;
	root = nullptr;
}

析构函数调用destory函数,后续遍历删除所有节点,这里用引用作为参数,可以直接删除传入的二叉树

插入递归

cpp 复制代码
bool insertx(const K& key)
{
	return _insertx(_root, key);
}

bool _insertx(node*& root, const K& key)
{
	if (root == nullptr)
	{
		root = new node(key);
		return true;
	}

	if (key < root->_key)
	{
		_insertx(root->_left, key);
	}
	else if (key > root->_key)
	{
		_insertx(root->_right, key);
	}
	else
	{
		return false;
	}
}

因为根节点外部无法获取所以套一层,如果是空,就创建节点。比key小就递归左边插入,大就递归右边插入,这里有一个链接的问题,创建节点后如何和父节点连接。只需要传入引用,递归的每一层就是父节点的子节点了,创建后自动连接

查找递归

cpp 复制代码
bool findx(const K& key)
{
	return _findx(_root, key);
}

bool _findx(node* root, const K& key)
{
	if (root == nullptr)
	{
		return false;
	}

	if (key < root->_key)
	{
		_findx(root->_left, key);
	}
	else if (key > root->_key)
	{
		_findx(root->_right, key);
	}
	else
	{
		return true;
	}
}

和插入一样

cpp 复制代码
bool earsex(const K& key)
{
	return _earsex(_root, key);
}

bool _earsex(node*& root, const K& key)
{
	if (root == nullptr)
	{
		return false;
	}

	if (key < root->_key)
	{
		_earsex(root->_left, key);
	}
	else if (key > root->_key)
	{
		_earsex(root->_right, key);
	}
	else
	{
		node* del = root;
		//左或右为空,直接改变当前节点,父节点自动连接
		if (root->_left == nullptr)
		{
			root = root->_right;
		}
		else if (root->_right = nullptr)
		{
			root = root->_left;
		}
		else
		{
			//两个节点都不为空,从左子树找最大的替换
			node* max = root->_left;
			while (max->_right)
			{
				max = max->_right;
			}

			std::swap(root->_key, max->_key);
			return _earsex(root->_left, key);
		}

		delete del;
		del = nullptr;
	}
}

删除同样传递引用,可以改变实参的值,递归寻找删除值,等于后开始删除。因为父节点自动连接,所以不需要保存父节点,如果一个节点为空的情况,直接将节点改变为不为空的子节点。如果都不为空,寻找左子树的最大节点,交换两个节点的值,再调用一次删除函数,传入删除节点的左子树就可以转换为一个节点为空的情况

cpp 复制代码
template <typename K>
struct TreeNode
{
	struct TreeNode<K>* _left;
	struct TreeNode<K>* _right;
	K _key;

	TreeNode(const K& key)
		:_left(nullptr), _right(nullptr), _key(key)
	{}
};

template <class K>
class BinaryTree
{
public:
	typedef struct TreeNode<K> node;
	BinaryTree() = default;
	BinaryTree(const BinaryTree<K>& x)
	{
		_root = copy(x._root);
	}

	BinaryTree<K>& operator=(BinaryTree<K> x)
	{
		std::swap(_root, x._root);
		return *this;
	}

	bool insert(const K& key)
	{
		if (_root == nullptr)
		{
			node* newnode = new node(key);
			_root = newnode;
			return true;
		}

		node* cur = _root;
		node* parent = nullptr;
		while (cur)
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}

		node* newnode = new node(key);
		if (key < parent->_key)
		{
			parent->_left = newnode;
		}
		else
		{
			parent->_right = newnode;
		}

		return true;

	}

	void inorder()
	{
		_inorder(_root);
		std::cout << std::endl;
	}
	void _inorder(node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_inorder(root->_left);
		std::cout << root->_key << " ";
		_inorder(root->_right);
	}

	bool find(const K& key)
	{
		if (_root == nullptr)
		{
			return false;
		}

		node* cur = _root;
		while (cur)
		{
			if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}

		return false;
	}

	bool earse(const K& key)
	{

		node* del = _root;
		node* parent = nullptr;
		while (del)
		{
			if (key < del->_key)
			{
				parent = del;
				del = del->_left;
			}
			else if (key > del->_key)
			{
				parent = del;
				del = del->_right;
			}
			else
			{
				//删除
				//左结点为空或右节点为空,父节点领养子节点
				if (del->_left == nullptr)
				{
					//删除节点是根节点,父节点为空,子节点成为根节点
					if (parent == nullptr)
					{
						_root = del->_right;
					}
					else
					{
						//删除节点是父节点的左还是右
						if (parent->_left == del)
						{
							parent->_left = del->_right;
						}
						else
						{
							parent->_right = del->_right;
						}
					}

					delete del;
					del = nullptr;
				}
				else if (del->_right == nullptr)
				{
					if (parent == nullptr)
					{
						_root = del->_left;
					}
					else
					{
						//删除节点是父节点的左还是右
						if (parent->_left == del)
						{
							parent->_left = del->_left;
						}
						else
						{
							parent->_right = del->_left;
						}
					}

					delete del;
					del = nullptr;
				}
				else
				{
					//两个节点都不为空,从左子树找最大的替换
					node* max = del->_left;
					//parent设置为空,如果删除根节点会出错,所以赋初始值
					node* parent = del;
					while (max->_right)
					{
						parent = max;
						max = max->_right;
					}

					std::swap(max->_key, del->_key);
					//不能再调用一遍函数删除,因为不是二叉搜索树了,找不到del
					if (parent == del)
					{
						parent->_left = max->_left;
					}
					else
					{
						//一般情况,右节点断开连接
						parent->_right = max->_left;
					}

					delete max;

				}

				return true;
			}
		}

		return false;
	}

	//递归
	bool insertx(const K& key)
	{
		return _insertx(_root, key);
	}

	bool _insertx(node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new node(key);
			return true;
		}

		if (key < root->_key)
		{
			_insertx(root->_left, key);
		}
		else if (key > root->_key)
		{
			_insertx(root->_right, key);
		}
		else
		{
			return false;
		}
	}

	bool findx(const K& key)
	{
		return _findx(_root, key);
	}

	bool _findx(node* root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (key < root->_key)
		{
			_findx(root->_left, key);
		}
		else if (key > root->_key)
		{
			_findx(root->_right, key);
		}
		else
		{
			return true;
		}
	}

	bool earsex(const K& key)
	{
		return _earsex(_root, key);
	}

	bool _earsex(node*& root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (key < root->_key)
		{
			_earsex(root->_left, key);
		}
		else if (key > root->_key)
		{
			_earsex(root->_right, key);
		}
		else
		{
			node* del = root;
			//左或右为空,直接改变当前节点,父节点自动连接
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else if (root->_right = nullptr)
			{
				root = root->_left;
			}
			else
			{
				//两个节点都不为空,从左子树找最大的替换
				node* max = root->_left;
				while (max->_right)
				{
					max = max->_right;
				}

				std::swap(root->_key, max->_key);
				return _earsex(root->_left, key);
			}

			delete del;
			del = nullptr;
		}
	}

	~BinaryTree()
	{
		destory(_root);
		_root = nullptr;
	}

	void destory(node*& root)
	{
		if (root == nullptr)
		{
			return;
		}

		destory(root->_left);
		destory(root->_right);
		delete root;
		root = nullptr;
	}

private:
	//前序遍历先创建节点,返回最上层的节点就是根节点
	//递归前链接
	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;
	}
private:
	node* _root = nullptr;
};

3. 二叉搜索树的应用

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

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

以词库中所有单词集合中的每个单词作为key,构建一颗二叉搜索树

在二叉搜索树中检索该单词是否存在,存爱则拼写正确,不存在则拼写错误

门禁系统等都是key,检测在不在

2.kv模型:每一个关键码,都有与之对应的值value,即<key, value>的键值对。这种在现实生活中非常常见:

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

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

例如字典,统计单词个数等就是kv,根据单词查找翻译

改造kv二叉树

模板参数加V类型,存储vlaue值,查找返回节点指针,就可以访问value了

cpp 复制代码
template <typename K, typename V>
struct TreeNode
{
	struct TreeNode<K, V>* _left;
	struct TreeNode<K, V>* _right;
	K _key;
	V _value;

	TreeNode(const K& key, const V& value)
		:_left(nullptr), _right(nullptr), _key(key), _value(value)
	{}
};

template <class K, class V>
class BinaryTree
{
public:
	typedef struct TreeNode<K, V> node;
	BinaryTree() = default;
	BinaryTree(const BinaryTree<K, V>& x)
	{
		_root = copy(x._root);
	}

	BinaryTree<K, V>& operator=(BinaryTree<K, V> x)
	{
		std::swap(_root, x._root);
		return *this;
	}

	bool insert(const K& key, const V& value)
	{
		if (_root == nullptr)
		{
			node* newnode = new node(key, value);
			_root = newnode;
			return true;
		}

		node* cur = _root;
		node* parent = nullptr;
		while (cur)
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}

		node* newnode = new node(key, value);
		if (key < parent->_key)
		{
			parent->_left = newnode;
		}
		else
		{
			parent->_right = newnode;
		}

		return true;

	}

	void inorder()
	{
		_inorder(_root);
		std::cout << std::endl;
	}
	void _inorder(node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_inorder(root->_left);
		std::cout << root->_key << " " << root->_value << std::endl;
		_inorder(root->_right);
	}

	node* find(const K& key)
	{
		if (_root == nullptr)
		{
			return nullptr;
		}

		node* cur = _root;
		while (cur)
		{
			if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}

		return nullptr;
	}

	bool earse(const K& key)
	{

		node* del = _root;
		node* parent = nullptr;
		while (del)
		{
			if (key < del->_key)
			{
				parent = del;
				del = del->_left;
			}
			else if (key > del->_key)
			{
				parent = del;
				del = del->_right;
			}
			else
			{
				//删除
				//左结点为空或右节点为空,父节点领养子节点
				if (del->_left == nullptr)
				{
					//删除节点是根节点,父节点为空,子节点成为根节点
					if (parent == nullptr)
					{
						_root = del->_right;
					}
					else
					{
						//删除节点是父节点的左还是右
						if (parent->_left == del)
						{
							parent->_left = del->_right;
						}
						else
						{
							parent->_right = del->_right;
						}
					}

					delete del;
					del = nullptr;
				}
				else if (del->_right == nullptr)
				{
					if (parent == nullptr)
					{
						_root = del->_left;
					}
					else
					{
						//删除节点是父节点的左还是右
						if (parent->_left == del)
						{
							parent->_left = del->_left;
						}
						else
						{
							parent->_right = del->_left;
						}
					}

					delete del;
					del = nullptr;
				}
				else
				{
					//两个节点都不为空,从左子树找最大的替换
					node* max = del->_left;
					//parent设置为空,如果删除根节点会出错,所以赋初始值
					node* parent = del;
					while (max->_right)
					{
						parent = max;
						max = max->_right;
					}

					std::swap(max->_key, del->_key);
					//不能再调用一遍函数删除,因为不是二叉搜索树了,找不到del
					if (parent == del)
					{
						parent->_left = max->_left;
					}
					else
					{
						//一般情况,右节点断开连接
						parent->_right = max->_left;
					}

					delete max;

				}

				return true;
			}
		}

		return false;
	}

	//递归
	bool insertx(const K& key, const V& value)
	{
		return _insertx(_root, key, value);
	}

	bool _insertx(node*& root, const K& key, const V& value)
	{
		if (root == nullptr)
		{
			root = new node(key, value);
			return true;
		}

		if (key < root->_key)
		{
			_insertx(root->_left, key);
		}
		else if (key > root->_key)
		{
			_insertx(root->_right, key);
		}
		else
		{
			return false;
		}
	}

	node* findx(const K& key)
	{
		return _findx(_root, key);
	}

	node* _findx(node* root, const K& key)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		if (key < root->_key)
		{
			_findx(root->_left, key);
		}
		else if (key > root->_key)
		{
			_findx(root->_right, key);
		}
		else
		{
			return root;
		}
	}

	bool earsex(const K& key)
	{
		return _earsex(_root, key);
	}

	bool _earsex(node*& root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (key < root->_key)
		{
			_earsex(root->_left, key);
		}
		else if (key > root->_key)
		{
			_earsex(root->_right, key);
		}
		else
		{
			node* del = root;
			//左或右为空,直接改变当前节点,父节点自动连接
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else if (root->_right = nullptr)
			{
				root = root->_left;
			}
			else
			{
				//两个节点都不为空,从左子树找最大的替换
				node* max = root->_left;
				while (max->_right)
				{
					max = max->_right;
				}

				std::swap(root->_key, max->_key);
				return _earsex(root->_left, key);
			}

			delete del;
			del = nullptr;
		}
	}

	~BinaryTree()
	{
		destory(_root);
		_root = nullptr;
	}

	void destory(node*& root)
	{
		if (root == nullptr)
		{
			return;
		}

		destory(root->_left);
		destory(root->_right);
		delete root;
		root = nullptr;
	}

private:
	//前序遍历先创建节点,返回最上层的节点就是根节点
	//递归前链接
	node* copy(node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}
		node* newnode = new node(root->_key, root->value);
		newnode->_left = copy(root->_left);
		newnode->_right = copy(root->_right);

		return newnode;
	}
private:
	node* _root = nullptr;
};

输入单词查询翻译

cpp 复制代码
BinaryTree<string, string> dict;
dict.insert("sort", "排序");
dict.insert("left", "左边");
dict.insert("right", "右边");
dict.insert("insert", "插入");
dict.insert("key", "钥匙");

string str;
while (cin >> str)
{
	TreeNode<string, string>* ret = dict.find(str);
	if (ret)
	{
		cout << ret->_value << endl;
	}
	else
	{
		cout << "unknow" << endl;
	}
}

统计水果出现的次数

cpp 复制代码
ring arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" , "南瓜"};
	BinaryTree<string, int> tree;
	for (auto ch : arr)
	{
		TreeNode<string, int>* ret = tree.find(ch);
		if (ret == nullptr)
		{
tree.insert(ch, 1);
		}
		else
		{
ret->_value++;
		}
	}

	tree.inorder();

性能分析

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

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

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2N log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: f r a c N 2 frac{N}{2} fracN2

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?这就需要AVL树和红黑树

4. 进阶题

根据二叉树创建字符串

创建字符串

思路

首先得到没有省略括号版的。利用前序遍历,递归左右子树之前加上括号,递归后的变量都不一样,为了字符串内容能叠加,需要每次加上递归后的内容。然后总结出括号可以省略的情况

1.当左右都为空的时候,括号都可以省略

2.右边为空的时候,可以省略

3.左边为空不能省略,因为如果右边不为空,无法区分是左右哪个节点

反推,左子树需要加括号的情况有两种,左边不为空或者右边不为空都不能省略括号。右子树只有不为空的时候不能省略

cpp 复制代码
/**
 * 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:
    string tree2str(TreeNode* root) {
        string str;
        if (root == nullptr) {
            return str;
        }
        str += to_string(root->val);
        // 省略括号
        if (root->left || root->right) {
            str += "(";
            str += tree2str(root->left);
            str += ")";
        }

        if (root->right) {
            str += "(";
            str += tree2str(root->right);
            str += ")";
        }

        return str;
    }
};

二叉树的最近公共祖先

公共祖先

思路

公共祖先即为相同的父节点,例如7和4的公共祖先就是2、5、3,最近的就是2。判断是不是最近的公共祖先可以遵循下面规则:

p和q节点分别在这个结点的一左一右,这个结点就是最近的公用祖先。如果p和q一个是另一个祖先,那么最近的公共节点就是祖先的这个结点

先弄一个函数,判断节点是不是在这棵树中,用来判断p和q在树中的左右情况。首先判断两个节点有一个是根节点,直接返回这个结点。用四个变量pleft,pright,qleft,qright表明节点情况,调用函数传入根的左树,如果pleft返回真,证明p在左树中,那么pright就是假,同样方法判断q。如果一左一右就找到了最近的公共节点,返回这个结点。如果p和q都在左子树,就递归到左子树,都在右子树就递归到右子树

cpp 复制代码
/**
 * 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 isTree(TreeNode* node, TreeNode* root)
    {
        if (root == nullptr)
        {
            return false;
        }

        if (node == root)
        {
            return true;
        }

        return isTree(node, root->left) ||
        isTree(node, root->right);
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        
        if (root == p || root == q)
        {
            return root;
        }

        bool pleft = isTree(p, root->left);
        bool pright = !pleft;
        bool qleft = isTree(q, root->left);
        bool qright = !qleft;

        //一个在左 一个在右
        if ((pleft && qright) || (pright && qleft))
        {
            return root;
        }

        //都在左就递归左
        if (pleft && qleft)
        {
            return lowestCommonAncestor(root->left, p, q);
        }
        else
        {
            return lowestCommonAncestor(root->right, p, q);
        }
        
    }

};

上面的方法效率不高,时间复杂度是一个等差数列,也就是 N 2 N^2 N2。寻找最近的公共节点还有其他方法,可以记录节点到根节点的路径,有了路径就成了前面的相交问题,让长的先走,然后不断出到交点位置返回,就是最近的公共节点

用一个栈,先压入根节点,然后递归左边,不断压入节点,左边没有就递归右边。如果这个结点的左右都不是,就弹出这个结点,返回上一层递归,找到节点就返回,不用继续往下走了

比如找4的路径,先压入3,递归压入5,6,6的左右都不是,弹出6,返回到递归5的右边,压入2,7,7不是弹出,压入4,4找到了,栈中的内容就是4的路径

cpp 复制代码
/**
 * 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* node, stack<TreeNode*>& s)
    {
        if (root == nullptr)
        {
            return false;
        }

        s.push(root);
        if (root == node)
        {
            return true;
        }
        if (findpath(root->left, node, s))
        {
            return true;
        }
        
        if (findpath(root->right, node, s))
        {
            return true;
        }

        s.pop();
        return false;
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
      
        stack<TreeNode*> s1;
        stack<TreeNode*> s2;
        findpath(root, p, s1);
        findpath(root, q, s2);

        while (s1.size() != s2.size())
        {
            if (s1.size() > s2.size())
            {
                s1.pop();
            }
            else
            {
                s2.pop();
            }
        }

        while (s1.top() != s2.top())
        {
            s1.pop();
            s2.pop();
        }

        return s1.top();
    }  

};

这时的时间复杂度就成了N

二叉搜索树与双向链表

二叉树转换双向链表

思路

这题不能用数组记录改变链接,因为空间复杂度是O(1),所以必须直接改变原链表的指向。这就是中序线索化的过程,用两个节点指针,一个cur当前节点,一个prev保存上一个节点,利用中序遍历,改变两个指针指向,prev的初始值是空,当prev不是空的时候,右结点指向cur,cur的左节点是prev,最后将cur赋值给prev。需要返回链表的头,根节点是中间位置,可以判断空不断取前驱,找到头返回

cpp 复制代码
/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:

	void pointconvert(TreeNode* cur, TreeNode*& prev)
	{
		if (cur == nullptr)
		{
			return;
		}

		pointconvert(cur->left, prev);
		cur->left = prev;
		if (prev)
		{
			prev->right = cur;
		}
		prev = cur;
		pointconvert(cur->right, prev);
	}

    TreeNode* Convert(TreeNode* pRootOfTree) {
		TreeNode* pre = nullptr;
		pointconvert(pRootOfTree, pre);

		TreeNode* head = pRootOfTree;
		while (head && head->left)
		{
			head = head->left;
		}
		return head;
    }
};

prev需要传引用,因为每层需要改变prev的指向位置

前序、中序遍历构建二叉树

构建二叉树

思路

前序和中序创建二叉树,需要不断用前序确定根,中序分割左右子树,前序记录一个下标,第一个数是二叉树的根,先创建节点,然后再中序中用根分割左右子树,所以还需要一个递归的左右区间,初始从数组范围开始,左边是0到根-1的位置,有边事根+1到最后一个数。根节点创建好,然后创建左子树的根节点,前序下标+1,就是第二个数,在左子树区间中继续查找分割左子树的左右区间。然后是右子树。如果区间不存在就直接返回

cpp 复制代码
/**
 * 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* recbulid(vector<int>& preorder, vector<int>& inorder, int& prei,
                       int begin, int end) {
        if (begin > end) {
            return nullptr;
        }
        int rooti = begin;
        while (rooti <= end) {
            if (preorder[prei] == inorder[rooti]) {
                break;
            }
            rooti++;
        }

        TreeNode* node = new TreeNode(preorder[prei++]);

        node->left = recbulid(preorder, inorder, prei, begin, rooti - 1);
        node->right = recbulid(preorder, inorder, prei, rooti + 1, end);

        return node;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int pflag = 0;
        return recbulid(preorder, inorder, pflag, 0, preorder.size() - 1);
    }
};

下面是部分递归展开图

二叉树的前序遍历

前序遍历

思路

循环方法实际上就是将递归改为循环。将每一个树都看做左节点和左节点的右子树。从根开始,先访问所有的左路节点,到叶子结点返回,用同样的方法访问每一个叶子结点的右子树,将右子树也看做左节点+左节点的右子树形式

用栈保存所有左路节点,cur记录当前节点,左路节点添加访问完,弹出一个访问右子树的左路

下面的树可以看做:

cpp 复制代码
/**
 * 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) {
        vector<int> v;
        stack<TreeNode*> s;

        TreeNode* cur = root;
        while (cur || !s.empty()) {
            // 遍历左路节点入栈
            while (cur) {
                v.push_back(cur->val);
                s.push(cur);
                cur = cur->left;
            }

            // 子问题方式访问左路节点的右子树
            TreeNode* top = s.top();
            s.pop();
            cur = top->right;
        }
        return v;
    }
};

二叉树的中序遍历

中序遍历

思路

中序和前序差不多,区别只是在于访问的顺序,中序是先左再根再右,所以访问应该在这个节点被弹出的时候

cpp 复制代码
/**
 * 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) {
        vector<int> v;
        stack<TreeNode*> s;

        TreeNode* cur = root;
        while (cur || !s.empty()) {
            while (cur) {
                s.push(cur);
                cur = cur->left;
            }

            TreeNode* top = s.top();
            s.pop();
            v.push_back(top->val);
            cur = top->right;
        }

        return v;
    }
};

二叉树后续遍历

后序遍历

后序顺序为先左再右,最后才是根,依然先访问左,要判断返回来的弹出节点能不能访问,有两种情况,如果右等于空。那么可以访问这个结点。可以用prev记录上一个访问了的结点,这个结点可以访问只有当右节点访问过了,右节点访问过了那prev一定是右节点

cpp 复制代码
/**
 * 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) {
        vector<int> v;
        stack<TreeNode*> s;

        TreeNode* prev = nullptr;
        TreeNode* cur = root;
        while (cur || !s.empty()) {
            while (cur) {
                s.push(cur);
                cur = cur->left;
            }

            // 空或者右访问过了,可以访问当前节点
            TreeNode* top = s.top();
            if (top->right == nullptr || prev == top->right) {

                s.pop();
                v.push_back(top->val);
                prev = top;
            } else {
                cur = top->right;
            }
        }

        return v;
    }
};
相关推荐
Algorithm15763 分钟前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
shinelord明12 分钟前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
Monly2119 分钟前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
boligongzhu19 分钟前
DALSA工业相机SDK二次开发(图像采集及保存)C#版
开发语言·c#·dalsa
Eric.Lee202120 分钟前
moviepy将图片序列制作成视频并加载字幕 - python 实现
开发语言·python·音视频·moviepy·字幕视频合成·图像制作为视频
小俊俊的博客20 分钟前
海康RGBD相机使用C++和Opencv采集图像记录
c++·opencv·海康·rgbd相机
7yewh22 分钟前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux
waicsdn_haha34 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
_WndProc36 分钟前
C++ 日志输出
开发语言·c++·算法
薄荷故人_37 分钟前
从零开始的C++之旅——红黑树及其实现
数据结构·c++