数据结构初阶:详解单链表(一)
🔥个人主页:胡萝卜3.0
🎬作者简介:C++研发方向学习者
📖个人专栏: 《C语言》《数据结构》 《C++干货分享》
⭐️人生格言:不试试怎么知道自己行不行
目录
顺序表问题与思考
正文
一、单链表
1.1 概念与结构
1.1.1 结点
1.1.2 链表的性质
1.1.3 链表的打印
二、实现单链表
2.1 单链表的结构
2.2 尾插
2.2 头插
2.3 尾删
2.4 头删
2.5 查找
2.6 在指定位置之前插入数据
2.7 在指定位置之后插入数据
2.8 删除指定位置上的结点
2.9 删除指定位置之后的结点
2.10 销毁单链表
三、完整代码
顺序表问题与思考
中间/头部的插入删除,时间复杂度为O(N)
增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。
增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入5个数据,后面没有数据插入了,那么就浪费了95个数据空间
那我们该如何解决以上问题呢?
ok,这时候单链表就闪亮登场了!!!接下来我们一起来学习单链表相关的知识。
正文
一、单链表
1.1 概念与结构
概念:链表是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
也就是说单链表在逻辑结构上是线性的,在物理结构上不一定是线性的
在淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车箱去掉/加上,是不会影响其他车厢,每节车厢都是独立存在的。
那么在链表里,每节“车厢”是什么样的呢?
1.1.1 结点
与顺序表不同的是,链表里的每节“车厢”都是独立申请下来的空间,我们称之为“节点/结点”
图中指针变量plist保存的是第一个结点的地址,我们称plist此时“指向”第一个结点,如果我们希望plist“指向”第二个结点时,只需要修改plist保存的内容为0x0012FFA0。
链表中每个结点都是独立申请的(即需要插入数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。
1.1.2 链表的性质
- 链式机构在逻辑上是连续的,在物理结构上不⼀定连续
- 结点⼀般是从堆上申请的
- 从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
结合前面学到的结构体知识,我们可以给出每个结点对应的结构体代码:
假设当前保存的结点为整型:
struct SListNode
{int data; //结点数据struct SListNode* next; //指针变量⽤保存下⼀个结点的地址
}
当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,也需要保存下一个结点的地址(当下一个结点为空时保存的地址为空)。
当我们想要从第一个结点走到最后一个结点时,只需要在当前结点拿上下⼀个结点的地址就可以了。
1.1.3 链表的打印
给定的链表结构中,如何实现结点从头到尾的打印?
思考:当我们想保存的数据类型为字符型、浮点型或者其他自定义的类型时,该如何修改?
二、实现单链表
在前面的学习中,我们知道单链表需要一个头结点,当我们不给链表中插入任何数据,头结点为一个空结点,那我们该如何定义一个空结点呢?
2.1 单链表的结构
typedef int SLTDataType;
typedef struct SListNode
{int data;//存储数据struct SListNode* next;//保存的是下一个节点的地址
}SLTNode;
有了单链表的结构,我们是不是应该给里面插入一些值呢?必须插入一些值,那我们是在尾部插入还是应该在头部插入呢?其实在单链表中,我们既可以在尾部插入,也可以在头部插入。
2.2 尾插
尾插尾插,不就是要找到链表的尾部,然后插入数据嘛,首先第一步为数据创建结点空间,然后找到链表的尾部,最后插入数据。
这里有个问题,刚开始,没有向链表中插入任何数据,链表是一个空结点,那我们该怎么办?
如果链表为空,我们直接将结点的地址给头结点!!!
我们要申请新的节点(需要malloc),我们单独封装一个函数。
现在新节点就申请好了,我们要让5和4节点连起来:
这就是为什么我们明明已经有phead这个指针,还要额外再定义一个指针pcur——
这样一来pcur在不断变化,phead保持不变,phead始终保存的是第一个节点的地址。在这里我不想改变phead,phead始终指向第一个节点,方便我们后面遍历完了如果还要再从头开始遍历的时候我们能够找到第一个节点的地址。
我们定义pcur,只要pcur不为空,我们就进入循环,pcur为空我们就跳出循环。
ok,看代码
为新节点创建空间
//为节点创建空间
SLTNode* SlBuyNode(SLTDataType num)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->data = num;newnode->next = NULL;return newnode;
}
尾插代码:
//尾插
void SLPushBack(SLTNode** pphead, SLTDataType num)
{assert(pphead);//为节点申请空间SLTNode* newnode = SlBuyNode(num);//头结点为空if (*pphead == NULL){*pphead = newnode;}else{//找到尾节点//头结点不为空SLTNode* ptail = *pphead;while (ptail->next != NULL){ptail = ptail->next;}ptail->next = newnode;}
}
尾插的时间复杂度为:O(N)
我相信会有很多小伙伴看到上面的代码会很疑惑?为什么是个二级指针?
ok,我们来看一下如果不传二级指针会发生什么?
当我们在编译器中进行调试的时候,发现程序执行完之后,链表中还是没有值,这是什么原因?
我们看到在调用尾插的代码时,实参部分传的是plist,这是传值调用,我们知道传值调用,形参改变不会影响实参,所以在这里我们要使用传址调用,当使用传址调用时,由于plist是一个一级指针,一级指针的地址需要用二级指针来接受
2.2 头插
我们需要将结点插入到头结点的前面,使新节点成为新的头结点
代码:
//头插
void SLPushFront(SLTNode** pphead, SLTDataType num)
{assert(pphead);//为新节点创建空间SLTNode* newnode = SlBuyNode(num);//newnode *ppheadnewnode->next = *pphead;*pphead = newnode;
}
头插时间复杂度为:O(1)
2.3 尾删
尾删操作需要找到尾节点以及尾节点的前一个结点,需要先将尾节点的前一个结点的next域置为NULL,然后释放尾节点,如果链表中只要一个头结点,直接将头结点的空间释放。
代码:
//尾删
void SLPopBack(SLTNode** pphead)
{//结点为空,不能删除assert(pphead && *pphead);//如果只有一个头结点if ((*pphead)->next==NULL){free(*pphead);*pphead = NULL;}else{SLTNode* prev = NULL;SLTNode* ptail = *pphead;//找到尾节点以及尾节点的前一个节点while (ptail->next != NULL){prev = ptail;ptail = ptail->next;}//prev ptailprev->next = NULL;free(ptail);ptail = NULL;}
}
尾插的时间复杂度为:O(N)
2.4 头删
头删删除的是头结点,首先要将头结点的下一个结点保存起来,然后释放头结点的空间,让头结点的下一个结点成为新的头结点。
代码:
//头删
void SLPopFront(SLTNode** pphead)
{//保留头结点的下一个节点SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}
头删的时间复杂度为:O(1)
2.5 查找
遍历链表,如果找到,则返回该结点的地址;如果没找到,返回NULL
//查找
SLTNode* SLFind(SLTNode** pphead, SLTDataType num)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur != NULL){if (pcur->data == num){return pcur;}pcur = pcur->next;}return NULL;
}
ok,既然已经学了查找的代码,那我们就可以实现在指定位置之前/之后插入/删除数据了。
2.6 在指定位置之前插入数据
当指定位置为头结点时,那不是头插嘛;如果指定位置不是头结点,我们需要找指定位置的前一个结点,并修改其中保存的指针指向。
代码
//在指定位置之前插入数据
void SLInsertFront(SLTNode** pphead, SLTNode* pos, SLTDataType num)
{//链表为空,不能插入assert(*pphead && pphead);//pos为空,不能插入assert(pos);//为新节点创建空间SLTNode* newnode = SlBuyNode(num);//如果pos为头结点,直接头插if (pos == *pphead){//头插SLPushFront(pphead, num);}else{//找到指定位置的前一个节点SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}//pcur newnode posnewnode->next = pos;pcur->next = newnode;}
}
时间复杂度为:O(N)
2.7 在指定位置之后插入数据
我们通过改变pos位置上的结点中的next指针,指针指向新结点,然后新结点中的next指针指向pos位置的后一个结点。
代码
//在指定位置之后插入数据
void SLInsertBack(SLTNode* pos, SLTDataType num)
{assert(pos);//为新节点创建空间SLTNode* newnode = SlBuyNode(num);newnode->next = pos->next;pos->next = newnode;
}
时间复杂度为:O(1)
2.8 删除指定位置上的结点
思路:如果pos位置为头结点,直接进行头删;如果pos位置不是头结点,需要找到pos位置的前一个结点,然后改变前一个结点中的next指针,使其指向pos位置的后一个结点,然后释放pos位置上的结点空间。
代码
//删除pos位置上的节点
void SLErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);if (pos == *pphead){//头删SLPopFront(pphead);}else{SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}//pcur pos pos->nextpcur->next = pos->next;free(pos);pos = NULL;}
}
2.9 删除指定位置之后的结点
改变pos位置中的指针指向,使其指向del结点的下一个结点,然后释放del结点的空间。
代码
//删除pos位置之后的节点
void SLEraseBack(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}
2.10 销毁单链表
遍历链表,从头结点开始释放,先保存头结点的下一个结点的地址,然后释放头结点的空间,下一个结点成为新的头结点,重复此操作,直至遇到NULL。
代码
//销毁链表
void SLDestory(SLTNode** pphead)
{assert(*pphead);SLTNode* pcur = *pphead;while (pcur != NULL){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
三、完整代码
SList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode
{int data;//存储数据struct SListNode* next;//保存的是下一个节点的地址
}SLTNode;
//打印链表
void SLPrint(SLTNode** pphead);
//尾插
void SLPushBack(SLTNode** pphead, SLTDataType num);
//头插
void SLPushFront(SLTNode** pphead, SLTDataType num);
//尾删
void SLPopBack(SLTNode** pphead);
//头删
void SLPopFront(SLTNode** pphead);
//查找
SLTNode* SLFind(SLTNode** pphead, SLTDataType num);
//在指定位置之前插入数据
void SLInsertFront(SLTNode** pphead, SLTNode* pos, SLTDataType num);
//在指定位置之后插入数据
void SLInsertBack(SLTNode* pos, SLTDataType num);
//删除pos位置上的节点
void SLErase(SLTNode** pphead,SLTNode* pos);
//删除pos位置之后的节点
void SLEraseBack(SLTNode* pos);
//销毁链表
void SLDestory(SLTNode** pphead);
SList.c
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
//打印节点中的数据
void SLPrint(SLTNode** pphead)
{SLTNode* pcur = *pphead;while (pcur != NULL){printf("%d ->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
//为节点创建空间
SLTNode* SlBuyNode(SLTDataType num)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->data = num;newnode->next = NULL;return newnode;
}
//尾插
void SLPushBack(SLTNode** pphead, SLTDataType num)
{assert(pphead);//为节点申请空间SLTNode* newnode = SlBuyNode(num);//头结点为空if (*pphead == NULL){*pphead = newnode;}else{//找到尾节点//头结点不为空SLTNode* ptail = *pphead;while (ptail->next != NULL){ptail = ptail->next;}ptail->next = newnode;}
}
//头插
void SLPushFront(SLTNode** pphead, SLTDataType num)
{assert(pphead);//为新节点创建空间SLTNode* newnode = SlBuyNode(num);//newnode *ppheadnewnode->next = *pphead;*pphead = newnode;
}
//尾删
void SLPopBack(SLTNode** pphead)
{assert(pphead);//如果只有一个头结点if ((*pphead)->next==NULL){free(*pphead);*pphead = NULL;}else{SLTNode* prev = NULL;SLTNode* ptail = *pphead;//找到尾节点以及尾节点的前一个节点while (ptail->next != NULL){prev = ptail;ptail = ptail->next;}//prev ptailprev->next = NULL;free(ptail);ptail = NULL;}
}
//头删
void SLPopFront(SLTNode** pphead)
{//保留头结点的下一个节点SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}
//查找
SLTNode* SLFind(SLTNode** pphead, SLTDataType num)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur != NULL){if (pcur->data == num){return pcur;}pcur = pcur->next;}return NULL;
}
//在指定位置之前插入数据
void SLInsertFront(SLTNode** pphead, SLTNode* pos, SLTDataType num)
{//链表为空,不能插入assert(*pphead && pphead);//pos为空,不能插入assert(pos);//为新节点创建空间SLTNode* newnode = SlBuyNode(num);//如果pos为头结点,直接头插if (pos == *pphead){//头插SLPushFront(pphead, num);}else{//找到指定位置的前一个节点SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}//pcur newnode pos//newnode->next = pos;pcur->next = newnode;newnode->next = pos;}
}
//在指定位置之后插入数据
void SLInsertBack(SLTNode* pos, SLTDataType num)
{assert(pos);//为新节点创建空间SLTNode* newnode = SlBuyNode(num);newnode->next = pos->next;pos->next = newnode;
}
//删除pos位置上的节点
void SLErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);if (pos == *pphead){//头删SLPopFront(pphead);}else{SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}//pcur pos pos->nextpcur->next = pos->next;free(pos);pos = NULL;}
}
//删除pos位置之后的节点
void SLEraseBack(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}
//销毁链表
void SLDestory(SLTNode** pphead)
{assert(*pphead);SLTNode* pcur = *pphead;while (pcur != NULL){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
void test01()
{SLTNode* plist = NULL;//尾插SLPushBack(&plist, 1);SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLPushBack(&plist, 5);SLPushBack(&plist, 6);SLPrint(&plist);//头插SLPushFront(&plist, 0);SLPrint(&plist);//尾删SLPopBack(&plist);SLPrint(&plist);//头删SLPopFront(&plist);SLPrint(&plist);//查找SLTNode* pos=SLFind(&plist, 1);//指定位置之前插入SLInsertFront(&plist, pos, 8);SLPrint(&plist);//指定位置之后插入SLInsertBack(pos, 7);SLPrint(&plist);//删除指定位置数据SLErase(&plist, pos);SLPrint(&plist);//删除指定位置之后的数据pos = SLFind(&plist, 8);SLEraseBack(pos);SLPrint(&plist);//销毁SLDestory(&plist);}
int main()
{test01();return 0;
}
运行截图: