听说你还不了解二叉树?赶紧进来轻松解决

news2024/12/23 5:35:43

✨个人主页: Yohifo
🎉所属专栏: 数据结构 | C语言
🎊每篇一句: 图片来源

  • Only by self-respect will you compel others to respect you.
    • 只有自尊才能迫使他人尊敬你。

道阻且长,行则将至


文章目录

  • 📘前言
  • 📘正文
    • 📖认识二叉树
    • 📖实现二叉树
      • 📃结构
      • 📃节点数
      • 📃树深度
      • 📃前、中、后序遍历
        • ✒️前序遍历
        • ✒️中序遍历
        • ✒️后序遍历
        • ✒️二叉树的销毁
    • 📖玩转二叉树
      • 📃构建树
      • 📃层序遍历
      • 📃判断是否为完全二叉树
    • 📖源码
    • 📖相关题目
  • 📘总结


📘前言

二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。简言之,二叉树是数据结构中非常重要的东西,在很多OJ试题和笔试题中,都会出现它的影子;至于高阶数据结构中的各种树,比如二叉搜索树AVL树红黑树B树等都是基于二叉树的高阶树。总之,现在把普通二叉树学好了,对以后的学习是十分有帮助的。

Tips:二叉树的学习与之前略有不同,我们不讨论普通二叉树的增删查改,因为对于普通二叉树来说,这些操作意义不大


📘正文

这是一棵很标准的满二叉树,可以一起来拜一拜(愿程序无Bug)
满二叉树

📖认识二叉树

如上图所示,这是一棵现实中的二叉树,我们看着很形象,也很容易理解 “二叉” 这个概念,不过计算机可不这样认为,在它眼中二叉树要么长这样 [1,2,3,4,5],要么长这样 1->2->3->4->5
没错,二叉树在计算机中可以有两种表示形式

  • 顺序结构
    • 即以数组的形式存储节点信息,这种结构一般用于存储完全二叉树
    • 比如之前学过的,因为数组正好符合完全二叉树连续存储的要求
  • 链式结构
    • 即以链表的形式存储节点信息,这种结构可以用于所有二叉树,本文代码结构也是以链式为主
    • 二叉树普遍都是不规则的,数组难以满足节点分散这个要求

知道结构后还需加以规则限制,二叉树的规则有

  • 空树也可以看作二叉树
  • 任何一棵二叉树,都可以分为根、左子树、右子树
  • 二叉树中不存在度大于2的节点(度即当前节点的子树数量)
  • 二叉树的子树有左右之分,顺序不可颠倒,即左边一定是左树
  • 任何一棵二叉树都只能由以下几种情况构成:
  • 构成二叉树的部分

关于二叉树的更多性质可以查看下图
二叉树的更多性质
好了,了解以上知识,就算碰到二叉树的门槛了,关于二叉树的具体实现,还得接着往下看
这是冲哥


📖实现二叉树

这部分主要是实现一些简单功能,涉及大量递归知识,系好安全带,准备出发

📃结构

