文章目录
- 1.单链表
-
- [1.1 概念与结构](#1.1 概念与结构)
-
- [1.1.1 结点](#1.1.1 结点)
- [1.1.2 链表的性质](#1.1.2 链表的性质)
- [1.1.3 链表的打印](#1.1.3 链表的打印)
- [1.2 实现单链表](#1.2 实现单链表)
1.单链表
1.1 概念与结构
概念:链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/ 加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。
在链表⾥,每节"⻋厢"是什么样的呢?

1.1.1 结点
与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为"结点/结点"
火车有一节一节车厢组成
链表由一个一个结点组成
结点有两个组成部分:
1)保存的数据
2)指针:始终保存下一个结点的地址
图中指针变量plist保存的是第⼀个结点的地址,我们称plist此时"指向"第⼀个结点,如果我们希望 plist"指向"第⼆个结点时,只需要修改plist保存的内容为0x0012FFA0。
链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针 变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。
1.1.2 链表的性质
1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续
2、结点⼀般是从堆上申请的
3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码:
假设当前保存的结点为整型:
c
struct SListNode
{
int data; //结点数据
struct SListNode* next; //指针变量⽤保存下⼀个结点的地址
};
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个结点的地址(当下⼀个结点为空时保存的地址为空)。
1.1.3 链表的打印

给定的链表结构中,如何实现结点从头到尾的打印?
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
printf("%d ->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
思考:当我们想保存的数据类型为字符型、浮点型或者其他⾃定义的类型时,该如何修改?
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
1.2 实现单链表
c
#include"SList.h"
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
printf("%d ->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//pphead是一级指针的地址,是二级指针
//如果pphead为空,我们无法通过pphead来访问头结点指针,也无法修改头结点指针
//*pphead是指向第一个结点的地址
//申请新空间
SLTNode* newnode = SLTBuyNode(x);
//特殊情况,链表为空
if(*pphead == NULL)//*pphead获取pphead头指针内容
{
*pphead = newnode;
}
else
{
//找链表的尾结点
SLTNode* ptail = *pphead;
while (ptail->next != NULL)
{
ptail = ptail->next;//这是在往后走
}
//找到了尾结点
ptail->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
//形参为SLTNode** pphead,是因为要对plist(存放的地址)本身进行修改
assert(pphead);
//给新结点申请空间
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;//赋值*pphead本身
*pphead = newnode;//此时newnode是第一个结点
}
//尾删
void SLTPopBack(SLTNode** pphead)//第一个结点可能会被删,会发生改变,所以是二级指针
{
//链表为空不能删除,也就是第一个结点的地址不能为空
//也就是说pphead不能为空,因为不能传空指针,第一个结点的地址也不能为空
assert(pphead&&*pphead);
//链表只有一个结点的情况
if ((*pphead)->next == NULL)//先解引用
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;//最后指向尾结点的前一个结点,为了将前一个结点的next置为空
SLTNode* ptail = *pphead;//最后指向尾结点
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
//要先让*pphead指向第二个结点,再删除第一个结点
SLTNode* next = (*pphead)->next;//先将第二个结点的地址存下来,释放掉第一个结点后,给*pphead赋值
free(*pphead);
*pphead = next;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
//不会改变第一个结点的内容,用*phead接收就行
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
return pcur;
pcur = pcur->next;
}
return NULL;
}
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
SLTNode* newnode = SLTBuyNode(x);//先创建一个新结点
if (pos == *pphead)
{
//头插
SLTPushFront(pphead, x);
}
else
{
//pos的前一个结点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//先连后断
newnode->next = pos->next;//先让newnode指向pos的下一个结点,再让pos->next=newnode
pos->next = newnode;
}
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead && pos);
SLTNode* prev = *pphead;
if (pos == *pphead)//pos是头结点
SLTPopFront(pphead);//头删
else
{
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos&&pos->next);
SLTNode* del = pos->next;//pos的下一个结点
//一旦调用 free(pos->next),pos->next 指向的内存就被系统回收了
//后续再访问 pos->next->next 就是访问已释放内存,会导致未定义行为,所以用del保存
pos->next = del->next;
free(del);
del = NULL;
}
//销毁链表
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;//先保存下一个结点,再释放
free(pcur);
pcur = next;
}
//*pphead始终保存第一个结点的地址,但是这个地址已经还给操作系统了
//*pphead此时是个野指针了,要置为空
*pphead = NULL;
}
c
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//链表的结构
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//打印
void SLTPrint(SLTNode* phead);
//申请一个节点的空间
SLTNode* SLTBuyNode(SLTDataType x);
//尾插一个
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插一个
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删一个数据
void SLTPopBack(SLTNode** pphead);
//头删一个
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDestroy(SLTNode** pphead);
c
#include"SList.h"
void test01()
{
//创建一个链表
SLTNode* node1 =(SLTNode*) malloc(sizeof(SLTNode));
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
//存数据
node1->data = 1;
node2->data = 2;
node3->data = 3;
node4->data = 4;
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
//打印链表
SLTNode* plist = node1;
SLTPrint(plist);
}
void test02()
{
SLTNode* plist = NULL;
//尾插
//SLTPushBack(plist, 1);传的是plist存的数据(也就是NULL的地址),而不是plist本身
//要让phead(形参)的改变影响实参,就要传plist的地址
//此时形参phead改为pphead,表示phead的地址,数据类型由SLTNode*变为SLTNode**
//除了特殊情况(数组等)都用&取地址
//SLTPushBack(&plist, 1);
//SLTPushBack(&plist, 2);
//SLTPushBack(&plist, 3);
//SLTPushBack(&plist, 4);
//SLTPrint(plist);
//尾删
//SLTPopBack(&plist);
//SLTPrint(plist);
//SLTPopBack(&plist);
//SLTPrint(plist);
//SLTPopBack(&plist);
//SLTPrint(plist);
//SLTPopBack(&plist);
//SLTPrint(plist);
//头插
SLTPushFront(&plist, 1);
SLTPrint(plist);
SLTPushFront(&plist, 2);
SLTPrint(plist);
SLTPushFront(&plist, 3);
SLTPrint(plist);
SLTPushFront(&plist, 4);
SLTPrint(plist);
//头删
//SLTPopFront(&plist);
//SLTPrint(plist);
//SLTPopFront(&plist);
//SLTPrint(plist);
//SLTPopFront(&plist);
//SLTPrint(plist);
//SLTPopFront(&plist);
//SLTPrint(plist);
//查找
SLTNode* pos = SLTFind(plist, 2);
if (pos)
printf("找到了\n");
else
printf("没找到\n");
//在pos之前的位置删除一个数据
//SLTInsert(&plist, pos, 100);
//SLTPrint(plist);
//删除pos结点
//SLTErase(&plist, pos);
//SLTPrint(plist);
//删除pos之后的结点
SLTEraseAfter(pos);
SLTPrint(plist);
//销毁
SListDestroy(&plist);
}
int main()
{
//test01();
test02();
return 0;
}
c
//当只剩下一个结点时,调用这个函数
void SLTPopBack(SLTNode** pphead)
{
assert(pphead&&*pphead);
SLTNode* prev = NULL;
SLTNode* ptail = *pphead;
while (ptail->next)//不执行
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;//当只剩下一个结点时,prev已经是NULL,不能对next进行赋值
free(ptail);
ptail = NULL;
}
c
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
SLTNode* del = pos->next;//pos的下一个结点
//一旦调用 free(pos->next),pos->next 指向的内存就被系统回收了
//后续再访问 pos->next->next 就是访问已释放内存,会导致未定义行为,所以先用del保存
pos->next = del->next;
free(del);
del = NULL;
}
c
//销毁链表
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;//先保存下一个结点,再释放
free(pcur);
pcur = next;
}
//*pphead始终保存第一个结点的地址,但是这个地址已经还给操作系统了
//*pphead此时是个野指针了,要置为空
*pphead = NULL;
}
销毁完之后的调试

尾插和尾删的时间复杂度O(N)
头插和头删的时间复杂度O(1)
总结:头插和头删用的多使用链表
尾插和尾删用的多使用顺序表
在指定位置之前插入数据时间复杂度O(n)在指定位置之前插入数据时间复杂度O(1)