【C++要笑着学】搜索二叉树 (SBTree) | K 模型 | KV 模型

news2024/10/7 5:23:10

   C++ 表情包趣味教程 👉 《C++要笑着学》

💭 写在前面半年没更 C++ 专栏了,上一次更新还是去年九月份,被朋友催更很久了hhh

本章倒回数据结构专栏去讲解搜索二叉树,主要原因是讲解 map 和 set 的特性需要二叉搜索树做铺垫,理解搜索二叉树有助于更好地理解 map 和 set 的特性。第二个原因是为了后期讲解查找效率极高的平衡搜索二叉树,随后再讲完红黑树,我们就可以模拟实现 map 和 set 了。二叉树的基础知识讲解在我的《树锯结构》专栏中有写,这里放上链接方便复习。

🔗 复习链接:【数据结构】二叉树的遍历


Ⅰ. 搜索二叉树(SearchBinaryTree)

0x00 搜索二叉树的概念

📚 概念:搜索二叉树又称为二叉排序树,它或者是一颗空树,或者是具有以下性质的二叉树:

  • 若其左子树不是空,则左子树上所有节点的值都小于根结点的值
  • 若其右子树不是空,则右子树上所有结点的值都大于根结点的值
  • 其左右子树必须都是二叉搜索树

  

至于叫它 "搜索二叉树",还是 "二叉搜索树",这个似乎也没有特别的规定,应该都是可以的。

但我更喜欢叫它 "搜索二叉树",因为这样翻译过来是 "Search Binary Tree",

 但是我并不是因为这样可以叫它 SB 树才喜欢的。

(而且这样也不文明,而且已经有 SB 树了,Size Balanced Tree)

而是因为  叫 "搜索二叉树 Search Binary Tree" 可以顺便记住它的性质:

Search Binary TreeS 也可以表示 SmallB 也可以表示 Big,SB 即左边小右边大!

(一般而言,搜索二叉树都是左边小右边大的)

 这样可以正好能对应它的性质:左子树的值 < 根 < 右子树的值

🔺 结论:任意一个子树都需要满足,左子树的值 < 根 < 右子树的值,才能构成二叉搜索树。 

0x01 搜索二叉树的优势

既然叫搜索二叉树,它肯定是用来搜索的,当满足搜索二叉树时你将可以快速地查找任意的值。

💭 举个例子: 查找 7

放到以前我们如果不用二分查找,可能会选择用暴力的方式去从头到尾遍历一遍。

但现在学了搜索二叉树,我们就可以轻松找到这个 7 了,不信你看:

STEP1:7 比 8 (根节点) 小,根据搜索二叉树的性质,它必然不会出现在右子树 (右边大) !

 所以,直接 🔒 锁定 左子树开始找:

STEP2: 7 比 3 大,根据性质它肯定不会出现在左子树 (左边小) !

 这次,直接 🔒锁定 右子树继续找: 

STEP3: 我们继续对比,7 比 6 大,所以在右边,就这么轻轻松松的找到了:

 搜索二叉树查找一个值的最坏情况,也只是查找高度次。 你就说爽不爽吧。

这就是搜索二叉树,它的搜索功能是真的名副其实的厉害!

0x02 二叉搜索树的时间复杂度:O(N)

上面的例子举得太丝滑了,会让人误以为搜索二叉树的时间复杂度是 O(logN) ……

但实际上是 O(N)  !!!

因为这棵树是有可能会 蜕化 的,极端情况下会蜕化成一个 "单边树" :

  

😢 最差情况:二叉搜索树退化为单边树(或类似单边),其平均比较次数为:

O(\frac{N}{2})\Rightarrow O(N)

 但是在好的情况下,其搜索效率也是非常可观的:

😍 最优情况:二叉搜索树为完全二叉树(或接近完全二叉树),其平均比较次数为:

O(log_2N)\Rightarrow O(logN)

  对于时间复杂度的分析我们要做一个悲观主义者,根据最差情况去定时间复杂度。

🔺 总结:搜索二叉树的时间复杂度为 O(N)

0x03 搜索二叉树的改良方案

 如果搜索二叉树蜕化成了单边树,其性能也就失去了,能否进行改进让它保持性能?

