高级数据结构 <二叉搜索树>

二叉搜索树

本文已收录至《数据结构(C/C++语言)》专栏!
作者:ARMCSKGT

CSDN


目录

  • 前言
  • 正文
    • 二叉搜索树的概念
    • 二叉搜索树的基本功能实现
      • 二叉搜索树的基本框架
      • 插入节点
      • 删除节点
      • 查找函数
      • 中序遍历函数
      • 析构函数和销毁函数(后序遍历销毁)
      • 拷贝构造和赋值重载(前序遍历创建)
      • 其他函数
    • 二叉搜索树的应用场景
      • key模型
      • key-value模型
    • 关于二叉搜索树
  • 最后


前言

前面我们学习了二叉树,但仅仅只是简单的二叉树并没有很大的用处,而本节的二叉搜索树是对二叉树的升级,其查找效率相对于简单二叉树来说有一定提升,二叉搜索树是学习AVL树和红黑树的基础,所以我们必须先了解二叉搜索树。


正文

二叉搜索树的概念


二叉搜索树(Binary search tree)也称二叉排序树或二叉查找树,是在普通二叉树基础上的升级版本,普通二叉树的利用价值不大,而二叉搜索树要求 左节点比根小,右节点比根大,二叉搜索树将数据按二分性质插入在树中,所以将数据存入 二叉搜索树 中进行查找时,理想情况下只需要花费 logN 的时间(二分思想),此时使用中序遍历可以得到一列有序序列,因此 二叉搜索树 的查找效率极高,具有一定的实际价值。

二叉搜索树名字的由来就是因为搜索(查找)速度很快!

二叉搜索树基本特点
一棵二叉树,可以为空;如果不为空则:

  • 如果左子树存在,则左子树根节点一定比根节点值要小
  • 如果右子树存在,则右子树根节点一定比根节点值要大
  • 左子树中的所有节点比根节点小,右子树中的所有节点比根节点大
  • 所有的节点值都不相同,不会出现重复值的节点
  • 所有子树都遵循这些性质

在这种性质下,使用中序遍历可以得到升序序列,如果将性质反转,即左比根大右比根小,则中序遍历可得到降序序列。

如上图的中树,中序遍历序列为:1 3 4 6 7 8 10 13 14


二叉搜索树的基本功能实现


二叉搜索树的基本框架

二叉搜索树的节点同样需要单独使用模板封装,且因为会用到比较函数,所以需要一个模板参数充当比较函数。

//节点类
template<class T>
struct TreeNode
{T _key;TreeNode<T>* _left;TreeNode<T>* _right;TreeNode():_key(T()), _left(nullptr), _right(nullptr){}TreeNode(const T& key):_key(key), _left(nullptr), _right(nullptr){}
};//默认比较函数
template<class T>
struct Compare
{bool operator()(const T& left, const T& right) { return left > right; }
};//二叉搜索树
template<class T, class Com = Compare<T>>
class BSTree
{//对节点类型 和 树类型 的重命名 方便使用using NodeType = TreeNode<T>; //相对于 typedef TreeNode<T> NodeType;using TreeType = BSTree<T, Com>; 
public:BSTree():_root(nullptr), _size(0){}
private:NodeType* _root; //根节点size_t _size;    //节点数量Com _com;        //比较函数
};

插入节点

对于插入函数,我们的目标是要找到合适的插入位置!

步骤

  • 检查root根节点,如果根节点为空则直接赋值为根节点。
  • 通过 key(插入值)参数查找最佳插入位置,如果遇到相等的,则返回false表示插入失败。
  • 在查找时记录迭代变量cur的前驱节点parent,当迭代变量为nullptr时,记录的前驱节点就是合适插入节点,插入在该前驱节点后即可。
  • 在链接插入时,比较插入值key与parent节点值的的大小,从而得知插入到左子树还是右子树,最终插入成功返回true。

代码实现(迭代版):

