文章目录
前言
数据结构的常见线性表,分别是顺序表,链表
,栈,队列
本篇给大家带来单向无头链表
的实现和讲解
为什么会有单链表?
- 动态顺序表储存数据存会造成
一定的消耗
- 链表存储数据是
按需扩容
,不会造成空间浪费 - 通常做复杂数据结构的
子部分
等单链表的相关文章写完,之后就出一篇详细顺序表和单链表的区别
一、链表的概念和结构
1.1 概念
链表是一种物理存储
结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
1.2 结构和存储
-
逻辑结构
-
物理结构
- 从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续,他们这种连续的结构主要是其指针域指向了下一个结点的地址。
- 现实中的结点一般都是从堆(heap)申请出来的。
- 从堆上申请空间,是按照一定的策略来分配的,两次申请的空间可以能连续,也可能不连续。比如连续:第一个结点地址是0x0023b11,下一个地址就是0x0023b15
二、链表的分类
2.1 单项和双向
2.2 带头和不带头
2.3 循环和非循环
综上所述以有8种链表,但是我们实际用的就**2种
**
-
无头单向非循环链表:结构简单,一般不会单独用来存放数据。实际中更多是作为
其他数据结构的子结构
。如:哈希桶、图的领接表等等。另外这种结构在笔试面试中出现很多。 -
带头双向循环链表:结构最复杂,一般用在单独存储数据。另外这个结构虽然复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
三、单链表的实现
一般我们写一个项目的时候,一共包含3个文件。
- 头文件.h:存放该项目所有需要的头文件,结构定义,函数声明。
- 函数定义文件.c:函数的定义和实现
- 测试文件.c:测试函数的功能
3.1 定义单链表的结构
一般定义结构前,我们会先重命名该结构的数据类型,因为这个结构可能会存放不止一种数据,可能会存放int、float、double等等,所以为了方便我们代码后期维护,重定义后只需要改变一处就可以了。
// 存放的数据类型
typedef int SListDataType;
单个节点包含两个部分:数据域(data)
和指针域(next)
- data:存放结点的数据部分
- next:存放下一个结点(结构体)的地址
// 单链表结点的定义
typedef struct SListNode
{
SListDataType data; // 数据域
struct SListNode* next; // 指针域
}SListNode;
这里的意思就是重命名,把
sturct SListNode 命名为SListNode
,大家可以看我们后面的代码我定义变量的使用,类型是SListNode
,正常情况在c语言里是不行,必须要加
关键字struct+结构体名
才可以定义,c++
里面就不要加关键字。
3.2 创建一个结点
每次插入的时候,我们都需要写很多重复的代码,所以可以把创建节点写成一个函数,后面方便调用。
思路:
- 申请一块结点结构体大小的空间
- 对他进行判断,是否为NULL
- 把数据放到data
- next指向NULL
- 最后返回申请成功的结点
// 创建一个节点
SListNode* CreateSListNode(SListDataType x)
{
// 申请一块空间
SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
// 判断是否为空
if (newNode == NULL)
{
printf("new node falied!");
exit(-1);
}
// 需要插入的数据放到data里
newNode->data = x;
// 该结点的next指向空
newNode->next = NULL;
// 返回申请的结点
return newNode;
}
3.3 尾插
思路:
- 利用assert,防止实参传过来NULL,这是不是说链表地址,而是直接传NULL
- 调用刚才创建结点的函数
- 判断链表是否为空,分两种情况,1. 链表为空 2. 链表不为空
为空就直接把创建的结点设为头节点,不为空就找尾,把尾结点的next指向新创建的结点
// 尾插
void SListPushBack(SListNode** pphead, SListDataType x)
{
// 1.防止传入NULL
assert(pphead);
// 2.创建一个结点
SListNode* newNode = CreateSListNode(x);
// 3.判断是否为链表
if (*pphead == NULL)
{
// 把创建的结点设为头节点
*pphead = newNode;
}
else
{
// 找尾
SListNode* tail = *pphead;
while (tail->next)
{
tail = tail->next;
}
tail->next = newNode;
}
}
这里大家想必有疑问,尾插函数的形参为什么是二级指针呢?
正常情况的下的尾插:
如果尾插函数
接收链表指针形参
是SListNote *phead,这时候phead是一级指针
,tail被赋值成为phead,tail此时指向的是链表的头结点,当链表不为空的时候,tail通过while循环找到链表的尾,将新的结点链接到链表的尾部。
特殊情况下的尾插:
如果尾插函数
接收链表地址形参
是SListNote *phead,当尾插的链表是空链表的时候,这是newNode就没有变化
,还是那个老生常谈的问题,这个赋值只是临时的拷贝
而已,大家可以看看我们创建结构体的那段代码,创建next变量的时候类型是指针类型,所以我们创建链表的时候也要用指针,但是你要通过函数来改变普通变量的话,需要传地址(指针)才能改变,如果传过来的指针变量,就需要用指针的指针
,就是二级指针
。
所以我们在测试文件的时,只要对链表的头结点进行修改时,就需要传二级指针
3.4 打印
尾插完成后,我们可以对插入的数据进行下输出,是否插入成功
思路:用我们创建一个临时变量cur来保存该链表的头部地址,因为我们一般不会利用链表的头部来遍历,头部是我们访问链表的唯一途径
,修改后数据不完整。
// 打印
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur)
{
// 打印数据
printf("%d -> ", cur->data);
// 迭代往下走
cur = cur->next;
}
printf("NULL\n");
}
注意:这里没有改变链表的头,所以不需要传二级指针
3.5 头插
思路:
- 调用创建结点函数
- 让新节点的next指向链表的头
- 把新节点设为头节点
// 头插
void SListPushFront(SListNode** pphead, SListDataType x)
{
// 防止实参传入NULL
assert(pphead);
SListNode* newNode = CreateSListNode(x);
newNode->next = *pphead;
*pphead = newNode;
}
3.5 尾删
思路:
- 空链表不能删除
- 删除分为两种情况:
- 正常情况:有两个以上结点,找尾结点的前一个,把尾结点前一个的next置为NULL
- 特殊情况:只有一个头结点,对头节点置NULL
- 释放该结点的空间。把结点置为NULL,只是他们之间没有连接了,但是原本申请的空间还在,不处理会造成
内存泄漏
// 尾删
void SListPopBack(SListNode** pphead)
{
assert(pphead);
assert(*pphead); // 空链表不能删除
// 特殊情况:只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
// 正常情况:找尾结点的前一个
SListNode* tail = *pphead;
SListNode* tailPrev = NULL;
while (tail->next)
{
tailPrev = tail;
tail = tail->next;
}
tailPrev->next = NULL;
// 释放空间
free(tail);
}
}
这里想必大家有疑问,为什么我
free掉没有置NULL
,没有置空就会变成野指针
,这一般是配套使用啊,有free就要有置空,这是一个好习惯,但是也分情况,因为这里的free是我在函数体内定义的,所以我们对它置空是没有意义的,只是临时变量而已,出了函数体就会被销毁。
3.6 头删
思路:
- 空链表不能删除
- 创建一个临时指针变量,用来存储头下一个节点(headNext)
- 把头节点释放
- 存储头下一个的节点(headNext)设为头节点
头尾相关操作的测试:
// 头删
void SListPopFront(SListNode** pphead)
{
// 空链表不能删除
assert(*pphead);
SListNode* headNext = (*pphead)->next;
free(*pphead);
*pphead = headNext;
}
3.7 查找
查找返回值分为两种,1.返回结点地址 2.返回结点的下标,我这里演示的
返回地址
返回结点地址更方便,可以直接修改的地址和数据,比如配合pos后面插入删除
,下标需要遍历,地址的话直接操作。
// 查找,返回节点的地址
SListNode* SListFind(SListNode* phead, SListDataType x)
{
assert(phead);
SListNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
3.8 在pos处插入
思路:
- 调用创建结点函数
- 插入分为两种情况
- 正常情况:找到pos结点的前一个(posPrev),posPrev的next设为新节点,新节点的next设为pos
- 特殊情况:pos为头结点,等于头插
// 在pos处插入
void SListInsert(SListNode** pphead, SListNode* pos, SListDataType x)
{
SListNode* newNode = CreateSListNode(x);
// 头插
if (pos == *pphead)
{
newNode->next = *pphead;
*pphead = newNode;
}
else
{
// 找pos的前一个结点
SListNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newNode;
newNode->next = pos;
}
}
3.9 在pos后面插入
思路:
- 调用创建结点函数
- 新节点的next设为pos的next,pos的next设为新结点,
顺序不要乱
// 在pos后面插入
void SListInsertAfter(SListNode* pos, SListDataType x)
{
assert(pos);
SListNode* newNode = CreateSListNode(x);
newNode->next = pos->next;
pos->next = newNode;
}
3.10 在pos处删除
思路:
- 空链表不能删除
- 删除分为两种情况
- 正常情况:找到pos结点的前一个(posPrev),posPrev的next指向pos的next
- 特殊情况:pos为头结点,等于
头删
- 释放pos
// 在pos处删除
void SListErase(SListNode** pphead, SListNode* pos)
{
// 特殊情况:头删
if (pos == *pphead)
{
SListNode* headNext = (*pphead)->next;
free(*pphead);
*pphead = headNext;
}
else
{
// 正常情况:找pos的前一个结点
SListNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = pos->next;
free(pos);
}
}
3.11 在pos后面删除
思路:
- 禁止删除pos后面是NULL的情况,如果是NULL,会造成对
空指针的引用
(NULL->next
) - 存储pos的后一个结点(posNext)
- pos的next指向posNext的next(pos后第二个结点)
- 释放posNext
// 在pos后面删除
// 在pos后面删除
void SListEraseAfter(SListNode* pos)
{
assert(pos);
assert(pos->next); // pos后面禁止是NULL,会造成空指针的引用
SListNode* posNext = pos->next;
pos->next = posNext->next;
free(posNext);
}
pos处和查找配合相关测试
3.12 pos处相关操作的特点
pos处操作:
- 时间复杂度是
O(N)
:需要找pos的前一个- 需要
二级指针
:可以对头结点修改
pos后面操作:
- 时间复杂度是
O(1)
:不用找结点。一级指针
:不能对头节点修改
3.13 销毁
这里的销毁
不像顺序表
,直接free掉链表的地址就可以,因为链表之间是通过指针域连接起来的
,这里free掉链表的地址就等于删除了一个节点而已,并且你再也找不到剩下的结点了,我们需要迭代着依次free
所有结点,才算销毁成功。
思路:
- 空链表不能销毁
- 存储当前节点的下个节点(curNext)
- 删除当前节点
- 利用下个节点迭代,依次这里操作,直到所有结点删除完毕
- 把头结点置为NULL
// 销毁
void SListDestroy(SListNode** pphead)
{
assert(pphead);
// 链表为空不用销毁
assert(*pphead);
SListNode* cur = *pphead;
SListNode* curNext = NULL; // 存放当前节点的下个节点
// 迭代销毁
while (cur)
{
curNext = cur->next;
free(cur);
cur = curNext;
}
*pphead = NULL;
}