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

二叉搜索树

本文已收录至《数据结构(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.mzph.cn/news/240340.shtml

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

相关文章

蓝牙物联网与嵌入式开发如何结合?

蓝牙物联网与嵌入式开发可以紧密结合&#xff0c;以实现更高效、更智能的物联网应用。以下是一些结合的方式&#xff1a; 嵌入式开发为蓝牙设备提供硬件基础设施和控制逻辑&#xff1a;嵌入式系统可以利用微处理器和各种外设组成的系统&#xff0c;为蓝牙设备提供硬件基础设施和…

基于ERC20代币协议实现的去中心化应用平台

文章目录 内容简介设计逻辑ERC20TokenLoanPlatform 合约事件结构体状态变量函数 Remix 运行实现部署相关智能合约存款和取款贷款和还款 源码地址 内容简介 使用 solidity 实现的基于 ERC20 代币协议的借贷款去中心化应用平台(极简版)。实现存款、取款、贷款、还款以及利息计算的…

爬虫API|批量抓取电商平台商品数据,支持高并发

随着互联网的快速发展&#xff0c;电商平台如雨后春笋般涌现&#xff0c;为消费者提供了丰富的购物选择。然而&#xff0c;对于许多商家和数据分析师来说&#xff0c;如何快速、准确地获取电商平台上的商品数据成为了一个难题。为了解决这个问题&#xff0c;我们开发了一个爬虫…

ModuleNotFoundError: No module named ‘tensorflow‘

直接运行pip install tensorflow安装成功之后&#xff0c;发现版本是tensorflow2.15.0 python的版本是3.9版本 导入包&#xff1a;import tensorflow 打包xxx.exe,调用之后提示错误 ModuleNotFoundError: No module named tensorflow 最后发现特定的python的版本对应特定的t…

基础数据结构(2):栈

1.栈的定义 栈是仅限在表尾进行插入和删除的线性表&#xff0c;栈又被称为后进先出的线性表 1.1栈顶和栈底 栈是一个线性表&#xff0c;我们允许插入和删除的一端称为栈顶 栈底和栈顶相对&#xff0c;实际上栈底的元素不需要关心 1.2入栈和出栈 栈元素的插入操作叫做入栈&…

润和软件HopeStage与亚信安全云主机深度安全防护系统完成产品兼容性互认证

近日&#xff0c;江苏润和软件股份有限公司&#xff08;以下简称“润和软件”&#xff09;HopeStage 操作系统与亚信科技&#xff08;成都&#xff09;有限公司&#xff08;以下简称“亚信安全”&#xff09;云主机深度安全防护系统完成兼容性测试。 测试结果表明&#xff0c;企…

12 Vue3中使用v-if指令实现条件渲染

概述 v-if指令主要用来实现条件渲染&#xff0c;在实际项目中使用得也非常多。 v-if通常会配合v-else-if、v-else指令一起使用&#xff0c;可以达到多个条件执行一个&#xff0c;两个条件执行一个&#xff0c;满足一个条件执行等多种场景。 下面&#xff0c;我们分别演示这三…

tamarin manual总结笔记2(tamarin实例)

最初的例子 我们将从一个简单的协议示例开始&#xff0c;该协议仅由两条消息组成&#xff0c;在这里以所谓的Alice-and-Bob表示法编写: C -> S: aenc(k, pkS) C <- S: h(k)在该协议中&#xff0c;客户端C生成一个新的对称密钥k&#xff0c;用服务器S的公钥pkS (aenc代表…

2023 英特尔On技术创新大会直播 |AI科技创新的引路者

英特尔大会 前言英特尔人工智能英特尔创新技术基于英特尔架构的科学计算总结 前言 英特尔技术创新大会是一个令人激动和启发的盛会。在这次大会上&#xff0c;我有幸观看了许多令人瞩目的科技创新和前沿技术的展示。这些展示不仅展示了英特尔作为科技巨头的实力&#xff0c;更…

浏览器原理篇—渲染阻塞

渲染阻塞 1.DOM 的解析 html 文档 边加载边解析 的&#xff1b;网络进程和渲染进程之间会建立一个共享数据的管道&#xff0c;网络进程接收到数据实时传递给渲染进程&#xff0c;渲染进程的 HTML 解析器&#xff0c;它会动态接收字节流&#xff0c;并将其解析为 DOM 2.字节流…

[每周一更]-(第38期):Go常见的操作消息队列

在Go语言中&#xff0c;常见的消息队列有以下几种&#xff1a; RabbitMQ&#xff1a;RabbitMQ是一个开源的AMQP&#xff08;高级消息队列协议&#xff09;消息代理软件&#xff0c;用于支持多种编程语言&#xff0c;包括Go语言。RabbitMQ提供了可靠的消息传递机制和灵活的路由…

基于SSM的在线学习系统的设计与实现论文

基于SSM的在线学习系统的设计与实现 摘要 随着信息互联网购物的飞速发展&#xff0c;一般企业都去创建属于自己的管理系统。本文介绍了在线学习系统的开发全过程。通过分析企业对于在线学习系统的需求&#xff0c;创建了一个计算机管理在线学习系统的方案。文章介绍了在线学习…

【习题】运行Hello World工程

判断题 1. DevEco Studio是开发HarmonyOS应用的一站式集成开发环境。 正确(True)错误(False) 正确(True) 2. main_pages.json存放页面page路径配置信息。 正确(True)错误(False) 正确(True) 单选题 1. 在stage模型中&#xff0c;下列配置文件属于AppScope文件夹的是&am…

信号与线性系统翻转课堂笔记7——信号正交与傅里叶级数

信号与线性系统翻转课堂笔记7——信号正交与傅里叶级数 The Flipped Classroom7 of Signals and Linear Systems 对应教材&#xff1a;《信号与线性系统分析&#xff08;第五版&#xff09;》高等教育出版社&#xff0c;吴大正著 一、要点 &#xff08;1&#xff0c;重点&a…

安全、效率、成本:混合云数据库管理的三重挑战!

随着业务需求的不断演变&#xff0c;数据在多云平台之间流动&#xff0c;给数据库管控带来了新的层次和复杂性。这给数据库管控带来了前所未有的挑战。企业可能面临着一系列问题&#xff0c;包括安全性挑战、管理复杂性、性能与效率问题、成本控制难题、缺乏统一的管理视图以及…

php反序列化漏洞原理、利用方法、危害

文章目录 PHP反序列化漏洞1. 什么是PHP反序列化漏洞&#xff1f;2. PHP反序列化如何工作&#xff1f;3. PHP反序列化漏洞是如何利用的&#xff1f;4. PHP反序列化漏洞的危害是什么&#xff1f;5. 如何防止PHP反序列化漏洞&#xff1f;6. PHP反序列化漏洞示例常见例子利用方法PH…

elementUI CDN引入本地文件报错,刷新页面报错

报错原因&#xff1a;vue.config.js的externals 配置中有外部cdn引入配置&#xff0c;而当前场景我的element是直接下载放在本地的&#xff0c;这时就需要将配置注释或者删除 webpack 中的 externals 配置项用于指定在打包时需要排除掉的模块&#xff0c;这些模块会被视为外部依…

Rust中peekable的使用

在 Rust 中&#xff0c;从迭代器中获取&#xff08;也就是“消费”&#xff09;一个元素时&#xff0c;每次调用 next 方法都会“消费”迭代器的一个元素&#xff0c;这意味着此元素被从迭代器中移除并返回给调用者&#xff0c; 一旦一个元素被消费&#xff0c;它就不能再次从同…

maven下载jar包失败

配置国内镜像 设置国内的仓库,比如: <!--阿里仓库--><mirror><id>alimaven</id><name>aliyun maven</name><url>https://maven.aliyun.com/repository/public/</url><mirrorOf>central</mirrorOf></mirror>…

医学实验室检验科LIS信息系统源码

实验室信息管理是专为医院检验科设计的一套实验室信息管理系统&#xff0c;能将实验仪器与计算机组成网络&#xff0c;使病人样品登录、实验数据存取、报告审核、打印分发&#xff0c;实验数据统计分析等繁杂的操作过程实现了智能化、自动化和规范化管理。 实验室管理系统功能介…