目录
一、链表
1、链表的概念及结构
概念:链表是一种物理存储结构上非连续 、非顺序的存储结构,数据元素的逻辑顺序 是通过链表中的指针链接次序实现的 。
这张图生动形象地呈现了链表的结构。
如同高铁一般,从头到尾一个连着一个。
2、分类
主要有两种类型的链表:单向链表和双向链表。在 单向链表 中,每个节点包含一个数据元素和一个指向下一个节点的引用。而在 双向链表中,每个节点有两个引用,一个指向前一个节点,另一个指向后一个节点。
- 从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续。
- 现实中的结点一般都是从堆上申请出来的。
- 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
本次讲解基础的单向链表。
二、实现单向链表
我们创建三个文件:
- 头文件LList.h用于调用库函数、声明结构体和函数。
- 源文件LList.c存储函数。
- 源文件text.c进行测试。
每个源文件都必须包含LList.h。
1、声明链表结构体
cpp
#include <stdio.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
- 将链表的数据类型用SLTDatatype这个别名代TT替int,以后程序中使用到元素数据类型时都替换成SLTDatatype,方便日后修改顺序表数据类型。
- 将结构体struct SListNode定义别名为SLTNode。
- 结构体成员data为链表节点数据,数据类型是
SLTDataType。
- next表示一个指向同类型结构体的指针,它指向单向链表下一个结点。
2、输出
cpp
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL) {
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
- 接收传入参数为结构体的地址
- 结构体指针cur指向头结点phead
- 循环遍历链表,当cur不指向尾节点,则打印输出当前节点数据,cur指向下一个节点。
- cur指向尾节点打印NULL。
3、头插&尾插
头插
cpp
void SLPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuyLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
- 插入数据会修改头结点,所以传入头结点指针的地址,使用二级指针接收。
- assert判断传入头节点指针的地址是否合法,为空则报错。(需要包含头文件<assert.h>)
- *pphead 不需要断言,如果传入的链表为空,也可以进行插入数据。
- 为新节点newnode开辟空间并将x储存其中,因为后续经常用到开辟空间,所以将这部分操作放入函数中。
- 新节点newnode的next指针指向头结点*pphead
- 头结点更新为newnode。
接下来讲解为新节点开辟空间的函数 BuyLTNode
新节点开辟空间
cpp
SLTNode* BuyLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("malloc fall");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
- 为新节点开辟空间,返回值为新节点的地址,所以函数类型为 SLTNode* 结构体指针类型。
- malloc函数为newnode开辟结构体大小个字节。
- 判断是否开辟成功,失败则打印错误信息,结束函数运行。
- 将新节点的数据data赋值为传入参数 x。
- next赋值为空。
尾插
cpp
void SLPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuyLTNode(x);
if (*pphead == NULL){
*pphead = newnode;
}
else {
SLTNode* tail = *pphead;
while (tail->next != NULL)
tail = tail->next;
tail->next = newnode;
}
}
- assert判断传入头节点指针的地址是否合法,为空则报错。
- 为新节点newnode开辟空间并将x储存其中。
- 插入时分两种情况:空链表 非空链表
- 如果链表为空则直接将
*pphead
指向新节点newnode
,使其成为新的链表的头节点。 - 如果链表不为空,则创建变量tail指向头结点,循环遍历链表使tail指向尾节点,将新节点地址赋值给tail的next,成功将新节点添加到链表尾部。
4、头删尾删
头删
cpp
void SLPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* del = *pphead;
*pphead = (*pphead)->next;
free(del);
}
- 第一个assert判断传入头节点指针的地址是否合法,为空则报错。
- 第二个assert判断链表头节点是否为空,为空无法删除,则报错。
- 定义变量del指向头节点,以便稍后释放该节点的内存。
- 头节点指向头节点的next,也就是指向后一个节点。
- 使用free函数释放del指向的已删除头节点空间。(需要包含头文件<stdlib.h>)
尾删
cpp
void SLPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* tail = *pphead;
while (tail->next->next) {
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
- 第一个assert判断传入头节点指针的地址是否合法,为空则报错。
- 第二个assert判断链表头节点是否为空,为空则报错。
- 链表只有一个节点时,直接释放头节点空间,然后置空。
- 链表有多个节点使,通过循环使变量 tail->next 找到尾节点,然后释放tail后一个节点的空间,也就是尾节点的空间,同时将其置空。
5、查找
cpp
SLTNode* STFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur) {
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
- 函数在单链表中查找包含特定数据值
x
的节点。 - 变量cur通过循环找到数据data等于x的节点。
- 找到则返回指向当前节点的指针
cur,否则返回值为空。
6、指定位置插入
指定位置之前
cpp
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (*pphead == pos){
SLPushFront(pphead, x);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos){
prev = prev->next;
}
SLTNode* newnode = BuyLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
-
第一个assert判断传入头节点指针的地址是否合法,为空则报错。
-
第二个assert判断传入指向链表中某个节点的指针pos是否合法,不存在则报错。
-
如果在头节点位置之前插入,则调用头插解决。
-
如果不是头节点位置,则创建一个指向链表头节点的指针
prev
,然后使用循环找到要插入位置pos
前面的节点。 -
创建一个新的节点
newnode
并将数据值x
存储在其中。 -
修改
prev
节点的next
指针,使其指向新节点newnode
,从而将新节点插入到pos
前面。
指定位置之后
cpp
void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuyLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
- assert判断传入指向链表中某个节点的指针pos是否合法,不存在则报错。
- 创建一个新的节点
newnode
并将数据值x
存储在其中。 - newnode的next指针指向pos的后一项。
- pos的next指向新节点newnode。
7、删除指定节点
cpp
void SLErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(*pphead);
if (pos = *pphead){
SLPopFront(pphead);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos) {
prev = prev -> next;
}
prev->next = pos->next;
free(pos);
}
}
- 第一个assert判断传入头节点指针的地址是否合法,为空则报错。
- 第二个assert判断链表头节点是否为空,为空则报错。
- pos节点为头节点,则调用头删解决。
- pos不为头节点,则创建变量prev指向头节点,通过循环找到pos节点的前一个节点。
- 将prev的next指向要删除的pos节点的下一个节点。
- 释放pos空间
8、删除指定节点的后一个节点
cpp
void SLEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
-
创建一个指向要删除的节点
pos
后面节点的指针next
,以便稍后释放该节点的内存。 -
修改
pos
节点的next
指针,将其指向next
的下一个节点,从而绕过要删除的节点,使链表不再包含它。 -
最后,使用
free
函数释放next
指向的节点的内存,完成删除操作。
9、单链表的销毁
cpp
void SListDestroy(SLTNode* pphead)
{
SLTNode* cur = pphead;
SLTNode* tmp = NULL;
while (cur != NULL) {
tmp = cur;
cur = cur->next;
free(tmp);
}
}
-
定义了两个指针,
cur
和tmp
,用于遍历链表并释放内存。开始时,cur
被初始化为链表的头节点指针pphead
。 -
这是一个循环,它会一直执行,直到
cur
变为NULL
,也就是遍历到链表的末尾。 -
在循环中,首先将
cur
赋值给tmp
,以便稍后释放cur
指向的节点的内存。 -
然后,将
cur
移动到下一个节点,即cur = cur->next;
-
最后,使用
free
函数释放tmp
指向的节点的内存,即释放链表中的一个节点,接着进行循环依次释放节点直到链表最后。
完整版
LList.h
cpp
#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);
//头插尾插
void SLPushFront(SLTNode** pphead, SLTDataType x);
void SLPushBack(SLTNode** pphead, SLTDataType x);
//头删尾删
void SLPopFront(SLTNode** pphead);
void SLPopBack(SLTNode** pphead);
// 单链表查找
SLTNode * STFind(SLTNode * phead, SLTDataType x);
// 在pos之前插入
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLInsertAfter(SLTNode* pos, SLTDataType x);
// 删除pos位置的值
void SLErase(SLTNode** pphead, SLTNode* pos);
// 删除pos位置后面的值
void SLEraseAfter(SLTNode* pos);
// 单链表的销毁
void SListDestroy(SLTNode* plist);
LList.c
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "LList.h"
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL) {
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
SLTNode* BuyLTNode(SLTDataType x)//为新元素开辟空间
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("malloc fall");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLPushFront(SLTNode** pphead, SLTDataType x)//头插
{
assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
//assert(*pphead); // 不能断言,链表为空,也需要能插入
SLTNode* newnode = BuyLTNode(x);//newnode是局部变量
newnode->next = *pphead;//头插后首节点next指向原有的首节点
*pphead = newnode;//将链表的头指针 *pphead 指向新插入的节点
}
void SLPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
SLTNode* newnode = BuyLTNode(x);
//两种情况
//空链表 非空链表
if (*pphead == NULL)//链表为空改变结构体指针
*pphead = newnode;
else {//不为空,则改变结构体的节点
SLTNode* tail = *pphead;
while (tail->next != NULL)
tail = tail->next;
tail->next = newnode;
}
}
void SLPopFront(SLTNode** pphead)
{
assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
assert(*pphead); // 链表为空,不能头删。(当然你还可以用温柔的检查)
SLTNode* del = *pphead;//指针del用于释放节点空间
*pphead = (*pphead)->next;
free(del);
}
void SLPopBack(SLTNode** pphead)
{
assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
assert(*pphead); // 链表为空,不能头删。(当然你还可以用温柔的检查)
//只有一个节点
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;//修改头节点为空
}
else {
//第一种增加前项变量
//SLTNode* prev = NULL;
//SLTNode* tail = *pphead;
//while (tail->next) {
// prev = tail;
// tail = tail->next;
//}
//free(tail);
//prev->next = NULL;
//第二种不新增变量
//改变结构体的节点
SLTNode* tail = *pphead;
while (tail->next->next) {
tail = tail->next;
}
free(tail->next);//将指向的最后一个节点释放
tail->next = NULL;
}
}
SLTNode* STFind(SLTNode* phead, SLTDataType x)//找到返回链表地址
{
SLTNode* cur = phead;
while (cur) {
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
// 在pos之前插入
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (*pphead == pos){//在头节点前插入等于头插
SLPushFront(pphead, x);
}
else {
SLTNode* prev = *pphead;//用于找到pos前的位置
while (prev->next != pos){
prev = prev->next;
}
SLTNode* newnode = BuyLTNode(x);
prev->next = newnode;//pos前一个位置next指向新开辟节点
newnode->next = pos;//新节点next指向pos
}
}
// 在pos之后插入
void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuyLTNode(x);
//下面两行不能调换顺序,否则无法链接新节点后项节点
newnode->next = pos->next;
pos->next = newnode;
}
void SLErase(SLTNode** pphead, SLTNode* pos)// 删除pos位置的值
{
assert(pphead);
assert(*pphead);//链表为空则不能删除
if (pos = *pphead){
SLPopFront(pphead);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos) {//找到pos前一个节点
prev = prev -> next;
}
prev->next = pos->next;//将pos前一个节点的next指向pos后一个节点
free(pos);//释放pos空间
}
}
void SLEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);//后项为空则不能删除
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
void SListDestroy(SLTNode* pphead)
{
SLTNode* cur = pphead;
SLTNode* tmp = NULL;
while (cur != NULL) {
tmp = cur;
cur = cur->next;
free(tmp);
}
}
text.c
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "LList.h"
void test1()
{
SLTNode* plist = NULL;
SLPushFront(&plist, 5);
SLPushFront(&plist, 4);
SLPushFront(&plist, 3);
SLPushBack(&plist, 6);
//SLPopFront(&plist);
SLTNode* pos = STFind(plist, 3);
SLInsert(&plist, pos, 99);
//pos = STFind(plist, 2);
//if (pos)
//{
// SLInsertAfter(pos, 20);
//}
//SLPopBack(&plist);
//SLPopBack(&plist);
//SLPopBack(&plist);
//SLPopBack(&plist);
//SLPopBack(&plist);
SLTPrint(plist);
}
int main()
{
test1();
return 0;
}