到目前为止我们C语言的学习告一段落了,算是勉强入门吧。😃 😃 😃 接下来我们会用c语言实现一些初阶数据结构,这个过程代码量很大,大家要自己多动手敲一下。
文章目录
前言
什么是线性表
?
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:
顺序表、链表、栈、队列、字符串
…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
例如下图所示:
顺序表和链表都属于线性表,本章主要讲解顺序表。
注:顺序表和链表可能每本书或者每个人的代码都有所差异,但是我们所要达到的目的都是一样的。
一、顺序表
顺序表的概念与结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改(主要操作是增删查改,也还有许多其他操作)。
可以把顺序表看作一个数组。
顺序表一般可以分为两种:
1. 静态顺序表:使用定长数组存储元素。
.
2. 动态顺序表:使用动态开辟的数组存储。
这里我们主要来讲讲动态顺序表,因为静态顺序表的空间大小是固定的,使用不方便,这也导致我们以后工作之类的时候一般不用静态的顺序表用的是动态的顺序表。
二、动态顺序表的实现
动态顺序表的实现一般使用3个文件来完成,一个Seqlist.h头文件,一个Seqlist.c的源文件,一个test.c的源文件
1、Seqlist.h头文件
这里面一般用来放着函数的声明,一些全局变量或者其他变量的声明或者重命名
#pragma once//防止头文件被重复包含
#include<stdio.h>
#include<assert.h>//断言头文件
#include<stdlib.h>//动态内存开辟等操作头文件
下面的结构体之类的重命名是为了方便识别和使用,大家可以重命名为自己想要的名字
typedef int SLDataType;
//这里我们将int重命名为SLDataType,因为我们可能操作的数据是字符或者是浮点数
//所以将int重命名,如果是操作其他数据直接修改这里的int就行
typedef struct Seqlist
{
SLDataType* a;//指针
SLDataType size;//有效数据个数
SLDataType count;//容量
}SL;//将struct Seqlist重命名为SL,这样更加方便使用
以下是接口函数:
void SLInit(SL* ps);//初始化数据
void SLPrint(SL* ps);//打印数据
void SLDelete(SL* ps);//释放/销毁
void SLPushback(SL* ps, SLDataType x);//尾插数据
void SLPushfront(SL* ps,SLDataType);//头插数据
void SLPopback(SL* ps);//尾删数据
void SLPopfront(SL* ps);//头删数据
int SLFind(SL* ps, SLDataType x);//查找数据
void SLInsert(SL* ps, SLDataType pos, SLDataType x);//任意位置插入数据
void SLErase(SL* ps, SLDataType pos);//任意位置删除数据
void SLModity(SL* ps, SLDataType pos, SLDataType x);//修改数据
接下来就是实现也就是定义的实现了,注意我们这里最后实现test.c文件,因为主函数在这个文件里面,包括菜单实现,接口的调用等等。如果提前写菜单会把自己累死,特别是调试的时候,头铁的读者可以试试哦!
2、Seqlist.c源文件
这个文件里面放的都是函数的定义了,我不会按照每一个接口调用的顺序来实现,而是先实现之后再调用接口
这里在test.c里面有一个变量:
SL s;
大家要注意
小前言
我们尽量在每个函数里面都开始断言一下,这样防止传参的指针是空指针,容易发现错误
然后小编是之前经过测试了的,这里test.c小编会把测试代码和菜单都给大家,就不重新在测试一遍了
1、包含头文件
这一点必不可少
#include"Seqlist.h"
2、初始化数据
void SLInit(SL* ps)//初始化
{
assert(ps);
ps->a = NULL;//先将指针初始化为空
ps->size = ps->count = 0;//有效数据个数和容量初始化为0
//这里可以进行连等
}
3、打印数据
void SLPrint(SL* ps)//打印
{
assert(ps);
for (int i = 0; i < ps->size; i++)//i小于ps->size也就是目前的有效数据个数
{
printf("%d->", ps->a[i]);//这里每打印一个数据后面加一个箭头更像顺序表
//ps->a[i]就是找到指针a,对指针a加i的地址解引用,得到下标为i的数据进行打印
}
printf("NULL\n");//这里打印完所有数据后再打印一个NULL然后换行
}
4、释放/销毁
void SLDelete(SL* ps)//释放/销毁
{
assert(ps);
free(ps->a);//动态内存释放
ps->a = NULL;//指针置空
ps->size = ps->count = 0;//有效数据个数和容量置0
}
接下来就是增删查改等操作了,在删除的时候我们不用考虑空间的大小,但是在插入数据的时候要考虑空间够不够,不够就要进行扩容
5、扩容
void SLChack(SL* ps)//扩容
{//下面插入数据操作进行之前先调用扩容函数判断需不需要扩容
if (ps->size == ps->count)//如果有效数据个数等于容量就扩容
{//这里的SLDataType就是我们之前重命名的int
SLDataType newcount = ps->count == 0 ? 4 : ps->count * 2;
//这里的newcount可以理解为新的容量。
//这段代码的意思是:ps->count也就是旧的容量是不是0,如果是就让旧容量的空间开辟4个整型空间,然后赋值给新容量
//如果旧容量不是0,而且需要扩容就扩容两倍然后赋值给新容量
SLDataType* p = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * newcount);
//这里就是动态内存扩容的操作,扩容的空间大小就是新容量的空间大小
if (p == NULL)
{
perror("realloc fail");
return;
}
else
{
ps->a = p;//扩容成功后将扩容的地址赋值给原来的指针,由原来指针进行维护
ps->count = newcount;//再将新容量赋值给旧容量。至此扩容完成
}
}
}
很多人都不清楚这里据扩容为什么扩2倍,因为扩2倍最合适。
如果扩容少了要多次扩容,如果扩容多了就会造成大量浪费;
如果扩容1.5倍这种小数倍的话很麻烦:
因为如果原数据只有3个怎么扩1.5倍,或许等我们以后学了c++会轻而易举,但是如果进行跨平台操作呢?
是不是会得到不一样的结果,所以扩2倍是合适的
6、尾插数据
void SLPushback(SL* ps, SLDataType x)//尾插
{
assert(ps);
SLChack(ps);//判断是否需要扩容
ps->a[ps->size] = x;
//将x数据插入ps->size的位置即可,因为原来有效数据是ps->size个,但是下标最大是ps->size-1
//将x插入到下标ps->size就是相当于在最后插入了数据
ps->size++;//这里有效数据个数加1
}
7、头插数据
void SLPushfront(SL* ps,SLDataType x)//头插
{
assert(ps);
SLchack(ps);//判断扩容
SLdatatype end = ps->size - 1;//end就是最后一个有效数据的下标
while (end >= 0)//如果下标大于等于0
{
ps->a[end + 1] = ps->a[end];将下标0到下标ps->size-1的元素全部右移,也就是从后向前移动
--end;
}
ps->a[0] = x;//将下标为0的地址插入x数据
ps->size++;
}
8、尾删数据
void SLPopback(SL* ps)//尾删
{
assert(ps && ps->size > 0);//断言一下,如果有效数据个数大于0就可以进行删除
if (ps->size == 0)
{
exit(-1);//这种是暴力判断,如果有效数据个数为0,直接终止整个程序
}
ps->size--;//尾删直接把有效数据个数减一就行
}
9、头删数据
void SLPopfront(SL* ps)//头删
{
assert(ps);
assert(ps->size > 0);//与上面一样
SLDataType end = 0;//end是下标,从0开始
while (end < ps->size - 1)
{
ps->a[end] = ps->a[end + 1];//将下标为1的有效数据到最后一个有效数据全部左移,也就是从前向后移动
++end;
}
ps->size--;//容量减一
}
10、任意位置插入数据
void SLInsert(SL* ps, size_t pos, SLDataType x)//任意位置插入
//pos是下标,x是插入数据
{//size_t pos;下标不会是负数
assert(ps && pos <= ps->size);//pos=ps->size就是尾插,无影响
SLChack(ps);
size_t end = ps->size;//这里end与pos类型对应,无符号整型
while (end > pos)
{//如果end下标大于pos下标,那么就将pos到end中间的数据全部右移
ps->a[end] = ps->a[end - 1];
--end;
}
ps->a[pos] = x;//插入x
ps->size++;
}
11、任意位置删除数据
void SLErase(SL* ps, SLDataType pos)//任意位置删除
{
assert(ps && pos < ps->size);//同上断言
SLDataType end = pos;//end下标被赋值为pos
while (end < ps->size - 1)//如果不是删除最后一个有效数据
{
ps->a[end] = ps->a[end + 1];//把删除数据右边的数据全部左移
++end;
}
ps->size--;
}
12、查找数据
int SLFind(SL* ps, SLDataType x)//查找
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
return i;
}
return -1;
}
查找数据是有返回值的,如果成功则返回下标,如果失败返回-1;查找没有更好的方法,因为我们不知道数据是不是有序的,不能使用二分,快排这个时候又比较慢,使用直接循环遍历即可
13、修改数据
这个就太简单了
void SLModity(SL* ps, SLDataType pos, SLDataType x)//修改
{
assert(ps && pos < ps->size);//同上
ps->a[pos] = x;//直接把下标对应值修改即可
}
到目前为止我们的函数定义已经全部完成了,接下来就是test.c文件了
3、test.c源文件
这个文件有两大部分,一部分是测试用例,一部分就是调用和菜单
1、测试用例
这里小编偷懒了,直接把测试用例全部放在一起了,读者可以放开多个函数测试
void TestSL1()
{
SL s;
SLInit(&s);//初始化
SLPushback(&s, 1);//尾插
SLPushback(&s, 2);
SLPushback(&s, 3);
SLPushback(&s, 4);
SLPushback(&s, 5);
SLPushback(&s, 6);
SLPrint(&s);//打印
SLPushfront(&s, 10);//头插
SLPushfront(&s, 20);
SLPushfront(&s, 30);
SLPushfront(&s, 40);
SLPrint(&s);
SLPopback(&s);//尾删
SLPopback(&s);
SLPopback(&s);
SLPrint(&s);
SLPopfront(&s);//头删
SLPopfront(&s);
SLPopfront(&s);
SLPrint(&s);
SLInsert(&s, 1, 10);//任意位置插入
SLPrint(&s);
SLErase(&s, 3);//任意位置删除
SLPrint(&s);
SLModity(&s, 3, 30);//任意位置修改
SLPrint(&s);
SLDelete(&s);
}
2、菜单和调用
void menu()
{
printf("-------------------------------\n");
printf("***0、退出程序 5、打印数据***\n");
printf("***1、尾插数据 2、头插数据***\n");
printf("***3、尾删数据 4、头删数据***\n");
printf("***6、任意插入 7、任意删除***\n");
printf("***8、查找数据 9、修改数据***\n");
printf("-------------------------------\n");
}//大家也可以用枚举的方法来,我这里没有用枚举
int main()
{
SL s;
SLInit(&s);//初始化
int n = 0;
do
{
menu();
printf("请选择操作:");
int x = 0;
int y = 0;
scanf("%d", &n);
switch (n)
{
case 1:
printf("请输入尾插数据,以-1截止\n");
do
{
scanf("%d", &x);
if (x != -1)
{
SLPushback(&s, x);
}
} while (x != -1);
break;
case 2:
printf("请输入头插数据,以-1截止\n");
do
{
scanf("%d", &x);
if (x != -1)
{
SLPushfront(&s, x);
}
} while (x != -1);
break;
case 3:
SLPopback(&s);
break;
case 4:
SLPopfront(&s);
break;
case 5:
SLPrint(&s);
break;
case 6:
printf("请输入插入下标和插入数据\n");
scanf("%d %d", &x, &y);
SLInsert(&s, x, y);
break;
case 7:
printf("请输入删除数据下标\n");
scanf("%d", &x);
SLErase(&s, 3);
break;
case 8:
printf("请输入查找数据下标\n");
scanf("%d", &x);
SLFind(&s, x);
printf("x下标数据为:%d\n", s.a[x]);
break;
case 9:
printf("请输入修改下标和修改数据\n");
scanf("%d %d", &x, &y);
SLModity(&s, x, y);
break;
case 0:
SLDelete(&s);//这里是释放/销毁函数
printf("退出程序\n");
break;
default:
printf("输入错误,请重新输入:");
}
} while (n);
return 0;
}
也行有的读者已经想到了,既然有任意位置插入删除数据,那么直接给一个首元素下标和最后一个元素下标调用对应函数不就可以了,的确可以,如下图:
void SLPushback(SL* ps, SLDataType x)//尾插
{
/*assert(ps);
SLChack(ps);//判断是否需要扩容
ps->a[ps->size] = x;
//将x数据插入ps->size的位置即可,因为原来有效数据是ps->size个,但是下标最大是ps->size-1
//将x插入到下标ps->size就是相当于在最后插入了数据
ps->size++;*/
SLInsert(ps, ps->size, x);
}
void SLPushfront(SL* ps,SLDataType x)//头插
{
/*assert(ps);
SLchack(ps);
SLdatatype end = ps->size - 1;//end就是最后一个有效数据的下标
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
//将下标0到下标ps->size-1的元素全部右移,也就是从后向前移动
--end;
}
ps->a[0] = x;//将下标为0的地址插入x数据
ps->size++;*/
SLInsert(ps, 0, x);
}
void SLPopback(SL* ps)//尾删
{
/*assert(ps && ps->size > 0);//断言一下,如果有效数据个数大于0就可以进行删除
if (ps->size == 0)
{
exit(-1);//这种是暴力判断,如果有效数据个数为0,直接终止整个程序
}
ps->size--;//尾删直接把有效数据个数减一就行*/
SLErase(ps, ps->size - 1);
}
void SLPopfront(SL* ps)//头删
{
/*assert(ps);
assert(ps->size > 0);
SLDataType end = 0;
while (end < ps->size - 1)
{
ps->a[end] = ps->a[end + 1];
//将下标为1的有效数据到最后一个有效数据全部左移,也就是从前向后移动
++end;
}
ps->size--;*/
SLErase(ps, 0);
}
这样确实可行,但是这样调用效率会有所下降,为了还是有很多地方直接头删头插、尾删尾插的,所以大家都掌握比较好
还有一个玩法,在我们查找数据的时候对这个数据进行修改或者插入删除操作
int x = 0;
scanf("%d", &x);//输入一个值
int pos = SLFind(&s, x);//查找返回值的下标
if (pos != -1)//如果不等于-1表示找到对应的下标了
{
SL....(&s, pos);//各种函数的调用
}
SLPrint(&s);//打印
玩法其实还是有一些的。
三、全部代码
1、Seqlist.h
#pragma once//防止头文件被重复包含
#include<stdio.h>
#include<assert.h>//断言头文件
#include<stdlib.h>//动态内存开辟等操作头文件
下面的结构体之类的重命名是为了方便识别和使用,大家可以重命名为自己想要的名字
typedef int SLDataType;
//这里我们将int重命名为SLDataType,因为我们可能操作的数据是字符或者是浮点数
//所以将int重命名,如果是操作其他数据直接修改这里的int就行
typedef struct Seqlist
{
SLDataType* a;//指针
SLDataType size;//有效数据个数
SLDataType count;//容量
}SL;//将struct Seqlist重命名为SL,这样更加方便使用
以下是接口函数:
void SLInit(SL* ps);//初始化数据
void SLPrint(SL* ps);//打印数据
void SLDelete(SL* ps);//释放/销毁
void SLPushback(SL* ps, SLDataType x);//尾插数据
void SLPushfront(SL* ps,SLDataType);//头插数据
void SLPopback(SL* ps);//尾删数据
void SLPopfront(SL* ps);//头删数据
int SLFind(SL* ps, SLDataType x);//查找数据
void SLInsert(SL* ps, SLDataType pos, SLDataType x);//任意位置插入数据
void SLErase(SL* ps, SLDataType pos);//任意位置删除数据
void SLModity(SL* ps, SLDataType pos, SLDataType x);//修改数据
2、Seqlist.c
#define _CRT_SECURE_NO_WARNINGS
#include"Seqlist.h"
void SLInit(SL* ps)//初始化
{
assert(ps);
ps->a = NULL;//先将指针初始化为空
ps->size = ps->count = 0;//有效数据个数和容量初始化为0
//这里可以进行连等
}
void SLPrint(SL* ps)//打印
{
assert(ps);
for (int i = 0; i < ps->size; i++)//i小于ps->size也就是目前的有效数据个数
{
printf("%d->", ps->a[i]);//这里每打印一个数据后面加一个箭头更像顺序表
//ps->a[i]就是找到指针a,对指针a加i的地址解引用,得到下标为i的数据进行打印
}
printf("NULL\n");//这里打印完所有数据后再打印一个NULL然后换行
}
void SLDelete(SL* ps)//释放/销毁
{
assert(ps);
free(ps->a);//动态内存释放
ps->a = NULL;//指针置空
ps->size = ps->count = 0;//有效数据个数和容量置0
}
void SLChack(SL* ps)//扩容
{//下面插入数据操作进行之前先调用扩容函数判断需不需要扩容
if (ps->size == ps->count)//如果有效数据个数等于容量就扩容
{//这里的SLDataType就是我们之前重命名的int
SLDataType newcount = ps->count == 0 ? 4 : ps->count * 2;
//这里的newcount可以理解为新的容量。
//这段代码的意思是:ps->count也就是旧的容量是不是0,如果是就让旧容量的空间开辟4个整型空间,然后赋值给新容量
//如果旧容量不是0,而且需要扩容就扩容两倍然后赋值给新容量
SLDataType* p = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * newcount);
//这里就是动态内存扩容的操作,扩容的空间大小就是新容量的空间大小
if (p == NULL)
{
perror("realloc fail");
return;
}
else
{
ps->a = p;//扩容成功后将扩容的地址赋值给原来的指针,由原来指针进行维护
ps->count = newcount;//再将新容量赋值给旧容量。至此扩容完成
}
}
}
void SLPushback(SL* ps, SLDataType x)//尾插
{
/*assert(ps);
SLChack(ps);//判断是否需要扩容
ps->a[ps->size] = x;
//将x数据插入ps->size的位置即可,因为原来有效数据是ps->size个,但是下标最大是ps->size-1
//将x插入到下标ps->size就是相当于在最后插入了数据
ps->size++;*/
SLInsert(ps, ps->size, x);
}
void SLPushfront(SL* ps,SLDataType x)//头插
{
/*assert(ps);
SLchack(ps);
SLdatatype end = ps->size - 1;//end就是最后一个有效数据的下标
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
//将下标0到下标ps->size-1的元素全部右移,也就是从后向前移动
--end;
}
ps->a[0] = x;//将下标为0的地址插入x数据
ps->size++;*/
SLInsert(ps, 0, x);
}
void SLPopback(SL* ps)//尾删
{
/*assert(ps && ps->size > 0);//断言一下,如果有效数据个数大于0就可以进行删除
if (ps->size == 0)
{
exit(-1);//这种是暴力判断,如果有效数据个数为0,直接终止整个程序
}
ps->size--;//尾删直接把有效数据个数减一就行*/
SLErase(ps, ps->size - 1);
}
void SLPopfront(SL* ps)//头删
{
/*assert(ps);
assert(ps->size > 0);
SLDataType end = 0;
while (end < ps->size - 1)
{
ps->a[end] = ps->a[end + 1];
//将下标为1的有效数据到最后一个有效数据全部左移,也就是从前向后移动
++end;
}
ps->size--;*/
SLErase(ps, 0);
}
void SLInsert(SL* ps, size_t pos, SLDataType x)//任意位置插入
//pos是下标,x是插入数据
{//size_t pos;下标不会是负数
assert(ps && pos <= ps->size);//pos=ps->size就是尾插,无影响
SLChack(ps);
size_t end = ps->size;//这里end与pos类型对应,无符号整型
while (end > pos)
{
ps->a[end] = ps->a[end - 1];
//如果end下标大于pos下标,那么就将pos到end中间的数据全部右移
--end;
}
ps->a[pos] = x;//插入x
ps->size++;
}
void SLErase(SL* ps, SLDataType pos)//任意位置删除
{
assert(ps && pos < ps->size);
SLDataType end = pos;
while (end < ps->size - 1)//如果不是删除最后一个有效数据
{
ps->a[end] = ps->a[end + 1];//把删除数据右边的数据全部左移
++end;
}
ps->size--;
}
int SLFind(SL* ps, SLDataType x)//查找
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
return i;
}
return -1;
}
void SLModity(SL* ps, SLDataType pos, SLDataType x)//修改
{
assert(ps && pos < ps->size);
ps->a[pos] = x;//直接把下标对应值修改即可
}
3、test.c
#define _CRT_SECURE_NO_WARNINGS
#include"Seqlist.h"
void TestSL1()
{
SL s;
SLInit(&s);//初始化
SLPushback(&s, 1);//尾插
SLPushback(&s, 2);
SLPushback(&s, 3);
SLPushback(&s, 4);
SLPushback(&s, 5);
SLPushback(&s, 6);
SLPrint(&s);//打印
SLPushfront(&s, 10);//头插
SLPushfront(&s, 20);
SLPushfront(&s, 30);
SLPushfront(&s, 40);
SLPrint(&s);
SLPopback(&s);//尾删
SLPopback(&s);
SLPopback(&s);
SLPrint(&s);
SLPopfront(&s);//头删
SLPopfront(&s);
SLPopfront(&s);
SLPrint(&s);
SLInsert(&s, 1, 10);//任意位置插入
SLPrint(&s);
SLErase(&s, 3);//任意位置删除
SLPrint(&s);
SLModity(&s, 3, 30);//任意位置修改
SLPrint(&s);
SLDelete(&s);
}
void menu()
{
printf("-------------------------------\n");
printf("***0、退出程序 5、打印数据***\n");
printf("***1、尾插数据 2、头插数据***\n");
printf("***3、尾删数据 4、头删数据***\n");
printf("***6、任意插入 7、任意删除***\n");
printf("***8、查找数据 9、修改数据***\n");
printf("-------------------------------\n");
}//大家也可以用枚举的方法来,我这里没有用枚举
int main()
{
SL s;
SLInit(&s);//初始化
int n = 0;
do
{
menu();
printf("请选择操作:");
int x = 0;
int y = 0;
scanf("%d", &n);
switch (n)
{
case 1:
printf("请输入尾插数据,以-1截止\n");
do
{
scanf("%d", &x);
if (x != -1)
{
SLPushback(&s, x);
}
} while (x != -1);
break;
case 2:
printf("请输入头插数据,以-1截止\n");
do
{
scanf("%d", &x);
if (x != -1)
{
SLPushfront(&s, x);
}
} while (x != -1);
break;
case 3:
SLPopback(&s);
break;
case 4:
SLPopfront(&s);
break;
case 5:
SLPrint(&s);
break;
case 6:
printf("请输入插入下标和插入数据\n");
scanf("%d %d", &x, &y);
SLInsert(&s, x, y);
break;
case 7:
printf("请输入删除数据下标\n");
scanf("%d", &x);
SLErase(&s, 3);
break;
case 8:
printf("请输入查找数据下标\n");
scanf("%d", &x);
SLFind(&s, x);
printf("x下标数据为:%d\n", s.a[x]);
break;
case 9:
printf("请输入修改下标和修改数据\n");
scanf("%d %d", &x, &y);
SLModity(&s, x, y);
break;
case 0:
SLDelete(&s);
printf("退出程序\n");
break;
default:
printf("输入错误,请重新输入:");
}
} while (n);
return 0;
}
//int x = 0;
//scanf("%d", &x);//输入一个值
//int pos = SLFind(&s, x);//查找返回值的下标
//if (pos != -1)//如果不等于-1表示找到对应的下标了
//{
// SL....(&s, pos);//各种函数的调用
//}
//SLPrint(&s);//打印
四、总结
顺序表其实比较简单,大家多敲就能掌握玩法。再小编的数据结构实现可能与大家的有所差异,但是目的都是一样的,这些大家不用在意,不管黑猫白猫,能抓老鼠的就是好猫。我们第一步只要能写出来,然后去学习其他人优秀的代码,相互取长补短就行。好了,本期内容就到这里了,我们下期见!