C++高级数据结构算法 | Binary Search Tree(二叉查找树)

版权声明:本文为博主整理文章,未经博主允许不得转载。 https://blog.csdn.net/ZYZMZM_/article/details/90633686


从二分查找算法说起

我们之前学习过查找算法中的二分查找。

二分查找又称为折半查找仅适用于事先已经排好序的顺序表

其查找的基本思路:首先将给定值 K K ,与表中中间位置元素的关键字比较,若相等,返回该元素的存储位置;若不等,这所需查找的元素只能在中间数据以外的前半部分或后半部分中。然后在缩小的范围中继续进行同样的查找。如此反复直到找到为止。

对比线性搜索的时间复杂度是 O ( n ) O(n) ,对于有序序列的二分搜索时间复杂度是 O ( l o g 2 n ) O(log_2 n) ,因此搜索效率比较高,例如:我们要在序列 { 35 , 37 , 47 , 51 , 58 , 62 , 73 , 88 , 93 , 99 } {\{35,37,47,51,58,62,73,88,93,99}\} 中查找指定元素,那么我们即可使用二分查找算法高效的完成此项工作。

下面是二分查找算法的非递归实现与递归实现的完整程序:

/* ****************************************
 * 非递归实现二分查找
 ******************************************/
int nonbinarySearch(vector<int> vec, int val)
{
	int low = 0;
	int high = vec.size() - 1;

	int middle = 0;
	while (low <= high)
	{
		middle = (low + high) >> 1;
		if (val < vec[middle])
		{
			high = middle - 1;
		}
		else if (val > vec[middle])
		{
			low = middle + 1;
		}
		else
		{
			return middle;
		}
	}
	return -1;
}

/* ****************************************
 * 递归实现二分查找
 ******************************************/
int binarySearch(vector<int> vec, int low, int high, int val);
int binarySearch(vector<int> vec, int val)
{
	int low = 0;
	int high = vec.size() - 1;

	return binarySearch(vec, low, high, val);
}

int binarySearch(vector<int> vec, int low, int high, int val)
{
	if (low > high)
	{
		return -1;
	}

	int middle = (low + high) >> 1;
	if (val < vec[middle])
	{
		return binarySearch(vec, low, middle - 1, val);
	}
	else if (val > vec[middle])
	{
		return binarySearch(vec, middle + 1, high, val);
	}
	else
	{
		return middle;
	}
}

/*
 *  Test Code
 */
int main()
{
	//35, 37, 47, 51, 58, 62, 73, 88, 93, 99
	vector<int> vec;
	vec.push_back(35);
	vec.push_back(37);
	vec.push_back(47);
	vec.push_back(51);
	vec.push_back(58);
	vec.push_back(62);
	vec.push_back(73);
	vec.push_back(88);
	vec.push_back(93);
	vec.push_back(99);

	cout << nonbinarySearch(vec, 93) << endl;
	cout << nonbinarySearch(vec, 21) << endl;

	cout << binarySearch(vec, 93) << endl;
	cout << binarySearch(vec, 21) << endl;
}

那么其实上述的搜索过程,便是一棵BST树的完整搜索过程


BST 树及其几条重要性质

二叉搜索树(Binary Search Tree),又称二叉排序树。它或者是一颗空树。或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上全部节点的值都小于它的根节点的值
  • 若它的右子树不空,则右子树上全部节点的值都大于它的根节点的值
  • 它的左、右子树也分别为二叉搜索树
  • 二叉查找树中没有键值相等的节点
  • 中序遍历二叉查找树便可得到一个有序序列。

例如,我们上面所讲的便是一棵二叉搜索树,当我们对它进行中序遍历时。就能够得到一个有序的序列 {35,37,47,51,58,62,73,88,93,99}。

二叉搜索树通常采用二叉链表作为存储结构。中序遍历二叉搜索树可得到一个关键字的有序序列,一个无序序列可以通过构造一棵二叉排序树变成一个有序序列,构造树的过程即为对无序序列进行排序的过程。每次插入的新的结点都是二叉搜索树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。

