从二分查找算法说起
我们之前学习过查找算法中的二分查找。
二分查找又称为折半查找,仅适用于事先已经排好序的顺序表。
其查找的基本思路:首先将给定值 ,与表中中间位置元素的关键字比较,若相等,返回该元素的存储位置;若不等,这所需查找的元素只能在中间数据以外的前半部分或后半部分中。然后在缩小的范围中继续进行同样的查找。如此反复直到找到为止。
对比线性搜索的时间复杂度是 ,对于有序序列的二分搜索时间复杂度是 ,因此搜索效率比较高,例如:我们要在序列 中查找指定元素,那么我们即可使用二分查找算法高效的完成此项工作。
下面是二分查找算法的非递归实现与递归实现的完整程序:
/* ****************************************
* 非递归实现二分查找
******************************************/
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}。
二叉搜索树通常采用二叉链表作为存储结构。中序遍历二叉搜索树可得到一个关键字的有序序列,一个无序序列可以通过构造一棵二叉排序树变成一个有序序列,构造树的过程即为对无序序列进行排序的过程。每次插入的新的结点都是二叉搜索树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。
二叉搜索树本质上是一种二叉树,所以我们之前所讲的二叉树的性质它都是适用的。
BST 树的结构定义
上面我们讲解的二分查找的过程就是BST树的搜索过程,BST树的每一个节点最多有两个孩子,而且左孩子的值 父节点的值 右孩子的值,下面定义 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);
}
}
}