如何做到不论按照上面次序插入关键码,二叉搜索树的性能均能达到最优?

搜索二叉树由于控制不了极端情况,与 O(logN) 失之交臂,但平衡二叉搜索树做到了。

"平衡二叉树的搜索效率极高"

严格意义上来说满二叉树才是 O(logN),完全二叉树是接近 O(logN) 。

而平衡搜索二叉树维持左右两边均匀程度,让它接近完全二叉树,从而让效率趋近 O(logN)

Ⅱ. 搜索二叉树的实现

0x00 搜索二叉树的定义

 搜索二叉树,SearchBinaryTree 名称实在是又臭又长!

我们不如取名为 SBTree,但是 SBTree 听起来好像有点不文明,我们还是叫 BSTree 吧。

这里我们用模板,模板参数我们给了一个 K,表示 key 的意思(模板参数并非一定要用 T)

template<class K> 
struct BSTreeNode {
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)
		: _left(nullptr)
		, _right(nullptr)
		, _key(key) {}
};

下面我们来定义整个树,BSTreeNode 也有些长了,我们不如将其 typedef Node 

这里我们构造函数都没必要写,它自己生成的就够用了:

template<class K> 
class BSTree {
	typedef BSTreeNode<K> Node;
private:
	Node* _root = nullptr;    // 懒得写构造函数了
};

0x01 搜索二叉树的插入

 我们先来实现最简单的插入操作:

  • 如果树为空,则直接新增结点,赋值给 root 指针。
  • 如果树不为空,按二叉搜索树性质查找插入位置,插入新节点。
bool Insert(const K& key);

Insert 的实现我们可以用递归,也可以用非递归,这一块递归比非递归更难理解。

秉着先难后易的态度,我们先讲比较难理解的非递归版本!

💡 分析

Step1:首先检查是否有根结点 _root,如果没有我们就 new 一个结点出来作为根结点。
此时插入成功,返回 true

Step2:插入就需要找到插入位置,我们定义一个 cur 变量,从根节点开始,
根据搜索二叉树 "SB" 性质,将 cur 结点的值与插入的值 x 进行大小比较。

  • 如果插入的值大于当前结点值,则将 cur 结点向右移动 cur=cur->_right ;
  • 如果插入的值小于当前节点值,就将 cur 结点向左移动 cur=cur->_left

值得注意的是,我们还需要额外记录一下 cur 的父结点,因为你不知道什么时候会碰 \textrm{null} 结束。
并且当我们找到插入位置后,仅仅 new 上一个新结点给 cur 是完成不了插入操作的!
因为直接这么做 cur 也只是一个局部变量而已,你需要 cur 跟上一层(cur 的父亲)相链接才行!
为了能找到上一层,所以我们还需要额外定义一个 prev 变量来记录 cur 的父结点,
在我们更换 cur 结点时记录父结点的位置 prev=cur 即可。
当然了,还有一种插入失败的情况,就是判断大小时出现等于的情况,返回 false 即可。
(重复的值是不允许插入的,默认情况是不允许冗余的!但是也有针对这个的变形,后续再说)

Step3:插入!new 一个新结点给 cur,此时 cur 只是一个局部变量,必须要和父亲链接,
此时应该链接父亲的左边,还是链接父亲的右边?我们不知道,所以我们需要再做一个比较!

  • 如果父节点的值大于插入的值,则将 cur 链接到父亲左边 prev->_left=cur
  • 反之将 cur 链接到父亲右边  prev->_right=cur

最后,插入成功返回 true,我们的插入操作就大功告成了。

💬 代码演示:Insert 接口的实现