扫描二维码关注公众号,回复: 6773133 查看本文章

二叉搜索树本质上是一种二叉树,所以我们之前所讲的二叉树的性质它都是适用的


BST 树的结构定义

上面我们讲解的二分查找的过程就是BST树的搜索过程,BST树的每一个节点最多有两个孩子,而且左孩子的值 &lt; &lt; 父节点的值 &lt; &lt; 右孩子的值,下面定义 BST树 相关的结构定义:

// BST树
template<typename T>
class BSTree
{
public:
	BSTree() :_root(nullptr) {}

private:
	// 定义BST树节点的类型
	struct BSTNode
	{
		BSTNode(T data = T())
			:_data(data)
			, _left(nullptr)
			, _right(nullptr)
			{}
		T _data;
		BSTNode* _left;
		BSTNode* _right;
	};

	BSTNode* _root; // 指向树的根节点
};

BST树的插入、删除、查询

BST树的插入(递归与非递归实现)

/* *******************************************************
 * 非递归实现BST树的插入操作
 * 对于插入操作的实现,首先我们分析有两种情况,一种是插入前BST树中
 * 并没有结点,这种情况下我们直接把该值作为根节点即可返回;第二种情况
 * 是树中已经存在结点,因此我们就需要根据BST树的性质进行搜索应该
 * 插入的结点,注意我们应该保存遍历到当前结点的父节点,因为当前结点有可
 * 能是我们要插入的结点,最后我们要判断应该插入父节点的左孩子还是右孩子
 * 函数的实现容易理解:大于当前结点值向右走,小于该当前结点值向左走,
 * 直到找到为空的结点。
 * 找到该结点位置后,我们还需要判断待插入结点与父结点的大小,大于父结点
 * 插入到右孩子,小于父结点插入到左孩子即可
 *********************************************************/
void noninsert(const T& val)
{
	if (_root == nullptr)
	{
		_root = new BSTNode(val);
		return;
	}

	BSTNode* Node = _root;
	
	/* 保存遍历到该结点的父节点 */
	BSTNode* pre = nullptr;
	while (Node != nullptr)
	{
		pre = Node;
		if (val < Node->_data)
		{
			Node = Node->_left;
		}
		else if (val > Node->_data)
		{
			Node = Node->_right;
		}
		else
		{
			return;
		}
	}
	
	/* 判断新结点插入到父节点的左孩子还是右孩子 */
	if (val < pre->_data)
	{
		pre->_left = new BSTNode(val);
	}
	else
	{
		pre->_right = new BSTNode(val);
	}
}

/* ****************************************
 * 递归实现BST树的插入操作
 ******************************************/
void insert(const T& val)
{
	_root = insert(_root, val);
}

BSTNode* insert(BSTNode* node, const T& val)
{
	if (node == nullptr)
	{
		return new BSTNode(val);
	}

	if (val < node->_data)
	{
		node->_left = insert(node->_left, val);
	}
	else if (val > node->_data)
	{
		node->_right = insert(node->_right, val);
	}
	else{}

	return node;
}

BST树的删除(递归与非递归实现)

/* *************************************************************
 * 非递归实现BST树的删除操作
 * 删除操作的实现较为复杂,因为会出现多种不同的情况
 * 1、第一种情况,要删除的是叶子节点(左右孩子为nullptr)
 * 2、第二种情况,要删除结点只有一个孩子
 * 3、第三种情况,要删除的结点同时存在左右孩子
 * 那么对于前两种情况,我们只需要将当前待删除结点的左或右孩子赋值给其
 * 父结点的左或右孩子即可。
 * 但是对于第三种情况,我们需要使用前驱或者后继结点代替该删除结点的值
 * 这里提到了前驱结点和后继结点:
 * 前驱结点:当前结点的左子树中值最大的元素
 * 后继结点:当前结点的右子树中值最小的元素
 * 我们在这里统一使用前驱结点替代待删除结点的值
 ***************************************************************/
