链表结构深度解析:从单向无头到双向循环的实现全指南
上篇博客实现动态顺序表时,我们会发现它存在许多弊端,如:
• 中间/头部的插⼊删除,时间复杂度为O(N)• 增容需要申请新空间,拷⻉数据,释放旧空间。会有不⼩的消耗。• 增容⼀般是呈2倍的增⻓,势必会有⼀定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插⼊了5个数据,后⾯没有数据插⼊了,那么就浪费了95个数据空间。
这些问题该怎么解决呢?
我们需要学习线性表的另一种实现方式--链表。
目录
1.链表的分类
2.单链表
2.1概念与结构
2.1.1结点
2.1.2链表的性质
2.2单链表的实现
2.2.1 SList.h
2.2.2 SList.c
3.双向链表
3.1.概念与结构
3.2双向链表的实现
3.2.1List.h
3.2.2 List.c
4.顺序表与链表比较
1.链表的分类
链表的结构⾮常多样,以下情况组合起来有8种(2 x 2 x 2)链表结构:
链表可从三个维度分类组合:
- 单向与双向:
- 单向链表:结点仅含一个指向下一结点的指针;
- 双向链表:结点含前驱、后继两个指针。
- 带头与不带头:
- 带头链表有专门头结点(不存数据),便于操作;
- 不带头链表首个结点即数据结点。
- 循环与不循环:
- 循环链表:最后结点指针指向头结点(带头)或首结点(不带头);
- 不循环链表:最后结点指针为
null
(单向)或后继为null
且首结点前驱为null
(双向)。
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 单链表和 双向带头循环链表。
1. 无头单向非循环链表:结构简单,⼀般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,⼀般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后⾯我们代码实现了就知道了。
2.单链表
2.1概念与结构
概念:链表是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
举个例子,这是一列火车:
淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。
在链表⾥,每节“⻋厢”是什么样的呢?
2.1.1结点
与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点”
结点的组成主要有两个部分:当前结点要保存的数据和保存下⼀个结点的地址(指针变量)。
图中指针变量 plist保存的是第⼀个结点的地址,我们称plist此时“指向”第⼀个结点,如果我们希望
plist“指向”第⼆个结点时,只需要修改plist保存的内容为0x0012FFA0。
链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。
2.1.2链表的性质
1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续2、结点⼀般是从堆上申请的3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码,假设当前保存的结点为整型:
struct SListNode {int data; //结点数据struct SListNode* next; //指针变量⽤保存下⼀个结点的地址
};
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数
据,也需要保存下⼀个结点的地址(直到下⼀个结点为空时保存的地址为空)。
2.2单链表的实现
注意:以下函数的一些参数有一些是二级指针,不是一级指针,使用时要格外注意。使用二级指针是因为要改变实参(使用一级指针的话,形参的变化影响不了实参)。
2.2.1 SList.h
#pragma once#include<stdio.h>
#include<assert.h>
#include<stdlib.h>typedef int SLTDataType;typedef struct SListNode
{SLTDataType data; //结点数据struct SListNode* next; //指针保存下⼀个结点的地址
}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);//在指定位置之前插⼊数据
void SLTInsert(SLTNode * *pphead, SLTNode * pos, SLTDataType x);//删除pos结点
void SLTErase(SLTNode * *pphead, SLTNode * pos);//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode * pos, SLTDataType x);//删除pos之后的结点
void SLTEraseAfter(SLTNode * pos);//销毁链表
void SListDestroy(SLTNode * *pphead);
2.2.2 SList.c
2.2.2.1创建结点,打印链表,销毁链表
#define _CRT_SECURE_NO_WARNINGS#include "SList.h"// 测试函数:手动创建并遍历一个单链表,最后释放内存
void test1()
{// 手动创建4个节点并初始化数据(实际开发中建议封装成函数)SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));n1->data = 1; // 节点1数据赋值为1SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));n2->data = 2; // 节点2数据赋值为2SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));n3->data = 3; // 节点3数据赋值为3SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));n4->data = 4; // 节点4数据赋值为4// 手动连接节点形成链表:1 -> 2 -> 3 -> 4 -> NULLn1->next = n2;n2->next = n3;n3->next = n4;n4->next = NULL; // 链表终止标志// 遍历链表并打印数据SLTNode* pcur = n1; // 从链表头节点开始遍历while (pcur){printf("%d ", pcur->data); // 打印当前节点数据pcur = pcur->next; // 移动到下一个节点}printf("\n"); // 打印换行// 安全释放链表内存(防止内存泄漏)SLTNode* current = n1; // 重新从头节点开始while (current){SLTNode* temp = current; // 临时保存当前节点地址current = current->next; // 先移动到下一个节点free(temp); // 释放当前节点内存}// 注意:此时n1~n4已成为野指针,不可再访问
}int main()
{test1(); // 执行测试函数return 0;
}
封装函数:
1.创建结点:
SLTNode* CreateNode(int data)
{SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));if (node == NULL){perror("malloc");exit(EXIT_FAILURE);}node->data = data;node->next = NULL;return node;
}
2.打印链表
void SLTPrint(SLTNode* phead)
{assert(phead);SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
3.销毁链表
void SListDestroy(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* current = *pphead;while (current){SLTNode* temp = current;current = current->next;free(temp);}*pphead = NULL;
}
2.2.2.2尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* new_node = CreateNode(x);if (*pphead == NULL){*pphead = new_node;}else{SLTNode* pcur = *pphead;while (pcur->next){pcur = pcur->next;}pcur->next = new_node;}
}void SLTPopBack(SLTNode** pphead)
{assert(pphead && *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;}
}
2.2.2.3头部插入删除
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* new_node = CreateNode(x);new_node->next = *pphead;*pphead = new_node;
}void SLTPopFront(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* tem = (*pphead)->next;free(*pphead);*pphead = tem;
}
2.2.2.3查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{assert(phead);SLTNode* pcur = phead;while (pcur){if (pcur->data = x){return pcur;}pcur = pcur->next;}return NULL;
}
2.2.2.4在指定位置插入删除
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);if (*pphead == pos){SLTPushFront(pphead,x);}else{SLTNode* new_node = CreateNode(x);SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}pcur->next = new_node;new_node->next = pos;}
}void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* new_node = CreateNode(x);new_node->next = pos->next;pos->next = new_node;
}void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);if (*pphead == pos){SLTPopFront(pphead);}else{SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}pcur->next = pos->next;free(pos);pos = NULL;}
}void SLTEraseAfter(SLTNode* pos)
{assert(pos);SLTNode* del = pos->next;pos->next = pos->next->next;free(del);del = NULL;
}
3.双向链表
3.1.概念与结构
带头双向循环链表是一种具有特定结构与特性的线性数据结构。其每个节点包含三部分:数据域(存储实际数据)、前驱指针(
prev
,指向前驱节点)与后继指针(next
,指向后继节点),以此实现双向链接。链表存在一个头结点(通常为哨兵节点,不存储有效数据),尾节点的next
指针指向头结点,头结点的 prev 指针指向尾节点,构成循环结构。从结构细节看:
- 头结点(head):作为链表起始点,
next
指向首个数据节点(如d1
),prev 指向尾节点(如d3
)。- 数据节点(如
d1
、d2
、d3
):
d1
的 prev 指向头结点head
,next
指向d2
;d2
的 prev 指向d1
,next
指向d3
;- 尾节点(
d3
):next
指向头结点head
,prev 指向d2
。
typedef int LTDataType;typedef struct listNode {LTDataType data;struct listNode* next;struct listNode* prev;
}LTNode;
3.2双向链表的实现
3.2.1List.h
#pragma once#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>typedef int LTDataType;typedef struct listNode {LTDataType data;struct listNode* next;struct listNode* prev;
}LTNode;LTNode* CreateNode(LTDataType x);//初始化
LTNode* LTInit2();
void LTInit1(LTNode** pphead);//打印链表
void LTPrint(LTNode* phead);//判断链表是否为空
bool LTEmpty(LTNode* phead);//尾插及尾删
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);//头插及头删
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);//查找
LTNode* LTFind(LTNode* phead, LTDataType x);//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x);//删除pos结点
void LTErase(LTNode* pos);//销毁链表
void LTDestroy(LTNode* phead);
3.2.2 List.c
3.2.2.1初始化链表,创建结点,打印链表,判断链表是否无有效数据
LTNode* CreateNode(LTDataType x)
{LTNode* new_node = (LTNode*)malloc(sizeof(LTNode));if (new_node == NULL){perror("malloc");exit(1);}new_node->data = x;new_node->prev = new_node->next = new_node;return new_node;
}void LTInit1(LTNode** pphead)
{assert(pphead);*pphead = CreateNode(-1);
}LTNode* LTInit2()
{LTNode* phead = CreateNode(-1);return phead;
}void LTPrint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d ", pcur->data);pcur = pcur->next;}printf("\n");
}bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}
3.2.2.2尾插及尾删
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);//phead->prev new_node pheadLTNode* new_node = CreateNode(x);new_node->next = phead;new_node->prev = phead->prev;phead->prev->next = new_node;phead->prev = new_node;
}void LTPopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));//phead->prev->prev phead->prev pheadLTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;free(del);
}
尾插

