线性结构__顺序及链式存储——慕课笔记
代码来源:https://www.icourse163.org/course/ZJU-93001#/info
2.1线性表及其实现
线性表:由同类型数据元素构成有序序列的线性结构
长度:表中元素的个数(表中没有元素——空表)
表头:表起始位置;表尾:表结束位置
抽象数据类型表示(ADT)
- 数据对象集
- 操作集
2.1.1多项式表示
多项式的关键数据:多项式系数n;各项系数ai及指数i
-
顺序存储结构直接表示(一维数组)
- 数组各分量对应多项式各项(下标——指数;数组元素——系数)
- 顺序存储结构表示非零项(结构数组)
- 链表结构存储非零项
2.1.2线性表的顺序存储实现
利用数组的连续存储空间顺序存放线性表的各元素
-
数据对象集的定义
- 访问下标为i的元素:Ptrl->Data[i]或L.Data[i]
- 线性表的长度:L.Last**+1** 或 PtrL->Last**+1**
注释:不是真正意义的表,一个结构里就已经包含了一个数组,而一个数组包含了全部存储的元素
typedef struct LNode *List;
struct LNode{
ElementType Data[MAXSIZE];
int last;//表尾 从0开始
};
struct LNode L;
List Ptrl
- 初始化
List MakeEmpty( )
{
List PtrL;
PtrL = (List )malloc( sizeof(struct LNode) );
PtrL->Last = -1;// -1代表此时链表没有元素
return PtrL;//返回指向表头的指针
}
- 查找元素(返回存储位置:下标)
int Find( ElementType X, List PtrL )
{
int i = 0;
while( i <= PtrL->Last && PtrL->Data[i]!= X )
i++;
if (i > PtrL->Last) return -1; /* 如果没找到,返回-1 */
else return i; /* 找到后返回的是存储位置 */
}
- 插入元素(先移动后插入)
- 移动情况:倒叙向后移动
- 边界条件
- 插入位置不合法(i < 1或 i > Last+2)
- 线性表位置已满(Last == MAXSIZE -1)
void Insert( ElementType X, int i, List PtrL ){
if (Ptrl->Last == MAXSIZE-1){
printf("表满");
return;// void 没有返回值
}
if (i < 1 || i > Ptrl->Last+2){
printf("位置不合法");
return;
}
//移动元素 倒叙向后移动
for (int j = Ptrl->Last; j >= i-1 ; --j)//第i个位置对应下标为i-1
Ptrl->Data[j+1] = Ptrl->Data[j];
Ptrl->Data[i-1] = X;//插入新元素
++Ptrl->Last;//表长加1
return;
}
-
删除元素(直接移动即可)
- 移动情况:顺序向前移动
- 边界条件:删除位置不合法(i < 1或 i > Last+1)
void Delete( int i, List PtrL ){ if (i < 1 || i > Ptrl->Last+1){ printf("不存在第%d个元素",i); return; } //移动元素 顺序向前移动 for (int j = i-1; j <= Ptrl->Last ; ++j)//第i个位置对应下标为i-1 Ptrl->Data[j] = Ptrl->Data[j+1]; --Ptrl->Last;//表长加1 return; }
2.1.3线性表的链式存储实现
逻辑上相邻的两个元素物理上不要求相邻;通过"链"建立起数据元素之间的逻辑关系
- 插入、删除元素不需要挪动元素,只需要修改链
- 数据对象集的定义
typedef struct LNode *List;
struct LNode{
ElementType Data;
List Next;//指向下一个结点的指针
};
struct Lnode L;
List PtrL;
- 求表长
int Length (const List PtrL )//指向表头的指针
{
List p = PtrL; /* p指向表的第一个结点*/
int j = 0;
while ( p ) {
//p≠NULL,则链表还未到结尾
p = p->Next;
j++; /* 当前p指向的是第 j 个结点*/
}
return j;
}
-
查找
- 按序号查找:FindKth(查找第K序号的元素)
- 成功:返回指向该节点的指针
- 失败:返回空指针
- 失败一:空表
- 失败二:序号超出链表的最大范围
- 非法输入:查找位置不合法(i < 1或 i > Last+1)
List FindKth( int K, List PtrL ){ List p = PtrL; int i = 1; while (p !=NULL && i < K ){ p = p->Next; i++; } if ( i == K ) return p; /* 找到第K个,返回指针 */ else return NULL; /* 否则返回空 */ }
- 按值查找: Find
- 成功:返回指向该节点的指针
- 失败:返回空指针
- 失败一:空表
- 失败二:找到最后一个结点还没找到该值
- 失败:返回空指针
- 按序号查找:FindKth(查找第K序号的元素)
List Find( ElementType X, List PtrL )
{
List p = PtrL;
while ( p!=NULL && p->Data != X )
p = p->Next;
return p;
}
-
插入
- 先构造一个新结点,用s指向
- 再找到链表的第 i-1个结点,用p指向
- 然后修改指针,插入结点 ( p之后插入新结点是 s)
- 边界条件:新结点插入表头(不注意的话会导致调用FindKth函数出错)
List Insert( ElementType X, int i, List PtrL )
{
List p,s;
if (i == 1){
//boundary condition 新结点插入表头
s = (ist)malloc(sizeof(struct Node));//申请新结点
s->Data = X;//填装新结点
s->Next = Ptrl;
return s;
}
p = FindKth( i-1, List PtrL );//找到第i-1个结点
if (p == NULL){
//找不到第i-1个结点
printf("参数i错");
return NULL;//返回空指针
}else{
s = (ist)malloc(sizeof(struct Node));//申请新结点
s->Data = X;//填装新结点
s->Next = p->Next;
p->next = s;
return Ptrl;//返回指向表头的指针
}
}
- 删除 (第i个结点)
- 边界条件(Attention)
- 删除结点为第一个结点
- 删除结点为末尾结点的下一个结点
- 步骤
- 先找到链表的第i-1个结点,用p指向
- 再用s指针要被删除的结点(p的下一个结点)
- 修改p的指向(p->Next = s->Next)
- 释放s所指向结点的空间
List Delete( int i, List PtrL )
{
List p, s;
if ( i == 1 ) {
/* 若要删除的是表的第一个结点 */
s = PtrL; /*s指向第1个结点*/
if (PtrL!=NULL) PtrL = PtrL->Next; /*从链表中删除*/
else return NULL;//空表
free(s); /*释放被删除结点 */
return PtrL;
}
p = FindKth( i-1, PtrL ); /*查找第i-1个结点*/
if ( p == NULL ) {
printf(“第%d个结点不存在”, i-1); return NULL;
} else if ( p->Next == NULL ){
printf(“第%d个结点不存在”, i);
return NULL;
} else {
s = p->Next; /*s指向第i个结点*/
p->Next = s->Next; /*从链表中删除*/
free(s); /*释放被删除结点 */
return PtrL;
}
}
2.2堆栈
2.2.1什么是堆栈(stack)
堆栈:具有一定操作约束的线性表
- 操作约束:只在一端(Top,栈顶)做插入、删除
- 插入数据:PUSH
- 删除数据:POP
- 遵循原则:LIFO(Last In First Out)
- 堆栈的抽象数据类型描述
- 数据对象集:一个有0个或多个元素的有穷线性表
- 操作集:长度为MaxSize的堆栈S∈Stack,堆栈元素item∈ElementType
2.2.2堆栈的顺序存储实现
由一个一维数组和一个记录栈顶元素位置(下标)的变量组成
#define MaxSize <储存数据元素的最大个数>
typedef struct SNode *Stack;
struct SNode{
ElementType Data[Maxsize];
int top;//记录栈顶元素的位置
};
void Push( Stack PtrS, ElementType item )
{
if ( PtrS->Top == MaxSize-1 ) {
printf(“堆栈满”); return;
}else {
PtrS->Data[++(PtrS->Top)] = item;//指向栈顶的下标先加一,后插入元素
return;
}
}
- 出栈(先弹出元素,然后指向栈顶的下标再减一)
ElementType Pop( Stack PtrS )
{
if ( PtrS->Top == -1 ) {
printf(“堆栈空”);
return;
}else
return PtrS->Data[(PtrS->Top)--] = item;
}
2.2.3堆栈的链式存储实现
栈的链式存储结构实际上就是一个单链表,叫做链栈。插入和删除操作只能在链栈的栈顶进行。
注:对于单向链表:只能在表头一端进行插入和删除的操作。表尾不可(删除操作后不能找到前一个结点)
- 2.2.3.1数据对象集的定义
typedef struct SNode *Stack;
struct SNode{
ElementType Data;
struct SNode *Next;//指向下一结点
};
-
2.2.3.2堆栈的链式存储的相关操作
-
初始化栈堆(头结点不放元素——只起链接作用)
- 堆栈初始化(建立空栈)
- 判断堆栈s是否为空
Stack CreateStack() { /* 构建一个堆栈的头结点,返回指针 */ Stack S; S =(Stack)malloc(sizeof(struct SNode)); S->Next = NULL; return S; } bool IsEmpty(Stack S) { /*判断堆栈S是否为空,若为空函数返回true,否则返回false */ return ( S->Next == NULL ); }
-
入栈(链表可无限延伸,不用判断栈堆是否满)
void Push( ElementType item, Stack S){
struct Stack TmpCell;
TmpCell = (struct SNode *)malloc(sizeof(struct SNode));
TmpCell->Element = item;//填充新结点
TmpCell->Next = S->Next;
S->Next = TmpCell;
}
- 出栈
ElementType Pop(Stack S){
struct Stack FirstCell;
ElementType TopElem;
if ( S ){
printf("堆栈空");return ERROR;
}else{
FirstCell = S->Next;//指向栈顶元素
S->Next = FirstCell->Next;
TopElem = FirstCell->Element;//弹出元素
free(FirstCell);
return TopElem;
}
};
2.2.4堆栈应用
-
请用一个数组实现两个堆栈,要求最大地利用数组空间,使数组只要有空间入栈操作就可以成功。
-
分析
- 两个堆栈分别从数组头和尾插入元素,腾出中部未使用的空间用于入栈元素
- 如何判别堆栈满:当两个栈的栈顶指针相遇(PtrS->Top2 – PtrS->Top1 == 1)时,表示两个栈都满
-
具体实现
- 定义
#define MaxSize <存储数据元素的最大个数> struct DStack { ElementType Data[MaxSize]; int Top1; /* 堆栈1的栈顶指针 */ int Top2; /* 堆栈2的栈顶指针 */ }S; //栈堆1,2空 //S.Top1 = -1; //S.Top2 = MaxSize;
- 入栈
void Push( struct DStack *PtrS, ElementType item, int Tag ){ /* Tag作为区分两个堆栈的标志,取值为1和2 */ if (PtrS->Top2 – PtrS->Top1 == 1){ printf("栈堆满"); return; } if (Tag == 1)//对第一个栈堆操作 PtrS->Data[++(PtrS->Top1)] = item; else //对第二个栈堆操作 PtrS->Data[--(PtrS->Top2)] = item; }
- 出栈(根据Tag讨论不同栈堆是否堆栈空)
ElementType Pop( struct DStack *PtrS, int Tag ){ /* Tag作为区分两个堆栈的标志,取值为1和2 */ if (Tag == 1){ if (Ptrs->Top1 == -1){ printf("栈堆1为空");return ERROR;//ERROR 预先定义的错误返回信息 } else return PtrS->Data[(PtrS->Top1)--]; }else{ if (Ptrs->Top2 == -1){ printf("栈堆2为空");return ERROR; }else return PtrS->Data[(PtrS->Top2)++]; } }
-
-
求表达式的值(后缀表达式求值)
从头到尾读取中缀表达式的每个对象,对不同对象按不同的情况处理。
- 后缀表达式:运算符号位于两个运算数之后
- 好处:利用堆栈即可求解表达式的值,不用判别左右运算符的优先级(不用判别到底先算谁,要不要等),直接见到运算符号就取运算数就行
- 运算数:直接输出
- 左括号:压入堆栈
- 右括号:将栈顶的运算符弹出并输出,直到遇到左括号(出栈,不输出)
- 运算符:
- 若优先级大于栈顶运算符时,则把它压栈
- 若优先级小于等于栈顶运算符时,将栈顶运算符弹出并输出;再比较新的栈顶运算符,直到该运算符大于栈顶运算符优先级为止,然后将该运算符压栈;
- 若各对象处理完毕,则把堆栈中存留的运算符一并输出。
2.3队列
具有一定操作约束的线性表
- 插入和删除操作:只能在一端插入,而在另一端删除
- 遵循原则:FIFO(First In First Out)
2.3.1队列的顺序存储实现
由一个一维数组和一个记录队列头元素位置的变量front以及一个记录队列尾元素位置的变量rear组成。
- 数据对象集的定义
- 队列头指向头一个元素的前一个元素
- 队列尾指向最后一个元素
#define MaxSize <储存数据元素的最大个数>
typedef struct QNode *Queue;
struct QNode {
ElementType Data[ MaxSize ];
int rear;//队列头元素位置
int front;//队列尾元素位置
};
-
数据对象集的相关操作
-
入队列
- 易错:PtrQ->Data[rear++] = item;
void AddQ( Queue PtrQ, ElementType item){
if((PtrQ->rear+1)% MaxSize == PtrQ->front ){
printf("队列满"); return;
}else{
PtrQ->rear = (PtrQ->rear+1)% MaxSize;
PtrQ->Data[PtrQ->rear] = item;
}
}
- 出队列
ElementType DeleteQ ( Queue PtrQ ){
if ( PtrQ->front == PtrQ->rear ) {
printf(“队列空”);
return ERROR;
} else {
PtrQ->front = (PtrQ->front+1)% MaxSize;//如果front=5,通过该变化使其又变成0
return PtrQ->Data[PtrQ->front];
}
}
注意:
-
如何识别队列空OR满?
- 法一:队列不放满元素(只放n-1个元素,留一个空的位置),利用front和rear的相对距离来判别
- 法二:另外增设一个变量。如记录当前队列数量的Size;或记录最后一次操作是删除OR插入的Flag
-
如何实现循环队列?
利用加1取余表达式——>Front和rear的数值范围一直都在[0,n-1]
-
front与rear指向的位置?
- front:指向队列首元素的前一个位置(假设a[0]有元素,则front = -1)
- rear:指向队列尾元素(尾元素a[2],则rear=2)
2.3.2队列的链式存储实现
以用一个单链表实现。插入和删除操作分别在链表的两头进行
- 链头:队列指针front(删除元素方便,很容易找到下一个元素)
- 链尾:队列指针rear(插入元素方便,删除不可行,不能找到上一个元素)
- 数据对象集的定义
- 队列头指向头一个元素
- 队列尾指向最后一个元素
struct Node{
//存放元素的结点
ElementType Data;
struct Node *Next;
};
struct QNode{
/* 链队列结构 */
struct Node *rear; /* 指向队尾结点 */
struct Node *front; /* 指向队头结点 */
};
typedef struct QNode *Queue;
Queue PtrQ;
- 数据对象集的相关操作
- 入队列
void AddQ( Queue PtrQ, ElementType item){
{
struct Node *TmpCell;
TmpCell = (struct Node*)malloc(sizeof(struct Node));
TmpCell->Data = item;//填充新结点
if ( PtrQ->front == NULL) {
//若队列为空
PtrQ->front = PtrQ->rear = Tmpcell;
}else
PtrQ->rear = Tmpcell;;//指向队列尾的下一个元素
return;
}
- 出队列(注意只有一个元素的临界情况)
ElementType DeleteQ ( Queue PtrQ )
{
struct Node *FrontCell;
ElementType FrontElem;
if ( PtrQ->front == NULL) {
printf(“队列空”); return ERROR;
}
FrontCell = PtrQ->front;
if ( PtrQ->front == PtrQ->rear) /* 若队列只有一个元素 */
PtrQ->front = PtrQ->rear = NULL; /* 删除后队列置为空 */
else
PtrQ->front = PtrQ->front->Next;//指向队列头的下一个元素
FrontElem = FrontCell->Data;
free( FrontCell ); /* 释放被删除结点空间 */
return FrontElem;
}
2.4应用实例
2.4.1多项式加法运算
主要思路:相同指数的项系数相加,其余部分进行拷贝
- 储存方法:采用不带头结点的单向链表,按照指数递减的顺序排列各项
struct PolyNode {
int coef; // 系数
int expon; // 指数
struct PolyNode *link; // 指向下一个节点的指针
};
typedef struct PolyNode *Polynomial;
Polynomial P1, P2;
- 实现算法
- 两个指针P1和P2分别指向这两个多项式第一个结点,不断循环
- 循环判定条件:同指数则合并系数;不同指数则取指数大的项存入结果多项式(同时指针向后移一项)
- 当某一多项式处理完时,将另一个多项式的所有结点依次复制到结果多项式中去
Polynomial PolyAdd (Polynomial P1, Polynomial P2){
Polynomial front, rear, temp;
int sum;
//建立临时空表头结点————>方便插入结点
rear = (Polynomial) malloc(sizeof(struct PolyNode));
front = rear; /* 由front 记录结果多项式链表头结点 */
while ( P1 && P2 ) /* 当两个多项式都有非零项待处理时 */
switch ( Compare(P1->expon, P2->expon) ) {
case 1:
Attach( P1->coef, P1->expon, &rear);
P1 = P1->link;
break;
case -1:
Attach(P2->coef, P2->expon, &rear);
P2 = P2->link;
break;
case 0:
sum = P1->coef + P2->coef;
if ( sum ) Attach(sum, P1->expon, &rear);
P1 = P1->link;
P2 = P2->link;
break;
}
/* 将未处理完的另一个多项式的所有节点依次复制到结果多项式中去 */
for ( ; P1; P1 = P1->link ) Attach(P1->coef, P1->expon, &rear);
for ( ; P2; P2 = P2->link ) Attach(P2->coef, P2->expon, &rear);
rear->link = NULL;
temp = front;
front = front->link; /*令front指向结果多项式第一个非零项 */
free(temp); /* 释放临时空表头结点 */
return front;
}
- 大小比较函数
int Compare(const int expon1,const int expon2){
if(expon1 > expon2)
return 1;
else if (expon1 < expon2)
return -1;
else (expon1 == expon2)
return 0;
}
- 结点接到尾巴的函数
void Attach( int c, int e, Polynomial *pRear )//结点指针的地址————>为了修改其值
{
/* 由于在本函数中需要改变当前结果表达式尾项指针的值, */
/* 所以函数传递进来的是结点指针的地址,*pRear指向尾项*/
Polynomial P;
P =(Polynomial)malloc(sizeof(struct PolyNode)); /* 申请新结点 */
P->coef = c; /* 对新结点赋值 */
P->expon = e;
P->link=NULL;
/* 将P指向的新结点插入到当前结果表达式尾项的后面 */
(*pRear)->link = P;
*pRear = P; /* 修改pRear值 */
}
2.5小白专场
一元多项式的加法与乘法运算
2.5.1.1多项式表示(仅表示非零项)
- 链表:动态性强;但编程略为复杂、调试比较困难
- 数组:编程简单,调试容易;但需要实现确定数组的大小
//链表的表示
//每个结点存放每项的指数以及系数
typedef struct PolyNode *Polynomial;
struct PolyNode{
int coef;//系数
int expon;//指数
Polynomial link;//指向下一点结点的指针
};
2.5.1.2程序框架
int main()
{
Polynomial P1, P2, PP, PS;
P1 = ReadPoly();
P2 = ReadPoly();
PP = Mult( P1, P2 );//多项式乘法结果
PrintPoly( PP );
PS = Add( P1, P2 );//多项式加法结果
PrintPoly( PS );
return 0;
}
2.5.1.3读多项式
-
Q: Rear 初值是多少?
-
思路1:Rear初始值为NULL,在Attach函数中根据Rear是否为NULL做不同处理(建立空链表——没有任何结点)
-
思路2:Rear指向一个空结点,Attach函数实现较为简单,不用判断Rear的值(建立带有一个空结点的链表)
Polynomial ReadPoly()
{
Polynomial P, Rear, t;
scanf("%d", &N);//多项式的项数
//建立带有一个空结点的链表
P = (Polynomial)molloc(sizeof(struct Polynomial));
P->link = NULL;
Rear = P;
while ( N-- ) {
scanf("%d %d", &c, &e);
Attach(c, e, &Rear);
}
t = P;P = P->link;free(t);//删除临时建立的表头空结点
return P;
}
- Attach的实现(思路2)
void Attach( int c, int e, Polynomial *pRear )
{
Polynomial P;
P = (Polynomial)malloc(sizeof(struct PolyNode));
P->coef = c; /* 对新结点赋值 */
P->expon = e;
P->link = NULL;
(*pRear)->link = P;
*pRear = P; /* 修改pRear值 */
}
2.5.1.4加法实现
Polynomial Add( Polynomial P1, Polynomial P2 )
{
……
t1 = P1; t2 = P2;
P = (Polynomial)malloc(sizeof(struct PolyNode)); P->link = NULL;
Rear = P;
while (t1 && t2) {
if (t1->expon == t2->expon) {
if (t1->coef + t2->coef ) Attach(t1->coef + t2->coef,t1->expon,&Rear);
else{
t1 = t1->link; t2 = t2->link;
}
}
else if (t1->expon > t2->expon) {
Attach(t1->coef,t1->expon,&Rear);
t1 = t1->link;//P1指针向后移
}
else {
Attach(t2->coef,t2->expon,&Rear);
t2 = t2->link;//P2指针向后移
}
while (t1) {
Attach(t1->coef,t1->expon,&Rear);
t1 = t1->link;//P1指针向后移
}
while (t2) {
Attach(t2->coef,t2->expon,&Rear);
t2 = t2->link;//P2指针向后移
}
t = P; P= P->link ; free(t);//释放表头空结点
return P;
}
2.5.1.5乘法实现
- 方法一
将乘法运算转变为加法运算
- 将P1当前项(ci,ei)乘P2多项式,再加到结果多项式里
- 遍历P1所有项,对P1每一项循环操作步骤一(黑->红->蓝->绿)
- 最后对所有的结果多项式合并∑Pi(P1每一个当前项乘P2多项式即为一个结果多项式Pi)
- 注意:不是只利用一个结果多项式来存放结果,否则后期无法合并
t1 = P1; t2 = P2;
P = (Polynomial)malloc(sizeof(struct PolyNode));
P->link = NULL; Rear = P;
while (t2) {
//求出P1当前项(ci,ei)乘P2多项式的结果多项式Pi
Attach(t1->coef*t2->coef, t1->expon+t2->expon, &Rear); t2 = t2->link; //系数相乘,指数相加
}
- 方法二:逐项加入
将P1当前项(c1i,e1i)乘P2当前项(c2i,e2i),并插入到结果多项式中
关键:找到插入位置(查找)
初始结果多项式可由P1第一项乘P2获得(如上)
Polynomial Mult( Polynomial P1, Polynomial P2 )
{
Polynomial P, Rear, t1, t2, t;
int c,e;
t1 = P1; t2 = P2;
//初始化结果多项式
if (!P1 || !P2) return NULL;//判断两个多项式是否为空
P = (Polynomial)malloc(sizeof(struct PolyNode));
P->link = NULL; Rear = P;
while (t2) {
/* 先用P1的第1项乘以P2,得到P */
Attach(t1->coef*t2->coef, t1->expon+t2->expon, &Rear);
t2 = t2->link;
}
t1 = t1->link;
/*将每一项结果多项式*/
while (t1) {
//外层嵌套 P1多项式每一项
t2 = P2; Rear = P;
while (t2) {
//内层嵌套 P2多项式每一项
e = t1->expon + t2->expon;
c = t1->coef * t2->coef;
//查找插入位置(结果多项式按照指数递减排列)
while (Rear->link){
if ( Rear->link->expon > e)
Rear = Rear->link;
else if (Rear->link->expon == e){
int sum = Rear->link->c + c;
if (sum)
Rear->link->coef += c;
else{
//删除结点
t = Rear->link; Rear->link = t->link; free(t);
}
}
else{
//插入结点(插在Rear->link的前面)
t = (Polynomial)malloc(sizeof(struct PolyNode));
t->expon = e;t->coef = c;//填充结点
t->link = Rear->link; Rear->link = t;
//不能漏掉,因为最后新插入一个结点,rear要指向这个结点,否则指向的是该结点的前一结点
Rear = Rear->link;
}
}
//P2多项式的下一项乘积任务
t2 = t2->link;
}
t1 = t1->link;
}
//释放表头空结点
t = P; P= P->link ; free(t);
return P;
}
2.5.1.6多项式输出
void PrintPoly( Polynomial P )
{
/* 输出多项式 */
int flag = 0; /* 辅助调整输出格式用 */
if (!P) {
printf("0 0\n"); return;}//链表为空
while ( P ) {
//第一项前不输出空格,其余项前面输出空格
if (!flag)
flag = 1;
else
printf(" ");
printf("%d %d", P->coef, P->expon);
P = P->link;
}
printf("\n");
}
2.5.2总结收获
- 指针使用之前一定要判断是否为NULL,否则会出错
- 创建带有一个空结点的链表更方便结点的插入(不用判断结点是否要插在第一个位置,不用修改头指针的指向)