/* 插入 */
bool Insert(const K& x) {
    /* 检查是否由根节点 */
    if (_root == nullptr) {    // 如果根节点为空指针
        _root = new Node(x);   // 创建一个新结点作为根结点
        return true;           // 插入成功,返回真
    }

    Node* prev = nullptr;      // 用于记录cur的父亲
    Node* cur = _root;         // 从根节点开始

    /* 找到插入位置 */
    while (cur != nullptr) {
        if (x > cur->_key) {          // 如果插入的值大于当前结点值,则向右移动
            prev = cur;               // 保存父节点
            cur = cur->_right;        // 令cur右滑
        }
        else if (x < cur->_key) {     // 如果插入的值小于当前结点值,则向左移动
            prev = cur;      
            cur = cur->_left;         // 令cur左滑
        }
        else {                        // 相等的情况,禁插
            return false;             // 插入失败,返回假
        }
    }

    /* 插入位置已找到,准备进行链接操作 */
    cur = new Node(x);      // 创建一个新结点,赋给cur,此时cur为局部,需与父结点链接
    if (prev->_key > x) {   // 如果父结点的值大于插入的值,则将cur链接到左边
        prev->_left = cur;
    } 
    else {                  // 如果父节点的值小于插入的值,则将cur链接到右边
        prev->_right = cur;
    }

    return true;   // 插入成功,返回真
}

再写一个中序遍历来测试一下插入的效果:

void InOrder(Node* root) {
    if (root == nullptr) {
        return;
    }

    InOrder(root->_left);            // 左
    cout << root->_key << " ";       // 值
    InOrder(root->_right);           // 右
}

模拟出一个测试用例:

void TestBSTree() {
	BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a) {
		t.Insert(e);
	}
	t.InOrder();  ❌ 没法传根
}

此时会出现一个问题,因为根是私有的,我们没办法把根传过去。

此时我们可以选择在类内部写一个成员函数 GetRoot 去取根,但是这里我们可以选择这么做:

	void InOrder() {
		_InOrder(_root);
	}