void nonremove(const T& val)
{
	if (_root == nullptr)
	{
		return;
	}

	BSTNode* CurNode = _root;
	BSTNode* preNode = nullptr;

	while (CurNode != nullptr)
	{
		if (val < CurNode->_data)
		{
			preNode = CurNode;
			CurNode = CurNode->_left;
		}
		else if (val > CurNode->_data)
		{
			preNode = CurNode;
			CurNode = CurNode->_right;
		}
		else
		{
			break;
		}
	}

	if (CurNode == nullptr)
	{
		return;
	}

	if (CurNode->_left != nullptr && CurNode->_right != nullptr)
	{
		BSTNode* OriNode = CurNode;
		preNode = CurNode;
		CurNode = CurNode->_left;		
		while (CurNode->_right != nullptr)
		{
			preNode = CurNode;
			CurNode = CurNode->_right;
		}
		OriNode->_data = CurNode->_data;
	}

	BSTNode* childNode = CurNode->_left;

	if (childNode == nullptr)
	{
		childNode = CurNode->_right;
	}

	/* 考虑只有两层的情况:删除根结点 */
	if (preNode == nullptr)
	{
		_root = childNode;
	}
	else
	{
		if (preNode->_left == CurNode)
		{
			preNode->_left = childNode;
		}
		else
		{
			preNode->_right = childNode;
		}
	}
	delete CurNode;
}

/* ****************************************
 * 递归实现BST树的删除操作
 ******************************************/
void remove(const T& val)
{
	_root = remove(_root, val);
}

BSTNode* remove(BSTNode* node, const T& val)
{
	if (node == nullptr)
	{
		return nullptr;
	}

	if (val < node->_data)
	{
		node->_left = remove(node->_left, val);
	}
	else if (val > node->_data)
	{
		node->_right = remove(node->_right, val);
	}
	else
	{
		if (node->_left != nullptr && node->_right != nullptr)
		{
			BSTNode* pre = node->_left;
			while (pre->_right != nullptr)
			{
				pre = pre->_right;
			}
			node->_data = pre->_data;
			node->_left = remove(node->_left, pre->_data); // 删除前驱节点
		}
		else if (node->_left != nullptr)
		{
			return node->_left;
		}
		else if (node->_right != nullptr)
		{
			return node->_right;
		}
		else
		{
			return nullptr;
		}
	}
	return node;
}

BST树的查询(递归与非递归实现)

/* ******************************************
 * 非递归实现BST树的查询操作
 * BST树的查询操作的实现首先需要判断根节点是否为空
 * 之后便可以遍历BST树查找元素,这里我们返回布尔值,
 * 有时候我们可能需要根据具体的情况,要让query
 * 返回查询到的结点,只需修改函数返回值类型和函数
 * 的返回语句即可。
 ********************************************/
bool nonquery(const T& val)
{
	if (_root == nullptr)
	{
		return false;
	}

	BSTNode* CurNode = _root;
	while (CurNode != nullptr)
	{
		if (val < CurNode->_data)
		{
			CurNode = CurNode->_left;
		}
		else if (val > CurNode->_data)
		{
			CurNode = CurNode->_right;
		}
		else
		{
			return true;
		}
	}
	return false;

}

/* **************************************************
 * 递归实现BST树的查询操作:非常类似于二分查找过程
 * 我们在上面的讲解中实现了二分查找的递归实现过程,那么
 * 我们递归遍历BST树的实现和二分查找的递归实现是非常相似的
 * 在二分查找中我们是以元素在顺序表中的下标来缩小查找范围的
 * 那么我们遍历BST树直接通过左右子树来缩小搜索范围即可。
 ****************************************************/
bool query(const T& val)
{
	return query(_root, val);
}
bool query(BSTNode* node, const T& val)
{
	if (node == nullptr)
	{
		return false;
	}

	if (val < node->_data)
	{
		return query(node->_left, val);
	}
	else if (val > node->_data)
	{
		return query(node->_right, val);
	}
	else
	{
		return true;
	}
}

