二叉搜索树
一、二叉查找法
-
二分查找法是一个查找算法,它的核心思想是进行数据折半查找
比如猜数字游戏
-
二分查找法的一个必要条件就是需要在有序的数组内才能使用
思路:
- 必须是在一个有序的数组内进行查找
- 先取数组的中间下标,判断查找的数据
- 如果大于,就在右边继续进行折半查找,否则在左边
- 如果相等,就说明找到了,否则就没有找到
- 第五节课排序算法中详细讲到过。
二、二叉搜索树
-
二叉搜索树又称之为 二叉查找树 Binary Search Tree,简称
B Tree
(B树) -
二叉搜索树包含二叉树的特性所以二叉搜索树有左右两个孩子且节点之间没有环状结构
-
二叉搜索树的特性:
1、树的最左边一定是最小的值
2、树的最右边一定是最大的值
3、二叉树搜索树根节点左边的那一整块的所有值一定都小于根节点的值
4、二叉树搜索树根节点右边的那一整块的所有值一定都大于根节点的值
5、所以对于一个二叉搜索树而言,一个节点的左子节点一定小于它,一个节点的右子节点一定大于它
-
每一个节点都符合二叉搜索树的特性
1、节点的左边比根节点要小
2、节点的右边比根节点要大
3、每个单独的节点也符合二叉搜索树的特性
-
二叉搜索树的优势:
1、高效的插入数据
2、高效的删除数据
3、动态的维护数据
4、快速的找到数据的最大值
5、快速找到数据最小值
-
缺点:每次插入递增或者递减的数据,效率反而变低
-
二叉搜索树不一定是一个完全二叉树
-
每个节点的键值大于左孩子
-
每个节点的键值小于右孩子
-
左右孩子为根仍是二叉搜索树
//用代码实现一个二叉搜索树
template<typename Key, typename Val>
class BinNode //二叉搜索树的节点类型
{
public:
Key key; //键
Val val; //值
BinNode* left; //左子树
BinNode* right;//右子树
BinNode(Key& key, Val& val) :key(key), val(val), left(NULL), right(NULL)
{
}
};
template<typename Key, typename Val>
class BST //二叉搜索树
{
protected:
using Node = BinNode<Key, Val>;//取别名,之后就可以一直用Node定义节点了
Node* root;
int count;
Node* _insert(Node* node, Key& key, Val& val)//递归进行插入元素
{
if (node == NULL)//如果递归到了空节点,构造一个新的节点
{
count++; //插入成功,就计数
return new Node(key, val);
}
if (key == node->key)//如果出现重复的数据,去重
{
return node;
}
if (key<node->key)//如果插入的节点小于当前节点,插入到当前节点的左边
{
node->left = _insert(node->left, key, val);
}
else //如果插入的节点大于当前节点,插入到当前节点的右边
{
node->right = _insert(node->right, key, val);
}
return node;
}
//删除数据
Node* _remove(Node* node, Key& key)
{
if (node == NULL)//如果节点指针都指向空了还没找到要删除的数据,那么就返回空
{
return NULL;
}
if (key<node->key)//如果要删除的key小于当前节点的key就进入语句,然后递归
{
node->left = _remove(node->left, key);
return node;
}
else if (key>node->key)//如果要删除的key大于当前节点的key就进入语句,然后递归
{
node->right = _remove(node->right, key);
return node;
}
else if (key == node->key)//找到了要删除的元素
{
if (node->left == NULL)//找到的这个要删除的元素的左节点为空
{
auto tmp = node->right;
delete node;
count--;
return tmp;
}
if (node->right == NULL)//找到的这个要删除的元素的右节点为空
{
auto tmp = node->left;
delete node;
count--;
return tmp;
}
//如果左右两边节点都不为空
Node* tmp = node->right;//把要删除的节点的右节点赋值给tmp
Node* tep_p = node; //把要删除的节点赋值给tep_p
while (tmp->left)//循环找出要删除的节点的右边子树的最左边节点
{
tep_p = tmp;
tmp = tmp->left;
}
//因为删除了节点后,就会让删除的这个节点的右子树的最左边节点代替这个删除了的节点,所以在这里我们可以把要删除的这个节点右子树最左边的节点拷贝给这个要删除的节点,然后删除那个右子树最左边的节点就可以了,这样写可以避免许多指针的变化问题
node->key = tmp->key;//拷贝
node->val = tmp->val;//拷贝
node->right->left = NULL;
Node* temp = node->right;
node->right = tep_p;
node->right->right = temp;
if (tmp->right != NULL)
{
tep_p->left = tmp->right;
}
else if (tmp->left != NULL)
{
tep_p->left = tmp->left;
}
else
{
tep_p->left = NULL;
}
delete tmp;
count--;
return node;
}
return node;
}
public:
//构造
BST()
{
root = NULL;
count = 0;
}
//析构函数
~BST()
{
std::queue<Node*> que;
if (root == NULL)
{
return;
}
que.push(root);
while (!que.empty())
{
auto tmp = que.front(); //获取队头
que.pop(); //队头出队
if (tmp->left)
{
que.push(tmp->left); //左子树入队
}
if (tmp->right)
{
que.push(tmp->right); //右子树入队
}
delete tmp;
}
}
//二叉搜索树的大小
int size()
{
return count;
}
//判断是否为空
bool empty()
{
return count <= 0;
}
//插入数据
void insert(Key& key, Val& val)
{
root = _insert(root, key, val);
}
//删除数据
void remove(Key& key)
{
root = _remove(root, key);
}
//获取树的最大值
Node* getMax()
{
if (root == NULL) //如果指向根节点的指针是指向空的
{
return NULL;//直接返回空
}
Node* tmp = root; //把树的根节点赋值给tmp这个临时变量
while (tmp->right) //右子树不为空就一直循环
{
tmp = tmp->right; //根据循环一直往最右边遍历
}
return tmp; //返回指向最右边节点的指针
}
//获取树的最小值
Node* getMin()
{
if (root == NULL) //如果指向根节点的指针是指向空的
{
return NULL;//直接返回空
}
Node* tmp = root; //把树的根节点赋值给tmp这个临时变量
while (tmp->left) //左子树不为空就一直循环
{
tmp = tmp->left; //根据循环一直往最左边遍历
}
return tmp; //返回指向最左边节点的指针
}
//层序遍历
void depthOut()
{
std::queue<Node*> que; //定义一个存储Node*类型队列que
que.push(root); //根节点入队
while (!que.empty())//如果队列不为空就,一直进行下列循环
{
auto tmp = que.front(); //获取队头,第一次的时候是这棵树的根节点
que.pop(); //队头出队
cout << tmp->key << ":" << tmp->val << endl;
if (tmp->left)
{
que.push(tmp->left); //左子树入队
}
if (tmp->right)
{
que.push(tmp->right); //右子树入队
}
}
}
};
************************************************************************************
//测试二叉搜索树的函数
void text()
{
BST<int,string> bst;
int cmd=-1;
int key;
string val;
cout << "请输入命令:" <<endl;
cout << "1.插入元素:" <<endl;
cout << "2.删除元素:" <<endl;
cout << "3.打印输出元素:" <<endl;
cout << "4.打印输出最大元素:" <<endl;
cout << "5.打印输出最小元素:" <<endl;
do
{
cin >> cmd;
switch(cmd)
{
case 1:
cout << "输入<key,val>:";
cin >> key >>val;
bst.insert(key,val);
break;
case 2:
cout << "输入<key>:";
cin << key;
bst.remove(key);
break;
case 3:
cout << "------------------------";
bst.depthOut();
break;
case 4:
{
auto n=bst.getMax();
if(n)
{
cout << n->key << ":" << n->val <<endl;
}
}
break;
case 5:
{
auto n=bst.getMin();
if(n)
{
cout << n->key << ":" << n->val <<endl;
}
}
break;
default:
cout << "错误,请重新输入" <<endl;
break;
}
}while(true);
}
************************************************************************************
//二叉搜索树详解
//1、首先定义了一个二叉搜索树的节点类BinNode,这个类里面有key键,val值,还有两个BinNode*类型的指针,这个两个指针分别是指向左节点,和右节点的指针。
//2、然后就是构造二叉搜索树的类了。这个类的保护区有一个取别名using Node = BinNode<Key, Val>;,有了这个取别名之后,我们定义二叉搜索树的节点就可以直接用Node*定义了,,保护区还有一个根节点Node* root;,和一个count,这个是用来存储,搜索树的节点个数的也就是树的大小,然后就是几个函数了,
//3、构造函数,当定义一个二叉搜索树的对象时BST<int,string> bst时,会让根节点的指针root指向空,count赋值为0。
//4、然后就是我们的插入函数了,这里们举个例子,按顺序插入,86 43 100 21 72 15 28 56 76 45 47这几个数,在这里我们不考虑val值,因为随便填都可以,首先根据插入函数,我们调用插入函数,然后填上key值86,进入insert函数,然后运行root = _insert(root, key, val);在_insert函数里面我们分别传入的是,指向根节点的指针root和要插入的数据key和val,首先插入的是86,先判断传进来的root是否为空,是为空的,然后count+1,然后一个Node节点,节点里面存入了数据86,返回给root,也就是让根节点指向第一个数据86,然后开始插入第二个数据43,继续调用insert函数,运行root = _insert(root, key, val);进入_insert函数,传入的是指向根节点的指针root和43 val,判断指针root是否置空,不是置空的,判断传入的要插入的这个数据43是否等于根节点的key,不等于,判断传入的这个数据43是否<根节点的key,是的,进入语句运行node->left = _insert(node->left, key, val);进入_insert函数,这次这个函数传进的参数是根节点里面的左子树指针和43 val,判断根节点的左子树指针是否指向空,是,返回new申请的一个Node节点给根节点的左子树指针,在返回一个node也就是root给root,然后插入100,和插入43基本相同只是left和right的区别,然后插入21,运行root = _insert(root, key, val);继续传入的是指向根节点的指针root 21和val,判断root是否置空,不置空,判断是否重复,不重复,判断传进来的数据21是否小于root->key,是的,进入语句,运行node->left=_insert(node->left, key, val);进入_insert函数,继续判断是否为空,是否重复,21是否小于root->left,是的继续进入node->left=_insert(node->left, key, val);再次判断,直到为空,这次置空了,然后new申请Node节点返回给root->left->left,然后回推,一路返回,最后返回root给root。其他的数据插入形式都和这差不多。