private:
    // 改为内部函数
	void _InOrder(Node* root) {
		if (root == nullptr) {
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

	Node* _root = nullptr;
};

干脆将刚才我们实现的中序设为 private 私有,然后再写一个 InOrder 放在公有的区域。

这就是在类内访问 _root 了,没有什么问题。

如此一来我们在类外就可以直接调用 InOrder,并且也不需要传递参数了。

int main(void)
{
	TestBSTree();

	return 0;
}

🚩 运行结果:

0x02 搜索二叉树的查找

 find 实现很容易,用和刚才一样的思路,从根结点开始查找。 

从根开始,如果要查找的值大于 cur 目前的值,则让 cur 往右走,反之往左走。

当查找得值与 cur 的值相等时则说明找到了,返回 true

cur 触及到空(while 循环结束)则说明找不到,返回 false

💬 代码实现:搜索二叉树的查找

/* 查找 */
bool Find(const K& target) {
    Node* cur = _root;                    // 从根结点开始查找

    while (cur != nullptr) {      
        if (target > cur->_key) {         // 如果目标值比当前结点值大,cur↘
            cur = cur->_right;       
        }
        else if (target < cur->_key) {    // 如果目标值比当前结点值小,cur↙
            cur = cur->_left;
        }
        else {                            // 如果目标值等于结点值,说明找到了
            /* 找到了,返回真 */
            return true;
        }
    }
    /* 没找到,返回假 */
    return false;
}

0x03 搜索二叉树删除

搜索二叉树真正困难的是删除,搜索二叉树删除的实现是有很有难度的。

删除的实现就需要一些 "奇技淫巧" 了,断然删除会毁树。

没有孩子或者只有一个孩子,可以直接删除,孩子托管给父亲。

两个还是没办法给父亲,父亲养不了这么多孩子,但是可以找个人替代父亲养孩子。

当然,也不能随便找,找的人必须仍然维持搜索二叉树的性质,这是原则。

"你不能说搞得我都不是搜索二叉树了,那还玩个锤子"

必须比左边的大,比右边的小。所以在家族中找是最合适的。

找左子树的最大值结点,或者右子树的最小值结点。

首先要查找元素是否在二叉搜索树中,如果不存在,则返回。

如果存在,那么删除的结点可能分为下面四种情况:

a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左孩子结点也有右孩子结点

看起来有待删除节点有 4 种情况,但实际上 a 和 b,或 a 和 c 可以合并。

📌 因此,真正的删除过程如下:

  • 情况B:删除该结点且使被删除结点的父结点指向被删除节点的左孩子结点 —— 直接删除。
  • 情况C:删除该节点且使被删除结点的父节点指向被删除节点的右孩子结点 —— 直接删除。
  • 情况D:在它的右子树中寻找中序下的第一个结点(值最小),用它的值填补到被删除结点中,再来处理该结点的删除问题 —— 替换法删除。

① 该结点无左孩子

如果要删除下面这颗二叉树的 10 节点和 4 节点:

我们还是定义一个 cur 变量,当 cur 找到 10 结点后,如果左侧为空情况如下:

  1. 若该结点为 root,直接让 root 等于它的右孩子结点。
  2. 对于删除 10 结点:若 cur==father->right,则令 parent->right = cur->right (如图所示)
  3. 对于删除 4 结点:若 cur==father->left,则令 parent->left=cur->right (如图所示)
  4. 最后删除 cur 结点

💬 代码演示:

if (cur->_left == nullptr) {
    /* 判断要删除的结点是否为根结点 */
    if (cur == _root) {
        _root = cur->_right;
    }
    else {
        if (cur == father->_right) {
            /* 如果 cur 比 father 大 */
            father->_right = cur->_right;
        }
        else {
            father->_left = cur->_right;
        }
    }
    delete cur;
    cur = nullptr;
}

② 该结点无右孩子

 如果要删除 14 结点,删除逻辑和删除左孩子是类似的:

💬 代码演示:

else if (cur->_right == nullptr) {
    /* 判断是否为根结点 */
    if (cur == _root) {
        _root = cur->_left;
    }
    else {
        if (cur == father->_right) {
            /* cur 比父结点大 */
            father->_right = cur->_left;
        }
        else {
            father->_left = cur->_left;
        }
    }
    delete cur;
    cur = nullptr;
}

③ 该结点有左右两个孩子

如果删除的结点有左右两个孩子,我们就在它的右子树中寻找中序的第一个结点。

与右子树的最小值进行替换,当然也可以选择左子树的最大值进行替换。

💭 例子:比如下面这颗子树,我们要删除 3 结点:

如果该结点有两个孩子,则采用如下替换法:

该结点和右子树的最小值或左子树的最大值进行值的替换,然后删除替换后的结点。

这里我们采用与右子树的最小值进行替换。

💬 代码演示:非递归版本的 Erase

bool Erase(const K& key) {
    Node* father = nullptr;
    Node* cur = _root;
    while (cur != nullptr) {
        if (cur->_key < key) {
            father = cur;
            cur = cur->_right;
        }
        else if (cur->_key > key)
        {
            father = cur;
            cur = cur->_left;
        }
        else {
            /* 找到了! 情况一:该节点没有左孩子   情况二:该节点没有右孩子 */
            if (cur->_left == nullptr) {
                /* 判断是否为根结点 */
                if (cur == _root) {
                    _root = cur->_right;
                }
                else {
                    if (cur == father->_right) {
                        //cur 比 father大
                        father->_right = cur->_right;
                    }
                    else {
                        father->_left = cur->_right;
                    }
                }
                delete cur;
                cur = nullptr;
            }
            else if (cur->_right == nullptr) {
                /* 判断是否为根结点 */
                if (cur == _root) {
                    _root = cur->_left;
                }
                else {
                    if (cur == father->_right) {
                        /* 如果 cur 比父结点大 */
                        father->_right = cur->_left;
                    }
                    else {
                        father->_left = cur->_left;
                    }
                }
                delete cur;
                cur = nullptr;
            }
            else {
                /* 有两个节点,替换 */
                Node* MinParNode = cur;
                Node* MinNode = cur->_right;
                while (MinNode->_left) {
                    MinParNode = MinNode;
                    MinNode = MinNode->_left;
                }
                swap(cur->_key, MinNode->_key);

                if(MinParNode->_left == MinNode) {
                    MinParNode->_left = MinNode->_right;
                }
                else {
                    MinParNode->_right = MinNode->_right;
                }
                delete MinNode;
                MinNode = nullptr;
            }
            return true;
        }
    }
    return false;
}

💡 解读:找到 3 结点中右子树的最小结点,替换它们的值。定义 MinParNode 为 cur,MinNode 为 cur 的右节点。首先让 MinNode 指向 3 的右孩子(1),然后一直向左边找知道找到 nullptr 为止,此时 MinNode 指向的就是最小的结点了,此时让 3 与 MinNode 的值交换即可。

交换后,删除 3 就变成了删除 MinNode,我们需要弄清 MinNode  和 MinParNode 的指向关系:

  • 如果 MinParNode 的左孩子是 MinNode,则让 MinParNode 的左指向 MinNode 的右。
  • 如果 MinParNode 的右孩子是 MinNode,则让 MinParNode 的右指向 MinNode 的右。(这里让 MinParNode 指向 MinNode 的右的原因是 MinNode 已是最小结点,不可能有左孩子了)

Ⅲ. 搜索二叉树的应用

0x00 K 模型

K 模型,即只有 key 作为关键码,结构中只需存储 key 即可,关键码就是需要搜索到的值。

💭 举个例子:对于单词 word,我们需要判断该单词是否拼写正确

  • 以单词集合中的每个单词作为 key,构建一个搜索二叉树。
  • 在二叉树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

0x01 KV 模型

KV 模型,每一个关键码 key,都有与之对应的值 Value,即 <Key, Value> 的键值对。

这就像 Python 中的 dict 字典类型一样,key 和 value 对应。

这在生活中也是非常常见的,比如英汉词典就是英文与中文的对应关系,通过英文可以快读检索到对应的中文,英文单词也可以与其对应的中文构建出一种键值对:

<word, chinese>

再比如统计单词次数,统计成功后,给定的单词就课快速找到其出现的次数,单词与其出现的次数就构建出了一种键值对:

<word, count>

💬 代码演示:我们实现一个简单的英汉词典 dict,可以通过英文找到对应的中文。

具体实现方式如下:

  • <单词, 中文含义>  以键值对构造搜索二叉树,值得注意的是,搜索二叉树需要比较,键值对比较时只比较 Key。
  • 查询英文单词时,只需给出英文单词,就可以快速检索到对应的 Key
namespace KV
{
	template<class K, class V>
	struct BSTreeNode
	{
		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key;
		V _value;

		//pair<K, V> _kv;

		BSTreeNode(const K& key, const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}
	};

	template<class K, class V>
	struct BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		BSTree()
			:_root(nullptr)
		{}

		bool Insert(const K& key, const V& value)
		{
			if (_root == nullptr)
			{
				_root = new Node(key, value);
				return true;
			}

			Node* parent = nullptr;
			Node* cur = _root;

			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}

			cur = new Node(key, value);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}

			return true;
		}

		Node* Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else
				{
					return cur;
				}
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			Node* parent = nullptr;
			Node* cur = _root;

			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					// 找到,准备开始删除
					if (cur->_left == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
								parent->_left = cur->_right;
							else
								parent->_right = cur->_right;
						}

						delete cur;
					}
					else if (cur->_right == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
								parent->_left = cur->_left;
							else
								parent->_right = cur->_left;
						}

						delete cur;
					}
					else
					{
						Node* minParent = cur;
						Node* min = cur->_right;
						while (min->_left)
						{
							minParent = min;
							min = min->_left;
						}

						cur->_key = min->_key;
						cur->_value = min->_value;


						if (minParent->_left == min)
							minParent->_left = min->_right;
						else
							minParent->_right = min->_right;

						delete min;
					}

					return true;
				}
			}

			return false;
		}

	

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}

	private:
		void _InOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}
	private:
		Node* _root;
	};

	void TestBSTree1()
	{
		// 字典KV模型
		BSTree<string, string> dict;
		dict.Insert("sort", "排序");
		dict.Insert("left", "左边");
		dict.Insert("right", "右边");
		dict.Insert("map", "地图、映射");
		//...

		string str;
		while (cin>>str)
		{
			BSTreeNode<string, string>* ret = dict.Find(str);
			if (ret)
			{
				cout << "对应中文解释:" << ret->_value << endl;
			}
			else
			{
				cout << "无此单词" << endl;
			}
		}
	}

	void TestBSTree2()
	{
		// 统计水果出现次数
		string arr[] = { "苹果", "西瓜","草莓", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
		BSTree<string, int> countTree;
		for (auto& str : arr)
		{
			//BSTreeNode<string, int>* ret = countTree.Find(str);
			auto ret = countTree.Find(str);
			if (ret != nullptr)
			{
				ret->_value++;
			}
			else
			{
				countTree.Insert(str, 1);
			}
		}

		countTree.InOrder();
	}
}


📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2023.4.8
❌ [ 勘误 ]   /* 暂无 */
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]. 

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

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

相关文章

RPC框架一,RMI远程调用实例

RPC框架一&#xff0c;RMI远程调用实例 网上找了好久关于RMI调用的实例&#xff0c;大多都是本地调用的&#xff0c;远程调用的示例很少&#xff0c;所以自己整理一版。 首先 从server端开始&#xff1a; 服务端############### 具体步骤&#xff1a; 1&#xff0c;写个RM…

【从零学Python基础】Python中的条件判断与循环

文章目录条件语句语法格式缩进和代码块空语句pass循环语句while循环for循环continue与break条件语句 条件语句能够表达如果...则...否则...这样的语义&#xff0c;这即是计算机基础中的逻辑判定&#xff0c;条件语句也叫分支语句 如果 我好好学习&#xff1a;   我一定会找到…

wav2lip:Accurately Lip-syncing Videos In The Wild

飞桨AI Studio - 人工智能学习与实训社区集开放数据、开源算法、免费算力三位一体&#xff0c;为开发者提供高效学习和开发环境、高价值高奖金竞赛项目&#xff0c;支撑高校老师轻松实现AI教学&#xff0c;并助力开发者学习交流&#xff0c;加速落地AI业务场景https://aistudio…