BST树的四种遍历(递归与非递归实现)

请参考我之前的博文:

《C++高级数据结构算法 | 二叉树的四种遍历算法详解(递归与非递归实现)》


BST 树相关的典型题目

BST树的高度、结点元素个数求解

/* ****************************************
 * 求BST树的结点元素个数
 * 基本思路就是我们递归遍历完树的所有结点,最后给
 * 结果加一即可
 * 递归实现的过程是一直遍历到_left为空,回溯,结点
 * 加一,然后继续遍历_right,也是上述过程,直到全部
 * 结点遍历完毕即可
 ******************************************/
int NodeNum()
{
	return NodeNum(_root);;
}

int NodeNum(BSTNode* Node)
{
	if (Node == nullptr)
	{
		return 0;
	}
	return 1 + NodeNum(Node->_left) + NodeNum(Node->_right);
}

/* ******************************************************
 * 求BST树的高度 / 层数
 * 整体思路就是我们将左子树遍历,再将右子树遍历,拿到它们高度的最大值
 * 加一即可。
 * 递归函数的实现过程就是,我们先遍历左子树,为nullptr后,遍历右子
 * 树,为nullptr后,函数回溯,层数加一。
 ********************************************************/
int Treelevel()
{
	return Treelevel(_root);
}

int Treelevel(BSTNode* node)
{
	if (node == nullptr)
	{
		return 0;
	}

	int left = Treelevel(node->_left);
	int right = Treelevel(node->_right);

	return (left > right ? left : right) + 1;
}

BST树区间元素查找:在BST树中,查找满足某一个区间元素范围的值

/* *********************************************************
 * 在BST树中,查找满足某一个区间元素范围的值
 * 该题目本身是非常容易实现的,我们只需要遍历树中的所有结点,然后每遍历
 * 到一个结点都判断一下该值是否在区间范围内即可。
 * 但是上述的效率并不高,我们应该充分利用BST树的性质:对BST树进行中序
 * 遍历后我们可以得到一个递增的有序序列。因此我们便可以根据这条性质
 * 对遍历添加条件,使得效率更高。
 * 具体的做法是我们中序遍历每遍历到一个结点后,我们判断一下该结点值与
 * 我们要查找的区间的的上限值,若该结点值已经高于区间范围,即大于区间
 * 上限值,那么直接返回即可,无需继续遍历,因为之后遍历到的值都肯定是
 * 大于区间上限的,那么这样就提高了效率。否则,判断该结点值是否在区间
 * 范围内,若在,则加入结果集中。
 ***********************************************************/
 
vector<T> getAreaDatas(T a, T b)
{
	vector<T> resVec;
	getAreaDatas(_root, resVec, a, b);
	cout << resVec.size() << endl;
	return resVec;
}
void getAreaDatas(BSTNode* node, vector<T> &resVec, T a, T b)
{
	if (node == nullptr)
	{
		return;
	}
	getAreaDatas(node->_left, resVec, a, b);

	if (node->_data > b)
	{
		return;
	}
	if (node->_data >= a && node->_data <= b)
	{
		resVec.push_back(node->_data);
	}

	getAreaDatas(node->_right, resVec, a, b);
}

BST求子树问题:给定一棵树,判断是否是当前BST的子树

/* ****************************************************
 * BST求子树问题:判断childTree是否是当前BST的子树
 * 首先通过query()查询到子树的在原树中的根节点,将其与子树根
 * 传入递归函数。
 * 原树结点与子树节点同时为空,返回成功
 * 原树结点为空,返回失败
 * 子树结点为空,返回成功
 * 上述条件都不满足后,判断原树与子树的结点值是否相同,
 * 不同直接匹配失败
 * 然后递归遍历原树左子树(匹配的根节点开始)与子树左子树、
 * 原树右子树(匹配的根节点开始)与子树右子树
 *****************************************************/
  
