这个作业属于哪个班级 | C语言–网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业03–树 |
这个作业的目标 | 学习树的结构设计及运算操作 |
姓名 | 骆锟宏 |
作者的话:如果你想对贯穿整个树应用的核心思想有一个大概的认识的话不妨从这个引入的小例子开始读起趴:
当Tree == NULL,return 0
。求左子树的高度lh
求右子树的高度rh
if(lh > rh)
return lh+1
else
return rh+1
int GetHeight(BinTree BT)
{
int rHeight, lHeight;
/*空树直接高度为零*/
if (BT == NULL)
{
return 0;
}
/*一个很妙的递归算法*/
lHeight = GetHeight(BT->Left);
rHeight = GetHeight(BT->Right);
if (lHeight > rHeight)
{
return (lHeight + 1);
}
else
{
return (rHeight + 1);
}
}
typedef ElemType SqBinTree[MaxSize];
//其中ElemType可以是任何基础数据结构或者自定义结构体。
正是因为顺序存储结构有这些方面的缺陷,所以,对于树的一般结构,我们引出了链式存储结构来对它进行处理。
typedef struct BiTNode
{
ElmentType data;//数据域
struct BiTNode* lchild;//左孩子
struct BiTNode* rchild;//右孩子
}BiTNode, * BiTree;
了解了树的具体的储存结构后,下一步就是要考虑如何根据已有的条件选择合适的存储结构,并创建出一个二叉树。
void CreateBiTree(string str, BiTree& T, int index)
{
if (index > str.size()-1)
{
T = NULL;
return;
}
if (str[index] == '#')
{
T = NULL;
}
else
{
T = new BiTNode;
T->data = str[index];
CreateBiTree(str, T->lchild, 2 * index);
CreateBiTree(str, T->rchild, 2 * index + 1);
}
}
如果根节点对应的下标是0,则需要改变:
CreateBiTree(str, T->lchild, 2 * index ); ---> CreateBiTree(str, T->lchild, 2 * index + 1);
CreateBiTree(str, T->rchild, 2 * index + 1); ---> CreateBiTree(str, T->rchild, 2 * index + 2);
void CreateBiTree(char str[], BiTree& T, int& index)
{
if (!str[index])//序列用字符数组来存
{
T = NULL;
return;
}
if (str[index] == '#')
{
index++;
T = NULL;
return;
}
else
{
T = new BiTNode;
T->data = str[index++];
CreateBiTree(str, T->lchild, index);
CreateBiTree(str, T->rchild, index);
}
}
//或者可以写成这样子:
void CreateBiTree(string str, BiTree& T, int& index)
{
if (index > str.size()-1)//序列用字符串来存
{
T = NULL;
return;
}
if (str[index] == '#')
{
T = NULL;
return;
}
else
{
T = new BiTNode;
T->data = str[index];
CreateBiTree(str, T->lchild, ++index);
CreateBiTree(str, T->rchild, ++index);
}
}
没错当给定的序列是按二叉链结构遍历得到的前序、中序或后序序列的话,就需要提供两种序列才能惟一确定一棵树,并且只给出先序和后序序列无法直接确定一棵树
if -> 当前输入的序列长度为零->返回空树或者令当前树指向空。
查找当前树的根节点在中序序列中的位置
左子树的序列个数 = 根节点在中序序列的地址 - 中序序列首地址;
递归构造左子树;
递归构造右子树;
void CreateBTree(BTree& T, char* preorder, char* inorder, int n)
//BTree CreateBTree(char* preorder, char* inorder, int n)
{
//BTree T;
char* pMove;
int distance;//代表中序序列中根结点距离序列头的距离;
// 1. 结点个数为0就不需要再建树了
if (n <= 0)
{
T = NULL;//这里别忘了要让树结点指向空!
return;
//return NULL;
}
//2. 创建新结点
T = new BTNode;
T->data = *preorder;
//3. 在中序序列中查找根节点的位置
for (pMove = inorder; pMove < inorder + n; pMove++)
{
if (*pMove == *preorder)
{
break;
}
}
distance = pMove - inorder;
//4.递归构造左、右子树
CreateBTree(T->lchild ,preorder + 1, inorder, distance);//构造左子树
CreateBTree(T->rchild ,preorder + distance + 1, pMove + 1, n - distance - 1);//构造右子树
//T->lchild = CreateBTree(preorder + 1, inorder, distance);//构造左子树
//T->rchild = CreateBTree(preorder + distance + 1, pMove + 1, n - distance - 1);//构造右子树
//return T;
}
T->data = postorder[n-1];
void CreateBTree(BTree& T, ElementType* postorder, ElementType* inorder, int n)
{
ElementType* pMove;
int distance;//代表中序序列中根结点距离序列头的距离;
//结点个数为0就不需要再建树了
if (n <= 0)
{
T = NULL;
return;
}
T = new BTNode;
T->data = postorder[n-1];
//在中序序列中查找根节点的位置
for (pMove = inorder; pMove < inorder + n; pMove++)
{
if (*pMove == postorder[n-1])
{
break;
}
}
distance = pMove - inorder;
CreateBTree(T->lchild, postorder, inorder, distance);//构造左子树
CreateBTree(T->rchild, postorder + distance, pMove + 1, n - distance - 1);//构造右子树
}
void
类型的。ElementType*
,这样在具体问题中,就只需要在开头定义处,根据具体情况对typedef
进行改动就可以啦。void PreorderPrintNodes(BinTree BT)
{
if (BT == NULL)
{
return;
}
cout << BT->Data;
PreorderPrintNodes(BT->Left);
PreorderPrintNodes(BT->Right);
}
void InorderPrintNodes(BinTree BT)
{
if (BT == NULL)
{
return;
}
InorderPrintNodes(BT->Left);
cout << BT->Data;
InorderPrintNodes(BT->Right);
}
void PostorderPrintNodes(BinTree BT)
{
if (BT == NULL)
{
return;
}
PostorderPrintNodes(BT->Left);
PostorderPrintNodes(BT->Right);
cout << BT->Data;
}
同样的,对于后序遍历也有一个很简单的记忆方法:不妨把整颗树当成一束二维的葡萄,我们像先序遍历那时候一样,从根结点出发,绕着这个葡萄的外围轮廓走,当我们的轮廓曲线包住只有一颗葡萄(也就一个结点)的时候,把这个结点摘下来,排上去,不断这样做,直到最后,把根节点放在最后,得到的序列就是后序遍历序列。
不妨拿棵树来举个试一试:
依然以PTA[7-4 jmu-ds-输出二叉树每层节点 (22 分)]的图为例:
这里给出一个用来测试三种特殊方法是否合理的程序:
文中所用的例子树的输入是:ABD#G###CEH###F#I##
#include
#include
using namespace std;
typedef char ElementType;
typedef struct BTNode {
ElementType Data;
struct BTNode* Left;
struct BTNode* Right;
}BTNode, * BiTree;
/*前序遍历法建二叉树*/
void CreateBiTree(string str, BiTree& T, int& index)
{
if (index > str.size() - 1)
{
T = NULL;
return;
}
if (str[index] == '#')
{
index++;
T = NULL;
return;
}
else
{
T = new BTNode;
T->Data = str[index++];
CreateBiTree(str, T->Left, index);
CreateBiTree(str, T->Right, index);
}
}
void PreorderPrintNodes(BiTree BT)
{
if (BT == NULL)
{
return;
}
cout << BT->Data;
PreorderPrintNodes(BT->Left);
PreorderPrintNodes(BT->Right);
}
void InorderPrintNodes(BiTree BT)
{
if (BT == NULL)
{
return;
}
InorderPrintNodes(BT->Left);
cout << BT->Data;
InorderPrintNodes(BT->Right);
}
void PostorderPrintNodes(BiTree BT)
{
if (BT == NULL)
{
return;
}
PostorderPrintNodes(BT->Left);
PostorderPrintNodes(BT->Right);
cout << BT->Data;
}
int main()
{
string str;
cin >> str;
BiTree tree;
int start_val = 0;
CreateBiTree(str, tree, start_val);
cout << "前序遍历序列是:";
PreorderPrintNodes(tree);
cout << endl;
cout << "中序遍历序列是:";
InorderPrintNodes(tree);
cout << endl;
cout << "后序遍历序列是:";
PostorderPrintNodes(tree);
cout << endl;
return 0;
}
作者的话:
仔细观察的话,你会发现采用递归方式去实现三序遍历本质上的差别只不过体现在了输出语句的位置上,如果是前序就放在最前面,中序就放在对左右子树递归遍历的中间,后序就放在后面。
事实上,这三种遍历方式还有非递归的做法,但不变的是,非递归的做法本质上只是把递归没有显化的过程显化了而已,是借用了队列的结构来进行具体的实现。至于具体的实现过程可以拜读一下这篇博客:写得比较详细。也可以参考课本第218页开始的内容。
博文转载二叉树非递归实现三序遍历的博文
版权声明:该文为CSDN博主「小心眼儿猫」的原创文章,遵循CC 4.0 BY-SA版权协议,今转载并附上原文出处链接及声明。
原文链接:https://blog.csdn.net/qq_40927789/article/details/80211318
原文作者:https://blog.csdn.net/qq_40927789
/*层序遍历法输出二叉树*/
void levelOrder(BiTree Troot)
{
//空树莫得打印
if (Troot == NULL)
{
cout << "NULL";
return;
}
int flag = 1;
queue Tqueue;
//if(Tqueue.empty())
Tqueue.push(Troot);
while (!Tqueue.empty())
{
BiTree tempPtr = Tqueue.front();
Tqueue.pop();
if (flag)
{
cout << tempPtr->data;
flag = 0;
}
else
{
cout << " " << tempPtr->data;
}
if (tempPtr->lchild)
{
Tqueue.push(tempPtr->lchild);
}
if (tempPtr->rchild)
{
Tqueue.push(tempPtr->rchild);
}
}
}
/*类层次遍历法获取树的最大宽度*/
int GetMaxWidth(BiTree T)
{
if (!T)
{
return 0;
}
queue Que;
int max = 1;
int len;//储存树每层元素的个数,同时也是每层队列的长度。
Que.push(T);
while (!Que.empty())
{
len = Que.size();
while (len > 0)//代表当前层还有元素在队列中
{
BiTree tempT = Que.front();
Que.pop();
len--;
if (tempT->lchild != NULL)
{
Que.push(tempT->lchild);
}
if (tempT->rchild != NULL)
{
Que.push(tempT->rchild);
}
}
if (Que.size() > max)
{
max = Que.size();
}
}
return max;
}
版权声明:该文为CSDN博主「流楚丶格念」的原创文章,遵循CC 4.0 BY-SA版权协议,今转载并附上原文出处链接及声明。
原文链接:https://blog.csdn.net/weixin_45525272/article/details/105837185
原文作者:https://yangyongli.blog.csdn.net/
从该文章对4种遍历方式的深入思考非常妙,用另一种很常识和有趣的方法让我们能够很简单地记住二叉树的三序遍历方法,这也提醒要让人能更直观地去了解某些知识点,可以尝试采用一种不那么专业性强的叙述方式去讲述,可能得到的效果会更好。
表达式树的代码
while(遍历没到字符串尾)
{
if 新节点存的数据的数字,就让这个新节点进入操作数栈,
if 新节点存的数据是操作符
if 运算符栈栈空,直接入栈。
else if 将入栈运算符等级更高,直接入栈
else 出栈一个栈内运算符为根节点,依次出栈两个操作数栈元素为右孩子左孩子建树,并将建好的树放入操作数栈。
}
if(运算符栈空)
{
return 操作数栈栈顶元素;
}
else
{
while(运算符栈不空)
{
不断出栈建树。
}
return 操作数栈栈顶元素;
}
void InitExpTree(BTree& T, string str)//建表达式的二叉树
{
stack numberStack;/*储存分支结点,或者说操作数的栈*/
stack signStack;/*储存根结点,或者说运算符的栈*/
char cur_char;
/*优先遍历输入的字符串*/
for (int i = 0; i < str.size(); i++)
{
cur_char = str[i];
if (In(cur_char))
{
while (1)
{
if (signStack.empty()
|| Precede(signStack.top()->data, cur_char) == '<')/*入栈的条件判断*/
{
BTree signTNode = new BiTNode;
signTNode->data = cur_char;
signStack.push(signTNode);
/*运算符入栈是该运算符流程结束的标志之一*/
break;
}
else if (Precede(signStack.top()->data, cur_char) == '>')/*出栈的条件判断*/
{
char sign = signStack.top()->data;
signStack.pop();
BTree newNumb;
BTree rTree = numberStack.top();
numberStack.pop();
BTree lTree = numberStack.top();
numberStack.pop();
/*然后开始建一个小二叉树*/
CreateExpTree(newNumb, lTree, rTree, sign);
numberStack.push(newNumb);
}
else if (Precede(signStack.top()->data, cur_char) == '=')
{
signStack.pop();
break;
}
}
}
else/*cur_char为数字*/
{
BTree numbTNode = new BiTNode;
numbTNode->data = cur_char;
numbTNode->lchild = NULL;
numbTNode->rchild = NULL;
numberStack.push(numbTNode);
}
}
if (signStack.empty())
{
T = numberStack.top();
return;
}
else
{
while (!signStack.empty())
{
char sign = signStack.top()->data;
signStack.pop();
BTree newNumb;
BTree rTree = numberStack.top();
numberStack.pop();
BTree lTree = numberStack.top();
numberStack.pop();
/*然后开始建一个小二叉树*/
CreateExpTree(newNumb, lTree, rTree, sign);
numberStack.push(newNumb);
}
T = numberStack.top();
return;
}
}
if(当前结点为NULL) return 0;//递归出口
if(当前的结点是数字);return 数字字符 - '0';
else//当前的结点是运算符:
switch (运算符)
case ‘+’:return 计算左子树的值 + 右子树的值
case ‘-’:return 计算左子树的值 - 右子树的值
case ‘*’:return 计算左子树的值 * 右子树的值
case ‘/’:if(右子树的值不为0)
{ return 计算左子树的值 /右子树的值 }
else
{ 输出error! }
double EvaluateExTree(BTree T)//计算表达式树
{
if (T == NULL)
{
return 0;/*该数字并未被利用*/
}
if (In(T->data))
{
switch (T->data)
{
case '+':
return EvaluateExTree(T->lchild) + EvaluateExTree(T->rchild);
case '-':
return EvaluateExTree(T->lchild) - EvaluateExTree(T->rchild);
case '*':
return EvaluateExTree(T->lchild) * EvaluateExTree(T->rchild);
case '/':
if (EvaluateExTree(T->rchild) == 0)
{
cout << "divide 0 error!" << endl;
exit(0);/*直接退出程序*/
}
else
{
return EvaluateExTree(T->lchild) / EvaluateExTree(T->rchild);
}
default:
break;
}
}
else/*当结点值为数字的时候*/
{
return ((double)T->data - '0');
}
}
typedef struct
{ ElemType data; //结点的值
int parent; //指向双亲的位置(伪指针)
} PTree[MaxSize];
typedef struct node
{ ElemType data; //结点的值
struct node *sons[MaxSons]; //指向孩子结点
} TSonNode;
typedef struct tnode
{
ElemType data; //结点的值
struct tnode *firstchild; //指向孩子结点
struct tnode *brother; //指向兄弟
} TSBNode,* TSBTree;
那么问题来了,哈夫曼树能解决什么问题捏?
typedef struct {
char data; //结点值
double weight; //权重
int parent; //双亲结点的位置
int lchild; //左孩子结点
int rchild; //右孩子结点
}HTNode;
n0=n2+1
,因此只需要知道叶子结点的个数 (n0) 就能知道哈夫曼树的结点的个数 (n = 2*n0-1) 了。void CreateHTree(HTree& T,int n)//建哈夫曼树
{
int i, k;
int lnode, rnode;//最小权重的两个结点的位置
double min1, min2;//两个最小的权重的值
//先将所有结点相关域置初值为-1
for (i = 0; i < 2*n - 1; i++)
{
T[i].parent = T[i].lchild = T[i].rchild = -1;
}
//构造哈夫曼树的其他 n-1 个结点并完善好亲子关系。
for (i = n; i <= 2 * n - 2; i++)
{
min1 = min2 = 1000000;//给大数是为了确保初始值比给出的权值都大
lnode = rnode = -1;
//查找权值最小的两个结点
//算法是在数组中找最小值和次小值
for (k = 0; k <= i - 1; k++)
{
//只在还没建树的结点中找元素来建树
if (T[k].parent == -1)
{
if (T[k].weight < min1)
{
min2 = min1, rnode = lnode;
min1 = T[k].weight, lnode = k;
}
else if (T[k].weight < min2)
{
min2 = T[k].weight, rnode = k;
}
}
}
T[i].weight = T[lnode].weight + T[rnode].weight;//赋权值
T[i].lchild = lnode, T[i].rchild = rnode;//双亲认子
T[lnode].parent = T[rnode].parent = i;//子认双亲
}
}
因为哈夫曼树这样建树的特点,所以哈夫曼树不会有度为一的结点,只会有二分支结点和叶子结点,并且被一起选出来的这两个值(如果他们是叶子节点的话)在哈夫曼编码上的特性也会体现为,这两个结点信息的编码除了最后一个数字不同外,其他前缀数字相同。
**但是这里要注意一个陷阱!**虽然这种方法是构建哈夫曼树最为简单的方法,但是并不是说哈夫曼树就只有这种构造方法!这里需要注意的是:WPL是唯一的,但是哈夫曼树不唯一。
对于当下这个特定序列,两种树的WPL一致,左图是传统方法做出来的哈夫曼树,而右图是WPL刚好等于哈夫曼树的WPL的该序列的一种树,那这种情况下,当然这棵树也可以被称为这个序列的哈夫曼树啦,而它的树的结构不同,哈夫曼编码也就自然不同了,但是不能质疑的一点是,它依然是正确的哈夫曼树。
所以对于某一特定的序列它构成的树的最小的WPL是唯一的,但是它的哈夫曼树不唯一!
(可选) 哈夫曼树代码设计(可以参考链式设计方法。)
相关补充–关于哈夫曼树的排序有需要去了解的有:
那么问题来了,为什么需要并查集呢?并查集解决什么问题,优势在哪里?
2.1 并查集基于顺序树的结构体:
typedef struct node {
int data; //结点对应元素的编号
int rank; //结点对应的秩(子树高度)
int parent; //结点对应的双亲下标
}UFSTree; //并查集树的结点类型
2.2 顺序树并查集的初始化:
void MAKE_SET(UFSTree* t, int n)
{
int i;
for (i = 0; i < n; i++)
{
t[i].data = i;
t[i].rank = 0;//秩初始化为0
t[i].parent = i;
}
}
2.3 顺序树并查集的查找:
int FIND_SET(UFSTree* t, int x)//在x所属的集合
{
//x是某个元素对应的编号。
if (x != t[x].parent)
{
return FIND_SET(t, t[x].parent);
}
//相等的时候代表找到树的根结点了,也就算找到所属集合;
//在顺序结构中,我们用根节点元素作为代表元素来表示一个树。
//可以用根节点元素的下标来定位根节点,以及其表示的树。
else
{
return x;
}
}
void UNION(UFSTree* t, int x, int y)//合并元素x所在的集合和元素y所在的集合
{
//合并前要先找集合
x = FIND_SET(t, x);
y = FIND_SET(t, y);
//找完之后要合并
if (t[x].rank > t[y].rank)
{
t[y].parent = x;
}
else
{
t[x].parent = y;
if (t[x].rank == t[y].rank)
{
t[y].rank++;
}
}
}
所以这里可以看出,再困难的问题本质上对其的处理都是划分到更简单的方向上去处理。在这里我们不妨猜想,其实无论问题的维度是多少个维度,最终的最终,我们都是对问题进行逻辑抽象,直到最后拆分到一维线性角度来处理。
输出二叉树的每层结点
目录树的代码
//先序顺序序列建树;
//递归先序遍历二叉树,给每个结点标记level;
//按层输出结点:
根节点入队
while(队不空)
{
取队首元素另存;
if(队首结点的层数==cur_level)//cur_level默认为1
if(cur_level == 1) 打印“ 1: ”
输出结点元素;
队首元素出队;
孩子不空孩子入队;
else
换行;cur_level变值。
cout << "cur_level:";
输出结点元素;
队首元素出队;
孩子不空孩子入队;
}
1.1 插入作为孩子(头插法)
1)如果当前结点没有第一个孩子,则直接插入作为第一个孩子结点。
2)如果当前结点有第一个孩子,则是否需要更改孩子:
<1假设要插入的对象是目录时:
1> 如果第一个孩子为文件,那直接头插,修改首孩为新结点。
2> 如果第一个孩子为目录,但是值比新节点大(字典序靠后),也更新新结点为首孩结点。
3> 如果第一个孩子为目录,但是值与新节点相等(字典序相等),那就让当前结点指向它的第一个孩子结点,然后删除新建的newNode,退出返回。
<2假设要插入的对象是文件时:
1> 如果第一个孩子为文件,但是值与新节点相等(字典序相等)那就让当前结点指向它的第一个孩子结点,然后删除新建的newNode,退出返回。
1.2 插入作为兄弟(插入排序)
// 先查找
<1假设要插入的对象是目录时:
while(值比我大 && 类型相同) 往下找
跳出时的位置就是目录插入的位置;
<2假设要插入的对象是文件时:
while(指针不空)
if (值比我大 && 类型相同)找到了位置
* 这两步可以放在一个循环里面,把文件还是目录的分支放循环里面讨论,
找到了位置后要提前退出!
//后插入(ptr是插入位置的前驱指针)
1>如果到表尾了(ptr->brother == NULL)直接尾插新结点
2>其他情况,先继承原来的brother关系,再修改ptr->brother = 新结点
class Solution {
public:
vector> levelOrderBottom(TreeNode* root) {
auto levelOrder = vector>();
if (!root) {
return levelOrder;
}
queue q;
q.push(root);
while (!q.empty()) {
auto level = vector();
int size = q.size();
for (int i = 0; i < size; ++i) {
auto node = q.front();
q.pop();
level.push_back(node->val);
if (node->left) {
q.push(node->left);
}
if (node->right) {
q.push(node->right);
}
}
levelOrder.push_back(level);
}
reverse(levelOrder.begin(), levelOrder.end());
return levelOrder;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/solution/er-cha-shu-de-ceng-ci-bian-li-ii-by-leetcode-solut/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
树的层次遍历可以使用广度优先搜索实现。从根节点开始搜索,每次遍历同一层的全部节点,使用一个列表存储该层的节点值。
如果要求从上到下输出每一层的节点值,做法是很直观的,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的尾部。 这道题要求从下到上输出每一层的节点值,只要对上述操作稍作修改即可:在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的头部。
为了降低在结果列表的头部添加一层节点值的列表的时间复杂度,结果列表可以使用链表的结构,在链表头部添加一层节点值的列表的时间复杂度是 O(1)O(1)。在 Java 中,由于我们需要返回的 List 是一个接口,这里可以使用链表实现;而 C++ 或 Python 中,我们需要返回一个 vector 或 list,它不方便在头部插入元素(会增加时间开销),所以我们可以先用尾部插入的方法得到从上到下的层次遍历列表,然后再进行反转。
//树的结构依然是使用二叉链树
//列表的容器用vector>,可以想象成有一条表头链,这个链的每个表头结点的元素又是一条链,这条内层链的每个结点的元素是整形数据。
//先让根节点入队列
while(队不空)
{
建每层的数据链 vector
保存该层的长度也就是队列长度(第一层只有根节点所以队列长度为1)
while(队列内还有该层元素)
{
取队首元素;
将队首元素的的值尾插入数据链;
队首元素出队;
如果队首元素的孩子不空,让孩子入队;
}
将数据链尾插入数据链表中(因为vector不方便头插);
}
尾插完再翻转数据链表得到的效果和头插一样。
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/solution/er-cha-shu-de-ceng-ci-bian-li-ii-by-leetcode-solut/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
auto
而不是new
,也就是说在面向对象编程的过程中,我们对一个变量进行动态内存申请更多使用auto
,其次,知道了容器的定义可以套娃使用----vector>
如果必要的话,另外学习一下vector容器的功能–reverse(levelOrder.begin(), levelOrder.end()),里面的begin(),end(),也是vector的功能,取开头元素和结尾元素
。