前面说过,二叉树主要有两种表现形式,关于数组的已经在前一篇文章中提过了,现在来说说链式结构(链式二叉树
任何一棵二叉树都有根、左、右三部分,细化到一个节点也是如此,因此链式二叉树在结构上分为以下三部分

  1. 数据域,负责存放当前节点的元素信息
  2. 左子树(左孩子),指向左树的指针
  3. 右子树(右孩子),指向右树的指针

代码实现如下

typedef char BTDataType;	//二叉树的数据类型

typedef struct BinaryTreeNode
{
	BTDataType data;	//存储节点的元素信息,每个节点都有
	struct BinaryTreeNode* left;	//左子树(左孩子)
	struct BinaryTreeNode* right;	//右子树(右孩子)
}BTNode;

注意: 因为链式二叉树每次都需要单独申请节点,因此没有初始化函数,但每个节点都有初始化状态: 数据域置0,左右子树都指向空二叉树的销毁需要借助递归+后序遍历的方式销毁,后面会提及

📃节点数

为了方便后续功能的讲解,先在假设存在一棵二叉树,形状如下所示,代码实现时由自己手动进行申请、赋值、链接
主角之一
关于二叉树的节点数

  • 对于二叉树来说,不为空的都可以称为一个节点,如上图所示,共计5个节点,其中节点 'A ’ 为根节点(root)
  • 统计二叉树节点需要巧妙利用递归,大问题化为小问题

如何递归呢?
众所周知,递归是个技巧,代码极其简洁,但不太好懂,也不太好调试,并且存在很多问题(栈溢出、运行时间慢等),但这丝毫不妨碍我们在这里使用它,理由如下:

  • 首先我们需要统计的是整棵二叉树的节点数,已知任何一棵二叉树都可以看成 n 棵树组成的树,而每二叉棵树都有根、左、右三部分组成
  • 其中如果根不为空,那么这个节点就是有效节点,可以参与统计,为空就不参与
  • 现在我们只考虑一个节点是否合法,如果合法,那么返回1,兵分两路走向它的左右子树,继续判断
  • 观察发现二叉树肯定存在边界,比如上图,最底层的空就是边界,当我们往下递归,碰到空时,直接返回,不再往下判断
  • 整个过程符合递归要求:有终止条件+接近手段,我们可以从根节点开始往下递出,最后返回每次判断所得到的节点数就行了(要么是0,要么是1),这就是递归
  • 这个思想比较重要,后面很多函数都是走的这个思想

长话短说,借助递归+二叉树的特性,将整个二叉树走一遍,如果发现当前节点为空,就不往下走,否则会一直往下走,总体路径为 根 -> 左 -> 右,最后会回归每次判断所得的节点数,整个过程如图所示,这是一个比较长的 动图,耐心看完
这是一个动图
代码实现如下所示

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	//一级指针,不能断言,不然就无法递归

	if (!root)
		return 0;

	return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);	//根 + 左 + 右
}

注意: 除了单纯统计二叉树节点数外,还有两个变种:统计叶子节点数统计第k层节点数,其中统计叶子节点数是在原代码基础上增加一个判断,即没有左右子树的节点才是叶子节点,才能被计数返回;至于统计第k层节点,需要借助k,每下潜一层,k-1,直到 k 为1时,才计数返回。这两个变种对代码的改动不大,篇幅有限,这里就不再展开叙述(完整源码中有)

📃树深度

二叉树的深度,指根节点的左右子树深度中的较大值,假如根的左子树深度为3,右子树的深度为1,那么整棵树的深度为3,同样的,需要借助递归,步骤如下

  • 设两个变量:leftDepthrightDepth ,分别用来存储左右子树的深度
  • 左右子树的深度即左右子树的节点数,统计方式与上面函数类型
  • 得到这两个深度后,判断谁是较大的一方,返回它
  • 这也是个典型的递归,终止条件为节点为空,接近手段为向下移动

一样的通过 动图 来演示整个过程,这次的动图会省去很多文字讲解,更加注重过程
统计深度
这个的代码量也很少,无非就是比上面多了两句

//二叉树的深度
int BinaryTreeDepth(BTNode* root)
{
	if (!root)
		return 0;

	//大问题化小问题:求左右子树的最大深度

	int leftDepth = BinaryTreeDepth(root->left);
	int rightDepth = BinaryTreeDepth(root->right);

	return (leftDepth > rightDepth ? leftDepth : rightDepth) + 1;	//左右根
}

📃前、中、后序遍历

学校最喜欢考的东西,其实很简单,我们直接三剑齐发,再附带一个销毁
遍历思想:

  • 前、中、后序思想一致,无非就是出发点和结束点不一样罢了
  • 前序:根出发,最右子树结束
  • 中序:最左子树出发,最右子树结束
  • 后序:最左子树出发,根结束
  • 三种方式遍历代码量可以说是完全一致,只不过顺序不同罢了

