上面的链接是顺序表的内容。这一篇博客中部份内容会使用到顺序表中提到的函数。若不清楚可以点击上面链接去查看
一、顺序表的缺陷
中间头部插入删除效率低 – 因为需要挪动数据 ⇒ {\Rightarrow} ⇒ 时间复杂度 O(N)
空间不够需要扩容 – 扩容有一定的消耗,其次有空间浪费的可能
例如: 我们100个空间不够了,藉由SLCheckCapacity()函数扩容后空间变为200,但我只插入5个数据,那剩下的95个空间就浪费了
链表可以解决顺序表的缺陷
在正式进入链表之前,让我们复习一下会使用到的概念
二、复习结构体相关概念
结构体中不能嵌套自身
因为在编译阶段就必须要确定结构体的大小,如果在结构体中嵌套自身,就没办法知道结构体大小
typedef struct Node { int data; struct Node next; // 错误,递归定义导致无法确定大小 } Node; // 这段代码会导致无限递归
如果嵌套的是自身结构体的指针呢?
此时就不会有问题发生,因为指针存的是地址,地址所占字节不是 4 就是 8 因此,结构体大小能确定
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node* next; // 结构体指针,指向另一个 Node 结构 } Node;
在链表或是其他数据结构中会经常使用结构体指针,因此在此做说明
三、指针概念复习
- 一级指针 : 指针所存储的是一个地址,一级指针通常用来储存一个变量的地址
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int a = 10;
int* p = &a;
printf("%p\n", &a);
printf("%p\n", p);
return 0;
}
- 二级指针 : 二级指针就是指向指针的指针,也就是说,二级指针所存储的是一个一级指针的地址
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main() {
int a = 10; // 普通变量
int* p = &a; // 一级指针,指向 a
int** pp = &p; // 二级指针,指向一级指针 p 的地址
printf("a 的值: %d\n", a);
printf("a 的地址: %p\n", &a);
printf("p 的值(a 的地址): %p\n", p);
printf("pp 的值(p 的地址): %p\n", pp);
printf("*pp 的值(即 p 的值,即 a 的地址): %p\n", *pp);
printf("**pp 的值(即 *p 的值,即 a 的值): %d\n", **pp);
return 0;
}
输出结果
四、链表
4.1 链表的结构与概念
4.1.1 链表的概念
链表是物理上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现
我们之前提到,链表属于线性表的一种。在线性表的逻辑结构中,数据元素是按顺序排列的,因此它在逻辑上是连续的。但在线性表的物理存储方式上,它不一定是连续的。链表就是一种典型的线性表,它采用链式结构,通过指针将各个节点连接在一起,因此在物理存储上不一定需要连续
4.1.2 链表的结构
4.1.3 链表的分类
链表可以根据单向双向、带头或不带头、循环或非循环组合成8种不同的链表结构
虽然有这么多种链表结构,但实际上最常用的两种结构为
单向不带头非循环链表
这种链表的结构较为简单。一般来说不会用来存储数据,通常是用来作为其他数据的子结构,如哈希桶、图的邻接表等。
不带头意味着我们的头不是一个结构体,而是一个结构体指针。
双向带头循环链表
这种链表的结构较为复杂,一般用双向带头循环链表来单独存储数据,实际上使用的链表数据结构都是双向带头循环链表。
此外,虽然双向带头循环链表的结构较为复杂,但等我们真正实现这种链表后会发现它有很多优势。这里可以先保留一个惊喜,等到我们真正实现时就会知道了。
对于不带头与带头的更多细节,我们会在介绍双向带头循环链表时在进一步比较两者的差异
五、 无头单向非循环链表的基本功能接口
在这边我们仅先对无头单向非循环链表的实现进行演示
5.1 事前准备
SList.h : 用以定义结构体、声明实现接口的函数,将使用到的库函数集合起来
SList.c : 用以定义实现接口的函数
Test.c : 用以测试接口
5.2 链表结构体定义
typedef int SLTDatatype;
// 链表结构体中主要存储两个东西。1. 当前节点的数据 2. 下一个节点的地址
typedef struct SListNode {
SLTDatatype data; // 存数据
struct SListNode* next; // 存下一个节点的地址
// 这里 struct SListNode 一定完整写出来,因为在C语言中,结构体名称不能当做类型
}SLTNode;
// 这里使用typedef 来为数据类型和结构体重命名,方便后面使用
5.3 链表初始化
SLTNode** plist = NULL; // 初始化
5.4 打印链表
void SLTPrint(SLTNode* phead) {
SLTNode* cur = phead; // 将cur 指向 phead
while (cur != NULL) { // 判断当前节点是否为空,不为空就打印
printf("%d->", cur->data);
cur = cur->next; // cur指向下一个节点
/*
为什么是cur = cur->next呢
我们在定义结构体的时后说过,结构体中储存的是一个数据和下一个节点的地址
cur = cur->next 相当于让cur的指向变成下一个节点
*/
}
printf("NULL\n");
}
5.5 创建节点
什么时候会需要用到创建节点?
- 尾插
- 头插
- 在指定位置 (pos) 前/后 插入
SLTNode* Create_node(SLTDatatype x) {
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
六、无头单向非循环链表节点增加功能接口
6.1 尾插
void SLTPushback(SLTNode** pphead, SLTDatatype x) {
assert(pphead);
SLTNode* newnode = Create_node(x);
// 假设只有一个节点
if (*pphead == NULL) {
*pphead = newnode;
}
// 找尾
else {
SLTNode* tail = *pphead;
while (tail->next != NULL) {
tail = tail->next;
}
tail->next = newnode; // 不是 tail = newnode。tail是局部变量
}
}
为什么要使用二级指针传递参数?
假设我们有一个空链表,这意味着链表的头指针 plist
指向空地址 0x00000000
(即 NULL
)。
当我们尝试向链表尾部插入一个新节点时,能直接把它挂在这个 NULL
后面吗?答案是否定的,因为 0x00000000
这个地址是操作系统受保护的区域,我们无法随意访问或修改它。因此,我们不能在 NULL
后插入新节点,而是需要让头指针 plist
直接指向新创建的节点,使其成为链表的第一个节点。
为什么需要二级指针?
要让 plist
指向新节点,我们实际上要修改 plist
本身的值,让它存储新节点的地址。然而,C 语言中的函数参数是值传递,如果我们只使用一级指针(即 STLNode* pphead
),那么 pphead
只是原始头指针的一份拷贝。在函数内部修改这个拷贝的值,并不会影响原来的 plist
,因此链表的头指针仍然指向 NULL
,新节点就无法被正确添加。
因此我们需要传递 plist
的地址,即使用二级指针 STLNode** pphead
。这样,函数内部对 *pphead
的修改就会直接影响到plist
( 当然前提是要传递 plist 的地址。
还是不明白的话可以往上看一下指针概念的复习
6.2 头插
void SLTPushfront(SLTNode** pphead, SLTDatatype x) {
assert(pphead); // 这里可以断言一下,避免有人传递错误的内容 ( pphead一定不为空 )
SLTNode* newnode = Create_node(x); // 创建节点
newnode->next = *pphead; //将新创节点指向原头节点
*pphead = newnode; // 头节点变成新创的节点
}
6.3 在pos位置前 / 后插入
6.3.1 pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x) {
assert(pos);
assert(pphead); // 一样断言ppheaad -- pphead 一定不为空
// 这个语句意味着pos的位置是头节点,在头节点前面插入新节点相当于头插
if (pos == *pphead) {
SLTPushfront(pphead, x); // 调用头插的函数
}
else {
// 找到pos的前一个位置
SLTNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
SLTNode* newnode = Create_node(x);
prev->next = newnode;
newnode->next = pos;
/*
上面这段代码的解释是:
因为我们是要在pos前面插入新的节点,所以势必得要知道原本哪个节点所指向的下一个节点是pos
这样才能去进行指向的修改
*/
}
}
6.3.2 pos位置后插入
// 在pos位置后插入就相对简单很多了。
void SLTInsert_after(SLTNode* pos, SLTDatatype x) {
assert(pos);
SLTNode* newnode = Create_node(x);
newnode->next = pos->next;
pos->next = newnode;
}
七、无头单向非循环链表节点删除功能接口
7.1 尾删
void SLTPopback(SLTNode** pphead) {
assert(pphead);
assert(*pphead);
SLTNode* tail = *pphead;
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* tail = *pphead;
SLTNode* prev = *pphead;
while (tail->next != NULL) {
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
7.2 头删
void SLTPopfront(SLTNode** pphead) {
assert(pphead);
assert(*pphead);
SLTNode* cur = *pphead;
*pphead = cur->next;
free(cur);
cur = NULL;
}
7.3 在pos位置删除 / 在pos位置后删除
7.3.1 在pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos) {
assert(pphead);
assert(pos);
if (*pphead == pos) {
SLTPopfront(pphead);
}
SLTNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
7.3.2 在pos位置后删除
void SLTErase_after(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
八、无头单向非循环链表节点查找功能接口
SLTNode* SLTFind(SLTNode* phead, SLTDatatype x) {
SLTNode* cur = phead;
while (cur) {
if (cur->data == x) {
return cur;
}
else {
cur = cur->next;
}
}
return NULL;
}
九、完整代码与执行结果
9.1 完整代码
9.1.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;
// 打印
void SLTPrint(SLTNode* phead);
//创建节点
SLTNode* Create_node(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);
// 在pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x);
// 在pos位置之前删除
void SLTErase(SLTNode** pphead, SLTNode* pos);
// 在pos位置之后插入
void SLTInsert_after(SLTNode* pos, SLTDatatype x);
// 在pos位置之前删除
void SLTErase_after(SLTNode* pos);
9.2.2 SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
//cur++;
}
printf("NULL\n");
}
// 创建节点
SLTNode* Create_node(SLTDatatype x) {
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("malloc fail"); // 只有系统API可以使用perror
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLTPushback(SLTNode** pphead, SLTDatatype x) {
assert(pphead);
SLTNode* newnode = Create_node(x);
// 找尾
if (*pphead == NULL) {
*pphead = newnode;
}
else {
SLTNode* tail = *pphead;
while (tail->next != NULL) {
tail = tail->next;
}
tail->next = newnode; // 不是 tail = newnode。tail是局部变量
}
}
void SLTPushfront(SLTNode** pphead, SLTDatatype x) {
assert(pphead);
SLTNode* newnode = Create_node(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SLTPopback(SLTNode** pphead) {
assert(pphead);
assert(*pphead);
SLTNode* tail = *pphead;
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL; // free完记得要设成空 不然可能会有野指针问题
}
else {
SLTNode* tail = *pphead;
SLTNode* prev = *pphead;
while (tail->next != NULL) {
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
void SLTPopfront(SLTNode** pphead) {
assert(pphead);
assert(*pphead);
SLTNode* cur = *pphead;
*pphead = cur->next;
free(cur);
cur = NULL;
}
SLTNode* SLTFind(SLTNode* phead, SLTDatatype x) {
SLTNode* cur = phead;
while (cur) {
if (cur->data == x) {
return cur;
}
else {
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 = Create_node(x);
prev->next = newnode;
newnode->next = pos;
}
}
// pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos) {
assert(pphead);
assert(pos);
if (*pphead == pos) {
SLTPopfront(pphead);
}
SLTNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
// pos后插入
void SLTInsert_after(SLTNode* pos, SLTDatatype x) {
assert(pos);
SLTNode* newnode = Create_node(x);
newnode->next = pos->next;
pos->next = newnode;
}
// pos后删除
void SLTErase_after(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
9.2 测试结果
9.2.1 尾插测试
// 尾插测试
void SList_Test1() {
SLTNode** plist = NULL; // 初始化
SLTPushback(&plist, 1);
SLTPushback(&plist, 2);
SLTPushback(&plist, 3);
SLTPushback(&plist, 4);
SLTPrint(plist);
}
输出结果
9.2.2 尾删测试
// 尾删测试
void SList_Test2() {
SLTNode** plist = NULL; // 初始化
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);
}
输出结果
9.2.3 头插测试
void SList_Test3() {
SLTNode** plist = NULL;
SLTPushfront(&plist, 4);
SLTPushfront(&plist, 3);
SLTPushfront(&plist, 2);
SLTPushfront(&plist, 1);
SLTPrint(plist);
}
输出结果
9.2.4 头删测试
void SList_Test4() {
SLTNode** plist = NULL;
SLTPushfront(&plist, 4);
SLTPushfront(&plist, 3);
SLTPushfront(&plist, 2);
SLTPushfront(&plist, 1);
SLTPrint(plist);
SLTPopfront(&plist);
SLTPrint(plist);
SLTPopfront(&plist);
SLTPrint(plist);
SLTPopfront(&plist);
SLTPrint(plist);
SLTPopfront(&plist);
SLTPrint(plist);
}
输出结果
9.2.5 在pos前 / 后插入及查找测试
// 1.
void SList_Test5() {
SLTNode** plist = NULL;
SLTPushfront(&plist, 4);
SLTPushfront(&plist, 3);
SLTPushfront(&plist, 2);
SLTPushfront(&plist, 1);
// 假设我们在2前插入 5
SLTNode* ret = SLTFind(plist, 2);
SLTInsert(&plist, ret, 5);
SLTPrint(plist);
}
// 2.
void SList_Test5() {
SLTNode** plist = NULL;
SLTPushfront(&plist, 4);
SLTPushfront(&plist, 3);
SLTPushfront(&plist, 2);
SLTPushfront(&plist, 1);
// 假设我们在3后插入 5
SLTNode* ret = SLTFind(plist, 3);
SLTInsert_after(ret, 5);
SLTPrint(plist);
}
输出结果
在pos前插入
在pos后插入
9.2.6 在pos位置删除 / 在pos位置后删除测试
// 1. 在pos位置删除
void SList_Test6() {
SLTNode** plist = NULL;
SLTPushfront(&plist, 4);
SLTPushfront(&plist, 3);
SLTPushfront(&plist, 2);
SLTPushfront(&plist, 1);
// 假设我们要删除2这个节点
SLTNode* ret = SLTFind(plist, 2);
SLTErase(&plist, ret);
SLTPrint(plist);
}
// 2. 在pos位置后删除
void SList_Test6() {
SLTNode** plist = NULL;
SLTPushfront(&plist, 4);
SLTPushfront(&plist, 3);
SLTPushfront(&plist, 2);
SLTPushfront(&plist, 1);
// 假设我们要删除2后面的节点
SLTNode* ret = SLTFind(plist, 2);
SLTErase_after(ret);
SLTPrint(plist);
}
输出结果
在pos位置删除
在pos位置后删除
以上为这次的内容。如果内容有错还请不吝啬的提出,看到会尽快改正!