CUDA编程基础与Triton模型部署实践

作者&#xff1a;王辉 阿里智能互联工程技术团队 近年来人工智能发展迅速&#xff0c;模型参数量随着模型功能的增长而快速增加&#xff0c;对模型推理的计算性能提出了更高的要求&#xff0c;GPU作为一种可以执行高度并行任务的处理器&#xff0c;非常适用于神经网络的推理计算…

电脑有自带的录屏功能吗?电脑录屏如何录人脸

案例&#xff1a;所有电脑都有自带的录屏功能吗&#xff1f; “在网上了解到电脑有录屏功能&#xff0c;但是我在我的电脑上又找不到。想问问小伙伴们是所有的电脑都有自带的录屏功能吗&#xff1f;怎样才能找到电脑自带的录屏功能&#xff1f;” 在日常使用电脑时&#xff0…

在 Visual Studio 中设置指针星号的位置

作为一个完美主义者&#xff0c;如果写出来的代码&#xff0c;让自己感觉到不那么舒服&#xff0c;你需要好好研究研究&#xff0c;如何解决这个问题。 在写代码的过程中&#xff0c;我碰到了这样的一个小问题。 一直以来&#xff0c;我对指针的星号的位置比较敏感&#xff0…

为什么软件架构重要?

作者&#xff1a;[美]伦巴斯等第2章为什么软件架构重要如果架构是答案&#xff0c;那么问题是什么&#xff1f;本章主要从技术角度讨论为什么架构重要。我们将研究13个重要原因。你可以利用它们来推动新架构的创建&#xff0c;或者对已有系统架构进行分析和优化。1)架构可以抑制…

守正创新 聚力前行 助力量化行业高质量发展 | 峰会资料文末获取

4月1日下午&#xff0c;ACLUB 2023专题峰会在上海陆家嘴圆满举行&#xff0c;近80家业内领先机构逾百人参加会议&#xff0c;其中上海地区优秀量化私募管理人占比七成。 本届峰会主题为“守正创新 聚力前行——助力量化行业高质量发展”。监管机构、券商、行业专家、三方机构、…

耳朵总是听到嗡嗡的声音 这是为什么 该怎么办

为什么会莫名听到嗡嗡的声音&#xff0c;这是什么因素导致的&#xff0c;吃什么药能缓解&#xff1f; 耳鸣&#xff0c;是一种缺乏外部声源情况下&#xff0c;耳内或颅内出现的嗡嗡、嘶鸣、车笛、喇叭等不成形的异常声幻觉。这种情况可能是一种声音&#xff0c;也可能是多种声音…

day25—编程题

文章目录1.第一题1.1题目1.2涉及的相关知识1.3思路1.4解题2.第二题2.1题目2.2思路2.3解题1.第一题 1.1题目 描述&#xff1a; 星际战争开展了100年之后&#xff0c;NowCoder终于破译了外星人的密码&#xff01;他们的密码是一串整数&#xff0c;通过一张表里的信息映射成最终…

