零,前言
在上一篇文章中我们介绍了顺序表这一种存储数据的结构。顺序表在增删查改的操作上有一些操作过于复杂且无法解决。
例如:
1,由于顺序表中数据连续,因此在中间或者头部的插入删除时,必须要移动元素,导致时间复杂度为O(N)。
2,扩容时需要申请新空间,拷贝数据释放空间会有消耗。
3,顺序表扩容时一般是两倍增长,但是我们如果当前容量为100,我只需要再存10个空间,我们此时扩容2倍扩容到了200个,那么剩下90个空间就会被浪费。
为了解决上述难题,引入了 “链表” 这个新的数据结构作为解决方案。
一,单链表
一,单链表的概念
链表分为单链表和双链表,我们首先介绍的是单链表。
链表与顺序表不同,链表中的每个数据在物理上不是连续的,也就是说链表中的数据的地址不是连续的,而是分散在内存里不同的位置。但是我们又必须要把每个数据连接在一起。我们又该如何做?
我们用结构体定义了 "节点" 。链表由许许多多的节点构成,每个节点由一个数据和一个指针组成,指针指向的是下一个节点,如
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data; //节点中的数据
SListNode* next; //指向下一个节点的指针
}
那么该链表的结构就如图所示:
可能很多人看到这个结构和代码后很懵,那么我们就先通过了解一下打印链表数据的操作来理解整个链表这个结构是如何运行的。
在链表中。我们规定链表尾节点的next指针指向NULL。
对于链表的打印,我们的传参只需要穿链表头节点(也就是第一个节点)的地址即可。
void SListprint(SLTNode * phead)//
{
SLTNode* cur = phead;
while(cur != NULL)
{
printf("%d",cur->data);
cur = cur->next;
}
}
理解链表第一步就是要理解cur = cur->next。 在题目中,首先将头指针phead赋给cur,cur是当前节点的地址,我们通过当前节点地址cur用结构体指针cur->data打印出了该节点存的数据后,我们需要知道下一个节点的地址,怎么办呢?
我们是否忘记了在结构体中还有一个next指针呢?它指向的是下一个节点的地址那么我们一但用cur = cur>next,我们将cur->next的值赋给cur后,cur这个指针就已经指向了下一个节点。
首先头指针是0x0012FFA0,那么cur的值也是0x0012FFA0。由于头节点中的next指针指向的是下一个节点的地址。因此,其指针的值就是下一个节点的地址:0x0012FFB0那么cur->next的值也就是0x0012FFB0,当我们用cur = cur->next时,我们便成功地将0x0012FFB0赋给了cur。
为什么要判断 cur != NULL呢?因为整个链表的尾节点的next指针指向的是NULL,因此当cur 等于NULL时,整个链表就已经遍历完,链表打印的函数将结束。
总而言之,链表用next指针将每个节点连接在了一起,由于next指针的缘故。只有知道上一个节点的地址才能求得当前节点的地址。
二,单链表的增删查改
我们理解单链表的结构后就要进行单链表增删查改的实现。
1)单链表的创建
创建一个 数据为x的节点
SLTNode* CreatSLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//用malloc开辟一个节点大小的空间newnode
if (newnode == NULL)
{
perror("malloc fail"); //如果开辟失败就返回空指针
return -1;
}
//开辟成功就继续
newnode->data = x; //将数据存放在节点中
newnode->next = NULL; //使下一个节点指向空
return newnode;
}
2)单链表的头插和头删
单链表的头插头删和顺序表不同,由于数据在内存中不是连续存放因此不需要移动数据。
头插
若要进行头插只需要将新节点的next指针指向头节点,即可将其连接起来。
void SListPushFront(SLTNode** pphead, SLTDateType x)
{
assert(pphead);
//创建新节点
SLTNode* newnode = CreatSListNode(x);
newnode->next = *pphead; //新节点的next指针指向原本的头节点
*pphead = newnode; //改变头指针的值
}
我们看到,在头插的传参中用了二级指针,这有什么用呢?
我们在以前学习过,在函数中传指针可以改变该指针所指向的值,传入二级指针即可以改变一级指针的值。
在题目中,我们头插了节点后该链表的头指针phead变成了新节点的地址,因此phead需要改变。由于phead的类型是指针,因此改变一级指针的值就应该用二级指针pphead。
如果不使用二级指针,那么newnode这个新创建的节点就无法插入到该链表内。
头删
然后将头指针指向下一个节点。然后用free销毁原来的头节点。
void SListPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead != NULL);//检查链表是否为空,为空就直接退出程序
SLTNode* cur = *pphead;//先用cur储存,因为在之后的操作里*pphead会被改变
*pphead = cur->next;
free(cur);
}
3)单链表的尾插和尾删
尾插和尾删都需要找“尾节点”,只有知道尾节点的地址才能进行尾插和尾删
尾插
进行尾插,即找到尾节点。然后把尾节点的next指针指向新节点,然后将新节点的next指针指向NULL。
void SListPushBack(SLTNode** pphead, SLTDateType x)
{
assert(pphead);
if (*pphead == NULL)
{
SLTNode* newnode = CreatSListNode(x);
*pphead = newnode; //头指针为空,代表链表本身为空时
}
else
{
SLTNode* tail = *pphead; //链表本身不为空时
while (tail->next) //找尾节点
{
tail = tail->next;
}
tail->next = newnode;
}
}
很多人有疑惑,为什么前面头插的代码改变指针的值要用二级指针,而tail->next = newnode;不需要用二级指针呢?因为上面代码和下面代码两个改变的东西不同。next在结构体中就是结构体变量,改变结构体变量的值就要用结构体指针tail。而上面头插改变的phead不是结构体变量而是单纯的一级指针。
尾删
尾删要分链表中只有一个节点或者有多个节点
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead != NULL);//判断链表是否为空
SLTNode* tail = *pphead;
if (tail->next == NULL)//链表仅有一个节点
{
free(tail);//释放空间
*pphead = NULL;//头指针值向空
}
else //链表有多个节点
{
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);//释放空间
tail->next = NULL;//尾节点的next指针指向空
}
}
4)链表的销毁
void SListDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)//依次释放每个节点空间
{
SLTNode* next = cur->next;//先用next保存当前指针,防止free之后找不到下一个节点
free(cur);
cur = next;
}
*pphead = NULL;
}
5)链表查找,插入与删除
查找
SLTNode* SListFind(const SLTNode* phead, SLTDateType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return -1;
}
—
插入
在pos之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
SListPushFront(pphead, x);//如果该节点在头部,那么相当于头插
else
{
SLTNode* cur = *pphead;
SLTNode* newnode = BuySListNode(x);
while (cur->next != pos)//找到pos处的节点
{
cur = cur->next;
assert(cur);//暴力检查判断找没找到
}
cur->next = newnode;
newnode->next = pos;
}
}
删除
删除pos处的节点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead == pos)//判断是否在头部
{
SListPopFront(pphead);
}
else
{
if (pos->next == NULL)//判断是否在尾部
SListPopBack(pphead);
else
{
SLTNode* cur = *pphead;
while (cur->next != pos)
{
cur = cur->next;
assert(cur);
}
cur->next = pos->next;
free(pos);
}
}
}
三,总结
单链表同样存在一些问题,例如在进行尾部的插入删除时,需要找尾,时间复杂度会较大,为了解决这种问题引入了双链表。下一篇文章将会介绍双链表。