最近学了完了结构体,指针,之后尝试创建了一个链表,在写对链表的增删操作的代码时,才发现自己对指针的很多理解都是错误的,学到了不少,所以打算写一个帖子,分享一如何建立一个链表。
什么是链表
链表是由一系列结点组成,它充分的利用了结构体指针。链表可以动态的进行存储分配,它的每一个结点在内存中都是随着进程生成的。链表是一个功能极为强大的数组,他可以在节点中定义多种数据类型,还可以根据需要随意增添,删除,插入节点。链表都有一个头指针,一般以head来表示,存放的是一个地址。链表中的节点分为两类,头结点和一般节点,头结点通常不来存放数据。链表中每个节点都分为两部分,一个数据域,一个是指针域。每一个结点内的指针都指向下一个结点,链表就像一节火车一样,head连着第一个结点,第一个结点再连着第二个结点,直到最后一个元素,该元素不再指向其它元素,它称为“表尾”,它的指针部分放一个“NULL”(表示“空地址”),链表到此结束。
单链表的实现
根据上面的描述你也知道了,链表的结点内存放的是不同数据类型的数据和一个指针用来指向下一个结点。这可以用结构体来实现,同时为了方便更改结构体内的数据类型,用typedef定义了LISTTYPE表示int型
typedef int LISTTYPE;
typedef struct ListStrs
{
LISTTYPE data;
struct ListStrs *next;
}ListStr;
这里用一个List.c进行封装
包含这些函数
申请结点
链表的每一个结点都是动态开辟(malloc)出来的,每一个结点的内存大小为该结构体的内存大小。
建立头结点和增添结点的操作都将通过调用这个函数进行。
拥有了一个结点以后,在建立下一个节点是,还需要考虑将它们链接起来的问题
链接结点的方法有头插法和尾插法,即将新增的结点链接在链表的头部或尾部
头插
头插相对来说比较简单,将新申请结点的next指向原头结点,然后将头结点改成新申请结点
尾插
如果当前头指针为空指针,说明没有结点,直接将新节点作为头结点即可;否则就要沿着链接的指针next找尾部在哪。
这样就完成了对链表的扩展。接下来是结点的删除,同样有头删和尾删两种方式。
头删
让头结点的下一个结点作为新的头指针,再将原头结点释放就行
尾删
一样要判断头指针是否就是尾部。是则直接删除头结点,否则寻找尾结点,删除后要让新尾结点的指针指向空。
指定地址插
在指定的结点后插入新结点
指定地址删
删掉指定位置后的结点,这个函数的返回值被我设置为了一个int类型,用于返回想要删除的结点不存在的情况。
销毁链表
遍历链表,挨个释放
打印链表
一个一个输出就行了
以下是代码以及注释:
List.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LISTTYPE;
typedef struct ListStrs
{
LISTTYPE data;
struct ListStrs *next;
}ListStr;
LISTTYPE *ListCreate(LISTTYPE data);
//表头/尾插入
void ListPushFrount(ListStr **pp, LISTTYPE data);
void ListPushTail(ListStr **pp, LISTTYPE data);
//表头/尾删除
void ListDelFrount(ListStr **pp);
void ListDelTail(ListStr **pp);
//指定位置插/删
void ListInstPos(ListStr **pp, LISTTYPE pos, LISTTYPE data);
int ListDelPos(ListStr **pp, LISTTYPE pos);
//销毁链表
void ListDestory(ListStr *p);
//打印链表
void Listprint(ListStr *head);
List.c
#define _CRT_SECURE_NO_WARNINGS
#include "List.h"
/*** 描述:用于创建一个结点。
* 函数:创建结点。
* 参数:data ,节点数据Data。
**/
LISTTYPE *ListCreate(LISTTYPE data)
{
ListStr *p = (ListStr *)malloc(sizeof(ListStr));
if(p == NULL)
{
perror("malloc");
exit(EXIT_FAILURE);
}
p->data = data;
p->next = NULL;
return p;
}
/*** 描述:用于在单链表的表头位置插入一个结点。
* 函数:单链表头插,在链表头部插入一个结点。
* 参数:pp ,要插入的链表的头结点指针的地址。注意是头结点!
* 参数:x ,结点数据Data。
**/
void ListPushFrount(ListStr **pp, LISTTYPE data)
{
assert(pp); //存放原头节点的指针的地址不应为空
ListStr *NewHead = ListCreate(data);
ListStr *Head = *pp; //这个结构体指针是用来存放原来的头结点的
NewHead->next = Head; //新头结点和原头结点链接
*pp = NewHead; //让新结点成为头结点
}
/**tips: 关于这里为什么使用二级指针 **p。
* 这是因为函数中的指针形参是调用时创建调用后销毁,相当于临时创建一个替身
* 若是这里用一级指针 *p,p将会新建一个内存空间,p里的内容是head的地址。
* (*p)将直接指向原来的head的内容,此后的(p = NewHead;)这一语句
* 是在将替身进行结点链接,原来的head并没有被链接上。
* 只有使用二级指针,让 *pp指向head才能对原来的head进行操作*/
/*** 描述:用于在单链表的尾部插入一个新结点。
* 函数:单链表尾插,在链表头部插入一个结点。
* 参数:pp ,要插入的链表的头结点指针的地址。注意是头结点!
* 参数:x ,结点数据Data。
**/
void ListPushTail(ListStr **pp, LISTTYPE data)
{
assert(pp); //同上
ListStr *NewTail = ListCreate(data);
//这里比头插入要多考虑一个问题——原头结点是不是指向空
if (*pp == NULL) //若头结点为一个空指针
{
*pp = NewTail; //给头结点
}
else
{
ListStr *Tail = *pp;
while (Tail->next != NULL) //遍历链表找到尾结点
{
Tail = Tail->next;
}
Tail->next = NewTail; //将新尾结点链接
}
}
/*** 描述:用于删去单链表头部的一个结点。
* 函数:单链表头删,删除表头一个结点。
* 参数:pp ,要删除的链表的头结点指针。注意是头结点!
**/
void ListDelFrount(ListStr **pp)
{
assert(pp); //同上
assert(*pp); //要删掉的结点的指针也不应为空
ListStr *newhead = (*pp)->next; //
free(*pp); //释放原头结点
*pp = newhead;
//ListStr *newhead = *pp; //存放原头结点
//*pp = newhead->next; //头结点后移
//free(newhead); //释放原头结点
}
/*** 描述:用于删去单链表尾部的一个结点。
* 函数:单链表尾删,删除表尾一个结点。
* 参数:pp ,要删除的链表的头结点指针。注意是头结点!
**/
void ListDelTail(ListStr **pp)
{
assert(pp); //同上
assert(*pp); //同上
if ((*pp)->next == NULL) //若是头结点下一个结点即为空
{
free(*pp); //将头结点释放
*pp = NULL; //释放完记得将指针指向空避免野指针!!
}
else
{
ListStr *tail = *pp; //存放原头结点
while (tail->next->next !=NULL) //遍历链表找尾结点
{
tail = tail->next;
}
free(tail->next); //将尾部清空
tail->next = NULL; //一样记得指空
}
}
/*** 描述:用于在链表的指定地址插入一个结点。
* 函数:指定位置插,在指定地方插入一个结点。
* 参数:pp ,要删除的链表的头结点指针。注意是头结点!
* 参数:pos ,要插入的结点在链表的第pos个结点后(包含头结点)。
* 若为负值,默认插在表头,若大于表长,插在表尾
* 参数:data ,结点的数据。
**/
void ListInstPos(ListStr **pp, LISTTYPE pos, LISTTYPE data)
{
assert(pp); //同上
if (pos <=0) //若小于等于0,插在头部
{
ListPushFrount(pp,data);
}
else
{
LISTTYPE i = 0;
ListStr *newone = ListCreate(data);
ListStr *tail = *pp;
if (*pp == NULL) //若头结点为一个空指针
{
*pp = newone; //插在头部
}
else
{
for (i = 0; (tail->next)!=NULL && i<pos-1; i++) //遍历链表
{
tail = tail->next;
}
newone->next = tail->next; //找到第pos个后把newone插入
tail->next = newone;
}
}
}
/*** 描述:这个函数用于在链表的指定地址后删除一个结点。
* 函数:指定位置删,删掉指定位置后一个结点。
* 参数:pp ,要删除的链表的头结点指针。注意是头结点!
* 参数:pos ,要插入的结点在链表的第pos个结点后(包含头结点)。
* 由于数据是很重要的,所以若是删除不存在的位置则不会进行删除操作,并返回0值
**/
int ListDelPos(ListStr **pp, LISTTYPE pos)
{
assert(pp); //同上
assert(*pp); //要删除的地址不应为空
if (pos <=0)
{
printf("Wrong address");
return 0;
}
else
{
LISTTYPE i = 0;
ListStr *deltone = *pp; //deltone结点将要被销毁
ListStr *pre = *pp; //用来存放deltone后的其他结点,以便删除deltone后再链接
while ((deltone->next!=NULL) && i<(pos-1))
{
pre = deltone;
deltone = deltone->next; //遍历链表
i++;
}
if ((deltone->next==NULL) || i>(pos-1)) //判断是否满足可删除条件(第pos个结点存在且不为空)
{
printf("Wrong address");
return 0;
}
else
{
//链接后续结点. //由于前面while循环中pre比deltone具有滞后性
pre->next = deltone->next; //这里pre指向的是deltone的前一个结点,这样的设
//计就把deltone独立出来了
free(deltone);
return 1;
}
}
}
/*** 描述:用于销毁链表
* 函数:销毁链表
* 参数:pp ,要删除的链表的头结点指针。注意是头结点!
**/
void ListDestory(ListStr **p)
{
assert(p); //要销毁的链表表头不能为空
ListStr *newhead = (*p)->next; //新结点,拿这个放后续链表
ListStr *tail = *p; //待删除结点,即头结点
while (tail !=NULL)
{
free(tail);
tail = newhead;
if (newhead != NULL)
newhead = newhead->next; //将后续链表提前,接继删除
}
free(tail);
*p = NULL; //指针指向空
}
/*** 描述:用于打印链表
* 函数:打印链表
* 参数:head ,要删除的链表的头结点指针。
**/
void Listprint(ListStr *head)
{
ListStr *p = head;
while (p != NULL)
{
printf("%d->", p->data);
p = p->next;
}
printf("NULL\n");
}