关于这三种遍历方式,我想直接通过三张动图解决,单独将没啥意义,复读而已,还不如动图来的直观

✒️前序遍历

前序遍历
芝士代码

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (!root)
	{
		printf("NULL ");
		return;
	}

	//前序:根左右
	printf("%c ", root->data);

	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}

✒️中序遍历

中序遍历
芝士代码

// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (!root)
	{
		printf("NULL ");
		return;
	}

	//中序:左根右
	BinaryTreeInOrder(root->left);
	
	printf("%c ", root->data);

	BinaryTreeInOrder(root->right);
}

✒️后序遍历

后序遍历
芝士代码

// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (!root)
	{
		printf("NULL ");
		return;
	}

	//后序:左右根
	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);

	printf("%c ", root->data);
}

✒️二叉树的销毁

二叉树的销毁其实和后续遍历差不多,不过是把打印换成了 free 当前节点,其实也很好理解,想要销毁整棵二叉树就得从最后一层开始往上销毁,如果先销毁的是上面的节点,那么下面的节点就丢失了,如此看来,只有后序符合这个要求,通过后序遍历能完美销毁整棵二叉树,代码如下

// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
	assert(root);	//传过来的这个二级指针不能为空

	//先销毁左孩子,再销毁右孩子,最后销毁根 ---> 后序
	if (!(*root))
		return;	//叶子,不必销毁

	//左右根
	BinaryTreeDestory(&((*root)->left));
	BinaryTreeDestory(&((*root)->right));	//取地址 --- 函数形参为二级指针

	free((*root));	//销毁当前节点
	(*root) = NULL;
}

注意:二叉树在销毁时,要传二级指针,不然形参的改变是无法影响外面实参。如果想使用一级指针的话,在调用完销毁函数后,还得手动把根节点置空,避免野指针问题


📖玩转二叉树

二叉树的热身环节已经结束了,现在准备进入更高难度的函数,带你从多种角度玩转二叉树
燃起来了

📃构建树

首先来看看这个热门题:根据一个已知数组(存放的是某二叉树前序遍历的结果,# 表示空),构建出原二叉树。 题目的意思很简单,就是提供某二叉树的前序遍历结果,包括空也提供了,让我们根据这个结果,还原出原来的二叉树,前序遍历我们已经解决了,反过来还不简单?步骤如下

  • 根据题目可知,数组中的 # 表示空,反过来说,如果遇到的不是 #,那就说明这是一个节点
  • 如果是 # ,直接 return NULL;否则就申请一个节点,将此节点看作根节点
  • 每次递归函数要么产生新节点,要么直接返回 NULL,利用前序遍历思想,在得到根节点后,递归链接其左右孩子,至于孩子是节点还是 NULL ,得看递归结果
  • 最后再返回当前节点信息,除了根节点可以返回出函数外,其他的节点信息都是返回给上一层节点,即成为他们的左右孩子,返回时,整棵树才会被链接起来

长话短说,这就是一个递归遍历数组+申请节点链接的程序,每次递归,都得保证数组递归遍历能往后走,前序思想为 根、左、右,大问题转小问题:先保证这个节点存在,再链接其左右孩子。代码实现时需要多加小心,比如传递数组下标时,要传地址,不然数组都走不下去,还有递归终止条件为当前数组值是否为 # ,接近手段就是数组的遍历,具体看**动图**实现:
构建二叉树
代码如下

// 通过前序遍历的数组"A B D # # E # H # # C F # # G # #"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
	assert(a);

	//如果一开始就为 # 就没必要创建了
	if (a[*pi] == '#')
	{
		(*pi)++;	//向后移动,找到下一个值
		return NULL;
	}

	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	assert(node);

	node->data = a[(*pi)++];	//赋值,并向后移动

	node->left = BinaryTreeCreate(a, n, pi);	//左右链接
	node->right = BinaryTreeCreate(a, n, pi);

	return node;	//最开始的节点就是根节点
}

