1. 二叉搜索树特点和基本操作1
- 左子树不为空的话,左子树值均小于等于根节点, 右子树不为空的话,右子树的值均大于等于根节点。
- 取等号的情况,只可能出现在根节点的一侧。
二叉搜索树的中序遍历:
- 中序遍历是先遍历左子树,再遍历右子树;
- 二叉搜索树的中序遍历结果是从小到大的升序。所以,二叉查找树又叫二叉排序树;
二叉搜索的基本操作:
节点定义:
struct TreeNode{
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
1. 二叉搜索树的查找
bool BST_search(TreeNode* node, int value) {
if (node->val == value) {
return true;
}
if (node->val < value) {
if (node->left) {
return BST_search(node->left, value);
}
else {
return false;
}
}
else {
if (node->right) {
return BST_search(node->right, value);
}
else {
return false;
}
}
}
2. 二叉搜索树的节点插入
void BST_insert(TreeNode* node, TreeNode* insert_node) {
if (insert_node->val < node->val) {
if (node->left) {
BST_insert(node->left, insert_node);
}
else {
node->left = insert_node;
}
}
else {
if (node->right) {
BST_insert(node->right, insert_node);
}
else {
node->right = insert_node;
}
}
}
代码通过传入待插入节点的指针而不是在需要插入的地方new一个节点,这样做的好处是:将二叉树的维护和内存管理分开了。健壮的代码应该尽量弱化这两件事情的耦合关系。
2 LeetCode 449. 序列化和反序列化二叉搜索树2
序列化是将数据结构或对象转换为一系列位的过程,以便它可以存储在文件或内存缓冲区中,或通过网络连接链路传输,以便稍后在同一个或另一个计算机环境中重建。(笔记:这个说明编码解码是有用的)
设计一个算法来序列化和反序列化二叉搜索树。 对序列化/反序列化算法的工作方式没有限制。 您只需确保二叉搜索树可以序列化为字符串,并且可以将该字符串反序列化为最初的二叉搜索树。
编码的字符串应尽可能紧凑。
注意:不要使用类成员/全局/静态变量来存储状态。 你的序列化和反序列化算法应该是无状态的
分析:
// 8
// / \
// 3 10
// / \ \
// 1 6 15
- 核心关键二叉搜索树,二叉搜索树可以从前序遍历的结果中再通过前序遍历的方式还原成二叉搜索树;
- 编码(序列化):对二叉搜索树的节点按照前序遍历方式转换为string;“8#3#1#6#10#15”;
- 解码节点(反序列化): 将编码后的字符串"8#3#1#6#10#15",先转换成TreeNode*节点,然后通过二叉树的前向遍历方式还原搜索二叉树1。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Codec {
public:
// Encodes a tree to a single string.
string serialize(TreeNode* root) {
string data;
BST_preorder(root, data);
return data;
}
// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
if (data.size() == 0) {
return nullptr;
}
// string -> node
// "8#3#1#6#10#15#" -> TreeNode* node
vector<TreeNode*> node_vec;
int val = 0;
for (int i=0; i<data.size(); i++) {
if (data[i] == '#') {
node_vec.push_back(new TreeNode(val));
val = 0;
}
else {
// "15" -> 15
val = val * 10 + data[i] - '0';
}
}
// node -> BST
for (int i=1; i<node_vec.size(); i++) {
BST_insert(node_vec[0], node_vec[i]);
}
return node_vec[0];
}
private:
// 12 -> "12#"
void change_int_to_string(int val, string &str_val) {
string tmp;
// 将数字12 -> "21"
while (val) {
tmp += val%10 + '0';
val = val/10;
}
// "21" -> "12"
for (int i=tmp.size()-1; i>=0; i--) {
str_val += tmp[i];
}
// "12" -> "12#"
str_val += '#';
}
void BST_preorder(TreeNode* node, string &data) {
if (!node) {
return ;
}
string str_val;
change_int_to_string(node->val, str_val);
data += str_val;
BST_preorder(node->left, data);
BST_preorder(node->right, data);
}
// 二叉搜索树的节点插入程序(二叉搜索树的构建)
void BST_insert(TreeNode* root, TreeNode* node) {
if (node->val < root->val) {
if (root->left) {
BST_insert(root->left, node);
}
else {
root->left = node;
}
}
else {
if (root->right) {
BST_insert(root->right, node);
}
else {
root->right = node;
}
}
}
};
// Your Codec object will be instantiated and called as such:
// Codec codec;
// codec.deserialize(codec.serialize(root));
3 LeetCode 315. 计算右侧小于当前元素的个数3
给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
示例:
输入:nums = [5,2,6,1]
输出:[2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
3.1 分治策略:归并排序基础上改进
本题在之前的递归小节笔记中5.2小节通过分治策略解决过:分治算法解决这个问题的核心思想是:
- 生序序列的逆序数数目都是0。 对升序序列按照升序归并排序,在向临时序列b插入元素时,右序列的指针j一直指向右序列首个数字;j的位移为0就是左序列数字对应的逆序数个数;
例如: 1, 2, 3, 4; 合并的过程中,右序列中的数字是在左序列全部插入到临时序列b中后才开始插入到临时序列b中,在此之前,指向右序列的指针j没有移动。正好左序列的数字的逆序数数目都是零; - 降序序列:[4,3,2,1] 的逆序数结果为 [3,2,1,0]。 对降序序列按照升序归并排序,在向临时序列b插入元素时,右序列的指针j一直向后移动直到指向右序列尾部元素为止。当轮到左序列中某个元素插入到临时序列b中时,指针j位移就表示右序列中有j位移个元素小于左序列中的这个元素。
- 从只有单个元素的左右序列合并,到有近一半数量的左右序列合并过程中,上述累加左序列元素对应的指针j的位移量就是题目要求的某个元素逆序数结果。
- 代码见 递归小节笔记中5.2小节通过分治策略
3.2 利用二叉搜索树的数据结构解决
利用二叉搜索树任一节点的左子树数值都小于或等于根节点,右子树都大于根节点的性质,可以实现对nums中每个数的逆序数count_small的统计。
count_small:示例中5的count_small就是2,即5的右面有两个数比自己小。也叫逆序数;
方法如下1:
- 建立二叉搜索树节点,并带有记录左子树节点数量的参数count;
- 将nums中元素逆置,然后将其转换成BSTNode节点,存入BSTNode节点池node_vec中;
- 从节点池node_vec中将节点插入到二叉搜索树中,并更新节点count值,同时也记录插入节点的count_small值;
- 每个节点的count更新策略是:如果插入节点的val小于等于当前节点,则当前节点的count自动累加1;
- 插入节点的count_small值更新策略是:插入节点大于当前节点root时,count_small = count_small + root->count + 1;
class Solution {
public:
vector<int> countSmaller(vector<int>& nums) {
// 边界情况
if (nums.size() == 0) return vector<int>();
vector<int> ans;
// BSTNode节点池
vector<BSTNode*> node_vec;
// 逆置的nums中每个数对应的count_small值
vector<int> count_smalls;
// 从后往前创建BSTNode节点
for (int i=nums.size()-1; i>=0; i--) {
node_vec.push_back(new BSTNode(nums[i]));
}
// 边界条件:第一个节点的count_small值为0
count_smalls.push_back(0);
// 将非第一个节点插入到二叉搜索树中,并计算count_small值
for (int i=1; i<node_vec.size(); i++) {
// 插入节点的count_small值
int count_small = 0;
BST_insert(node_vec[0], node_vec[i], count_small);
count_smalls.push_back(count_small);
}
// 恢复节点顺序
for (int i=count_smalls.size()-1; i>=0; i--) {
ans.push_back(count_smalls[i]);
// 将外部不再使用的数据删除
delete node_vec[i];
}
return ans;
}
private:
// 带左子树节点数量count的节点定义
struct BSTNode {
int val;
int count;
BSTNode* left;
BSTNode* right;
BSTNode(int x): val(x), left(nullptr), right(nullptr), count(0) {}
};
// 二叉搜索树的节点插入
void BST_insert(BSTNode* root, BSTNode* insert_ndoe, int &count_small) {
// 插入节点小于等于当前节点
if (insert_ndoe->val <= root->val) {
// 统计当前节点的左子树节点数目
root->count ++;
if (root->left) {
BST_insert(root->left, insert_ndoe, count_small);
}
else {
root->left = insert_ndoe;
}
}
else { // 插入节点大于当前节点,则出现题目所求的count_small
// 因为本段代码的待插入节点的顺序是逆置后的nums
count_small += root->count + 1;
if (root->right) {
BST_insert(root->right, insert_ndoe, count_small);
}
else {
root->right = insert_ndoe;
}
}
}
};
4. 小节 😃
- 二叉搜索树可以从前序遍历结果中恢复成二叉搜索树;所以,二叉搜索树具有编码和解码的功能;
- 二叉搜索树还具有左子树小于或等于其根节点,右子树大于其根节点的性质,这个性质可以用来统计逆序数(即有某数的某一侧有多少个数小于或大于某数);
- int -> string 底层实现:通过取余和除法,不断取出int数的低位上的数字,将其存入string类型的临时变量tmp中。最后逆序tmp就得到了结果。