[数据结构]链表之单链表
前言
在学习链表之前,我们已经学习了顺序表了
根据顺序表的特点,我们可以发现顺序表有优点,也有一些缺陷。
所以根据顺序表的缺点,链表就横空出世啦~
1.链表
1.1链表的概念及结构
**概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 **
而链表的结构其实就像一辆火车一样
火车有着一个个车厢,链表也有着一个个节点/结点。
在链表(SListl),每个节点(SListNode)都存储着一个数据(data),还有一个指向下个节点的指针(next)
单链表用结点存储了数据以及下一个结点的地址,因此结点一般分为多个部分,即数据域与指针域,数据域存储有效数据,指针域存储下一个结点的地址。同时单链表有只有一个指针域,双链表有两个指针域。
我们也可以看到最后一个节点是指向一个空指针。
1.2单链表与顺序表的区别与优缺点
这是两种不同的存储结构,我们先谈谈区别吧,顺序表是顺序存储结构,它的特点是逻辑关系上相邻的两个元素在物理位置上也相邻。但是链表不同,链式存储结构的特点是不需要逻辑上相邻的元素在物理位置上也相邻。因为链式存储结构可以通过结点中的指针域直接找到下一个结点的位置。
**单链表的优缺点:
1.优点:可以按照实际所需创建结点增减链表的长度,更大程度地使用内存。
2.缺点:进行尾部或者任意位置上插入或删除时时间复杂度和空间复杂度较大,每次都需要通过指针的移动找到所需要的位置,相对于顺序表查找而言效率较低。
通过这里,我们可以简单对比一下上面顺序表的优缺点
**顺序表的优缺点:
1.优点:可以通过下标直接访问所需要的数据
2.缺点:不能按实际所需分配内存,只能使用malloc或者realloc函数进行扩容,容易实现频繁扩容,容易导致内存浪费与数据泄露等问题。
1.3八种链表类型、
在了解链表的类型之前,我们需要了解链表的几个特点:
1.单向和双向
2.带哨兵位和不带哨兵位
3.循环和非循环
我们可以通过组合的方式,如:单向带头循环链表,双向不带头非循环链表
每一层都有2种类型,所以2*2 *2总共有8种类型链表
单向带头循环链表
单向带头非循环链表
单向不带头循环链表
单向不带头非循环链表
双向带头循环链表
双向带头非循环链表
双向不带头循环链表
双向不带头非循环链表
2.单链表的实现
下面我们实现的单链表是很多数据结构的子结构,也就是单向不带头非循环链表。
2.1单链表的结构定义
单链表的结构与顺序表是完全不同的,分为俩个部分数据域和指针域
typedef struct SListNode
{
int data;
struct SListNode* next;//存储下一个节点的地址
}SListNode;
我们也可以使用typedef操作符重命名,方便后面接口操作。
2.2单链表结点的创建
单链表的创建,不用像顺序表一样需要初始化的,是由一个个节点组成,需要多少个结点就创建多少个结点,我们可以单独写出一个测试函数测试单链表的创建,方便调试,下面我们利用上面这个结构创建一个单链表。
void SListTest1()
{
SListNode* slist = NULL;
SListNode* n1 = malloc(sizeof(SListNode));
SListNode* n2 = malloc(sizeof(SListNode));
SListNode* n3 = malloc(sizeof(SListNode));
n1->data = 1;
n2->data = 2;
n3->data = 3;
n1->next = n2;
n2->next = n3;
n3->next = NULL;
slist = n1;
SListPrint(slist);
}
int main()
{
SListTest1();
return 0;
}
2.3单链表的基本操作的接口
typedef int SLTDataType;//统一数据类型
SLTDataType* BuySListNode(SLTDataType x);//申请结点
void SListPrint(SListNode* phead);//打印链表
void SListPushBack(SListNode** pphead, SLTDataType x);//尾插
void SListPushFront(SListNode** pphead, SLTDataType x);//头插
void SListPopBack(SListNode** pphead);//尾删
void SListPopFront(SListNode** pphead);//头删
SLTDataType *SListFind(SListNode* pphead, SLTDataType x);//查找
//在pos位置之前插入(pos第几个元素的地址)
//和顺序表的下标越界不同,链表不存在下标,pos地址必须为find函数找到
void SListInsert(SListNode** pphead, SListNode*pos, SLTDataType x);
//在pos位置之后插入
void SListInsertAfter(SListNode* pos, SLTDataType x);
//删除pos位置
void SListErase(SListNode** pphead, SListNode* pos);
//删除pos下一位的位置
void SListEraseAfter( SListNode* pos);
void SListDestroy(SListNode** pphead);//摧毁链表
2.3.1如何分别传送一级指针与二级指针
我们可以看到上面的接口,有的使用一级指针,而有的则使用二级指针。
这是为什么呢?->这是因为有些接口需要涉及改变实参(更改节点内部的内容),所以
需要二级指针传参,如果使用一级指针改变实参是无法修改实参的,使用一级指针只会修改这个一级指针的指针变量的地址,无法找到实参的地址。
比如:如果要改变链表的头指针就传二级指针,改变头指针不能传一级指针因为传送的过程就是拷贝的过程,相当于将头指针复制了一份,形参的改变不会影响实参,因此要改变链表的头指针需要传送二级指针。
2.4单链表的申请结点和打印链表
2.4.1申请结点
我们进行操作的单链表是不带头结点的
所以我们进行其他插入操作的时候,是需要申请一个结点插入的
SLTDataType* BuySListNode( SLTDataType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
printf(" malloc NULL\n");
exit(-1);
}
else {
newnode->data= x;
newnode->next = NULL;
}
return newnode;
}
这里我们需要注意新结点如果为空的情况,如果为空,则退出程序
2.4.2打印链表
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
简单的遍历链表打印
2.5 assert断言的使用
assert这个函数在指针传参的时候非常好用,在链表中我们用来判断链表指针是否为空。
而且assert函数在调试的时候非常好用,一旦指针为空,会立刻报错,**而且会帮我提示报错在哪个文件和在哪行代码。**在以后实战项目中对我们有很好的帮助,所以要善于利用assert函数
2.6 单链表的插入
2.6.1 单链表的头插
void SListPushFront(SListNode** pphead, SLTDataType x)
{
assert(pphead);
SListNode* newnode = BuySListNode(x);
newnode->next= *pphead;
*pphead = newnode;
头插涉及修改实参,采用二级指针传参
assert判断指针
头插非常简单,申请一个新节点并插入。
2.6.2 单链表的尾插
void SListPushBack(SListNode** pphead, SLTDataType x)
{
assert(pphead);//不能为空,就算slist为空,但是slist也是一个指针变量,不会为空
SListNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else {
// 找尾
SListNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
// 错误写法,这里没有链接起来
/*SListNode* tail = *pphead;
while (tail != NULL)
{
tail = tail->next;
}
tail = newnode;*/
}
}
尾插则需要有一些细节,
-
判断链表是否为空,若为空则将头指针指向新节点
if (*pphead == NULL) { *pphead = newnode; }
-
在找尾的时候要注意循环条件,
此时pTail->pNext != NULL 成立,pTail指针后移
此时pTail->pNext != NULL 成立,pTail指针后移
注意,此时后面没有结点了,则此时pTail所指向的结点里面的指针域存放的是空指针,即pTail->pNext为空,pTail刚好指向最后一个结点。我们再来看看循环条件为pTail != NULL的情况
此时pTail != NULL 成立,pTail指针后移
此时pTail != NULL 依然成立,pTail指针后移
此时pTail != NULL 依然成立,pTail指针后移
我们可以发现pTail != NULL这个条件执行时,当pTail指向尾结点时也不会停止,因此该循环条件是错误的
2.6.3单链表的指定位置插入
在pos位置之前插入
//在pos位置插入
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
//pos是第一个节点
//pos不是第一个节点
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else {
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SListNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
在pos位置之后插入
单链表可以通过前一个结点找到下一个结点,不能通过后面的结点找到前面的结点。因此,在pos位置后面插入就没必要向之前在pos位置之前插入时需要通过指针的循环移动找节点了,因为通过pos可以直接找到下一个结点
// 在pos位置之后插入
void SListInsertAfter(SListNode * pos, SLTDataType x)
{
assert(pos);
SListNode* next = pos->next;
SListNode* newnode = BuySListNode(x);
pos->next = newnode;
newnode->next = next;
/*SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;*/
}
第一种我们也可以一个next记录一下pos下一节点的地址,思路比较清晰
2.7单链表的删除
2.7.1单链表的头删
void SListPopFront(SListNode** pphead)
{
assert(pphead);
//1,空
//2.非空
if (*pphead == NULL)
{
return;
}
else
{
SListNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
这里我们需要注意的就是我们*pphead是否为空
2.7.2单链表的尾删
void SListPopBack(SListNode** pphead)
{
assert(pphead);
// 也可以暴力检查为空的情况
//assert(*pphead != NULL);
// 1、空
// 2、一个节点
// 3、多个节点
if (*pphead == NULL)// 温柔检查
{
return;
}
else if (( * pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SListNode* prev = NULL;
SListNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
/*SListNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;*/
}
}
- 尾部删除时要考虑三种情况,链表是否为空,链表只有一个结点和链表有多个结点
在多个节点进行尾删的时候,第一种写法我们也可以通过创建一个prev记录一下tail前一个结点的地址,再进行尾删后方便找到新一个尾部结点,将尾部结点的next置为空。
2,7,3单链表的指定位置删除
//删除pos位置
void SListErase(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SListPopFront(pphead);
}
else {
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
**当pos等于头指针时,相当于链表的头删,我们可以直接调用之前写过的头删函数SListPopFront()**进行复用
2.8单链表的查找
SLTDataType* SListFind(SListNode* pphead, SLTDataType x)
{
SListNode* cur = pphead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
单链表的查找也很容易,通过遍历链表找到储存对应数据的结点并返回指针
2.9单链表的销毁
void SListDestroy(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
if (cur)
{
SListNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
单链表的销毁也非常轻松,一样通过cur指针遍历数组,用next指针记录下一个结点地址,并删除当前结点,当cur为空结束循环,将*pphead置为空,返回初始化指针。
3.结语
以上就是本人对链表以及单链表的认识和回顾,希望大家帮忙点赞支持一下!