代码随想录算法训练营第十八天|235.二叉搜索树的最近公共祖先,701.二叉搜索树中的插入操作,450.删除二叉搜索树节点

235.二叉搜索树的最近公共祖先

701.二叉搜索树中的插入操作

450.删除二叉搜索树节点 

 235.二叉搜索树的最近公共祖先

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉搜索树:  root = [6,2,8,0,4,7,9,null,null,3,5]

示例 1:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6 
解释: 节点 2 和节点 8 的最近公共祖先是 6。

示例 2:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。

说明:

  • 所有节点的值都是唯一的。
  • p、q 为不同节点且均存在于给定的二叉搜索树中。

 

在二叉搜索树(BST)中找到两个节点pq的最低公共祖先(LCA)。在二叉搜索树中,最低公共祖先的特点是它的值在pq的值之间(包括等于pq的情况)。如果pq的值都小于当前节点,那么它们的公共祖先位于当前节点的左子树;如果它们的值都大于当前节点,那么它们的公共祖先位于当前节点的右子树。如果pq分别位于当前节点的两侧,那么当前节点就是它们的最低公共祖先。

代码逻辑

  • 首先检查当前节点是否为NULL,如果是,则返回NULL
  • 然后,如果pq的值都小于当前节点的值,递归地在当前节点的左子树中查找。
  • 如果pq的值都大于当前节点的值,递归地在当前节点的右子树中查找。
  • 如果上述两种情况都不满足(即当前节点的值介于pq的值之间),说明找到了最低公共祖先,返回当前节点。

这个实现利用了BST的性质来优化搜索过程,避免了不必要的遍历。

C++ 

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/class Solution {private:TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) {if (cur == NULL) return cur;// 中if (cur->val > p->val && cur->val > q->val) {   // 左TreeNode* left = traversal(cur->left, p, q);if (left != NULL) {return left;}}if (cur->val < p->val && cur->val < q->val) {   // 右TreeNode* right = traversal(cur->right, p, q);if (right != NULL) {return right;}}return cur;}
public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {return traversal(root, p, q);}
};

python

class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightclass Solution:def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':# 如果当前节点为空,或者p、q之一就是当前节点,则直接返回当前节点if not root or root == p or root == q:return root# 根据BST的性质在左子树或右子树中查找if p.val < root.val and q.val < root.val:# p和q都在当前节点的左侧return self.lowestCommonAncestor(root.left, p, q)elif p.val > root.val and q.val > root.val:# p和q都在当前节点的右侧return self.lowestCommonAncestor(root.right, p, q)else:# 当前节点就是p和q的最低公共祖先return root

迭代法

在二叉搜索树(BST)中查找两个节点的最低公共祖先(LCA)时,可以使用迭代法代替递归来减少空间复杂度。迭代法的核心思想是利用BST的性质:对于树中的任意节点y,如果pq的值都小于y,则pq都在y的左子树中;如果pq的值都大于y,则pq都在y的右子树中;否则,y就是pq的最低公共祖先。

C++

#include <iostream>struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};class Solution {
public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {// 当前节点初始化为根节点TreeNode* current = root;while (current) {// 如果p和q的值都小于当前节点的值,则在左子树中查找if (p->val < current->val && q->val < current->val) {current = current->left;}// 如果p和q的值都大于当前节点的值,则在右子树中查找else if (p->val > current->val && q->val > current->val) {current = current->right;}// 否则,当前节点就是LCAelse {return current;}}return nullptr;}
};

python

class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightclass Solution:def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':# 当前节点初始化为根节点current = rootwhile current:# 如果p和q的值都小于当前节点的值,则在左子树中查找if p.val < current.val and q.val < current.val:current = current.left# 如果p和q的值都大于当前节点的值,则在右子树中查找elif p.val > current.val and q.val > current.val:current = current.right# 否则,当前节点就是LCAelse:return currentreturn None

701.二叉搜索树中的插入操作

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。

注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。

示例 1:

输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]
解释:另一个满足题目要求可以通过的树是:

示例 2:

输入:root = [40,20,60,10,30,50,70], val = 25
输出:[40,20,60,10,30,50,70,null,null,25]

