链表是一种物理结构上非连续,非顺序的存储结构。数据元素的顺序是通过链表中的指针链接起来的。
顺序表在插入数据时,可能会扩容,造成一定的消耗,如果新插入的数据量小,也会造成一定的空间浪费。链表可以很好的解决这两个问题。但链表也有其他不足。
一、链表的分类
根据单向或双向,带头或不带头,循环或不循环,链表可以分为八类。
其中头结点只保留链表第一个结点的地址,负责找到链表第一个结点,头结点不存储有效结点。
单向或双向
循环或者不循环
在物理结构上,链表是由一个个结点组成的,如图是零散的。
在逻辑结构上,可以用一个个箭头表示可以找到下一个结点
其中pList就是头结点,0x12ff40是地址,存储的是下一个结点的地址,这个结点不存储有效数据。
如果是循环链表,最后一个结点存储的是第一个结点的地址。
二、常用单链表接口
这里的单链表指的是不带头不循环单向链表。
下面就开始实现一个链表。需要自定义一个结构体类型,成员变量由一个数据和一个指针构成。
typedef int linkDataType; //方便对数据的类型进行修改
typedef struct linkListNode
{
linkDataType val;
struct linkListNode* next;
}Node;
将结构体重命名为Node方便书写。
对于单链表需要实现的一些接口有这些。
//尾插
void LTpushback(Node** pphead, linkDataType x);
//头插
void LTpushfront(Node** pphead, linkDataType x);
//尾删
void LTpopback(Node** pphead);
//头删
void LTpopfront(Node** pphead);
//查找
Node* LTfind(Node* phead, linkDataType x);
//删除指定位置
void LTErase(Node** pphead, Node* pos);
//销毁
void LTDestroy(Node** pphead);
//打印,这个函数只是用来验证其他接口的正确性
void LTPrint(Node* phead);
开始一一实现。
1.尾插
对于尾插,第一步是要找到尾结点,然后链接上即可。由于实现的链表没有头结点,所以在链接的时候需要分结点数为0和不为0两种情况。
在结点数为0时:申请的结点可以看作是尾结点,所以可以直接返回,结束函数。
结点数不为0时:先找尾,在链接
代码如下
void LTpushback(Node** pphead, linkDataType x)
{
assert(pphead);
//创建新结点
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode)
{
printf("malloc failed!");
return;
}
newNode->val = x;
newNode->next = NULL;
//一个结点都没有
if (!(*pphead))
{
(*pphead) = newNode;
return;
}
//多于一个结点,先找尾结点
Node* tail = (*pphead);
while (tail->next)
{
tail = tail->next;
}
//链接到尾
tail->next = newNode;
}
使用二级指针传参,是因为实参是指针。
尾结点的指针域为空,因此可以使用循环来找尾。链接就是将新结点的地址赋值给原来尾结点的指针域。
2.头插
同尾插,头插也分结点数位0和不为0
//头插
void LTpushfront(Node** pphead, linkDataType x)
{
assert(pphead);
//创建新结点
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode)
{
printf("malloc failed!");
return;
}
newNode->val = x;
newNode->next = NULL;
//没有结点
if (!(*pphead))
{
(*pphead) = newNode;
return;
}
//修改链接
newNode->next = *pphead;
*pphead = newNode;
}
3.尾删
尾删需要分两种情况,只有一个结点和多于一个结点。结点数如果为零,直接返回函数即可。
先找尾结点的前一个结点,在删除尾结点。删除后需要将前一个结点的指针域置空。
//尾删
void LTpopback(Node** pphead)
{
assert(pphead); //不能传一个空地址
assert(*pphead); //链表不可以是空链表
//只有一个结点
if (!(*pphead)->next)
{
free(*pphead);
*pphead = NULL;
return;
}
//多于一个结点,先找尾
Node* tail = (*pphead)->next;
Node* cur = *pphead;
while (tail->next)
{
cur = tail;
tail = tail->next;
}
//删除结点
free(tail);
cur->next = NULL;
}
创建两个变量,cur指向第一个结点,tail指向后一个,当tail->next==NULL时,tail指向的结点就是尾结点,释放后,需要将前一个的指针域置空,防止野指针的发生。
4.头删
头删只需要保留下一个结点的地址,在删除第一个结点即可。
//头删
void LTpopfront(Node** pphead)
{
assert(pphead); //不能传一个空地址
assert(*pphead); //链表不可以是空链表
//只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
//有多个结点
Node* front = (*pphead)->next;
free(*pphead);
*pphead = front;
}
5.查找
查找一个数据,循环遍历链表即可。
//查找
Node* LTfind(Node* phead, linkDataType x)
{
assert(phead);
Node* cur = phead;
while (cur)
{
if (cur->val == x)
{
return cur;
}
cur = cur->next;
}
printf("数据不存在\n");
return NULL;
}
6.删除指定位置
删除头尾结点其实就是头删和尾删。删除指定位置时,需要不改变链表结构,即删除位置的前后结点需要链接起来,整个链表不能断。
创建一个遍历保存前一个结点的地址先改变链表的链接,在删除结点。不能先删除结点在改变链接,因为找不到下一个结点,即图中值为4的结点。
//指定位置删除
void LTErase(Node** pphead, Node* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
Node* prev = *pphead;
//删除的结点是第一个
if (prev == pos)
{
prev = pos->next;
*pphead = prev;
free(pos);
return;
}
//找pos位置的前一个结点
while (prev->next != pos)
{
prev = prev->next;
}
//删除
prev->next = pos->next;
free(pos);
}
7.销毁和打印
这两个函数都是遍历一遍即可。销毁时需要注意保留下一个结点的地址。
//销毁
void LTDestroy(Node** pphead)
{
assert(pphead);
assert(*pphead);
Node* cur = *pphead;
while (cur)
{
Node* next = cur->next;
free(cur);
cur = next;
}
}
//打印
void LTPrint(Node* phead)
{
Node* cur = phead;
while (cur)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
三、其他接口
//指定位置前插入
void LTInsert(Node** pphead, linkDataType x, Node* pos)
{
assert(pphead);
assert(*pphead);
//创建新结点
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode)
{
printf("malloc failed!");
return;
}
newNode->val = x;
Node* cur = *pphead;
//如果pos指向第一个结点
if (cur == pos)
{
newNode->next = pos;
*pphead = newNode;
return;
}
//找pos位置的前一个结点
while (cur->next != pos)
{
cur = cur->next;
}
//插入
newNode->next = pos;
cur->next = newNode;
}
//在指定位置之后插入数据
void SLTInsertAfter(Node* pos, linkDataType x)
{
assert(pos);
//创建新结点
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode)
{
printf("malloc failed!");
return;
}
newNode->val = x;
newNode->next = pos->next;
pos->next = newNode;
}
//删除指定位置后的全部结点
void LTEraseAfter(Node** pphead, Node* pos)
{
while (pos->next)
{
Node* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
}
单链表有部分缺陷,比如不能随机访问中间结点,找前一个结点时需要遍历,这些缺陷双向循环链表可以解决。
关于单链表就介绍到此了。