注意: 参数【数组下标】,需要传地址,不然数组遍历就无法进行下去

📃层序遍历

层序遍历,又被称为 广度优先遍历,之前是前、中、后序都属于深度优先遍历,所谓广度优先,就是一层一层的遍历,比如最开始的演示二叉树层序遍历结果为:A B C D E
层序遍历不必依靠递归,但是需要借助队列,因为队列的性质很符合广度这个要求

  • 队列的性质:先进先出,后进后出

具体实现步骤:

  • 核心思想:先将根节点入队,然后出队,带根节点的下一层入队(如果存在的话)
  • 当根节点入队,出队打印后,把第二层的节点入队
  • 如此重复,直到每层所有节点遍历完毕
  • 循环终止条件是队列是否为空,当队列为空时,说明整棵二叉树都入过队了

这个层序遍历得看看 动图 ,光凭文字不好描述:
层序遍历
芝士代码

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)	//需要借助队列
{
	if (!root)
	{
	    printf("队列为空!\n");
		return;
	}

	//思路:根节点先入队,出队时,带左右孩子入队(如果存在的话)
	//如此重复,直到队空
	Queue tmp;
	QueueInit(&tmp);
	QueuePush(&tmp, root);	//根节点入队
	
	while (!QueueEmpty(&tmp))
	{
		//取队头节点,出队
		QListDataType node = QueueFront(&tmp);
		QueuePop(&tmp);	//出队
		printf("%c ", node->data);	//打印元素

		//判断左右节点是否存在,存在就入队
		if (node->left)
			QueuePush(&tmp, node->left);
		if (node->right)
			QueuePush(&tmp, node->right);
	}

	QueueDestroy(&tmp);
}

注意: 这里借助了队列,需要引出相关头文件,入队列的元素是指向二叉树节点的指针,即二叉树节点 BinaryTreeNode*队列相关头文件中,需要特别注意一下,把队列元素类型修改为对应类型

📃判断是否为完全二叉树

这是力扣上的中等题,牛客上的困难题,也是本文的压轴戏
完全二叉树指连续的二叉树,判断是否为完全二叉树的关键就是 判断当前树是否连续(每一层都要连续),涉及到层序遍历,一样需要借助队列,不过循环终止条件和入队条件不同,也不需要打印了,只是多了一个判断,步骤如下:

  • 提前统计出二叉树的节点树,存储在变量 countNode 中,循环 countNode
  • 核心思想仍然为 出上一层,带下一层
  • 层序遍历中的打印当前出队得到的节点,会被替换成判断当前节点是否为空
  • 层序遍历中,为空的节点是入不了队的,但这里不管当前节点的左右子树是否为空,都入队,假如不是完全二叉树,那么肯定就存在循环未终止的情况下,出队取到空节点
  • 仔细想想,用节点数作为循环终止条件,如果是完全二叉树,是肯定取不到空节点的,因为它根本没机会入队
  • 这样一来,问题就很好解决了,无非就是 入队、出队、判断、入队 如此重复

不多解释,看 动图
判断是否为完全二叉树
代码在这里

// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	//空树也是完全二叉树
	if (!root)
		return true;

	//完全二叉树一定是有序的,需要利用层序遍历思想
	//循环节点次
	//取节点的时候,如果节点为空,说明不是完全二叉树
	Queue tmp;
	QueueInit(&tmp);
	QueuePush(&tmp, root);	//根节点入队
    
	int countNode = BinaryTreeSize(root);	//获取节点数

	while (countNode--)
	{
		QListDataType node = QueueFront(&tmp);
		QueuePop(&tmp);

		//如果取到空,说明不是完全二叉树
		if (!node)
			return false;

		//左右孩子都入队
		QueuePush(&tmp, node->left);
		QueuePush(&tmp, node->right);
	}

	QueueDestroy(&tmp);
	return true;
}

