
目录
前言
由顺序表的问题及思考,我们又有了链表这一概念。
一、链表的概念及结构
概念:链表是一种 物理存储结构上非连续 、非顺序的存储结构,数据元素的 逻辑顺序 是通过链表 中的指针链接 次序实现的 。

二、链表各函数的实现
结构体定义
- 在定义结构体时,struct SListNode才是结构体类型名,在未完成重命名操作前,不能使用重命名后的名字
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;//在定义结构体时,struct SListNode才是结构体类型名,在未完成重命名操作前,不能使用重命名后的名字
}SLTNode;
SLTPrint 打印
- 不要看到指针就断言,这里的打印函数不需要assert断言,因为链表的数据是存放在结构体中的,为空同样可以打印,而之前学过的顺序表打印时则需要断言,这是因为它的数据不是存放在结构体中的,而是存放在一个数组空间,该结构体是必须存在的,由结构体内的指针存放该空间的地址,结构体内有指向数组的指针、size、capacity,如果不断言,那么就可能出现错误(数组内容是否为空是由size决定的)
- 遍历链表时,不能使用指针++,因为链表内每个节点都是单独malloc出来的,地址是不连续的,如果整个空间都是连续的那么指针++就可以跳过一个结构体大小的字节,就可以实现遍历。
- 循环条件不能为cur -> next != NULL 因为这样会丢掉最后一个数据
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
//while (cur->next != NULL)
//while(cur != NULL)
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
//cur++;
}
printf("NULL\n");
}
SLTPushBack 尾插
- 首先开辟新节点,强转为结构体指针类型,并判断是否成功开辟
- 如果链表不为空,寻找尾节点,该节点的next指针指向NULL,循环条件不能写为 tail != NULL 因为这样循环会使tail指向到NULL,应为tail -> next != NULL
- 找到后不能写为 tail = newnode 因为tail只是一个局部变量,将新节点地址赋值给tail毫无用处,tail销毁后什么也没有改变,实际上应为 tail ->next = newnode 这样赋值才是真正的串联起尾节点与新插入的节点,将新节点的地址赋值给上一个节点的next
- 尾插本质:原尾节点中要储存新尾节点的地址
- 如果链表为空,即phead == NULL 那么就把新开辟的节点赋值给 phead,但是一定要注意,我们在主函数中会创建 SLTNode * plist == NULL,把它作为头节点,如果要改变plist指针的值,我们要传该指针的地址!即二级指针。
- 但是在后面要改变结构体内的next指针进行链接时,不需要再使用二级指针,因为我们要改变是结构体,使用结构体指针即可
例如:
void Func(int* ptr) { ptr = (int* )malloc(sizeof(int)); } int main() { int *px = NULL; Func(px); return 0; }对ptr开辟空间不会改变px的值
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
// 找尾
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
SLTPushFront 头插
- 头插依旧需要传二级指针,因为必有一种情况(链表为空)需要改变头节点的值。
- 链表为空或不为空,头插操作一样
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
SLTPopBack 尾删
- 删除时要找到尾的上一个节点,将该节点的next 赋值为NULL,不能直接将尾节点释放,这样的话会使倒数第二个指针成为野指针
- 若只有一个数据,上面程序就不适合了,需要单独讨论,释放头节点并置为空
- 若没有数据,温柔/暴力检查


