C语言数据结构-链表
1.单链表
1.1概念与结构
数据结构是存储并管理数据。
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
1.2结点
与顺序表不同的是,链表里的每节“车厢”都是独立申请下来的空间,我们称之为“结点”。
结点的组成主要有两个部分:当前结点要保存的数据和保存下一个结点的地址(指针变量)。
链表结点的组成部分:要存储的数据+保存下一个结点地址的指针。
图中指针变量plist保存的是第一个结点的地址,我们称plist此时“指向”第一个结点,如果我们希望plist“指向”第二个结点时,只需要修改plist保存的内容为0x0012FFA0.
链表中每个结点都是独立申请的(即需要插入数据时才去申请一块结点的空间),我们需要通过指针变量来保存下一个结点位置才能从当前结点找到下一结点。
指向下一个结点的地址,就可以找到下一个结点。
假设当前保存的结点为整型:
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;//结点数据
struct SListNode* next;//指针变量用保存下一个结点的地址,用的是指针,指向下一个节点的指针,下一个结点的类型也是结构体
}SListNode;
3.2 链表性质
1.链式机构在逻辑上是连续的,在物理结构上不一定连续
2.结点一般是从堆上申请的
3/从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续。
数组:数据与数据之间的地址是连续的。
链表:数据与数据之间的地址不一定是连续的。
链表也是线性表的一种。
逻辑结构:一定是线性的。
物理结构:不一定是线性的。
结合前面学到的结构体知识,我们可以给出每个结点对应的结构体代码。
当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,也需要保存下一个结点的地址(当下一个结点为空时保存的地址为空)。
当我们想要从第一个结点走到最后一个结点时,只需要在当前结点拿上下一个结点的地址就可以了。
1.3链表的打印
链表实现从头到尾打印:
listnode.c:
// 单链表打印
void SListPrint(SListNode* plist)
{
SListNode* pcur = plist;
while (pcur != NULL)
{
printf("%d-> ", pcur->data);
pcur = pcur->next;/将pcur移动到下一个结点
//把下一个节点的地址给了pcur,就相当于移动到下一个结点了。
}
printf("NULL\n");
在这里直接使用plist来遍历结果也是一样的,重新创建个pcur指针,是为了避免由于指针指向的改变,导致无法重新找到链表的首结点。
链表同样也有增删改查等操作,接下来我们来实现单链表的头插和尾插
1.4实现单链表
1.4.1 插入
要插入,就要向操作系统申请结点大小的空间,存储要插入的数据。不论是头插还是尾插,都需要向操作系统申请结点大小的一块空间,所有将向操作动态申请一块空间,分装成一个函数:
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("malloc fail!\n");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
需要注意的是:
传值:形参的改变不会影响实参
传地址:形参的改变影响实参
接受一级指针的地址用二级指针
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
assert(pplist);
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL)
{
//如果pplist为空,那么申请的结点就为首节点,直接将首节点指向*pplist
//链表为空
*pplist = newnode;
}
else
{
//非空
SListNode* ptail = *pplist;//防止找不到头节点
首先我们要先找到该链表的尾节点。
while (ptail->next)
{
ptail = ptail->next;//相当于遍历,挪过去了 .
}
ptail->next = newnode;
}
}
时间复杂度为:O(n)
如果只传一级指针,函数内部修改这个指针并不会影响到函数外部的指针;传二级指针就不同了。二级指针指向的是头指针的地址。在函数内部通过二级指针修改头指针的值,就可以真正改变头指针的值,使得在函数外部也可以看到链表头指针的更新。
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
assert(pplist);
SListNode* newnode = BuySListNode(x);//申请一个节点.
newnode->next = *pplist;//指向第一个节点的指针
*pplist = newnode;//现在的头节点是nednode了,将pplist指向newnode。
}
时间复杂度为:O(1)
1.4.2删除
// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
assert(pplist&&*pplist);//链表不能为空,就是指向链表第一个结点的指针不能为空
//要删除尾节点,就需要找到尾节点,需要遍历。
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else {
//需要定义一个游标,将尾节点的前一个结点保存下来,然后直接将NULL指向游标,就是删除了。
SListNode* pcur = NULL;
SListNode* ptail = *pplist;
while (ptail->next)
{
//赋值
pcur = ptail;
ptail = ptail->next;
}
pcur->next = NULL;
free(ptail);
ptail = NULL;
}
}
// 单链表头删
void SListPopFront(SListNode** pplist)//** pplist是第一个结点
{
assert(pplist && *pplist);//传过来的参数不能为空,链表不能为空
SListNode* next = (*pplist)->next;