示例 3:

输入:root = [4,2,7,1,3,null,null,null,null,null,null], val = 5
输出:[4,2,7,1,3,5]

提示:

  • 树中的节点数将在 [0, 104]的范围内。
  • -108 <= Node.val <= 108
  • 所有值 Node.val 是 独一无二 的。
  • -108 <= val <= 108
  • 保证 val 在原始BST中不存在。

将一个值插入到二叉搜索树(BST)中,并保持BST的性质。代码中使用了递归方法来找到合适的位置插入新节点。如果树为空,就直接创建一个新节点作为根节点。如果树不为空,递归遍历树直到找到合适的插入位置,然后根据值的大小插入到左子树或右子树。

代码逻辑

  1. 递归遍历:通过traversal函数递归遍历BST,parent变量用于记录当前节点的父节点,以便在找到插入位置时可以直接插入。
  2. 插入新节点:当到达空位置(cur == NULL)时,根据新节点的值与父节点的值的比较结果,决定是将新节点作为左子节点还是右子节点插入。
  3. 树为空的特殊处理:如果BST为空(root == NULL),则直接创建新节点作为根节点。

然而,原始C++代码实现中有一个潜在问题:parent变量的初始值被设置为一个新的TreeNode实例,这个实例并未被真正用作树的一部分,可能会导致内存泄漏或不必要的内存分配。在实际插入操作中,如果根节点非空,parent变量的初始值并未被使用。

C++

class Solution {
private:TreeNode* parent;void traversal(TreeNode* cur, int val) {if (cur == NULL) {TreeNode* node = new TreeNode(val);if (val > parent->val) parent->right = node;else parent->left = node;return;}parent = cur;if (cur->val > val) traversal(cur->left, val);if (cur->val < val) traversal(cur->right, val);return;}public:TreeNode* insertIntoBST(TreeNode* root, int val) {parent = new TreeNode(0);if (root == NULL) {root = new TreeNode(val);}traversal(root, val);return root;}
};

python

class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightclass Solution:def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:if not root:return TreeNode(val)def insert(node, val):if not node:return TreeNode(val)if val > node.val:node.right = insert(node.right, val)else:node.left = insert(node.left, val)return nodereturn insert(root, val)

针对C++实现的优化主要集中在避免使用递归来降低空间复杂度,并提高代码执行效率。通过迭代方法寻找正确的插入位置

  • 这个版本的代码使用了迭代方法来寻找新节点的正确插入位置。这种方法避免了递归调用,减少了函数调用栈的使用。
  • 通过迭代遍历树,我们检查新值应该插入到当前节点的左侧还是右侧。如果找到一个空位置(即current->leftcurrent->rightnullptr),就在那里插入新节点。
  • 这种方法保持了二叉搜索树的特性,同时提高了代码的执行效率,特别是在处理大树时。

 

C++

#include <iostream>struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};class Solution {
public:TreeNode* insertIntoBST(TreeNode* root, int val) {// 如果树为空,直接创建并返回新节点作为根节点if (root == nullptr) {return new TreeNode(val);}// 使用迭代寻找正确的插入位置TreeNode* current = root; // 当前节点while (true) {// 如果插入的值大于当前节点的值if (val > current->val) {// 如果当前节点的右子节点为空,则在此位置插入新节点if (current->right == nullptr) {current->right = new TreeNode(val);break; // 插入后退出循环} else {// 否则继续向右子树移动current = current->right;}} else {// 如果当前节点的左子节点为空,则在此位置插入新节点if (current->left == nullptr) {current->left = new TreeNode(val);break; // 插入后退出循环} else {// 否则继续向左子树移动current = current->left;}}}return root; // 返回未修改的根节点}
};

python 

class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightclass Solution:def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:# 如果根节点为空,直接返回新创建的节点if not root:return TreeNode(val)# 定位插入位置current = rootwhile current:# 如果插入的值大于当前节点的值if val > current.val:# 如果右子节点为空,插入新节点并退出循环if not current.right:current.right = TreeNode(val)break# 否则,向右子树移动else:current = current.right# 如果插入的值小于或等于当前节点的值else:# 如果左子节点为空,插入新节点并退出循环if not current.left:current.left = TreeNode(val)break# 否则,向左子树移动else:current = current.leftreturn root