在Node终端实现NewBing对话功能

目录 前言 准备工作 工作原理 功能设计 实现过程 基础概念 代理 请求 socket 控制台输入模块 配置文件 bingServer请求 bingSocket消息 子线程入口部分 主线程部分 工具函数 效果展示 写在最后 前言 ChatGPT在当下已然成为炙手可热的话题了&#xff0c;随着…

MAX14866 16通道高电压模拟开关(不需要高电压供电)

总体介绍 MAX14866 是一个16通道高电压模拟开关&#xff0c;主要用在超声应用的高压多路传输中。 每一个通道的状态可以由一个高速的SPI接口控制&#xff0c;最高时钟为30MHz 详细介绍 MAX14866 是一个单刀单掷开关&#xff0c;以下是等效电路图 MAX14866由一个带有16位串…

什么是Lambda表达式?

什么是Lambda表达式 可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式&#xff1a;它没有名称&#xff0c;但它有参数列表、函数主体、返回类型&#xff0c;可能还有一个可以抛出的异常列表。 匿名&#xff1a;它不像普通的方法那样有一个明确的名称&#xff1…

Ae:材质选项

在 Ae 中&#xff0c;一个图层开启 3D 之后&#xff0c;会多出几何选项 Geometry Options和材质选项 Material Options两个属性组。材质用于 3D 对象的表面&#xff0c;而材质选项就是这些表面的属性&#xff0c;支配着对象与光线交互的方式。展开材质选项的快捷键&#xff1a;…

数据结构入门-9-线段树字典树并查集

文章目录一、线段数Segment Tree1.1 线段树的优势1.1.2 数组实现线段树1.2 线段树结构1.2.1 创建线段树1.2.2 线段树中的区间查询1.2.3 线段树的更新二、字典树 Trie1.2 字典树结构1.2.1 创建Trie1.2.2 Trie查询三、并查集3.1 并查集的实现3.1.1 QuickFind3.1.1 QuickUnion初始…

事件触发模式 LT ET ?EPOLLIN EPOLLOUT 各种情况总结。【面试复盘】【学习笔记】

麻了&#xff0c;对 epoll 的触发机制理解不深刻…面试又被拷打了… 下面总结一下各种情况&#xff0c;并不涉及底层原理&#xff0c;底层原理看这里。 文章结构可以看左下角目录、 有什么理解的不对的&#xff0c;请大佬们指点。 先说结论&#xff0c;下面再验证&#xff…

WRF-UCM 高精度城市化气象动力模拟、WRF+WRF-UCM 模拟气象场

查看原文>>>&#xff08;WRF-UCM&#xff09;高精度城市化气象动力模拟技术与案例应用 目录 模型基础理论 模型平台从零安装讲解 城市模块在线耦合&#xff08;WRFWRF-UCM&#xff09;模拟案例讲解 WRFWRF-UCM如何模拟气象场 实际应用及案例分析 其他大气相关推…

PostgreSQL插件—数据恢复工具pg_recovery使用详解

说明 pg_recovery 是一款基于PostgreSQL的数据恢复工具。针对表做了 update/delete/rollback/dropcolumn 后的数据恢复。 版本支持 pg_revovery当前支持 PostgreSQL 12/13/14 。 安装 下载插件 墨天轮下载地址&#xff1a;https://www.modb.pro/download/434516github下载地…

吃鸡录屏怎么录到自己的声音 吃鸡录屏怎么隐藏按键

很多人在玩吃鸡游戏时喜欢将自己的游戏过程录制下来&#xff0c;特别是很多游戏主播会录制视频&#xff0c;录制后将视频分享到社交平台。但是在录制时经常会遇到很多问题&#xff0c;如声音、画面清晰度和完整性等。接下来就来分享一下吃鸡录屏怎么录到自己的声音&#xff0c;…

pytorch单机多卡训练

多卡训练的方式 以下内容来自知乎文章&#xff1a;当代研究生应当掌握的并行训练方法&#xff08;单机多卡&#xff09; pytorch上使用多卡训练&#xff0c;可以使用的方式包括&#xff1a; nn.DataParalleltorch.nn.parallel.DistributedDataParallel使用Apex加速。Apex 是 N…