文章目录
- 一:线性表
- 二:数组
- 2.1数组在内存中的存储
- 三:链式结构
- 四:单链表
- 4.1概念与结构
- 4.1.1概念
- 4.1.2 结构(节点)
- 4.1.3链表的性质
- 4.1.4链表的打印
- 4.2实现单链表
- 结语
欢迎大家来到我的博客,给生活来点impetus。
今天我们来学习单链表。
在学习单链表之前,我们先了解线性表。
为什么呢?首先举一个例子,在生活中了解苹果和梨首先得知道他是水果。
同理,单链表(SList)也是线性表的一种。
一:线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是⼀种在实际中⼴泛使⽤的
数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
二:数组
2.1数组在内存中的存储
我们先来看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i < 10; i++)
{
printf("&arr[%d] = %p\n ", i, &arr[i]);
}
return 0;
}
再来看结果:
因为int(整形)占4个字节,所以每个数组匀元素的地址相差4。
从这里我们可以推出:
数组在内存中的存储是连续的。
图像如下:
扩展:顺序表的底层逻辑就是数组。
三:链式结构
在数据结构中,链式结构是一种存储数据的方式。
它由节点组成,每个节点包含数据部分和指向下一个节点的指针(在单链表中)。就像火车车厢,每节车厢装着货物(数据),还有连接下一节车厢的挂钩(指针)。
四:单链表
4.1概念与结构
4.1.1概念
链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
4.1.2 结构(节点)
与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/结点”,结点的组成主要有两个部分:
当前结点要保存的数据和保存下⼀个结点的地址(指针变量)。
图中指针变量plist保存的是第⼀个结点的地址,我们称plist此时“指向”第⼀个结点,如果我们希望plist“指向”第⼆个结点时,只需要修改plist保存的内容为0x0012FFA0
链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。
使用链表大大降低了内存的消耗,但是每次需要就会去开辟空间,大大降低了程序的执行效率。
4.1.3链表的性质
1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续
2、结点⼀般是从堆上申请的
3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续。
结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码:
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;//存储的数据多样,为了能够容易改变数据类型,重定义int型
struct SListNode* next;
}SLTNode;
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个结点的地址(当下⼀个结点为空时保存的地址为空)。
当我们想要从第⼀个结点⾛到最后⼀个结点时,只需要在当前结点拿上下⼀个结点的地址就可以了。
4.1.4链表的打印
给定的链表结构中,如何实现结点从头到尾的打印?
4.2实现单链表
在一种数据结构中,我们会对数据进行增删查改。下面我们来书写一下这些代码。
细节
1:参数是否需要传递头结点的参数取决于是否遍历,影响上节点要遍历(再来设一个指针为了能够找到头结点),影响下节点不遍历直接pos=pos->next即可
2:传一级指针or二级指针,看该操作是否会改变头结点的指向位置,会(二级),反之一级
3;指定位置之前比指定位置之后传递的参数多一个,因为会涉及遍历,需要传递指向头结点的参数
4;断言pos而非*pphead,若影响pos之后不遍历,根本不会传递pphead或phead参数,所以就统一断言pos
我们用对链表进行操作来说明这些细节:
总代码:
SList.c
#include"SList.h"
//打印链表
//细节1:限制条件 2:地址不连续,不要使用自增自减,应该使用赋值语句找到下一个节点
void SLTPrint(SLTNode* phead)//效果,1->2->3->...->NULL
{
SLTNode* pcur = phead;
while (pcur != NULL)//如果我写成pcur->next!=NULL的话,把下面的那句代码移上来,会导致最后一个节点的数据遍历不到,具体的话自己画图来理解
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
//创建节点并完成初始化
//细节1:这里需要返回他的地址,提供给后面代码使用2:判断空间是否开辟成功
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//这里不要再写structure了,因为我已经重定义了,否则就是 struct struct ListNode
if (newnode == NULL)
{//开辟不成功
perror("malloc failed!");
exit(1);
}
//开辟成功+初始化
newnode->data = x;
newnode->next = NULL;//不赋值的话现在就就是野指针,需要NULL去管理野指针
return newnode;//因为我要插入的是节点,而不是整形数据,所以必须返回节点的地址
}
//链表尾插数据
//细节:这里需要用到二级指针,对形参的改变需要影响实参
//如果这样写,传值调用(void SLTPushBack(SLTNode* phead, SLTDataType x))
//出了函数过后,phead变量被销毁,只有对形参改变,而plist实参还是NULL并没有改变,打印的结果就只有NULL了
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = SLTBuyNode(x);
//如果链表为空,phead就直接指向newnode,此时newnode是首节点
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//链表不为空,找尾结点,将尾结点和新节点连接起来
SLTNode* ptail = *pphead;//这里为什么要有这条赋值语句?
//因为*pphead==plist,若用*pphead去遍历,导致plist跟着改变,打印时传的是plist,导致打印的时候找不到头节点
while (ptail->next)//等价于ptail->next != NULL
{
ptail = ptail->next;
}
//出循环代表ptail此时是NULL,将其赋值为下一个节点的地址
ptail->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);//防止传过来的是一个NULL
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;//头结点的next指向下一个节点的地址
*pphead = newnode;//头结点往前去一个
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)//在这里,->优先级比*高,所以要加一个()
{
//此时链表中还有一个节点,prev不可能在往前,因为初始值为NULL,不能够对prev指针访问操作,不能对空指针解引用
free(*pphead);
*pphead = NULL;
}
else
{
//此时链表中的节点>1个,能够找到prev,此时prev不为NULL
SLTNode* prev = NULL;
SLTNode* ptail = *pphead;
while (ptail->next != NULL)
{
prev = ptail;
ptail = ptail->next;
}
//prev ptail
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur != NULL)//同SLTPrint一样这样写为了能够遍历到所有节点中的元素,如写为pcur->next!=NULL,最后一个节点数据访问不到
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//未找到
return NULL;
}
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
if (pos==*pphead)//(*pphead)->next == NULL
{
//头插,此时只有一个节点,找不到pos,prev死循环,这种情况单独讨论
SLTPushFront(pphead, x);
}
else
{
//>一个节点,此时可以找到prev
//pos在头结点之后--->找pos前驱节点
SLTNode* newnode = SLTBuyNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)//如果此时只有一个节点,prev会陷入死循环,因为开始prev==pos,prev在向后走,找不到pos,这种情况单独讨论
{
prev = prev->next;
}
//在这里先处理左边的节点,或先处理右边的节点最后的效果是一样的,为了统一下方,都先处理右方的节点
newnode->next = pos;
prev->next = newnode;
}
}
//在指定位置之后插⼊数据(这种不可能是头插了)
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
//这里与上一个方法区别,这里必须先处理右边的节点,如果先处理左边的节点,在处理右边时此时指向的是newnode,右边的节点就找不到了
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
if (pos == *pphead)
{
//此时只有一个节点,头删
SLTPopFront(pphead);
}
else
{
//此时节点数>1,可以找到prev,进而将pos之前的节点与之后的节点联系起来
SLTNode* prev = *pphead;
while (prev->next != pos)//这里只要找到pos即可,是prev在pos的前面一个
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);//要有数据可删,pos下一个数据也可删
SLTNode* del = pos->next;
//pos del del->next
pos->next = del->next;
free(del);
del = NULL;
}
//销毁链表
//细节:从后删的话,删除的上一个节点就找不到了,所以最好从前往后删,代码跟简洁
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur!=NULL)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
结语
感谢大家认真倾听我的博客,山高路远,仍需前行。成为最好的自己。