450.删除二叉搜索树节点

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

  1. 首先找到需要删除的节点;
  2. 如果找到了,删除它。

示例 1:

输入:root = [5,3,6,2,4,null,7], key = 3
输出:[5,4,6,2,null,null,7]
解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
另一个正确答案是 [5,2,6,null,4,null,7]。

示例 2:

输入: root = [5,3,6,2,4,null,7], key = 0
输出: [5,3,6,2,4,null,7]
解释: 二叉树不包含值为 0 的节点

示例 3:

输入: root = [], key = 0
输出: []

实现了在二叉搜索树(BST)中删除一个具有特定键值的节点的功能,并且处理了多种情况,确保删除操作后,树仍然保持BST的性质。

功能分析

  1. 节点未找到:如果当前节点为空,则表示未找到要删除的节点,直接返回nullptr
  2. 找到节点:如果找到了要删除的节点(root->val == key),根据其子节点的情况有以下几种处理方式:
    • 叶子节点:如果节点是叶子节点(没有子节点),则直接删除该节点,并返回nullptr
    • 单子节点:如果节点只有一个子节点(左子节点或右子节点),则删除该节点,并用其子节点替代它,返回替代后的子节点。
    • 双子节点:如果节点有两个子节点,则找到右子树中最左侧的节点(右子树中的最小节点),将要删除节点的左子树接到这个最左侧节点的左侧,然后删除节点,并用其右子节点替代它。
  3. 递归删除:如果要删除的节点值大于当前节点值,递归删除左子树中的节点;如果要删除的节点值小于当前节点值,递归删除右子树中的节点。

C++