void SLTPopBack(SLTNode** pphead)
{
// 暴力检查
assert(pphead);
assert(*pphead);
// 温柔的检查
//if (*pphead == NULL)
// return;
// 1、只有一个节点
// 2、多个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
// 找尾
//SLTNode* prev = NULL;
//SLTNode* tail = *pphead;
//while (tail->next != NULL)
//{
// prev = tail;
// tail = tail->next;
//}
//free(tail);
//tail = NULL;
//prev->next = NULL;
SLTNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
SLTPopFront 头删
- 只用分为俩种情况,大于等于一个数据、没有数据
- 创建新的结构体指针first,用于存储第一个结构体,在释放第一个结构体空间时,释放first即可,不能释放pphead

void SLTPopFront(SLTNode** pphead)
{
// 暴力检查
assert(pphead);
assert(*pphead);
// 温柔的检查
//if (*pphead == NULL)
// return;
SLTNode* first = *pphead;
*pphead = first->next;
free(first);
first = NULL;
}
SListFind 查找
- 定义结构体指针,遍历链表,找到值与给定的相等的节点,并返回该节点地址
- 修改链表中某节点的值一般都要使用查找函数
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
SListInsert 前插入
- 如果要插入的位置是第一个时,就是头插(在某位置插入,是在该位置之前,所以不可能尾插)
- 否则,就找到该节点的上一个节点,进行插入操作
- 二级指针pphead必不为空,因为它的值是一级指针plist的地址,一般情况下它是不为空的,但是以防万一有错误的传参,所以要断言。
- 要找的结构体指针pos也不能为空,这样会导致找不到该节点,所以也需要断言。
是否断言需要分析:
- 空链表可以打印,不需要断言链表指针
- 空链表可以插入,不需要断言链表指针
- 空链表不能删除,需要断言链表指针
// pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pos);
assert(pphead);
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
// 找到pos的前一个位置
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
SLTErase 删除
- 首先要断言pphead和pos,又因为断言pos的同时,间接的断言了链表不为空,所以可以不用断言 *pphead,但是为了代码更健壮,我们加上 *pphead 的断言
- free后再本函数内置空没有用,因为此时形参改变不了实参的值,所以可以让使用者再外面使用删除函数后自己置空,也可以传二级指针pos,在函数内使其置空
面试题: 可以在不使用头指针情况下实现在指定节点前插入功能吗?
可以,在该节点后插入新节点,然后交换两节点数据,即可实现前插功能
那么能实现删除功能吗?
也是可以的,同理,交换该节点与其后的节点数据,删除其后节点的数据即可,但是当该节点为尾节点时,就不能实现该功能了
// pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
//assert(*pphead);
if (*pphead == pos)
{
SLTPopFront(pphead);
}
else
{
// 找到pos的前一个位置
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
//pos = NULL;
}
}
SLTInsertAfter 后插入
- 创建新节点
- 插入赋值操作,注意交换顺序,避免节点自我链接导致死循环
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
SLTEraseAfter 后删除
- 断言该节点的下一节点是否为空
- 如果使用pos->next = pos->next->next方法需要提前记录要删除的那个节点,避免内存泄漏
// pos位置后面删除
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
//SLTNode* del = pos->next;
//pos->next = pos->next->next;
//free(del);
//del = NULL;
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
SLTDestroy 释放
- 节点释放后,结构体指针还是指向该节点的,但是节点释放后,该空间已归还操作系统,里面的值是随机的,再次使用该节点赋值会造成野指针情况
注意,下面这种形式代码是错误的,是对指针的认识不正确造成的,可以画图理解
void SLTDestroy(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
SLTNode* tmp = cur;
free(cur);
//cur = cur->next; 野指针
cur = tmp->next; //同样错误,没区别
}
}
正确代码:
void SLTDestroy(SLTNode** pphead)
{
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* tmp = cur->next;
free(cur);
//cur = cur->next; 野指针
cur = tmp; //同样错误,没区别
}
//phead = NULL; 这里置空是没用的,改变不了phead的值,可以让使用者外部置空
*pphead = NULL;
}
三、完整链表代码
1.SList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//struct SListNode
//{
// SLTDataType data;
// struct SListNode* next;
//};
//
//typedef struct SListNode SLTNode;
void SLTPrint(SLTNode* phead);
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);
// pos
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// pos
void SLTErase(SLTNode** pphead, SLTNode* pos);
// pos
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//
void SLTEraseAfter(SLTNode* pos);
void SLTDestroy(SLTNode** phead);
2.SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
//while (cur->next != NULL)
//while(cur != NULL)
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
//cur++;
}
printf("NULL\n");
}
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
// 找尾
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SLTPopBack(SLTNode** pphead)
{
// 暴力检查
assert(pphead);
assert(*pphead);
// 温柔的检查
//if (*pphead == NULL)
// return;
// 1、只有一个节点
// 2、多个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
// 找尾
//SLTNode* prev = NULL;
//SLTNode* tail = *pphead;
//while (tail->next != NULL)
//{
// prev = tail;
// tail = tail->next;
//}
//free(tail);
//tail = NULL;
//prev->next = NULL;
SLTNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
void SLTPopFront(SLTNode** pphead)
{
// 暴力检查
assert(pphead);
assert(*pphead);
// 温柔的检查
//if (*pphead == NULL)
// return;
SLTNode* first = *pphead;
*pphead = first->next;
free(first);
first = NULL;
}
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
// pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pos);
assert(pphead);
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
// 找到pos的前一个位置
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
// pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
//assert(*pphead);
if (*pphead == pos)
{
SLTPopFront(pphead);
}
else
{
// 找到pos的前一个位置
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
//pos = NULL;
}
}
// pos后面插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
// pos位置后面删除
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
//SLTNode* del = pos->next;
//pos->next = pos->next->next;
//free(del);
//del = NULL;
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
//释放
void SLTDestroy(SLTNode** pphead)
{
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* tmp = cur->next;
free(cur);
//cur = cur->next; 野指针
cur = tmp; //同样错误,没区别
}
//phead = NULL; 这里置空是没用的,改变不了phead的值,可以让使用者外部置空
*pphead = NULL;
}
3. Test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void TestSList1()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
}
void TestSList2()
{
SLTNode* plist = NULL;
SLTPushFront(&plist, 1);
SLTPushFront(&plist, 2);
SLTPushFront(&plist, 3);
SLTPushFront(&plist, 4);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
}
void TestSList3()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
}
int main()
{
TestSList3();
return 0;
}
//void Func(int y)
//{
// y = 1;
//}
//void Func(int* p)
//{
// *p = 1;
//}
//
//int main()
//{
// int x = 0;
// Func(&x);
//
// return 0;
//}
//void Func(int* ptr)
//{
// ptr = (int*)malloc(sizeof(int));
//}
//
//int main()
//{
// int* px = NULL;
// Func(px);
// free(px);
//
//
// return 0;
//}
//void Func(int** pptr)
//{
// *pptr = (int*)malloc(sizeof(int));
//}
//
//int main()
//{
// int* px = NULL;
// Func(&px);
//
// free(px);
//
// return 0;
//}
总结
熟记其中各函数定义时的小细节以防出错,此外还可自行编写菜单功能方便使用者使用。
以上就是今天要讲的内容,本文仅仅简单介绍了单链表如何编写以及其中的小小细节,要掌握单链表还要多多理解记忆与练习。
下一节,小帅会带大家一起练习单链表的各种经典习题。最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

本文详细介绍了链表的基本概念、结构以及如何实现链表的插入、删除、打印等操作。通过示例代码展示了链表的头插、尾插、查找、前插入、删除、后插入和后删除等功能,并提供了完整代码实现。同时,文中强调了在处理链表时需要注意的指针管理和内存分配问题。
494

被折叠的 条评论
为什么被折叠?



