详细介绍:【二叉搜索树】:程序的“决策树”,排序数据的基石

news/2025/12/8 20:00:12/文章来源:https://www.cnblogs.com/yangykaifa/p/19323463

前言:终于来更新了,这几天一直在赶进度,这些底层的东西听懂加实现需要很长时间,大家在看完之后一定要自己动手,要不然白学,接下来四篇学的内容都是面试与笔试的高频考点,小伙伴们要重视起来哦。看这几篇之前,这篇“【二叉树与堆】:从“根”本说起,一起爬满数据的枝桠!”一定要学会哦!

目录

一、二叉搜索树的概念

二、二叉搜索树的实现

1. ⼆叉搜索树的插⼊

2.⼆叉搜索树的查找

3. ⼆叉搜索树的删除

三、二叉搜索树的应用模型

1. Key模型(纯关键词搜索)

2. Key/Value模型(关键词与对应值)

四、二叉搜索树的性能分析

五、二叉树进阶算法题

1. 根据二叉树创建字符串

2. 正向层序遍历

3. 反向层序遍历

4. 最近公共祖先

5. 从前序/后序与中序遍历序列构造二叉树

6. 非递归前序遍历/中序遍历

7. 非递归后序遍历

8. 将二叉搜索树转化为排序的双向链表



一、二叉搜索树的概念

二叉搜索树,也称为二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  1. 若它的左子树不为空,则左子树上所有节点的值小于等于根节点的值

  2. 若它的右子树不为空,则右子树上所有节点的值大于等于根节点的值

  3. 它的左右子树也分别为二叉搜索树

(所有节点!所有节点!不是只有它的孩子那一个结点符合大小关系!)

        简单来说,它就像一个有严格家规的家族:每个节点的左孩子都比自己小(或相等),右孩子都比自己大(或相等)。

注意点:

二叉搜索树可以支持插入相等的值,也可以不支持,具体取决于使用场景。我们后续要学习的C++ STL容器中,mapset不支持插入相等键值,而multimapmultiset支持。

二、二叉搜索树的实现

树的结构还和以前一样

template
struct BTNode
{T _key;BTNode* _left;BTNode* _right;BTNode(const T& key):_key(key), _left(nullptr), _right(nullptr){}
};

1. ⼆叉搜索树的插⼊