void isChildTree(BSTree childTree)
{
	cout << isChildTree(&childTree) << endl;
}
bool isChildTree(BSTree* childTree)
{
	BSTNode* curNode = queryNode(childTree->_root->_data);
	if (curNode != nullptr)
	{
		return isChildTree(curNode, childTree->_root);
	}
	return false;
}

bool isChildTree(BSTNode* parent, BSTNode* child)
{
	if (parent == nullptr && child ==nullptr)
	{
		return true;
	}
	
	if (parent == nullptr)
	{
		return false;
	}

	if (child == nullptr)
	{
		return true;
	}

	if (parent->_data != child->_data)
	{
		return false;
	}
	
	return isChildTree(parent->_left, child->_left) &&
		isChildTree(parent->_right, child->_right);
}

BST的LCA问题:求寻找最近公共祖先结点

/* ***************************************************
 * BST树的LCA问题:获取BST树中两个节点的最近公共祖先
 * LCA问题求解主要在于充分理解BST树的性质,因为要作为两个
 * 结点的公共祖先,必须满足,该结点的值一定在两结点值的中间位置
 * 若该结点的值同时大于传入的两个结点的值,那么该结点向左子树遍历
 * 使其值变小
 * 若该结点的值同时小于传入的两个结点的值,那么该结点向右子树遍历
 * 使其值变大
 * 若该结点的值在传入的两个结点值之间,则找到,返回。
 ****************************************************/

void getLCA(T a, T b)
{
	BSTNode*node =  getLCA(_root, a, b);
	cout << node->_data << endl;
}
BSTNode* getLCA(BSTNode* node, T a, T b)
{
	if (node == nullptr)
	{
		return nullptr;
	}

	if (node->_data > a && node->_data > b)
	{
		return getLCA(node->_left, a, b);
	}
	else if (node->_data < a && node->_data < b)
	{
		return getLCA(node->_right, a, b);
	}
	else
	{
		return node;
	}
}

BST树的镜像反转问题

/* *************************************************
 * BST树的镜像反转问题代码实现
 * 交换左右孩子的值,然后递归的遍历左子树和右子树的所有节点,
 * 对遍历到的每个元素都做上述操作,便可实现BST树的镜像反转。
 ***************************************************/
 
void mirror()
{
	mirror(_root);
}

void mirror(BSTNode* node)
{
	if (node == nullptr)
	{
		return;
	}

	BSTNode* ptmp = node->_left;
	node->_left = node->_right;
	node->_right = ptmp;
	
	mirror(node->_left);
	mirror(node->_right);
}

重建BST树:已知BST树的前序遍历和中序遍历,重建BST树

/* **********************************************************
 * 已知BST树的前序遍历和中序遍历,重建BST树
 * 首先我们需要分析前序序列和中序序列的特点,首先前序遍历中的每个元素
 * 便是一个根节点,首元素是整棵树的根节点,之后元素是子树的根节点。
 * 而中序遍历中,我们根据前序遍历到的结点找到其在中序遍历中的位置
 * 那么在该位置左边变为以该位置为根的子树的左子树,同理,在该位置的
 * 右边便是以该位置为根的子树的右子树
 * 我们遍历前序序列中(递归),然后中序序列中(循环)的元素找到相应位置,
 * 然后进行分割,递归完成左子树和右子树的创建,不断缩小区间,完成BST
 * 树的创建
 *  
 * 该题目我们的主要焦点在于区间的分割问题,我们使用一个简单的实例说明
 * 
							  40
						20         60
					  18   25    45  70
					10	19	 30        90
					
 * 该树的前序序列是 : 40 20 18 10 19 25 30 60 45 70 90
 * 该树的中序序列是 : 10 18 19 20 25 30 40 45 60 70 90
 * i                             j
 * 40 20 18 10 19 25 30 60 45 70 90
 * (k遍历中序序列 : k = m)
 * m                             n
 * 10 18 19 20 25 30 40 45 60 70 90
 * 我们根据 pre[i] = in[k] 找到前序序列值在中序序列中的位置 k 
 * 然后递归重建左子树 i+1,i+(k-m),m,k-1 ,其中i+(k-m)是前序遍历的
 * 然后递归重建右子树 i+(k-m)+1,j,k+1,n
 ************************************************************/
 
