您现在的位置是:首页 >学无止境 >【数据结构】单链表(详解)网站首页学无止境
【数据结构】单链表(详解)
【数据结构】单链表(详解)
所属专栏:初始数据结构
博主首页:初阳785
代码托管:chuyang785>
感谢大家的支持,您的点赞和关注是对我最大的支持!!!
博主也会更加的努力,创作出更优质的博文!!
关注我,关注我,关注我,重要的事情说三遍!!!!!!!!
1.前言
前面我们已经用顺序表方式来实现接口函数,但是使用顺序表实现接口函数是有一些缺陷的。
- 空间不够了需要增容,增容是要付出代价。
- 头部或者中间插入删除数据的时候,需要挪动数据,挪动数据的额时候也是有消耗的。
- 避免频繁扩容,我们满了基本上是扩2倍,可能就会导致一定的空间浪费。
- 顺序表要求数据从开始位置连续存储那么我们在头部或者中间插入删除数据就需要挪动数据,效率不高。
针对顺序表的缺陷就设计出了单链表。
- 按需要申请空间,用不了了就释放空间(更合理的使用了空间)。
- 头部或中间插入删除数据,不需要挪动数据。
- 不浪费空间。
1.1本章节重点
本章节的重点是熟练掌握二级指针的使用,以及形参和实参之间的关系。
1.2 什么是单链表
所谓链表重在一个链字,顾名思义就是形象认为用一条链子讲各个数据链接起来,顺着条链子就可以找到其他的数据。
概念:
链表是一种物理上存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
图像解析:
最后是以NULL为结束标志的。
1.3 结构体设计
typedef struct SlistNode
{
int data;//存放数据
struct SlistNode* next;//存放下一个数据的地址
}SLTNode;
1.4结构体传参
SLTNode* plist = NULL;
SListPushBack(&plist, 1);//先随便那个函数结构当作测试用例
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPushBack(&plist, 5);
注意这里我们传的是plist的地址。而我们的plist同时有时结构体的指针,也就是说我们传过去的是一个二级指针。
至于我们为什么要传二级指针后面我们会讲到。
2. SList.h展示
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDateType;
//定义结构体
typedef struct SlistNode
{
int data;
struct SlistNode* next;
}SLTNode;
//打印
void SListPrint(SLTNode* phead);
//尾插
void SListPushBack(SLTNode** pphead, SLTDateType x);
//头插
void SListPushFront(SLTNode** pphead, SLTDateType x);
//尾删
void SListPopBack(SLTNode** pphead);
//头删
void SListPopFront(SLTNode** pphead);
//查找
SLTNode* SListFind(SLTNode* phead, SLTDateType x);
//给定给位置在pos前面插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
//给定一个位置在pos后面插入
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDateType x);
//给定pos位置删除
void SListErase(SLTNode** pphead, SLTNode* pos);
//给定一个位置删除pos后面的数据
void SListEraseAfter(SLTNode* pos);
//回收空间
void SListDestroy(SLTNode** pphead);
3. SList.c展示
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
SLTNode* BuyListNode(SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newnode);
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d-> ", cur->data);
cur = cur->next;
}
printf("NULL
");
}
//尾插
void SListPushBack(SLTNode** pphead, SLTDateType x)
{
assert(pphead);
//先开辟一开空间
SLTNode* newnode = BuyListNode(x);
//判断当没有数据的时候即:NULL
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
//头插
void SListPushFront(SLTNode** pphead, SLTDateType x)
{
assert(pphead);
SLTNode* newnode = BuyListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
//分情况
//1.只有一个节点的时候
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//2.有两个及两个以上的节点的时候
else
{
法一:
SLTNode* tail = *pphead;
找到尾节点的前一个
//SLTNode* prev = NULL;
找到尾插点
//while (tail->next != NULL)
//{
// prev = tail;
// tail = tail->next;
//}
//free(tail);
//tail = NULL;
//prev->next = NULL;
//法二:
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
//头删
void SListPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//查找
SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
int i = 0;
SLTNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
//给定给位置在pos前面插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
assert(pphead);
assert(pos);
SLTNode* newnode = BuyListNode(x);
SLTNode* posPrev = *pphead;
if (pos == *pphead)
{
newnode->next = *pphead;
*pphead = newnode;
}
else
{
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newnode;
newnode->next = pos;
}
}
//给定一个位置在pos后面插入
void SListInsertAfter( SLTNode* pos, SLTDateType x)
{
assert(pos);
SLTNode* newnode = BuyListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//给定pos位置删除
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
assert(*pphead);
if (*pphead == pos)
{
*pphead = pos->next;
free(pos);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next != NULL);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
//回收空间
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
//法1:
//SLTNode* tail = *pphead;
//SLTNode* front = tail;
//while (tail != NULL)
//{
// tail = tail->next;
// free(front);
// front = tail;
//}
//free(front);
//front = NULL;
//法二
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
4. 各个接口函数的实现
在进行学习的时候,博主建议小伙伴们结合画图来理解。
画图不仅可以更直观的看出程序是怎么实现的,还可以加深我们对单链表的记忆哦。
4.1 尾插
void SListPushBack(SLTNode** pphead, SLTDateType x)
{
assert(pphead);
//SLTNode* newnode = BuyListNode(x);//这里后面分装一个函数来开辟空间
//先开辟一开空间
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newnode);
newnode->data = x;
newnode->next = NULL;
//判断当没有数据的时候即:NULL
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
我们要插入数据,第一步肯定是先开一块空间,里面存放数据和NULL,这就用到了我们的动态内存开辟函数malloc,并把我们开辟好的空间的地址叫做newnode(新节点的意思)
接着就是将他们链接在一起。
这里用到了一个if语句判断我们链表是否有数据,如果没有我们就让我们开辟的一个空间作为我们的第一个数据,然后我们记住他第一个数据的地址记作*pphead。
而else后面的就是有数据之后就往数据后面插入数据就行了。
现在我们回到刚才的问题:为什么要传二级指针呢?
原因是我们咋插入第一个数据的时候是要改变我们传过来的值的,如果我们只是传plist过来了,我们接受的时候就变成了。
void SListPushBack(SLTNode* pphead, SLTDateType x)
这个时候我们的pphead是形参是实参的临时拷贝,而我们知道改变我们的新参是不能改变实参的,如果要改变实参,就必须通过传地址过来,通过解引用的方法来改变他的值,所以这里如果要想改变plist实参,就必须传plist的地址过来。
4.2 打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d-> ", cur->data);
cur = cur->next;
}
printf("NULL
");
}
这里的化就是比较简单的打印,也不需要传二级指针。
4.3 头插
4.3.1内存开辟函数
我们在讨论头插的时候就会想到,我们的尾插和头插总的讲都是插入数据,都是用先创建一个空间,在将他们一一链接起来,所以我们们就换想到能不能分装一个开辟内存的函数,来实现开辟内存呢?
SLTNode* BuyListNode(SLTDateType x)
{
//开辟一块空间
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newnode);
newnode->data = x;
newnode->next = NULL;
return newnode;
}
这个样的化就可以避免代码累赘了。
4.3.2插入
void SListPushFront(SLTNode** pphead, SLTDateType x)
{
assert(pphead);
//开辟一块空间
SLTNode* newnode = BuyListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
我们的头插也是比较好理解的。
4.4 尾删
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
//分情况
//1.只有一个节点的时候
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//2.有两个及两个以上的节点的时候
else
{
//法一:
SLTNode* tail = *pphead;
//找到尾节点的前一个
SLTNode* prev = NULL;
//找到尾插点
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
法二:
//while (tail->next->next != NULL)
//{
// tail = tail->next;
//}
//free(tail->next);
//tail->next = NULL;
}
}
单链表有一个特点就是他是单向的,你只能找到下一个数据,但是你找不到前一个数据。
这个时候我们就要定义一个新的变量(prev)来记住我们要删除的前一个数据地址。
在定义一个(tail)变量来找到我们的尾节点,找到后free,在是prev->next=NULL;
同样的这里也是要判断是否只剩下最后一个数据的情况,因为如果我们只剩下最后一个的化我们的prev=NULL,而prev->next是对NULL解引用,这明显是一个错误,所以我们的分两种情况,第一种是有两个及上的数据,第二个是只有一个数据的时候。
4.5 头删
void SListPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
头删的化就没什么好说的了。
直接就是将我们的 *pphead向前推进,然后free吊前面的。
同时这里也不许要分情况,因为就算只有一个数据了我们最后 *pphead也是指向NULL的。
4.6 查找
SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
int i = 0;
SLTNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
查找到话就是一一遍历。
4.7 给定一个位置在这个位置的前面插入数据
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
assert(pphead);
assert(pos);
//开辟一块空间
SLTNode* newnode = BuyListNode(x);
//当只有一个数据的时候
if (pos == *pphead)
{
newnode->next = *pphead;
*pphead = newnode;
}
//当有两个及以上的数据的时候
else
{
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newnode;
newnode->next = pos;
}
}
这里同样的也是要注意一点就是当只有一个数据的时候
4.8 给定一个位置在这个位置的后面插入数据
void SListInsertAfter( SLTNode* pos, SLTDateType x)
{
assert(pos);
SLTNode* newnode = BuyListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
如果是在后面插入数据的话就不会像上面那样麻烦了,也不用分情况了,因为如果我们在前面一个位置插入的话那么必定会有一个数据,我们就只要往这个数据后面插入即可。
4.9 给定一个位置删除这个位置的数据
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
assert(*pphead);
if (*pphead == pos)
{
*pphead = pos->next;
free(pos);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
如果我们想要删除中间的某个数据并且删除后还要将他们链接起来,前面我们说了,单链表是单向的,只能找到后面的数据,找不到前面的数据,这个时候我们就要定义一个变量来记住我们前面数据的地址,然后将他们链接起来。
注意:这里也是要分两种情况的,和上面的给定一个位置在这个位置的前面插入数据是一样的道理。
4.10 给定一个位置在删除这个位置前面的数据
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next != NULL);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
这里如果我们是删除pos前面的数据的话我们就不用在找到它前面的地址了,而是直接删除后面的数据,因为我们知道单链表是可以找到后面的数据的。
4.11 释放空间
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
//法一:
//SLTNode* tail = *pphead;
//SLTNode* front = tail;
//while (tail != NULL)
//{
// tail = tail->next;
// free(front);
// front = tail;
//}
//free(front);
//front = NULL;
//法二:
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
5 text.c展示
最后就是我们的text.c源文件了。
我们所写的以上函数接口都是实现功能的,二我们的主函数的一个作用就是测试我们的接口函数是否是正确的。
这里教大家一个小技巧,就是我们没实现一个接口的时候,我们都用一个函数来测试它的功能,这样的好处是我们可以减少我们在调试的时候用到其他的接口函数,这样不仅可能影响到我们的调试,而且还费时。
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void TestList1()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPushBack(&plist, 5);
SListPrint(plist);
}
void TestList2()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SListPrint(plist);
}
void TestList3()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
SListPopBack(&plist);
}
void TestList4()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SListPopFront(&plist);
SListPopFront(&plist);
SListPopFront(&plist);
SListPopFront(&plist);
SListPopFront(&plist);
SListPrint(plist);
}
void TestList5()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SLTNode* pos=SListFind(plist, 1);
int i = 1;
while (pos)
{
printf("第%d个节点:%p->%d
", i++, pos, pos->data);
pos= SListFind(pos->next, 1);
}
}
void TestList6()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SLTNode* pos = SListFind(plist, 5);
SListInsert(&plist, pos, 50);
SListPrint(plist);
}
void TestList7()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SLTNode* pos = SListFind(plist, 1);
SListInsertAfter(&plist, pos, 50);
SListPrint(plist);
}
void TestList8()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPushFront(&plist, 5);
SLTNode* pos = SListFind(plist, 1);
SListErase(&plist, pos);
pos = SListFind(plist, 2);
SListErase(&plist, pos);
pos = SListFind(plist, 3);
SListErase(&plist, pos);
pos = SListFind(plist, 4);
SListErase(&plist, pos);
pos = SListFind(plist, 5);
SListErase(&plist, pos);
SListPrint(plist);
SListDestroy(&plist);
}
int main()
{
//TestList1();
//TestList2();
//TestList3();
//TestList4();
//TestList5();
//TestList6();
//TestList7();
//TestList8();
return 0;
}