1、单链表中按位置查找
a.原理
通过传递的位置,返回该位置对应的地址,放到主函数定义的指针变量中。
我们认为位置从:有数据的节点开始计数
即如下结构:
查找位置,就是返回该位置对应的空间地址。
b.代码说明
Ⅰ.子函数名的说明
查找指定位置的元素的子函数,返回该位置对应的地址,即指针类型,所以返回值是结构体指针类型。
不需要修改链表中的数据,所以形参不需要引用。
用一个整型变量形参接收位置参数。
Ⅱ.定义结构体指针变量,使其指向第一个数据节点
代码如下
LNode* p = L->next;//此时p指向第一个元素节点。L指向的是头节点,使用->头节点内部的next域,得到第一个元素节点的地址
Ⅲ.子函数内部的判断
如果查找的位置是0,则默认返回的是头节点,也就不需要操作了,直接将传来的头指针返回即可。头指针内部放的就是头节点对应的地址。
若是位置小于0,则表示查找的位置不合法,即返回NULL。
代码如下
if (0 == i)//判断变量和常量是否相等,通常将常量写在左侧
{return L;
}
if (i < 0)
{return NULL;
}
Ⅳ.利用循环,找到对应的位置
利用while循环,使指针不断后移动,知道指向指定的位置即可。
循环的条件是:指针不为空,且当前位置小于指定的位置。
循环内执行的操作是:不断的将指针p重新赋值为当前指针执行的空间中的next值,再进行位置的++操作。
当循环结束后,此时的p刚好指向指定的位置空间。
代码如下
while (p && j < i)
{p = p->next;j++;
}
Ⅴ.循环结束后,返回指向指定位置的指针
代码如下
return p;
Ⅵ.整体代码如下
LinkList GetElem(LinkList L, int i)//我们认为位置从有数据的元素节点开始计数。
{int j = 1;LNode* p = L->next;//此时p指向第一个元素节点。L指向的是头节点,使用->头节点内部的next域,得到第一个元素节点的地址if (0 == i)//判断变量和常量是否相等,通常将常量写在左侧{return L;}if (i < 0){return NULL;}while (p && j < i){p = p->next;j++;}return p;
}
2、单链表中按值查找
a.原理
利用循环,便利链表,在子函数中定义结构体指针变量,使其指向第一个数据节点,获取该节点中的数据,与指定的数据进行相比:
若相等则返回该节点对应的地址;
若是不等,则将指针赋值为当前节点的next值,使其指向下一个节点。利用循环,再次比较。
b.子函数代码说明
Ⅰ.子函数名的说明
最终返回的是该值所在的节点的地址,所以需要返回的是一个结构体指针类型:LinkList
。
因为不需要修改链表中的数据,所以不需要引用。
查找的值的类型必须与传递的值的类型一致,所以需要定义一个相同类型的形参进行接收。
代码如下
LinkList LocateElem(LinkList L, ElemType e)
Ⅱ.在子函数内部定义一个结构体指针类型变量
使该指针变量指向第一个数据元素。
代码如下
LinkList p = L->next;
Ⅳ.利用while循环进行遍历链表
while
循环的条件就是指针不为空。
在循环内判断该指针对应的data域与指定的值是否相同,相同则返回该指针,不同则将指针赋值为当前指针的next
域。再次循环。
代码如下
while (p)
{if (p->data == e){return p;}p = p->next;
}
3、单链表插入元素
α.在中间插入元素
a.原理
申请新的节点空间,将插入的数据放到该空间的数据域中。
然后再找到插入的位置的地址,将申请的节点的next
域赋值为找到的地址。(即实现原位置数据后移操作)
最后将存放找到的地址的变量重新赋值为刚申请的节点的地址。(即实现节点插入操作)
利用查找位置的函数,传入的是该位置对应的下标,所以需要将位置-1
,得到下标后再传入。
查找位置的函数,返回的就是插入位置的前一个节点的地址,将返回的地址放到p指针中,通过p指针访问next域,才能得到插入位置的地址。
向第i
个位置插入数据,利用GetElem
函数找到第i-1的位置的地址,而不是找到第i
个位置的原因:
若是返回第i个位置地址,此时直接访问data
域,则会覆盖原来的数据。
b.代码说明
Ⅰ.子函数名的说明
调用该函数是否插入成功,所以返回一个bool
类型,即返回值为bool
类型。
不需要改变链表中的值,所以不需要引用链表。
用一个整型变量,用于存放插入的指定位置。
定义一个与插入类型相同的变量,用于接收插入的数据。
代码如下
bool ListFrontInsert(LinkList L, int i, ElemType e)
插入到第i
个位置,就必须拿到第i
个位置的地址,第i
个位置的地址放在第i-1
节点的next
域中。所以需要借助按位置查找函数,找到第i-1
位置的节点对应的地址。
所以需要定义一个结构体节点指针,用于接收按位置查找的返回值,即插入位置的前一个位置的地址。
代码如下:
LinkList p = GetElem(L, i - 1);
Ⅱ.找到插入位置i
前一个位置的地址
找到插入位置的前一个节点的地址,利用GetElem
函数即可。所以当向第i个位置插入数据时,就需要先找到第i-1位置对应的地址。GetElem
函数返回的是指向指定位置的指针,所以我们要找前一个位置的指针时,传入的数据需要-1
。
代码如下
LinkList p = GetElem(L, i - 1);
此时返回的指针p,内部的地址就是插入位置的前一个节点的地址。
Ⅲ.判断插入位置的前一个节点指针
若是为空,则返回false,不为空后才可以继续插入。
代码如下
if (NULL == p)
{return false;
}
Ⅳ.申请节点空间
代码如下
LinkList s = (LNode*)malloc(sizeof(LNode));//为新插入的节点申请空间
Ⅴ.给新的节点数据域赋值
代码如下
s->data = e;
Ⅵ.将插入的新节点的next域赋值为找到的地址next域:实现后移操作
代码如下
s->next = p->next;
Ⅶ.将存放找到的地址的变量重新赋值为申请节点的地址
代码如下
p->next = s;
Ⅷ.操作成功后返回true
代码如下
return true;
Ⅸ.整体代码如下
找到第i个位置,并返回指向该位置的指针
LinkList GetElem(LinkList L, int i)//我们认为位置从有数据的元素节点开始计数。
{int j = 1;LNode* p = L->next;//此时p指向第一个元素节点。L指向的是头节点,使用->头节点内部的next域,得到第一个元素节点的地址if (0 == i)//判断变量和常量是否相等,通常将常量写在左侧{return L;}if (i < 0){return NULL;}while (p && j < i){p = p->next;j++;}return p;
}
向第i个位置插入数据
bool ListFrontInsert(LinkList L, int i, ElemType e)
{LinkList p = GetElem(L, i - 1);//这个p内部的是指向第i-1位置的地址,插入的是第i个位置if (NULL == p){return false;}LinkList s = (LNode*)malloc(sizeof(LNode));//为新插入的节点申请空间s->data = e;s->next = p->next;//实现后移操作p->next = s;//实现插入操作return true;
}
4、单链表删除节点
a.原理
利用GetElem()
函数,找到删除位置的前驱节点,返回该前驱节点的地址,保存该节点中next域的地址。这个地址指向的就是要删除的节点的地址。备份该节点的地址,将该节点中的next域的地址赋值到前驱节点的next域中,然后释放保存的节点的地址(即要删除的节点的地址,也就是前面备份的地址),再将原来用于备份该节点的地址的指针置空即可。
删除第2个位置的节点,
第一步:先找到指向第一个位置的指针,利用GetElem
函数查找即可。
备份第二个节点的地址,
第二步:再将第一个位置的next域赋值为第二个节点的next域值,
第三步:最后将释放第二个节点即可,利用第一步备份的地址访问到第二个节点。
若是不备份,则再第二步执行完后,会导致第二个节点的地址丢失,无法访问和释放第二个节点。
结构如下
若是不找到前驱节点的地址,则无法访问到前驱节点的next域,即无法完成第二步操作。所以必须借助GetElem()
函数,找到前驱节点的地址。
b.代码说明
Ⅰ.头文件的说明
返回一个bool
类型,用于说明删除成功与否。传入进行删除的链表名,一个记录删除位置的变量。
代码如下
bool ListDelete(LinkList L, int i)
Ⅱ.找到删除位置的前驱
代码如下
LinkList p = GetElem(L, i - 1);//找到删除位置的前驱节点
说明
此时的p指针,内部存放的就是指向删除节点的前面节点的地址。该指针指向的空间内部的next域,就是要删除的位置的节点对应的地址。
Ⅲ.判断前驱节点的指针是否存在
代码如下
if (NULL == p)//判断前驱节点是否存在
{return false;
}
说明
只要在后面需要使用指针,访问对应的域,都需要判断该指针是否为空。
Ⅳ.判断删除的位置是否存在
代码如下
LinkList q = p->next;//此时q指向的就是删除位置的节点对应的地址
if (NULL == q)
{return false;//要删除的位置不存在
}
说明
p
指针指向的是前驱节点的地址,p->next
指向的就是要删除的节点的地址。将其放在指针变量q中。若q
指针为空,则表示p
指向的就是最后一个节点的地址,无法进行删除操作。
Ⅴ.备份要删除的节点的地址
代码如下
LinkList q = p->next;//此时q指向的就是删除位置的节点对应的地址
Ⅵ.进行断链操作
代码如下
p->next = q->next;//断链
Ⅶ.释放要删除的节点空间
代码如下
free(q);//释放q指向的地址,即对应节点的空间
q = NULL;//将q这个指针置空
free()函数只是释放该指针指向的空间,没有改变该指针的指向,所以还需要将该指针置空,才算彻底结束。
Ⅷ.返回成功true
代码如下
return true;
Ⅸ.整体代码如下
bool ListDelete(LinkList L, int i)
{LinkList p = GetElem(L, i - 1);//找到删除位置的前驱节点if (NULL == p->next)//预防删除的位置是否存在{return false;}LinkList q = p->next;//此时q指向的就是删除位置的节点对应的地址p->next = q->next;//断链free(q);//释放q指向的地址,即对应节点的空间q = NULL;//将q这个指针置空return true;
}
5、双向链表
a.双向链表的说明
结构如下图
文字说明
相对于单链表,双链表比单链表多一个指针,用于指向该节点的前驱节点,即内部放的是前驱节点的地址。
单链表中的指针放的是该节点的后继节点的地址。
b.结构体定义如下
typedef struct DNode {ElemType data;//数据域struct DNode* prior;//前驱指针struct DNode* next;//后继指针 }DNode,*DLinkList;//进行重命名结构体和结构体指针
c.插入节点的说明
结构如下
文字说明
申请新的空间,并将该空间的地址放到结构体指针S中。
将新申请的节点,插入到第i个位置,先找到第i-1位置的节点,并将该节点的地址放到结构体指针P中。
①将新申请的节点的next赋值为前驱节点的next中的值,代码如下:S->next=P->next
。
②将后继节点的prior域重新赋值为新申请的节点地址,代码如下:P->next->prior=S
。
此时①②,实现了后继节点可以指向申请的空间,申请的空间也可指向后继节点:即新申请的空间的next
域放的是后继节点的地址,后继节点的piror
域放的是申请的空间的地址。
③将前驱节点的地址放到新申请的空间的prior域中,代码如下:S->prior=P
。
④将新申请的空间的地址放到前驱节点的next域中,代码如下:P->next=S
此时③④,实现了前驱节点的next域可以指向新申请的空间,新申请的空间的prior域可以指向前驱节点。即前驱节点空间中的next域放的是新申请的空间地址,新申请的空间的prior域放的是前驱节点的地址。
d.头插法新建双向链表
头插法的逻辑说明
新的节点的prior指向头节点;
新的节点的next指向头节点后面的一个节点;
头节点的next指向新的节点;
头节点后面的节点的prior指向新的节点。
即第一个申请的数据节点,再链表形成后会放到最后一个。(或者说每一个申请的数据节点,都直接放在头节点后面)这样的插入称为头插法。
Ⅰ.子函数名的说明
代码如下
DLinkList Dlist_head_insert(DLinkList& DL)
说明
返回一个结构体指针,函数参数引用主函数中的头指针DL
Ⅱ.新建结构体指针变量,用于接收申请的新空间地址
代码如下
DNode* s; int x;
说明
整型变量x
用于存放到节点数据域中的数据。
Ⅲ.申请新的节点空间
代码如下
DL = (DLinkList)malloc(sizeof(DNode))
;
说明
malloc()
函数用于申请空间,sizeof(DNode)
:用于计算申请的空间大小,单位是字节。内部是结构体名,表示申请和该结构体定义时一样的空间。将申请的空间强制类型转换为结构体指针类型,并将其保存在结构体指针变量DL
中。
DL
是头指针,此处申请的空间节点是头节点。
Ⅳ.将新申请的空间置空
代码如下
DL->prior = NULL;
DL->next = NULL;
说明
将指向前驱节点和指向后继节点的指针域置空。
Ⅴ.读取数据,利用循环进行申请空间并建立链表
代码如下
scanf("%d", &x);//从标准输入读取数据,放到变量中暂时存储
//3 4 5 6 7 9999
while (x != 9999)
{s = (DLinkList)malloc(sizeof(DNode));//此时申请的节点是数据节点s->data = x;s->next = DL->next;//①if (DL->next != NULL){DL->next->prior = s;//④}s->prior = DL;//使插入的节点指向前一个节点②DL->next = s;//使原来的节点指向新插入的节点③scanf("%d", &x);//继续读取数据
}
新申请第一个数据节点时结构如下:
第一个数据节点有三步:
①新的节点的next域放的是前一个节点的next中的内容。
②新的节点的prior域放的是头节点对应的地址,指向头节点。
③前一个节点的next域放的是新的节点的地址,指向新的节点对应的空间。
申请第二个及以后的数据节点时的结构如下:
总结:
①新申请的节点的next内是头节点后面的节点地址,指向头节点后面的节点
④头节点后面的一个节点的prior指向新申请的节点
②新申请的节点的prior内是头节点,
③头节点的next指向新申请的节点。
Ⅵ.返回头指针即可
代码如下
return DL;
Ⅶ.整体代码如下
DLinkList Dlist_head_insert(DLinkList& DL)
{DNode* s; int x;DL = (DLinkList)malloc(sizeof(DNode));//带头节点的链表,此处的节点是头节点,DL指向头节点DL->prior = NULL;DL->next = NULL;scanf("%d", &x);//从标准输入读取数据,放到变量中暂时存储//3 4 5 6 7 9999while (x != 9999){s = (DLinkList)malloc(sizeof(DNode));//此时申请的节点是数据节点s->data = x;s->next = DL->next;//若是DL->next内部是一个地址,则表示将该地址放到s->next中,即s->next访问的是存放在DL->next内部的地址对应的空间if (DL->next != NULL){DL->next->prior = s;}s->prior = DL;//使插入的节点指向前一个节点DL->next = s;//使原来的节点指向新插入的节点scanf("%d", &x);//继续读取数据}return DL;
}
e.双链表打印
代码同单链表一致
Ⅰ.子函数名的说明
代码如下
void PrintDList(DLinkList DL)
说明
不需要返回,不需要引用,直接用形参接收即可
Ⅱ.改变头指针指向,使其指向第一个数据节点
代码如下
DL = DL->next;//改变头指针指向,使其指向第一个数据节点
说明
此时的DL
指针指向第一个数据节点
Ⅲ.利用循环,先输出,在改变指针
代码如下
while (DL != NULL){printf("%3d", DL->data);DL = DL->next;}
说明
因为在开始已经使DL
指向用一个数据节点的地址,可以直接访问,所以循环处直接判断该指针是否为空即可
不为空时,先输出,直接访问进行输出即可,再改变DL
,使其指向下一个节点的地址,访问当前节点的next
域即可。
Ⅳ.整体代码如下
void PrintDList(DLinkList DL)
{DL = DL->next;//改变头指针指向,使其指向第一个数据节点while (DL != NULL){printf("%3d", DL->data);DL = DL->next;}printf("\n");
}
f.尾插法
结构说明
尾插法:每次新申请的节点,放在最后。
双链表的尾插法步骤说明:
先申请一个头节点,定义尾指针变量,初始化时令尾指针指向头节点。然后借助循环,在循环中申请新的数据节点,存放数据域,然后将尾指针的next域赋值为新的节点,新的节点的prior赋值为尾指针指向的地址,最后将尾指针赋值为新节点即可。离开循环后,将尾指针指向的节点的next域置空即可。
总结:最后形成的链表:头节点的prior为NULL,尾节点的next为NULL。
Ⅰ.子函数名的说明
代码如下
DLinkList Dlist_tail_insert(DLinkList& DL)
说明
返回一个结构体指针类型,引用主函数的头指针变量。
Ⅱ.定义变量,用于读取输入的数据域的值
代码如下
int x
Ⅲ.申请头节点的空间,放到头指针中
代码如下
DL=(DLinkList)malloc(sizeof(DNode))
说明
利用malloc()
申请新的空间,空间大小利用sizeof()
计算一个结构体大小即可。
malloc()
申请的空间无类型,需要强制类型转换为结构体指针类型后再赋值到头指针变量中。
Ⅳ.定义两个指针变量
代码如下
DNode*s,*r=DL;
说明
*s
指针,用于接收新申请的节点空间;*r
指针,用于记录尾指针。
注意:最初的尾指针,和头指针保持一致,指向第一个头节点。
Ⅴ.将头节点的prior置空
代码如下
DL->prior=NULL;
Ⅵ.读取数据
代码如下
scanf("%d",&x);
说明
后面将x变量中的数据放到数据域中即可。
Ⅶ.利用循环将数据放到数据域中,并不断申请新的节点
代码如下
while (x != 9999)
{s = (DLinkList)malloc(sizeof(DNode));//申请的第一个数据节点s->data = x;r->next = s;s->prior = r;r = s;//r指向新的表尾节点scanf("%d", &x);//继续读取新的数据
}
说明
在循环内,先申请一个节点空间,放到指针变量s中。然后给该节点的数据域进行赋值为x变量。
r指针开始指向的是头节点,给头节点的next赋值为新申请的节点地址。
将新申请的节点的prior域赋值为前一个节点的地址,也就是r指针保存的。
然后再使r指针指向最后一个节点,也就是新申请的节点s。
Ⅷ.循环结束后,将尾节点的next域赋值为空
代码如下
r->next = NULL;//尾节点的next指针赋值为NULL
说明
r
指针在每次循环结束时,都指向最后一个节点,所以在循环外,借助r
指针,直接访问next
域即可。
Ⅸ.最后返回头指针即可
代码如下
return DL;
Ⅹ.整体代码如下
DLinkList Dlist_tail_insert(DLinkList& DL)
{int x;//用于临时存放数据域的数据DL = (DLinkList)malloc(sizeof(DNode));DNode* s, * r;//r代表尾指针DL->prior = NULL;//3 4 5 6 7 9999scanf("%d", &x);while (x != 9999){s = (DLinkList)malloc(sizeof(DNode));//申请的第一个数据节点s->data = x;r->next = s;s->prior = r;r = s;//r指向新的表尾节点scanf("%d", &x);//继续读取新的数据}r->next = NULL;//尾节点的next指针赋值为NULLreturn DL;//返回头指针
}
g.双链表的查找节点
等同于单链表的查找节点
h.插入到指定位置
Ⅰ.子函数名的说明
代码如下
bool DListFrontInsert(DLinkList DL, int i, ElemType e)
说明
返回bool
类型,用于说明查找是否成功;
使用形参接收头指针DL
即可;
定义整型变量,用于记录插入的位置;
定义相同类型的变量,用于临时保存插入的数据。
Ⅱ.找到插入位置的前驱节点
代码如下
DLinkList p = GetElem(DL, i - 1);//找到该位置的前驱节点,返回的是前驱节点的地址指针
说明
找前驱节点是为了修改前驱节点的next域,保证插入后仍是一条完整的链表。
此时的p指针,指向的是插入位置的前驱节点。
Ⅲ.判断插入位置的前驱节点是否存在
代码如下
if (NULL == p)//注意:插入的位置可以不存在,但是插入的前驱节点一定存在:即插在最后一个节点的后面
{return false;//若是插入位置的前驱节点不存在,才报错
}
说明
若是插入位置的前驱节点不存在,则不能插入。插入的位置可以不存在,此时表示插入在最后一个节点的后面。
Ⅳ.申请节点空间
代码如下
DLinkList s = (DLinkList)malloc(sizeof(DNode));
说明
此时的s指针,指向新申请的节点。
Ⅴ.插入节点
代码如下
s->data = e;//存放数据
s->next = p->next;//实现新申请的节点指向后面的节点
p->next->prior = s;//实现原位置的节点指向新申请的节点
s->prior = p;//实现新申请的节点指向前驱节点
p->next = s;//实现前驱节点指向新申请的节点
说明
即需要完成:新申请的节点的next域存放原位置的空间地址;
改变原位置的节点的前面指向,使其指向新的节点;
此时完成新申请的节点与后面的节点相互指向。
在完成新申请的节点prior域存放前驱节点的地址;
前驱节点的next域存放新申请的节点的地址;
此时完成新申请的节点与前面的节点相互指向。
Ⅵ.返回true
代码如下
return true
Ⅶ.整体代码如下
bool DListFrontInsert(DLinkList DL, int i, ElemType e)
{DLinkList p = GetElem(DL, i - 1);//找到该位置的前驱节点,返回的是前驱节点的地址指针if (NULL == p)//注意:插入的位置可以不存在,但是插入的前驱节点一定存在:即插在最后一个节点的后面{return false;//若是插入位置的前驱节点不存在,才报错}DLinkList s = (DLinkList)malloc(sizeof(DNode));s->data = e;//存放数据s->next = p->next;//实现新申请的节点指向后面的节点p->next->prior = s;//实现原位置的节点指向新申请的节点s->prior = p;//实现新申请的节点指向前驱节点p->next = s;//实现前驱节点指向新申请的节点return true;
}
Ⅷ.插入节点的说明
参考本文件中:c.插入节点的说明
i.删除节点
Ⅰ.子函数名的说明
代码如下
bool DListDelete(DLinkList DL, int i)
说明
返回bool
类型,用于说明删除是否成功;
使用普通形参,用于接收头文件;
定义整型变量,用于记录删除的位置。
Ⅱ.找到删除位置的前驱节点
代码如下
DLinkList p = GetElem(DL, i - 1);//找到删除位置的前驱节点
说明
利用GetElem()
函数找到删除位置的前驱节点,此时的p指针指向的是前驱节点
Ⅲ.判断前驱节点是否存在
代码如下
if (NULL == p)
{return false;
}
说明
p指针此时指向的是前驱节点的空间
Ⅳ.判断删除位置的节点是否存在
代码如下
DLinkList q = p->next;//此时q指向删除位置的空间
if (NULL == q)//判断删除的位置是否存在
{return false;
}
说明
定义结构体类型指针q
,用于指向删除位置的节点。因为p
指针指向的是前驱节点,前驱节点的next
域放的就是删除位置的地址,直接访问即可。所以此时的q
指针,就是指向删除位置的节点。利用if
语句,判断该节点是否存在,若是不存在直接返回false
即可。
q指针,也是用于备份删除位置的地址,否则后面修改后,无法访问到删除的空间地址,进而无法释放该空间。
Ⅴ.进行断链操作
代码如下
p->next = q->next;//断链
if (q->next != NULL)
{q->next->prior = p;
}
说明
q指针指向的是删除位置的节点,p指针指向的是前驱节点。p->next=q->next:此句用于实现让前驱节点直接访问到后面的节点,即跳过删除的节点。
若是后继节点存在,即不为空,则将其的prior指向前驱节点即可,即if语句中就是用于实现此处的代码。
Ⅵ.释放指向该空间的指针
代码如下
free(q);
说明
也就是释放前面备份的空间地址
Ⅶ.将备份指针置空
代码如下
q=NULL;
Ⅷ.返回true
代码如下
return true;
Ⅸ.整体代码如下
bool DListDelete(DLinkList DL, int i)
{DLinkList p = GetElem(DL, i - 1);//找到删除位置的前驱节点if (NULL == p){return false;}DLinkList q = p->next;//此时q指向删除位置的空间if (NULL == q)//判断删除的位置是否存在{return false;}p->next = q->next;//断链if (q->next != NULL){q->next->prior = p;}free(q);//释放对应节点的空间q = NULL;return true;
}
6.循环单链表和循环双链表的概念
a.循环单链表
循环单链表就是将最后一个节点的next赋值为头指针。
即尾指针直接访问next域,赋值为头指针即可。
b.循环双链表
循环双链表就是将最后一个节点的next域赋值为头指针;
将头结点的prior域赋值为尾指针。
7.静态链表
即一个结构体数组,结构体包含数据域和一个整型变量,这个整型变量用于记录后面一个节点对应的下标,通过下标访问下一个节点。
8.总结
链表只有在头插法、尾插法改变头指针的值的时候,才需要借助引用,其他删除、插入都不需要引用。
创建头指针,可以初始化为NULL