bool Insert(const T& key)
{if (_root == nullptr){NodeType* newnode = new NodeType(key);_root = newnode;_size = 1;return true;}NodeType* parent = _root;NodeType* cur = _root;while (cur){parent = cur;//节点值小于keyif (_com(key, cur->_key)) cur = cur->_right;//节点值大于keyelse if (_com(cur->_key, key)) cur = cur->_left;else return false;}NodeType* newnode = new NodeType(key);//比较节点值key与parent节点值的大小,插入在正确的位置if (_com(key, parent->_key)) parent->_right = newnode;else parent->_left = newnode;++_size;return true;
}

注意:parent指针不能赋值为nullptr,当只有一个根节点时,插入会发生空指针访问!
insert
当然,迭代可以实现插入,递归也可以,思想相同,但是实现上有一定差异。


关于递归版插入函数
因为有递归的存在,所以需要两个参数:一个用于查找的key和递归参数root节点地址。但是这个函数并不对外暴露,我们对外暴露的是一个key参数的函数,调用内部递归函数。
这里巧妙的是,我们传递的参数是对节点的引用,那么我们在当前递归函数中的修改,可以影响上一层的节点(父节点)。
假设当前节点为root,那么当我们递归root->left时,此时root参数变为root->left,我们修改root就是对上一层root->left修改,这样,当我们检查到root->left为nullptr时,创建新节点并构建链接关系然后返回即可完成插入新节点。
同样的,如果插入成功返回true,插入失败返回false。



代码实现(递归版):

bool RecuInsert(const T& key) //递归插入-外部调用接口
{return _RecuInsert(key, _root);
}bool _RecuInsert(const T& key, NodeType*& root) //递归插入-实际调用函数
{//发现空节点直接链接 对节点的引用会自动完成对节点的链接if (root == nullptr){NodeType* newnode = new NodeType(key);root = newnode;return true;}//递归继续查找最佳插入位置if (_com(key, root->_key)) return _RecuInsert(key, root->_right);else if (_com(root->_key, key)) return _RecuInsert(key, root->_left);return false;
} 

可以发现,递归加持节点引用帮我们省去了很多麻烦,代码也很简洁,但迭代和递归各有优劣,我们都做介绍!


删除节点

对于删除函数,与插入类似,需要先查找值为key的节点,然后分情况删除

步骤

  • 通过key值从根节点开始遍历,寻找等值节点,cur逐个遍历节点,parent记录cur的前驱节点
  • 如果根节点为nullptr或cur遍历为nullptr,则没有可删除的节点,返回false
  • 如果找到节点,则开始分情况删除,删除后返回true

这里的难点是删除时,如何保证树的序列和链接关系,分为三种情况:

  • 被删节点左右子树为空 (直接删除)
  • 被删节点左子树或右子树为空 (托孤,将自己的子节点拜托给父节点管理)
  • 被删节点左右子树都不为空 (找一个替代节点来管理)

实现代码(迭代版):

bool Erase(const T& key)
{if (_root == nullptr) return false;//删除节点NodeType* parent = nullptr;NodeType* cur = _root;//找节点while (cur){//节点值小于keyif (_com(key, cur->_key)){parent = cur;cur = cur->_right;}//节点值大于keyelse if (_com(cur->_key, key)){parent = cur;cur = cur->_left;}else //找到了 开始删除{if (cur->_right == nullptr) //删除的节点只有左子树{NodeType* DelNode = cur;//改变链接关系//如果要删除的是根节点if (cur == _root) _root = cur->_left;else //非根节点{if (parent->_left == cur) parent->_left = cur->_left;else parent->_right = cur->_left;}delete DelNode;}else if (cur->_left == nullptr) //删除的节点只有右子树{NodeType* DelNode = cur;//改变链接关系//如果要删除的是根节点if (cur == _root) _root = cur->_right;else //非根节点{if (parent->_left == cur) parent->_left = cur->_right;else parent->_right = cur->_right;}delete DelNode;}else //子节点都在{//找替代 左子树的最大节点(最右节点) 右子树的最小节点(最左节点)//去左子树中找最大节点//NodeType* maxParent = cur;//NodeType* maxLeft = cur->_left;//while (maxLeft->_right)//{//	maxParent = maxLeft;//	maxLeft = maxLeft->_right;//}//cur->_key = maxLeft->_key;接管替代节点的右孩子//if (maxParent->_left == maxLeft) maxParent->_left = maxLeft->_left;//else maxParent->_right = maxLeft->_left;//delete maxLeft;//去右子树中找最小节点NodeType* minParent = cur;NodeType* minRight = cur->_right;while (minRight->_left){minParent = minRight;minRight = minRight->_left;}cur->_key = minRight->_key;//接管替代节点的右孩子if (minParent->_left == minRight) minParent->_left = minRight->_right;else minParent->_right = minRight->_right;delete minRight;}--_size;return true;}}return false; //找不到节点
}

将代码结合下图理解,就能知道这些情况到底在干什么了。


被删节点只有左子树或右子树时:
我们只需要让被删节点的父节点托管子节点即可,即让爷爷节点接管孙子节点。
>注意:如果被删节点是根节点,还需要特殊处理,修改根节点_root的值。

被删节点左右子树都存在:
此时我们需要找一个替代节点来接管左右子树,接管节点必须保证接管后树的整体形态和性质不变。
于是我们可以选择左子树中的最大节点(maxLeft) 或 右子树中的最小节点(minRight),两个节点中的其中一个,将该节点值覆盖被删节点的值转而删除该节点即可,该替代节点一定是叶子节点,可以转换为直接删除。
因为 左子树的最大节点 小于和最接近 当前根节点 ,右子树中的最小节点大于和最接近
所以我们在删除节点前,需要寻找合适的替代节点来接管左右孩子,维护树的形态,在寻找合适节点时,需要 记录替代节点的前驱节点,在被删除后及时更新替代节点父节点的链接关系

这里我们并不是实际删除了11节点,而是采用伪删除法,替换节点值,转而删除替代节点。
这里使用伪删除法,将问题转化为删除叶子节点,省去了很多麻烦!


关于递归版删除函数
同样的,递归函数需要在内部单独实现,外部对递归函数重新封装。
我们在插入函数中使用对节点地址的引用解决了很多问题,同样的,在删除函数中,我们也使用了对节点的引用,这样可以做到 在不同的栈帧中,删除同一个节点,而非临时变量,同时递归删除还用到了一种思想:转换问题的量级。

因为是对节点的引用,所以当我们遍历到被删节点时,先记录被删除节点的地址,因为是对节点的引用,则在节点数大于1的情况下,当前函数中的root节点地址必然是对某根节点的左子树节点或右子树节点的引用,我们对其做出修改会直接影响链接关系,如果被删节点只有左子树或右子树,直接将其左子树或右子树赋值给当前函数中root即可,然后删除记录的节点,如果被删节点左右子树都存在,则同样需要找左子树最大节点或右子树最小节点作为替代节点,因为节点值交换了,所以被删节点转换成了替代节点,所以继续调用递归删除替代节点即可。

实现代码(递归版):

bool RecuErase(const T& key) //递归删除-外部接口
{return _RecuErase(key, _root);
}bool _RecuErase(const T& key, NodeType*& root) //递归删除-实际调用函数
{if (root == nullptr) return false;//节点值比key小,递归去右子树中寻找 否则去左子树中寻找if (_com(key, root->_key)) return _RecuErase(key, root->_right);else if (_com(root->_key, key)) return _RecuErase(key, root->_left);else //找到了{NodeType* delNode = root; //记录要删除的节点if (root->_left == nullptr) root = root->_right;else if (root->_right == nullptr) root = root->_left;else //两个子节点都存在{//找一个替代//找左边的最大节点NodeType* cur = root->_left;while (cur->_right) cur = cur->_right;//找右边的最小节点//NodeType* cur = root->_right;//while (cur->_left) cur = cur->_left;//将要删除的值与替代节点交换T tmp = root->_key;root->_key = cur->_key;cur->_key = tmp;return _RecuErase(key, root->_left); //转而删除子节点//return _RecuErase(key, root->_right); //转而删除子节点}delete delNode;return true;}return false;
}

关于删除需要注意的:

  • 涉及更改链接关系的操作,都需要保存父节点的信息
  • 左右子树都为空时,表示删除根节点root,此时 parent 为空,不必更改父节点链接关系,更新根节点root的信息后,删除目标节点即可,这种情况需要特殊处理。
  • 左右子树都不为空时,parent 要初始化为 cur,避免后面的野指针或空指针的问题。

删除函数细节比较多,需要结合代码多多理解!
关于搜索二叉树的删除函数,还有一道题,大家可以尝试:删除二叉搜索树中的节点


查找函数

查找函数相对比较简单,一个变量cur向下遍历即可。

步骤

  • 当cur节点值小于key时cur走向右子树,大于则走向左子树
  • 当cur遍历到值为key的节点时返回true
  • 当根节点root或cur遍历到nullptr时,表示树中不存在该节点,返回false

实现代码(迭代版):

		bool Find(const T& key){if (_root == nullptr) return false;NodeType* cur = _root;while (cur){if (_com(key, cur->_key)) cur = cur->_right;else if (_com(cur->_key, key)) cur = cur->_left;else return true;}return false;}

关于递归版查找函数
递归版查找函数也需要实现一个内部的递归函数,然后使用外部调用接口封装。
同样的,查找节点也有递归版本,其实现比较简单,当root小于key时递归遍历其右子树,大于则遍历其左子树,等于时返回true,root为nullptr时,返回false。

实现代码(递归版):

bool RecuFind(const T& key) //删除函数-外部接口
{return _RecuFind(key, _root);
}bool _RecuFind(const T& key, NodeType* root) //删除函数-实际调用函数
{if (root == nullptr) return false;if (_com(key, root->_key)) return _RecuFind(key, root->_right);else if (_com(root->_key, key)) return _RecuFind(key, root->_left);else return true;return false;
}

中序遍历函数

中序遍历函数会变遍历边打印,最终打印出的节点序列成有序。
这个函数比较简单,我们在第一次接触二叉树时就已经接触到了,但是因为我们需要递归,所有需要在内部实现一个递归函数,使用外部接口调用即可。

void MidBfd() //中序遍历-外部接口
{_MidBfd(_root);cout << endl;
}void _MidBfd(NodeType* root) //中序遍历-实际调用函数
{if (root == nullptr) return;_MidBfd(root->_left);cout << root->_key << " ";_MidBfd(root->_right);
}


乱序插入后,中序遍历打印有序。


析构函数和销毁函数(后序遍历销毁)

销毁一棵二叉树,我们需要先销毁子树再销毁根节点,那么后序遍历再合适不过了。
因为销毁函数需要后序遍历,递归销毁,所以我们需要单独封装一个带节点指针参数的递归函数来销毁树。
当析构函数在析构时调用销毁函数后置空根节点指针即可!

~BSTree() //析构函数
{Destroy(_root);_root = nullptr;
}void Destroy(NodeType* root) //后序销毁
{if (root == nullptr) return;Destroy(root->_left);Destroy(root->_right);delete root;
}

拷贝构造和赋值重载(前序遍历创建)

编译器默认的拷贝构造默认是浅拷贝,当浅拷贝根节点指针后销毁时便会出现异常。

递归拷贝函数: 所以我们必须实现一个可以拷贝一棵树且返回根节点地址的函数,这个函数我们采用前序遍历,前序遍历一棵树,每遍历一个节点就创建一个节点然后递归创建其左子树和右子树,最后返回根节点地址。

拷贝构造函数:我们只需要调用拷贝函数拷贝另一棵树然后将根节点地址赋值给本对象的_root即可(实现了拷贝构造函数就必须实现一个默认构造函数)。

赋值重载函数:我们重新赋值一棵树时需要先销毁当前对象的树,再调用拷贝函数拷贝这棵树,不过这样做显得很繁琐。我们可以将赋值重载函数参数改为传值传参,这样传值传参会调用拷贝构造拷贝一棵临时的树,然后我们调用swap将我们需要赋值树的节点地址交换,就完成了,当函数执行完成,临时变量会调用析构函数销毁树,因为我们把原来的树交换给了临时变量对象,所以临时变量会帮我们销毁而不需要我们自己销毁,这样就节省了我们的操作步骤。

实现代码:

BSTree(const TreeType& bst) //拷贝构造:_root(nullptr), _size(0)
{_root = Copy(bst._root);_size = bst._size;
}TreeType& operator=(TreeType bst) //赋值重载
{swap(bst); //我们自己实现的交换函数return *this;
}NodeType* Copy(const NodeType* root) //前序拷贝一棵树
{if (root == nullptr) return nullptr;NodeType* newnode = new NodeType(root->_key);newnode->_left = Copy(root->_left);newnode->_right = Copy(root->_right);return newnode;
}

其他函数

剩下的函数是比较简单的基础函数:

  • 获取节点数量
  • 交换函数
  • 清空节点

size_t size() { return _size; }void swap(TreeType& bst) //交换函数
{//也可以调用库中的swapNodeType* root = bst._root;bst._root = _root;_root = root;Com com = bst._com;bst._com = _com;_com = com;size_t sz = bst._size;bst._size = _size;_size = sz;
}void clear() //清空节点
{Destroy(_root);_root = nullptr;
}

二叉搜索树的应用场景


二叉搜索树凭借着极快的查找速度,有着一定的实战价值,常用的查找模型是 key查找模型key / value 查找模型 及 存储模型。


key模型

key模型其实就是我们上面实现的树,节点中只有一个值,一般适用于在集合中查找某个参数在不在

应用场景:

  • 门禁系统
  • 单词拼写检查
  • . . . . . .

//简易字典
int main()
{BSTree<string> bst;bst.Insert("中国");bst.Insert("CSDN");bst.Insert("BIT");bst.Insert("C++");bst.Insert("668");while (true){string tmp;cout << "请输入>>> ";cin >> tmp;if (bst.Find(tmp)) cout << "在词典中" << endl;else cout << "不在词典中" << endl;}return 0;
';;}


单值key的意义本身就是判断在不在,判断在不在也需要查找,二叉搜索树比较合适。


key-value模型

key-value模型需要存储两个值,其中用来对比(插入删除的依据)的是key,同时存储value (仅存储,value没用任何其他意义) 建立key-value的映射关系,这是一种典型的哈希思想。


应用场景:

  • 电话号码查询快递信息
  • 词典互译
  • . . . . . .

我们将key模型的代码微微改动就可以实现key-value模型的二叉搜索树。
这里我们简单实现一下。

//二叉搜索树KV
template<class KT, class VT, class Com = Compare<KT>>
class KVBSTree
{using NodeType = TreeNode<pair<KT, VT>>;using TreeType = KVBSTree<KT, VT, Com>;
public:KVBSTree():_root(nullptr), _size(0){}KVBSTree(const TreeType& bst):_root(nullptr), _size(0){_root = Copy(bst._root);_size = bst._size;}TreeType& operator=(TreeType bst){swap(bst); //我们自己实现的交换函数return *this;}bool Insert(const KT& key, const VT& value){if (_root == nullptr){NodeType* newnode = new NodeType({ key,value });_root = newnode;_size = 1;return true;}NodeType* parent = _root;NodeType* cur = _root;while (cur){parent = cur;//节点值小于keyif (_com(key, cur->_key.first)) cur = cur->_right;//节点值大于keyelse if (_com(cur->_key.first, key)) cur = cur->_left;else return false;}NodeType* newnode = new NodeType({ key,value });if (_com(key, parent->_key.first)) parent->_right = newnode;else parent->_left = newnode;++_size;return true;}bool Erase(const KT& key){if (_root == nullptr) return false;//删除节点NodeType* parent = nullptr;NodeType* cur = _root;//找节点while (cur){//节点值小于keyif (_com(key, cur->_key.first)){parent = cur;cur = cur->_right;}//节点值大于keyelse if (_com(cur->_key.first, key)){parent = cur;cur = cur->_left;}else //找到了 开始删除{if (cur->_right == nullptr) //删除的节点只有左子树{NodeType* DelNode = cur;//改变链接关系//如果要删除的是根节点if (cur == _root) _root = cur->_left;else //非根节点{if (parent->_left == cur) parent->_left = cur->_left;else parent->_right = cur->_left;}delete DelNode;}else if (cur->_left == nullptr) //删除的节点只有右子树{NodeType* DelNode = cur;//改变链接关系//如果要删除的是根节点if (cur == _root) _root = cur->_right;else //非根节点{if (parent->_left == cur) parent->_left = cur->_right;else parent->_right = cur->_right;}delete DelNode;}else //子节点都在{//找替代 左子树的最大节点(最右节点) 右子树的最小节点(最左节点)//去左子树中找最大节点//NodeType* maxParent = cur;//NodeType* maxLeft = cur->_left;//while (maxLeft->_right)//{//	maxParent = maxLeft;//	maxLeft = maxLeft->_right;//}//cur->_key = maxLeft->_key;接管替代节点的右孩子//if (maxParent->_left == maxLeft) maxParent->_left = maxLeft->_left;//else maxParent->_right = maxLeft->_left;//delete maxLeft;//去右子树中找最小节点NodeType* minParent = cur;NodeType* minRight = cur->_right;while (minRight->_left){minParent = minRight;minRight = minRight->_left;}cur->_key = minRight->_key;//接管替代节点的右孩子if (minParent->_left == minRight) minParent->_left = minRight->_right;else minParent->_right = minRight->_right;delete minRight;}--_size;return true;}}return false; //找不到节点}pair<pair<KT, VT>, bool> Find(const KT& key) //key-value模型 通过key找value{//这里使用pair再套一层pair,用于返回查询的结果是否有效//false表示查询返回值无效if (_root == nullptr) return { {},false };NodeType* cur = _root;while (cur){if (_com(key, cur->_key.first)) cur = cur->_right;else if (_com(cur->_key.first, key)) cur = cur->_left;else return { cur->_key,true };}return { {},false };}size_t size() { return _size; }void swap(TreeType& bst) //交换函数{//也可以调用库中的swapNodeType* root = bst._root;bst._root = _root;_root = root;Com com = bst._com;bst._com = _com;_com = com;size_t sz = bst._size;bst._size = _size;_size = sz;}void clear() //清空节点{Destroy(_root);_root = nullptr;}//中序遍历打印void MidBfd(){_MidBfd(_root);cout << endl;}~KVBSTree(){Destroy(_root);_root = nullptr;}private://前序拷贝一棵树NodeType* Copy(const NodeType* root){if (root == nullptr) return nullptr;NodeType* newnode = new NodeType(root->_key);newnode->_left = Copy(root->_left);newnode->_right = Copy(root->_right);return newnode;}//中序void _MidBfd(NodeType* root){if (root == nullptr) return;_MidBfd(root->_left);cout << root->_key.first << " : " << root->_key.second << endl;_MidBfd(root->_right);}//后序销毁void Destroy(NodeType* root){if (root == nullptr) return;Destroy(root->_left);Destroy(root->_right);delete root;}private:NodeType* _root; //根节点size_t _size;       //节点数量Com _com;        //比较函数
};

关于pair:
pair是C++自带的一个用于存储key-value的对象。

还有一个函数make_pair,传递两个参数(key / value),快速构建pair对象。


简易词典:

int main() 
{ KVBSTree<string, string> bst;bst.Insert("china", "中国");bst.Insert("fruit", "水果");bst.Insert("god", "神");bst.Insert("great", "伟大");bst.Insert("blue", "蓝色");while (true){string str;cout << "请输入>>> ";cin >> str;auto ret = bst.Find(str);if (ret.second) cout << ret.first.first << " : " << ret.first.second << endl;else cout << "词典中没有该词!" << endl;}return 0; 
}


关于二叉搜索树


本章介绍了最基本的二叉搜索树,因为其左右性质,其查找速度很快。

关于二叉搜索树的时间复杂度:最快 O(logn),最慢 O(n)

我们仔细分析可以发现,当二叉搜索树插入有序序列时,会变成链表!

当二叉搜索树的高度等于节点数,则查找速度就是O(n)
为了解决这个问题,大佬们发明了 AVL树红黑树 等,降低二叉搜索树的高度,以加速查找。

AVL树 和 红黑树 的时间复杂度近似为:O(logn)
后面我们将详细介绍!


最后

本节我们介绍了二叉搜索树,讲解了二叉搜索树的相关概念,为后面AVL树和红黑树的学习做铺垫,本节我们只是实现了最基本的代码,在AVL树和红黑树中,我们将实现更多功能,来完善我们的二叉搜索树。

本次 <二叉搜索树> 就先介绍到这里啦,希望能够尽可能帮助到大家。

如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!

本节涉及代码:二叉搜索树博客代码

🌟其他文章阅读推荐🌟
数据结构初级<二叉树>
C++ <继承>
C++ <STL容器适配器>
Linux进程间通信
Linux软硬链接和动静态库
🌹欢迎读者多多浏览多多支持!🌹

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/222907.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode 435 无重叠区间

题意理解&#xff1a; 给定一个区间的集合 intervals 要求需要移除区间&#xff0c;使剩余区间互不重叠 目标&#xff1a;最少需要移除几个区间。 解题思路&#xff1a; 采用贪心思路解题&#xff0c;什么是全局最优解&#xff0c;什么是局部最优解。 全局最优解&#xff0c;删…

【华为鸿蒙系统学习】- 如何利用鸿蒙系统进行App项目开发|自学篇

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 &#x1f4ab;个人格言:"没有罗马,那就自己创造罗马~" 目录 创建鸿蒙第一个App项目 项目创建 工程目录区 预览区 运行Hello World 基本工程目录 ws:工…

[node]Node.js 模块系统

[node]模块系统 Node.js中的模块系统模块的使用模块的导入模块的导出导出多个值导出默认值导出可传参的函数 文件查找策略从文件模块缓存中加载从原生模块加载从文件加载 Node.js中的模块系统 为了让Node.js的文件可以相互调用&#xff0c;Node.js提供了一个简单的模块系统。 …

docker容器内 获取宿主机ip

可以使用命令 --add-host jargatewayip:192.168.0.47 \ 需要注意,这里不能是 127.0.0.1 ,所以要找到服务器局域网的ip 命令示例 docker run -it \-p 80:80 \-p 443:443 \--name nginx \--network app --hostname nginx \-e TZAsia/Shanghai \--add-host jargatewayip:192.16…

modbus异常错误码说明

异常错误码说明 其中物理离散量输入和输入寄存器只能有I/O系统提供的数据类型&#xff0c;即只能是由I/O系统改变离散量输入和输入寄存器的数值&#xff0c;而上位机程序不能改变的数据类型&#xff0c;在数据读写上表现为只读&#xff0c;而内部比特或者物理线圈和内部寄存器或…

图灵日记之java奇妙历险记--数据类型与变量运算符

目录 数据类型与变量字面常量数据类型变量语法格式整型变量浮点型变量字符型变量希尔型变量类型转换自动类型转换(隐式)强制类型转换(显式) 类型提升不同数据类型的运算小于4字节数据类型的运算 字符串类型 运算符算术运算符关系运算符逻辑运算符逻辑与&&逻辑或||逻辑非…

蚂蚁集团5大开源项目获开放原子 “2023快速成长开源项目”

12月16日&#xff0c;在开放原子开源基金会主办的“2023开放原子开发者大会”上&#xff0c;蚂蚁集团主导开源的图数据库TuGraph、时序数据库CeresDB、隐私计算框架隐语SecretFlow、前端框架OpenSumi、数据域大模型开源框架DB-GPT入选“2023快速成长开源项目”。 &#xff08;图…

MySQL数据库 视图

目录 视图概述 语法 检查选项 视图的更新 视图作用 案例 视图概述 视图(View)是一种虚拟存在的表。视图中的数据并不在数据库中实际存在&#xff0c;行和列数据来自定义视图的查询中使用的表&#xff0c;并且是在使用视图时动态生成的。 通俗的讲&#xff0c;视图只保存…

React学习计划-React16--React基础(四)生命周期和diffing算法,key的作用

1. 生命周期 1. 声命周期的三个阶段&#xff08;旧&#xff09; 初始化阶段&#xff1a;由ReactDOM.render()触发—初次渲染 1. constructor() 2. componentWillMount() 3. render() 4. componentDidMount() > 常用一般在这个钩子中做一些初始化的事情&#xff0c;例如&am…

SpringBoot Elasticsearch全文搜索

文章目录 概念全文搜索相关技术Elasticsearch概念近实时索引类型文档分片(Shard)和副本(Replica) 下载启用SpringBoot整合引入依赖创建文档类创建资源库测试文件初始化数据创建控制器 问题参考 概念 全文搜索&#xff08;检索&#xff09;&#xff0c;工作原理&#xff1a;计算…

数据结构和算法-二叉排序树(定义 查找 插入 删除 时间复杂度)

文章目录 二叉排序树总览二叉排序树的定义二叉排序树的查找二叉排序树的插入二叉排序树的构造二叉排序树的删除删除的是叶子节点删除的是只有左子树或者只有右子树的节点删除的是有左子树和右子树的节点 查找效率分析查找成功查找失败 小结 二叉排序树 总览 二叉排序树的定义 …

Pooling方法总结(语音识别)

Pooling layer将变长的frame-level features转换为一个定长的向量。 1. Statistics Pooling 链接&#xff1a;http://danielpovey.com/files/2017_interspeech_embeddings.pdf The default pooling method for x-vector is statistics pooling. The statistics pooling laye…

基于单片机设计的指纹锁(读取、录入、验证指纹)

一、前言 指纹识别技术是一种常见的生物识别技术&#xff0c;利用每个人指纹的唯一性进行身份认证。相比于传统的密码锁或者钥匙锁&#xff0c;指纹锁具有更高的安全性和便利性&#xff0c;以及防止钥匙丢失或密码泄露的优势。 基于单片机设计的指纹锁项目是利用STC89C52作为…

安全、高效的MySQL DDL解决方案

MySQL作为目前应用最广泛的开源关系型数据库&#xff0c;是许多网站、应用和商业产品的主要数据存储。在生产环境&#xff0c;线上数据库常常面临着持续的、不断变化的表结构修改&#xff08;DDL&#xff09;&#xff0c;如增加、更改、删除字段和索引等等。其中一些DDL操作在M…

HarmonyOS开发:超详细了解项目的工程结构

当我们熟练的掌握了DevEco Studio之后&#xff0c;就可以创建项目进行练习了&#xff0c;和市场上大多数IDE一样&#xff0c;DevEco Studio也给我们提供了很多的实例模板&#xff0c;当然了&#xff0c;对于大多数移动端开发者而言&#xff0c;这些模板和我们的UI设计有着很大的…

RTP/RTCP/RTSP/SIP/SDP/RTMP对比

RTP&#xff08;Real-time Transport Protocol&#xff09;是一种用于实时传输音频和视频数据的协议。它位于传输层和应用层之间&#xff0c;主要负责对媒体数据进行分包、传输和定时。 RTCP&#xff08;Real-Time Control Protocol&#xff09;是 RTP 的控制协议&#xff0c;…

Android 权限申请

在Android中&#xff0c;从Android 6.0&#xff08;API级别23&#xff09;开始&#xff0c;应用在运行时需要动态申请权限。以下是一些步骤来动态申请权限&#xff1a; 在应用的清单文件&#xff08;AndroidManifest.xml&#xff09;中声明需要的权限。例如&#xff0c;如果应…

Socket.D 基于消息的响应式应用层网络协议

首先根据 Socket.D 官网 的副标题&#xff0c;Socket.D 的自我定义是&#xff1a; 基于事件和语义消息流的网络应用协议。官网定义的特点是&#xff1a; 基于事件&#xff0c;每个消息都可事件路由所谓语义&#xff0c;通过元信息进行语义描述流关联性&#xff0c;有相关的消…

【湖仓一体尝试】MYSQL和HIVE数据联合查询

爬了两天大大小小的一堆坑&#xff0c;今天把一个简单的单机环境的流程走通了&#xff0c;记录一笔。 先来个完工环境照&#xff1a; mysqlhadoophiveflinkicebergtrino 得益于IBM OPENJ9的优化&#xff0c;完全启动后的内存占用&#xff1a; 1&#xff09;执行联合查询后的…

CVE-2023-49898 Apache incubator-streampark 远程命令执行漏洞

项目介绍 Apache Flink 和 Apache Spark 被广泛用作下一代大数据流计算引擎。基于大量优秀经验结合最佳实践&#xff0c;我们将任务部署和运行时参数提取到配置文件中。这样&#xff0c;带有开箱即用连接器的易于使用的 RuntimeContext 将带来更轻松、更高效的任务开发体验。它…