class Solution {
public:TreeNode* deleteNode(TreeNode* root, int key) {if (root == nullptr) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了if (root->val == key) {// 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点if (root->left == nullptr && root->right == nullptr) {///! 内存释放delete root;return nullptr;}// 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点else if (root->left == nullptr) {auto retNode = root->right;///! 内存释放delete root;return retNode;}// 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点else if (root->right == nullptr) {auto retNode = root->left;///! 内存释放delete root;return retNode;}// 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置// 并返回删除节点右孩子为新的根节点。else {TreeNode* cur = root->right; // 找右子树最左面的节点while(cur->left != nullptr) {cur = cur->left;}cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置TreeNode* tmp = root;   // 把root节点保存一下,下面来删除root = root->right;     // 返回旧root的右孩子作为新rootdelete tmp;             // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧)return root;}}if (root->val > key) root->left = deleteNode(root->left, key);if (root->val < key) root->right = deleteNode(root->right, key);return root;}
};

python

class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightclass Solution:def deleteNode(self, root: TreeNode, key: int) -> TreeNode:if not root:return None  # 没找到删除的节点if root.val == key:if not root.left and not root.right:return None  # 叶子节点,直接删除elif not root.left:return root.right  # 只有右子节点elif not root.right:return root.left  # 只有左子节点else:cur = root.rightwhile cur.left:cur = cur.left  # 找到右子树最左侧的节点cur.left = root.left  # 将要删除节点的左子树接到cur的左侧return root.right  # 删除节点,返回右子节点elif root.val > key:root.left = self.deleteNode(root.left, key)else:root.right = self.deleteNode(root.right, key)return root

优化删除节点的代码主要关注减少代码复杂度和提高执行效率。在二叉搜索树(BST)中删除节点时,特别是当目标节点有两个子节点的情况下,一个常见的优化方法是寻找目标节点右子树的最小节点(或左子树的最大节点),然后用它来替代目标节点的值,并在该子树中递归删除那个替代的节点。这种方法避免了直接操作树的结构,只需修改节点的值,简化了节点替换的逻辑。

C++

#include <iostream>struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};class Solution {
public:TreeNode* deleteNode(TreeNode* root, int key) {if (!root) return nullptr;if (root->val == key) {if (!root->left) return root->right;if (!root->right) return root->left;TreeNode* minNode = root->right;while (minNode->left) minNode = minNode->left;root->val = minNode->val;  // 替换值root->right = deleteNode(root->right, root->val);  // 删除替代节点} else if (root->val > key) {root->left = deleteNode(root->left, key);} else {root->right = deleteNode(root->right, key);}return root;}
};

 python

class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = rightclass Solution:def deleteNode(self, root: TreeNode, key: int) -> TreeNode:if not root:return Noneif root.val == key:if not root.left:  # 只有右子节点或无子节点return root.rightif not root.right:  # 只有左子节点return root.left# 有两个子节点,找右子树的最小节点替代删除节点minNode = root.rightwhile minNode.left:minNode = minNode.leftroot.val = minNode.val  # 替换值root.right = self.deleteNode(root.right, root.val)  # 删除替代节点elif root.val > key:root.left = self.deleteNode(root.left, key)else:root.right = self.deleteNode(root.right, key)return root

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

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

相关文章

基于Java+Springboot+vue体育用品销售商城平台设计和实现

基于JavaSpringbootvue体育用品销售商城平台设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写> 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领…

在 Vue 中将 DOM 导出为图片

你好&#xff0c;我是小白Coding日志&#xff0c;一个热爱技术的程序员。在这里&#xff0c;我分享自己在编程和技术世界中的学习心得和体会。希望我的文章能够给你带来一些灵感和帮助。欢迎来到我的博客&#xff0c;一起在技术的世界里探索前行吧&#xff01; 在日常的工作中&…

【 Maven 】花式玩法之多模块项目

目录 一、认识Maven多模块项目 二、maven如何定义项目的发布策略 2.1 版本管理 2.2 构建配置 2.3 部署和发布 2.4 依赖管理 2.5 发布流程 三、使用Jenkins持续集成Maven项目 四、总结 如果你有一个多模块项目&#xff0c;并且想将这些模块发布到不同的仓库或目标位置&…

在UE5中使用OverlayMaterial制作多材质效果

UE5.1中新增了OverlayMaterial&#xff0c;可以让物体套用2个材质球效果&#xff0c;如A材质球为正常材质内容&#xff0c;B材质球为菲涅尔&#xff0c;或是B材质球是法线外拓描边等&#xff0c;该功能类似Unity的多pass效果&#xff0c;方便了日常使用。 下面就讲将怎么用Ove…

开源软件的利弊

目录 开源软件 优势 免费 透明 可更改 可协作 影响力 坏处 安全隐患 良莠不齐 学习成本 持续性问题 未知风险 开源软件 开源软件是一种基于开放协作和共享的软件开发模式&#xff0c;其利弊对于软件产业和社会发展具有重要意义 优势 免费 谁能拒绝不要钱的东西…

多线程相关(1)

线程调度 线程状态&#xff1a;状态切换阻塞与唤醒阻塞唤醒 wait 与 sleep创建线程方式 线程是cpu任务调度的最小执行单位&#xff0c;每个线程拥有自己独立的程序计数器、虚拟机栈、本地方法栈。 线程状态&#xff1a; 线程状态包括&#xff1a;创建、就绪、运行、阻塞、死亡…

2024.2.20

使用多进程完成两个文件的拷贝&#xff0c;父进程拷贝前一半&#xff0c;子进程拷贝后一半&#xff0c;父进程回收子进程的资源 #include<myhead.h> int main(int argc, const char *argv[]) {char str[100]"";puts("please input str:");//从终端读…

软件实例分享,饭店餐饮会员卡管理系统怎么弄会员充值怎么记账

软件实例分享&#xff0c;饭店餐饮会员卡管理系统怎么弄会员充值怎么记账 一、前言 以下软件教程以 佳易王餐饮会员管理系统软件V16为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 1、会员可以登记电子会员卡或使用vip卡片 2、卡类型可以自由…

【LeetCode】无权图的最短路精选7题——单源、多源

目录 无权图的单源最短路问题&#xff1a; 1. 迷宫中离入口最近的出口&#xff08;中等&#xff09; 2. 最小基因变化&#xff08;中等&#xff09; 3. 单词接龙&#xff08;困难&#xff09; 4. 为高尔夫比赛砍树&#xff08;困难&#xff09; 无权图的多源最短路问题&a…

疾控中心污水采样过程中会遇到哪些困难

在疾控中心的污水采样过程中&#xff0c;可能会遇到多种困难。 首先&#xff0c;污水的成分可能非常复杂&#xff0c;包括各种细菌、病毒、寄生虫、重金属、化学物质等&#xff0c;这给采样带来了很大的挑战。其次&#xff0c;污水中的有害物质可能会对采样设备和人员造成损害…

预处理详解

目录 预定义符号介绍 ​编辑 预处理指令 #define #define 定义标识符 #define 定义宏 #define 替换规则 #define中#和##的使用 带副作用的宏参数 宏和函数的对比 命令行定义 预处理指令 #undef 预处理指令 #include 头文件被包含的方式&#xff1a; 本地文件包含 …

【Unity】【VR开发】Unity云同步功能使用心得

【背景】 有时出差,旅行等等也带着电脑,晚上想要继续编辑项目,就需要用到云同步功能。目前实践下来,发现有些内容可以同步,有些内容则是不可以同步的,总结如下。 【如何云同步一个本地项目】 UnityHub的项目面板中有两个选项卡:项目和云端项目。 鼠标挪动到想要云同步…

c++类和对象新手保姆级上手教学(中)

前言&#xff1a; 类和对象中篇&#xff0c;这里讲到的前4个默认成员函数&#xff0c;是类和对象中的重难点&#xff0c;许多资料上的讲法都非常抽象&#xff0c;难以理解&#xff0c;所以我作出这篇总结&#xff0c;分享学习经验&#xff0c;以便日后复习。 目录 6个默认成员…

土壤墒情监测站的工作原理

TH-TS600土壤墒情自动监测站是一种用于自动检测土壤墒情的仪器&#xff0c;它可以实时监测土壤的水分含量和温度&#xff0c;并将数据传输到数据中心或监测中心进行分析和处理。 土壤墒情自动监测站通常由传感器、数据采集器、数据传输设备和数据处理软件等部分组成。传感器是…

Laravel02 路由基本概念和用法 给视图传递请求参数

Laravel02 路由基本概念和用法 1. 路由的基本概念2. 给视图传递请求参数 1. 路由的基本概念 routes文件夹下的web.php是用来定义路由规则的。 自己定义一个路径 2. 给视图传递请求参数 在laravel里使用一个辅助函数request来快速获取请求参数

NX/UG二次开发—CAM—平面铣边界准确设置方法

大家在对平面铣设置边界时&#xff0c;经常遇到边界方向与自己期望的不一致&#xff0c;有些人喜欢用检查刀路是否过切来判断&#xff0c;但是对于倒角、负余量等一些情况&#xff0c;刀路本来就是过切的。对于多边界&#xff0c;可以根据选择的曲线来起点和面的方向来确定&…

Camera2 createCaptureSession源码分析

当应用调用CameraManager#openCamera获取到已打开的camera设备后&#xff0c;会调用createCaptureSession方法来完成camera stream创建和stream的相关配置。在createCaptureSession方法中&#xff0c;首先将应用的surfaces信息封装成可跨binder传递的OutputConfiguration对象&a…

ACE 中的Active Object模式

Active Object 设计模式&#xff1a; 1&#xff09; 根据对象被调用的方式&#xff0c;可以将对象分为两类: Passive Object和Active Object。Passive 和 Object和调用者在同一个线程中&#xff0c;这就是我们通常所用的函数调用。而Active Object和调用在不同的线程中&#xf…

Leo赠书活动-16期 名校毕业生教材

Leo赠书活动-16期 名校毕业生教材 ✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; 赠…

大厂的数据质量中心系统设计

日常工作中&#xff0c;数据开发上线完一个任务后并不是就可以高枕无忧&#xff0c;时常因上游链路数据异常或者自身处理逻辑的 BUG 导致产出的数据结果不可信。而问题发现可经历较长周期&#xff08;尤其离线场景&#xff09;&#xff0c;往往是业务方通过上层数据报表发现数据…