注意: 在判断时,是判断当前节点是否为空,而非节点中的数据;上文中所有动图可能存在丢帧的情况,在录制时好好的,转成 gif 动图就出现这种情况了,如果感觉动图表意不明的,可以联系我查看原视频(更清晰、更丝滑)


📖源码

源码在我的 Gitee 仓库中,可以点击这里跳转
写麻了,做图也做麻了


📖相关题目

二叉树这部分也是有很多OJ试题,感兴趣的同学的尝试着去做一做

二叉树OJ试题集


📘总结

以上就是二叉树的全部内容了,为了确保每种功能的直观性,我几乎为每个函数都配上了动图(制作不易,且看且珍惜),回顾全文,我们从何为二叉树出发,学习了二叉树的基础功能,最后还涉及了二叉树的部分中等知识,相信你在看完本文后,一定能学到很多干货,轻松理解二叉树

如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正

星辰大海

相关文章推荐
关于“堆”,看看这篇文章就够了(附堆的两种应用场景)
数据结构 | 单链表
数据结构 | 栈和队列

感谢支持

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

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

相关文章

java的vo实体类字段和返回前端接收的字段大小写不一致

java的vo实体类字段和返回前端接收的字段不一致 后台的vo字段和前端接收到的出现大小写一直的情况 例如: 后台字段 /** a_lwa */private String aLwa;前端实际接收到的 这是由于使用lombok插件导致的 解决办法 实体类添加注解强制绑定 /** a_lwa */JsonProperty("aLwa&…

【JavaWeb开发-Servlet】day07-学生成绩管理系统-实现登录注册功能

目录 一、登录功能 1、实现登录页面 ①新建一张登录表:login ②插入一条测试数据 ③新建一个login.jsp登录界面 ④此表的实体Teacher类: ⑤ 然后在写一个dao方法,需要根据用户名查询登录表中的用户信息 ⑥业务层中我们直接调用dao层方法 ⑦在…

【负荷预测】基于灰色理论负荷预测的应用研究(Matlab代码实现)

👨‍🎓个人主页:研学社的博客 💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜…

南京邮电大学编译原理实验一(词法分析器的构造)

文章目录一、 实验目的和要求二、实验环境(实验设备)三、实验原理及内容(一)设计概要1、C语言子集2、单词及编码3、状态转换图(二)实现分析(三)结果分析四、实验小结(包括问题和解决方法、心得体…

文本纠错--N-gram--Macbert模型的调用以及对返回结果的处理

文本根据词典进行纠错 输入一段可能带有错误信息的文字, 通过词典来检测其中可能错误的词。 例如:有句子如下:中央人民政府驻澳门特别行政区联络办公室1日在机关大楼设灵堂    有词典如下:中国人民,中央人民&#x…

红帽曹衡康:开源是企业数字化转型的机遇

在数据成为企业核心资产的今天,为了更好地降本增效,实现业务创新和增长,越来越多的企业都开启了数字化转型之路。然而对于绝大多数企业来说,这条转型之路都并非一帆风顺。事实上,数字化转型在为企业开启更多机遇的同时…

爆火Chatgpt注册 chatgpt使用 完全指南

1 chatgpt 简介 ChatGPT是一种语言模型,它被训练来对对话进行建模。它能够通过学习和理解人类语言来进行对话,并能够生成适当的响应。ChatGPT使用了一种叫做Transformer的神经网络架构,这是一种用于处理序列数据的模型,能够在输入…

Java System类

JavaSystem类\huge{Java \space System类}Java System类 System类概述 简而言之:SystemSystemSystem类就是一个工具类,直接调用对应的方法来使用即可,不需要也不能被实例化。 常用方法 ①. exit() 退出Java虚拟机 //执行这个exit(0)代码之…

[附源码]计算机毕业设计的花店售卖系统的设计与实现Springboot程序

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

GPU——占用情况和进程情况的参数解读表

GPU——占用情况和进程情况的参数解读表一、GPU占用情况二、GPU的进程情况参考资料一、GPU占用情况 名称功能说明Fan风扇转速在0 (%) 到100 (%) 之间变动Temp摄氏温度CPerf性能状态从P0到P12,P0表示最大性能,P12表示状态最小性能Persistence-M持续模式的…

微服务框架 SpringCloud微服务架构 微服务保护 31 限流规则 31.2 流控模式【关联】

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式,系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 微服务保护 文章目录微服务框架微服务保护31 限流规则31.2 流控模式【关联】31.2.1 流控模式31.2.2 流控模式 - 关联31.2.3 小结31 限流规则…

OpenCV入门(C++/Python)- 使用OpenCV色彩空间(七)

在本教程中,了解计算机视觉中使用的流行色彩空间,并将其用于基于颜色的分割。 使用OpenCV色彩空间不同的颜色空间RGB颜色空间LAB颜色空间YCrCB 颜色空间HSV颜色空间如何使用这些颜色空间进行分割简单方法文章内容如下:首先,我们将…

Linux线程的创建

用户态创建线程: 线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。pthread_create不是一个系统调用,是Glibc库的一个函数。 在nptl/pthread_creat.c里面找到了这个函数: int __pthread_create_2_1 (pthread…

直播预告 | SOFAChannel#31《RPC 框架设计的考和量》

SOFARPC 是蚂蚁集团开源的一款基于 Java 实现的 RPC 服务框架,为应用之间提供远程服务调用能力,具有高可伸缩性,高容错性,目前蚂蚁集团所有的业务的相互间的 RPC 调用都是采用 SOFARPC。SOFARPC 为用户提供了负载均衡,…

web课程设计网页制作、基于HTML+CSS大学校园班级网页设计

🎉精彩专栏推荐👇🏻👇🏻👇🏻 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 💂 作者主页: 【主页——🚀获取更多优质源码】 🎓 web前端期末大作业…

代码随想录刷题Day60 | 84. 柱状图中最大的矩形

代码随想录刷题Day60 | 84. 柱状图中最大的矩形 84. 柱状图中最大的矩形 题目: 定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 求在该柱状图中,能够勾勒出来的矩形的最大面积。 示例 1: …

C++调用OpenCV实现图像阈值处理

1 前言 在计算机视觉技术中,阈值处理是一种非常重要的操作,它是很多高级算法的底层处理逻辑之一。比如在使用OpenCV检测图形时,通常要先对灰度图像进行阈值(二值化)处理,这样就得到了图像的大致轮廓&#…

English Learning - L1 站在高处建立灵魂 2022.12.5

English Learning - L1 站在高处建立灵魂 2022.12.51.1 到底什么是语法1.2 为什么要学习语法口语分广义和狭义讲母语的人为啥不学语法?作为一名二语习得者口语中可不可以没有有语法?1.3 英语(听说读写)的核心金字塔理论关于词汇量…

免费内网穿透工具测评对比,谁更好用 2

文章目录1. 前言2. 对比内容2.1 http协议功能及操作对比2.1.1 网云穿的http设置2.1.2 Cpolar的http设置2.2 使用感受对比3. 结语1. 前言 上篇文章,笔者对比了网云穿和Cpolar的直观内容,包括网站界面、客户端界面和内网穿透设置界面。总的来说&#xff0…

保姆级教程:手把手教你使用深度学习处理文本

大家好,今天给大家分享使用深度学习处理文本,更多技术干货,后面会陆续分享出来,感兴趣可以持续关注。 文章目录NLP技术历程准备数据标准化词元化Tokenization(文本拆分)技术提升建立索引表使用TextVectoriz…