void rebuild(vector<T> pre, vector<T> in)
{
	_root = rebuild(pre, in, 0, pre.size() - 1, 0, in.size() - 1);
}
BSTNode* rebuild(vector<T> pre, vector<T> in, int i, int j, int m, int n)
{
	if (i > j || m > n)
	{
		return nullptr;
	}

	BSTNode* Node = new BSTNode(pre[i]);
	for (int k = m; k <= n; k++)
	{
		if (pre[i] == in[k])
		{
			Node->_left = 
			rebuild(pre, in, i + 1, i + (k - m), m, k - 1);
			
			Node->_right = 
			rebuild(pre, in, i + (k - m) + 1, j, k + 1, j);
			
			return Node;
		}
	}
	return Node;
}

判断二叉树是否是一棵BST树

/* *****************************************************
 * 给出一棵二叉树,判断该二叉树是否是一棵BST树
 * 此题目我们应该充分利用BST树的性质求解,若该数是一棵BST树,那
 * 么我们使用中序遍历得到的应该是一个递增的有序序列,因此我们首先
 * 每次遍历前记录当前结点,再进行遍历,每次更新,相当于我们记录下
 * 了该结点前边遍历到结点,然后遍历到后我们判断该结点的值是否是大于
 * 我们记录的值,若不是,直接返回false即可。
 * 另外我们使用递归,应该要提高效率,因此我们对左子树进行递归时也
 * 判断其返回值,若为flase那么没有必要继续下去了。
 *******************************************************/
/**
 * 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 isValidBST(TreeNode* root) {
        if(!root) return true;
        
        TreeNode *prev = nullptr;

        return isBST(root, prev);
    }
    
    bool isBST(TreeNode* root, TreeNode* &prev)
    {
        if(root == nullptr)
            return true;

        if(!isBST(root->left, prev))
            return false;

        if(prev != nullptr && root->val <= prev->val)
            return false;

        prev = root;
        return isBST(root->right, prev);
    }
};

获取中序遍历倒数第k个节点的值

/**
 * 获取中序遍历倒数第k个节点的值
 * 要求倒数第k个结点,我们把问题简化一下,可以将题目变形为获取中序遍历
 * 第n个结点的值,通过结点总数减去k便得到了n,即我们们应该遍历的结点总数。
 * 注意,因为我们不再函数中做打印,因此我们需要将返回值保存在变量中,
 * 带出递归函数。注意一定不能在递归函数中进行返回操作。
 * 基本思路就是中序遍历结点值,然后使得n每次都递减,直到n减到0时,便说明
 * 我们已经中序遍历了n个结点了,也就是倒数第k个结点,因此将该结点的值存入
 * 返回值变量中,然后函数直接return即可。
 * 当n减为负时,递归函数直接返回,提高执行效率。
 */
int getLastKValue(int k)
{
	int sumNode = NodeNum();
	if (k == 0 || k > sumNode)
	{
		cout << "ERROR ARGV" << endl;
		return -1;
	}
	k = sumNode - (k - 1);
	//k = sumNode - k;
	int res = 0;
	getLastKValue(_root, k, res);
	return res;
}

void getLastKValue(BSTNode* node, int &k, int &result)
{
	if(node != nullptr)
	{
		if (k >= 0)
		{
			getLastKValue(node->_left, k, result);

			--k;
			if (k == 0)
			{
				result = node->_data;
				return;
			}

			getLastKValue(node->_right, k, result);
		}
	}
}

猜你喜欢

转载自blog.csdn.net/ZYZMZM_/article/details/90633686