插入过程遵循一个简单的原则:“比大小,找位置”

  • 树为空:直接创建新节点作为根节点。

  • 树不为空:从根开始,将插入值与当前节点比较:

    • 插入值 大于 当前节点值 → 往右子树

    • 插入值 小于 当前节点值 → 往左子树

    • 插入值 等于 当前节点值(且允许重复)→ 按约定方向走(统一向左或向右)

  • 直到找到空位置,插入新节点。

	bool Insert(const K& key){if (_root == nullptr){_root = new node(key);return true;}//树为空,直接创建新节点作为根节点node* cur = _root;  //树不为空,从根开始比较node* parent = nullptr;while (cur){if (cur->_key > key){parent = cur;cur = cur->_left;}//插入值小于当前节点值 → 往左子树走else if (cur->_key < key){parent = cur;cur = cur->_right;}//插入值大于当前节点值 → 往右子树走else  return false;//这里实现不支持插入的}//直到走空停下来,插入新节点cur = new node(key);if (parent->_key > key){parent->_left = cur;}else{parent->_right = cur;}return true;}

为什么增加parent指针?

        保证在插入新节点时,能找到其父节点并正确连接。 parent->_key > key;这一步判断左右孩子,parent->_left = cur; 这一步进行链接。所以下面只要有链接需要,都要增加parent指针。

2.⼆叉搜索树的查找

查找过程与插入类似,也是“比大小,定方向”:

  • 从根开始比较:

    • 查找值 大于 当前节点值 → 往右子树

    • 查找值 小于 当前节点值 → 往左子树

    • 查找值 等于 当前节点值 → 找到目标

  • 最多查找树的高度次,如果走到空还没找到,说明该值不存在。

注意:如果支持重复值,通常要求找到中序遍历的第一个匹配值。

//不支持重复值
bool Find(const K& key)
{node* cur = _root;while (cur){if (cur->_key > key){cur = cur->_left;}else if (cur->_key < key){cur = cur->_right;}else  return true;}return false;
}

怎么查找中序遍历的第一个重复值

        中序遍历是“左根右”,找到相等的不要停,再往左树走找相等的,直到左树为空,就是中序遍历的第一个重复值。

Node* Find(const K& key) {Node* cur = _root;Node* firstFound = nullptr; // 记录第一个找到的节点while (cur){if (cur->_key < key)cur = cur->_right;else if (cur->_key > key)cur = cur->_left;else{// 找到匹配的节点,但可能不是中序的第一个// 继续往左走,尝试找到更早出现的相同值firstFound = cur; // 记录当前找到的cur = cur->_left; // 继续向左寻找}}return firstFound; // 返回找到的第一个节点(可能为nullptr)
}

3. ⼆叉搜索树的删除

        这个就有一些难度了,我认为难点只有两个,一个是删完这个结点仍然要保持规则,一个是删完之后要重新链接其附近的结点。因为我们链接时只用看该结点的左右孩子,那么我们就可以分为四类。

(1)删除叶子结点N(左右孩子都为空)

        这可太好了,直接删除就好了,不会影响其他结点。

(2)删除只有右孩子的结点N

        把N结点的⽗亲对应的孩⼦指针指向N的右孩⼦,直接删除N结点。比如N结点是其父亲的左结点,那么就把父亲的左结点指向N的右孩⼦,再删除N,由于N结点是其父亲的左结点,所以以N为根的树的所有结点都小于N的父亲,不会影响规则。

(3)删除只有左孩子的结点N

        与上面类似,将N结点的父节点的对应指针指向该节点的左孩子。

我们看这三种情况,都是先判断N结点是其父亲的什么结点,然后把N结点不空的那一个孩子赋给N结点对应其父亲的结点,要是都为空,那就随便赋呗,反正是空,最后删除就行。所以可以写成一种。

if (cur->_left == nullptr)
{if (parent == nullptr)_root = cur->_right;//防止删根else if (parent->_left == cur)parent->_left = cur->_right;elseparent->_right = cur->_right;delete cur;return true;
}
else if (cur->_right == nullptr)
{if (parent == nullptr)_root = cur->_left;else if (parent->_left == cur)parent->_left = cur->_left;elseparent->_right = cur->_left;delete cur;return true;
}
else
{ //......
}

(4)删除有两个孩子的结点N

        这时候我们直接删除就无法链接了,所以要用替换法。那用哪个结点替换后可以符合规则呢?答案是结点N的左子树的最右结点或右子树的最左结点。

        如果用其左子树的最右结点,那么就可以保证,我比N的所有左结点都大,而又因为我在N的左子树,我又比N的所有右结点都小,而且我不可能破坏我与我父亲的关系,因为我是在N的子树里的结点。如果用其右子树的最左结点,也是一样。

替换后如何链接

        以用其左子树的最右结点替换为例,我们直接把值赋给要删除的结点cur即可,cur的链接关系不用变。我们最后是要删除拿来替换的结点,而它一定是没有右结点,又返回到上面的情况了。要注意cur的左结点就没有右结点的情况,要单独考虑(比如下面删除10)

node* lmaxnp = cur;
node* lmaxn = cur->_left;
while (lmaxn->_right)
{lmaxnp = lmaxn;lmaxn = lmaxn->_right;
}
//找到左子树的最右结点
cur->_key = lmaxn->_key;
//替换
if (lmaxnp->_left == lmaxn)
lmaxnp->_left = lmaxn->_left;
else
lmaxnp->_right = lmaxn->_left;
//这么写是为了防止cur->_left就是要找的最大值
delete lmaxn;

完整代码:

template
struct BTNode
{T _key;BTNode* _left;BTNode* _right;BTNode(const T& key):_key(key), _left(nullptr), _right(nullptr){}
};
template
class BSTree
{
private:typedef BTNode node;node* _root = nullptr;void _InOrder(node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}
public:BSTree() = default;bool Insert(const K& key){if (_root == nullptr){_root = new node(key);return true;}node* cur = _root;node* parent = nullptr;while (cur){if (cur->_key > key){parent = cur;cur = cur->_left;}else if (cur->_key < key){parent = cur;cur = cur->_right;}else  return false;}cur = new node(key);if (parent->_key > key){parent->_left = cur;}else{parent->_right = cur;}return true;}bool Find(const K& key){node* cur = _root;while (cur){if (cur->_key > key){cur = cur->_left;}else if (cur->_key < key){cur = cur->_right;}else  return true;}return false;}bool Erase(const K& key){node* cur = _root;node* parent = nullptr;while (cur){if (cur->_key > key){parent = cur;cur = cur->_left;}else if (cur->_key < key){parent = cur;cur = cur->_right;}else{if (cur->_left == nullptr){if (parent == nullptr)_root = cur->_right;//防止删根else if (parent->_left == cur)parent->_left = cur->_right;elseparent->_right = cur->_right;delete cur;return true;}else if (cur->_right == nullptr){if (parent == nullptr)_root = cur->_left;else if (parent->_left == cur)parent->_left = cur->_left;elseparent->_right = cur->_left;delete cur;return true;}else{node* lmaxnp = cur;node* lmaxn = cur->_left;while (lmaxn->_right){lmaxnp = lmaxn;lmaxn = lmaxn->_right;}cur->_key = lmaxn->_key;if (lmaxnp->_left == lmaxn)lmaxnp->_left = lmaxn->_left;elselmaxnp->_right = lmaxn->_left;//这么写是为了防止cur->_left就是要找的最大值delete lmaxn;return true;}}}return 0;}//中序输出后,数据可以从小到大排列void InOrder(){_InOrder(_root);cout << endl;}
};

三、二叉搜索树的应用模型

1. Key模型(纯关键词搜索)

结构中只存储key,用于判断某个值是否存在:

  • 场景1:小区车牌识别系统 - 判断车牌是否在授权列表中

  • 场景2:单词拼写检查 - 判断单词是否在词典中

2. Key/Value模型(关键词与对应值)

每个key对应一个value,用于通过key查找对应的value:

  • 场景1:中英词典 - 通过英文单词查找中文释义

  • 场景2:停车场计费系统 - 通过车牌查找入场时间,计算停车费用

  • 场景3:单词频率统计 - 统计每个单词在文章中出现的次数

Key/Value模型的二叉搜索树实现与Key模型类似,只是在节点中增加了value成员,插入和查找时以key为依据,但可以访问和修改对应的value。

下面我实现一个Key/Value模型的二叉搜索树,这主要用到我们泛型编程的思想

(这只是开始,后面几篇容器的封装才真正考验你泛型编程的功底!)

#include 
#include 
using namespace std;
template
struct BSTNode {K _key;V _value;BSTNode* _left;BSTNode* _right;BSTNode(const K& key, const V& value): _key(key), _value(value), _left(nullptr), _right(nullptr) {}
};
//支持重复键
template
class BSTree {typedef BSTNode Node;
public: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) {parent = cur;if (cur->_key < key) {cur = cur->_right;} else if (cur->_key > key) {cur = cur->_left;} else {// 键已存在,根据需求处理:// 1. 返回false表示插入失败// 2. 更新值cur->_value = value;return true;}}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;Node* firstFound = nullptr;while (cur) {if (cur->_key < key) {cur = cur->_right;} else if (cur->_key > key) {cur = cur->_left;} else {// 找到匹配节点,记录并继续向左寻找更早的匹配firstFound = cur;cur = cur->_left;}}return firstFound;}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;elseparent->_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;elseparent->_right = cur->_left;}delete cur;} else {Node* rightMinParent = cur;Node* rightMin = cur->_right;while (rightMin->_left) {rightMinParent = rightMin;rightMin = rightMin->_left;}cur->_key = rightMin->_key;cur->_value = rightMin->_value;if (rightMinParent->_left == rightMin)rightMinParent->_left = rightMin->_right;elserightMinParent->_right = rightMin->_right;delete rightMin;}return true;}}return false;}void InOrder() {_InOrder(_root);cout << endl;}// 统计某个键出现的次数int Count(const K& key) {return _Count(_root, key);}
private:void _InOrder(Node* root) {if (root == nullptr) return;_InOrder(root->_left);cout << root->_key << ":" << root->_value << " ";_InOrder(root->_right);}//递归思想int _Count(Node* root, const K& key){if (root == nullptr) return 0;int count = 0;if (root->_key == key) {count = 1;}// 由于可能有重复键,需要搜索左右子树return count + _Count(root->_left, key) + _Count(root->_right, key);}Node* _root = nullptr;
};

四、二叉搜索树的性能分析

二叉搜索树的性能高度依赖于树的形状:

  • 最优情况:树为完全二叉树(或接近完全二叉树),高度为 log₂N,此时增删查改的时间复杂度为 O(logN)

  • 最差情况:树退化为单支树(类似链表),高度为 N,此时时间复杂度退化为 O(N)

正因为存在退化为O(N)的风险,单纯的二叉搜索树无法满足实际需求,这才催生了AVL树红黑树等平衡二叉搜索树。我们下节讲。

与二分查找的对比
有人可能会问:二分查找也有O(logN)的查找效率,为什么还需要二叉搜索树?
二分查找有两大局限:

  1. 需要数据存储在支持随机访问的结构中(如数组),且必须有序

  2. 插入和删除数据效率很低,需要移动大量元素

而二叉搜索树在保持较好查找效率的同时,也提供了高效的插入和删除能力。

五、二叉树进阶算法题

(点击名称即可跳转到对应OJ题)

下面问题统一的树的结构:

struct TreeNode
{
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode* left, TreeNode* right) : val(x), left(left), right(right) {}
};

1. 根据二叉树创建字符串

问题核心:分析加括号的时机,只有一个结点有左树无右树时,才可以不加右树的括号

关键步骤:前序递归

  • 根节点:直接输出节点值。

  • 左子树:左子树不管是否为空,必须加括号,除非两个子树全是空。

  • 右子树:右子树存在,就给右子树加括号;不存在就不加。

string tree2str(TreeNode* root)
{string res;if (root == nullptr) return res;//访问跟res += to_string(root->val);//遍历左树(当左树空,右树不空时只用于打括号)if (root->left || root->right){res += '(';res += tree2str(root->left);res += ')';}//遍历右树if (root->right){res += '(';res += tree2str(root->right);res += ')';}return res;
}

2. 正向层序遍历

        之前讲过层序遍历了,不同点是这个需要用动态二维数组存储,所以要加一个size记录每一层结点的数量,确保上一层出完队列后,它带入且仅带入了下一层的全部结点。

vector> levelOrder(TreeNode* root)
{vector> vv;queue q;if (root){q.push(root);}while (!q.empty()){size_t size = q.size();vector v;while (size--){if (q.front()->left)q.push(q.front()->left);if (q.front()->right)q.push(q.front()->right);v.push_back(q.front()->val);q.pop();}vv.push_back(v);}return vv;
}

3. 反向层序遍历

结尾把数组倒置就行

vector> levelOrder(TreeNode* root)
{vector> vv;queue q;if (root){q.push(root);}while (!q.empty()){size_t size = q.size();vector v;while (size--){if (q.front()->left)q.push(q.front()->left);if (q.front()->right)q.push(q.front()->right);v.push_back(q.front()->val);q.pop();}vv.push_back(v);}reverse(vv.begin(), vv.end());return vv;
}

4. 最近公共祖先

关键点:两个结点一定分别分布在最近公共祖先的左右两棵子树中,或者其中一个就是最近公共祖先!搞清楚这个问题其实就解决了。

方法一:依次找每个有用结点(要是都在左边,右子树就不用找了)的左右子树,直到成功在两个子树中找到。

方法二:用栈记录根节点到p、q的路径,找最后一个公共节点

// //最近公共祖先
//方法一
bool Seeknode(TreeNode* root, TreeNode* p)
{if (!root) return false;if (root == p) return true;bool a = Seeknode(root->left, p);if (a) return true;bool b = Seeknode(root->right, p);return b;
}
TreeNode* lowestCommonAncestor1(TreeNode* root, TreeNode* p, TreeNode* q)
{if (root == p || root == q){return root;}bool pInLeft, pInRight, qInLeft, qInRight;//注意题目中说一定存在pInLeft = Seeknode(root->left, p);pInRight = !pInLeft;  //减少递归的重要步骤qInLeft = Seeknode(root->left, q);qInRight = !qInLeft;  //减少递归的重要步骤if ((pInLeft && qInRight) || (qInLeft && pInRight)){return root;}else if (pInLeft && qInLeft){return lowestCommonAncestor1(root->left, p, q);}else{return lowestCommonAncestor1(root->right, p, q);}
}
//方法二
bool NodePath(TreeNode* root, TreeNode* p, stack& st)
{if (!root) return false;st.push(root);if (root == p)  return true;if (NodePath(root->left, p, st))  return true;if (NodePath(root->right, p, st))  return true;st.pop();return false;
}
//递归找路径(难点)
TreeNode* GetIntersection(stack st1, stack st2)
{if (st1.size() > st2.size()){while (st1.size() != st2.size()){st1.pop();}}else if (st1.size() < st2.size()){while (st1.size() != st2.size()){st2.pop();}}while (st1.top() != st2.top()){st1.pop();st2.pop();}return st1.top();
}
TreeNode* lowestCommonAncestor2(TreeNode* root, TreeNode* p, TreeNode* q)
{stack st1;stack st2;NodePath(root, p, st1);NodePath(root, q, st2);TreeNode* res = GetIntersection(st1, st2);return res;
}

5. 从前序/后序与中序遍历序列构造二叉树

核心思想利用前序确定根节点,利用中序划分左右子树,递归构建

关键步骤

  • 确定根节点:前序遍历的第一个元素就是当前子树的根

  • 定位中序位置:在中序遍历中找到根节点位置,左边是左子树,右边是右子树

  • 递归构建

    • 左子树:前序下一元素 + 中序左半部分

    • 右子树:前序后续元素 + 中序右半部分

//从前序与中序遍历序列构造二叉树
TreeNode* _buildTree1
(vector& preorder, vector& inorder, int& prei, int inbegin, int inend)
{if (inbegin > inend)  return nullptr;TreeNode* root = new TreeNode;int temp = preorder[prei];prei++;root->val = temp;int rootIndex;for (rootIndex = inbegin; rootIndex <= inend; rootIndex++){if (temp == inorder[rootIndex])  break;}root->left = _buildTree1(preorder, inorder, prei, inbegin, rootIndex - 1);root->right = _buildTree1(preorder, inorder, prei, rootIndex + 1, inend);return root;
}
TreeNode* buildTree1(vector& preorder, vector& inorder)
{int i = 0;TreeNode * res = _buildTree1(preorder, inorder, i, 0, inorder.size() - 1);return res;
}

        后序也是一样,左右根,就是倒着来,先确立根,在确定右子树,然后左子树。

6. 非递归前序遍历/中序遍历

        其实就是用循环和栈模拟递归,因为递归不就是不断地压栈再出栈吗?
        把结点存在栈中, 方便类似递归回退时取父路径结点。跟这里不同的是,这里把一棵二叉树分为两个部分:先访问左路结点,再访问左路结点的右子树。
        这里访问右子树要以循环,从栈依次取出这些结点,循环子问题的思想访问左路结点的右子树。

//非递归前序遍历
vector preorderTraversal(TreeNode* root)
{vector v;stack st;if (!root) return v;TreeNode* cur = root;while (cur || !st.empty()){//访问左路结点,左路结点入栈while (cur){v.push_back(cur->val);st.push(cur);cur = cur->left;}if (!st.empty()){//从栈中依次访问左路结点的右子树cur = st.top();st.pop();//循环子问题方式访问左路结点的右子树cur = cur->right;}}return v;
}
//非递归中序遍历
vector inorderTraversal(TreeNode* root)
{vector v;stack st;if (!root) return v;TreeNode* cur = root;while (cur || !st.empty()){while (cur){st.push(cur);cur = cur->left;}if (!st.empty()){cur = st.top();st.pop();v.push_back(cur->val);//只是访问时机发生变化cur = cur->right;}}return v;
}

7. 非递归后序遍历

      可以按上面的思路做,但是有一个难点是,由于后序遍历的顺序是左子树 → 右子树 → 根,所以当取到左路节点的右子树时,我们不知道它是否访问过了。例如下图:

        我们栈顶是2时,有可能是访问6之后,回到2,发现还有右子树继续访问7;也可能是访问7之后回到2来访问根。所以我们需要做标记,我认为这个方法操作难度其实挺大的,我先放这种解法,之后我会再讲一种简单的。

vector postorderTraversal(TreeNode* root) {TreeNode* cur = root;stack s;vector v;TreeNode* prev = nullptr;while (cur || !s.empty()){//访问一颗树的开始while (cur){s.push(cur);cur = cur->left;}TreeNode* top = s.top();// top结点的右为空 或者 上一个访问结点等于他的右孩子// 那么说明(空)不用访问 或者 (不为空)右子树已经访问过了// 那么说明当前结点左右子树都访问过了,可以访问当前结点了if (top->right == nullptr || top->right == prev){s.pop();v.push_back(top->val);prev = top;}else{// 右子树不为空,且没有访问,循环子问题方式右子树cur = top->right;}}return v;
}

方法二:

出现以上问题的本质是我的根在最后访问,而我要访问左右子树又必须经过根。那如果我倒着来,变成右左根,再把数组倒置,不是也可以吗?如果我能保证我每棵子树都可以先访问根,再将他的左子树压入栈,之后再将右子树压入栈(后进先出),那就保证了右左根。所以代码如下:

//非递归后序遍历
vector postorderTraversal(TreeNode* root)
{//根右左vector v;stack st;if (!root) return v;st.push(root);while (!st.empty()){TreeNode* temp = st.top();st.pop();v.push_back(temp->val);if (temp->left) st.push(temp->left);if (temp->right) st.push(temp->right);//它的右子树在栈顶,而每次离开时又会带入它的左右子树,//所以当它的右子树没有走完时,它不可能走到左树}reverse(v.begin(), v.end());return v;
}

8. 将二叉搜索树转化为排序的双向链表

中序遍历二叉搜索树时,遍历顺序本身就是有序的。

思路:

  • 使用两个指针:cur表示当前中序遍历到的节点,prev表示上一个中序遍历的节点

  • 当遍历到cur节点时,将cur->left指向prev,建立指向前驱的链接

  • 此时cur->right无法指向中序下一个节点,因为还不知道下一个节点是谁

  • 但是可以通过prev->right指向cur,为上一个节点建立指向后继的链接

即:

  • 每个节点的左指针在遍历到该节点时修改,指向前驱节点

  • 每个节点的右指针在遍历到下一个节点时,通过前一个节点的prev->right = cur来修改,指向后继节点

//将二叉搜索树转化为排序的双向链表
void InOrderConvert(Node* cur, Node*& prev)
{if (cur == nullptr)return;InOrderConvert(cur->left, prev);cur->left = prev;if (prev)  prev->right = cur;prev = cur;InOrderConvert(cur->right, prev);
}
Node* treeToDoublyList(Node* root)
{if (root == nullptr)return nullptr;Node* prev = nullptr;InOrderConvert(root, prev);Node* head = root;while (head->left){head = head->left;}head->left = prev;prev->right = head;return head;
}



后记:这几道算法题是非常重要的,大家一定要重点掌握。后面学习AVL与红黑树会难一些,但也是面试的高频考点,如果有帮助大家可以点个红心支持一下。

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

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

相关文章

thinkphp6 request /i /s等转换

在ThinkPHP框架中,`$this->request->post(type/s, )`里的`/s`是一个参数过滤和类型转换的标识。 具体含义:- `/s` 表示将获取到的参数转换为字符串类型(string)。 如果不加`/s`,默认情况下ThinkPHP会根据参…

An Explainable KG-RAG-Based Approach to Evidence-Based Fake News Detection Using LLMs

方法主张类型分类:采用微调后的 DeBERTaV3-large 模型,识别输入主张的类型,具体包括 “立场声明”“事件 / 属性主张”“因果主张”“数值主张”“引文验证” 五类。示例检索:运用 BM25 算法,从训练集中检索与输入…

10 种低情商行为

10 种低情商行为🔍 10 种低情商行为打断别人说话→ 不尊重他人表达权永远抬杠→ 把交流变辩论赛,消耗他人耐心过度分享负能量→ 情绪倾倒,透支他人善意玩笑开过界→ 用他人痛点博笑点,制造尴尬 当众揭短 → 公开他…

科学破解“睡眠信用卡”困局:狄耐克脑电波交互技术,重塑健康作息新模式

深夜加班的屏幕蓝光尚未熄灭,周末补觉的闹钟却已响起——这种“先透支、后偿还”的“睡眠信用卡”模式,正成为当代都市人群的普遍生活常态。 然而,越来越多医学研究证实,碎片化、补偿式的睡眠不仅无法修复身体机能…

2025 最新免费降 AI 率网站测评!13 款中英文工具实测,哪个最好用?

2025 最新免费降 AI 率网站测评!13 款中英文工具实测,哪个最好用?随着 AI 写作工具的普及,学术论文、公众号文案等内容的 AI 生成检测也愈发严格,降 AI 率工具成为刚需!为了帮大家避开踩坑,实测了 13 款支持中文…

Google 开源项目风格指南

每个较大的开源项目都有自己的风格指南:关于如何为该项目编写代码的一系列约定(有时候会比较武断)。当所有代码均保持一致的风格,在理解大型代码库时更为轻松。官方网站 https://zh-google-styleguide.readthedocs…

男士洗面奶哪个牌子最好?露卡菲娅山茶花洗面奶,排行榜单热销款揭秘!

敏感肌秋冬新选择:露卡菲娅山茶花洗面奶 作为一个屏障受损三年的敏感肌,选洁面简直是在“拆盲盒”——皂基类洗后紧绷脱皮,普通氨基酸又总洗不干净闷闭口,直到遇见露卡菲娅山茶花洗面奶,才终于找到肌肤的“安全区…

软件构造大作业:儿童故事管理平台的开发

课程实验及大作业主题:儿童故事管理平台的开发 最终目标: 构建一个集故事生成、插图创作、语音合成于一体的儿童故事管理平台。用户可以提供关键词,平台自动生成一个图文并茂、并配有朗读音频的完整故事。 核心要求…

flex布局精进: align-items: stretch;属性

问题1:多个盒子排列时,不同盒子内容不同,导致高度不同 此时如果想让所有盒子的高度/宽度一致: display: flex; align-items: stretch; 含义,flex布局下,卡片高度、宽度拉伸,使其与高度(宽度)最大的孩子保持一致…

v-if

v-else和v-else-if 1.作用:辅助v-if进行判断渲染 2.语法: v-else: v-else后面不需要加任何语句 v-else-if: v-else-if="表达式"注意事项: 1.v-if是如果的意思 条件为:true会渲染 条件为:false不渲染 …

英语_阅读_Paddling a dragon boat_待读

Paddling a dragon boat is no piece of cake. 划龙舟可不是件容易的事。 Besides the key time required to be part of a team, its a full-body exercise. 除了需要投入大量时间成为团队一员外,这也是一项全身性的…

langchain4j 学习系列(6)-结构化输出(参数提取)

继续学习langchain4j,玩过dify的朋友想必对"参数提取器"这个节点很熟悉,示例: 参数提取器可以很方便的从“非结构的自然语言”中,提取出结构化的结果。下面来看看langchain4j如何实现类似功能:public s…

小型功率三极管S9013、SOT-23介绍

S9013简介 S9013三极管是一种NPN型三极管,以硅为主要材料,属于小型的功率三极管,这种三极管很常见,有插件TO-92、贴片SOT-23两种封装,它有三个引脚,分别是基极b,集电极C,发射极e,对于插件的S9013,字体面向自…

Python 简单基础教程

Python 是一门简洁、易上手的编程语言,广泛应用于数据分析、Web 开发、人工智能、自动化等领域。本教程从零基础开始,带你掌握 Python 核心基础。 一、环境准备 1. 安装 Python安装注意:Windows:勾选「Add Python …

屏幕上那一行刺眼的红色 `Time Limit Exceeded`,是不是你我再熟悉不过的场景?

本文提供了一种与AI协作进行算法优化的新思路。通过一个结构化的AI指令模板,引导AI扮演“算法陪练”的角色,不仅能解决代码性能问题,更能帮助开发者深入理解优化原理,真正提升个人能力。无论是深夜在 LeetCode 上奋…

【论术】项目复盘总结-响应式界面

工作的意义和动力本质上是终身学习者在自由市场中的创造性表达 —— 佚名 项目需求:首页需要在用户屏幕下实现自适应并尽量维持UI稿中的样式,而用户的分辨率不能保持在1920*1080,缩放比率通常在150%,且界面图片要确保…

if 的虚拟语气和省略形式

时态 从句谓语动词 主句谓语动词现在 \(\text{did/were}\) \(\text{would/coud/should/might + do}\)过去 \(\text{had done}\) \(\text{would/coud/should/might + have done}\)将来 \(\text{did/were, were to do, s…

Ubuntu 架构磁盘清理的手段

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

西电2025硕士网课——人工智能安全与伦理练习答案

西电2025年研究生网课人工智能安全与伦理(北航雨课堂) 答案参考于https://github.com/LazzyXP/AI-Security-and-Ethics-BeiHang-Univer...,修改了一部分错误的答案第一章-AI安全与伦理概述AI 解释生成系统的手段包括…

高级语程序设计第八次作业

这个作业属于哪个课程:https://edu.cnblogs.com/campus/fzu/gjyycx/ 这个作业要求在哪里:https://edu.cnblogs.com/campus/fzu/gjyycx/homework/15590 学号:102500302 姓名:陈婧妍 编写并运行书本第11章11.13编程练…