尾删
3.2.2.3头插及头删
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* new_node = CreateNode(x);//phead new_node phead->nextnew_node->next = phead->next;new_node->prev = phead;phead->next->prev = new_node;phead->next = new_node;
}void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTNode* del = phead->next;del->next->prev = phead;phead->next = del->next;free(del);del = NULL;
}

头插

头删
3.2.2.4查找,在指定位置插入及删除
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* new_node = CreateNode(x);new_node->next = pos->next;new_node->prev = pos;pos->next->prev = new_node;pos->next = new_node;
}void LTErase(LTNode* pos)
{assert(pos);assert(!(pos->prev == pos && pos->next == pos));pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);
}

在指定位置之后插入

删除指定位置结点
3.2.2.5销毁链表
void LTDestroy(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* del = pcur;pcur = pcur->next;free(del);}free(phead);
}
4.顺序表与链表比较
不同点 | 顺序表 | 链表(单链表) |
存储空间上 | 物理上⼀定连续 | 逻辑上连续,但物理上不⼀定连续 |
随机访问 | ⽀持O(1) | 不⽀持:O(N) |
任意位置插⼊或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插⼊ | 动态顺序表,空间不够时需要扩容和空间浪费 | 没有容量的概念,按需申请释放,不存在空间浪费 |
应⽤场景 | 元素⾼效存储+频繁访问 | 任意位置⾼效插⼊和删除 |