简介
此模块为学校数据结构必修课学习内容的记录与分享
全文大体分为一下几个部分
- 模块解释
- 可视化GIF合集
- 整体复现
- 最终结果
模块解释
定义静态链表
链表主体
typedef struct StaticLinkedNode {
char data;
int next;
} *NodePtr;
信息数组
typedef struct StaticLinkedList {
NodePtr nodes; // 这是一个指针
int *used; // 这里声明的是一个数组 储存节点是否被使用的信息
} *ListPtr;
这样创建将链表主体和链表信息分开,更加模块化,方便初学者理解
初始化静态链表
ListPtr InitLinkedList() {
ListPtr TemPtr = (ListPtr) malloc(sizeof(struct StaticLinkedList));
// 分配默认大小个node的空间
TemPtr->nodes = (NodePtr) malloc(sizeof(struct StaticLinkedNode) * DEFAULT_SIZE);
// 分配默认大小个int的空间 储存节点是否被使用的信息
TemPtr->used = (int *) malloc(sizeof(int) * DEFAULT_SIZE);
// 初始化静态表头节点
TemPtr->nodes[0].data = '\0';
TemPtr->nodes[0].next = -1; // 相当于单链表中的NULL
// 初始化used数组
TemPtr->used[0] = 1;
for(int i = 1; i < DEFAULT_SIZE; i++) {
TemPtr->used[i] = 0;
}
return TemPtr;
}
分配空间以及初始化节点占用信息(used数组)
插入节点
void InsertNode(ListPtr ParaList, char ParaData, int ParaPos) {
int p,q;
int i;
p = 0;
for(i = 0; i < ParaPos; i++) {
p = ParaList->nodes[p].next;
if(p == -1) {
printf("The position %d is beyond the scope of the list.\n", ParaPos);
return;
}
}
// 寻找未使用的节点
for(i = 1; i < DEFAULT_SIZE; i++) { // 注意这里从 1 开始
if(ParaList->used[i] == 0) {
printf("Space at %d allocated.\n", i);
ParaList->used[i] = 1;
q = i;
break;
}
}
if (i == DEFAULT_SIZE){
printf("No space.\n");
return;
}
ParaList->nodes[q].data = ParaData;
printf("Linking ~~~\n");
ParaList->nodes[q].next = ParaList->nodes[p].next;
ParaList->nodes[p].next = q;
}
主要流程:
- 查找闲置节点
- 查找闲置节点的前置节点
- 对闲置节点进行赋值
- 更新前置节点标记
- 更新新添加节点的占用信息(used数组)
删除节点
void DeleteNode(ListPtr ParaList, char ParaData) {
int p,q;
p = 0;
while((ParaList->nodes[p].next != -1) && (ParaList->nodes[ParaList->nodes[p].next].data != ParaData)) {
p = ParaList->nodes[p].next;
}
if(ParaList->nodes[p].next == -1) {
printf("The data %c is not in the list.\n", ParaData);
return;
}
q = ParaList->nodes[p].next;
ParaList->nodes[p].next = ParaList->nodes[q].next;
// “初始化节点”
ParaList->used[q] = 0;
}
主要流程:
- 定位待删除节点
- 判断后置节点是否存在
- 更新前置节点信息
- 重置目标节点信息(used数组)
节点打印
void PrintList (ListPtr ParaList) {
int p = ParaList->nodes[0].next;
while(p != -1) {
printf("%c", ParaList->nodes[p].data);
p = ParaList->nodes[p].next;
}
printf("\n");
}
简单呈现节点数据
节点详细内容输出
void PrintMemory(ListPtr paraListPtr) {
int i;
printf("Now output the memory.\n");
printf("The address of the list: %ld\n", paraListPtr);
printf("The address of nodes: %ld\n", paraListPtr->nodes);
printf("The address of used: %ld\n", paraListPtr->used);
printf("The contents the memory: [data, next, used]\n");
for (i = 0; i < DEFAULT_SIZE; i ++) {
printf("[%c, %d, %d]\n", paraListPtr->nodes[i].data, paraListPtr->nodes[i].next, paraListPtr->used[i]);
}
}void PrintMemory(ListPtr paraListPtr) {
int i;
printf("Now output the memory.\n");
printf("The address of the list: %ld\n", paraListPtr);
printf("The address of nodes: %ld\n", paraListPtr->nodes);
printf("The address of used: %ld\n", paraListPtr->used);
printf("The contents the memory: [data, next, used]\n");
for (i = 0; i < DEFAULT_SIZE; i ++) {
printf("[%c, %d, %d]\n", paraListPtr->nodes[i].data, paraListPtr->nodes[i].next, paraListPtr->used[i]);
}
}
由于相对位置的特点存在,不方便顺序输出实际链表中每个节点的位置,因此这里只输出能反映静态链表的关键信息
测试函数
void TestAll(){
ListPtr tempList = InitLinkedList();
PrintList(tempList);
PrintMemory(tempList);
InsertNode(tempList, 'H', 0);
PrintMemory(tempList);
InsertNode(tempList, 'e', 1);
PrintMemory(tempList);
InsertNode(tempList, 'l', 2);
PrintMemory(tempList);
InsertNode(tempList, 'l', 3);
PrintMemory(tempList);
InsertNode(tempList, 'o', 4);
PrintMemory(tempList);
printf("Deleting 'e'.\n");
DeleteNode(tempList, 'e');
PrintMemory(tempList);
printf("Deleting 'a'.\n");
DeleteNode(tempList, 'a');
printf("Deleting 'o'.\n");
DeleteNode(tempList, 'o');
PrintList(tempList);
InsertNode(tempList, 'x', 2);
PrintList(tempList);
PrintMemory(tempList);
}
main 函数
int main(){
TestAll();
return 0;
}
可视化GIF合集
示例动图中-2表示空余状态、-1表示尾节点状态二,其它数据表示链接节点的相对序号
静态表的创建
结点的插入
在链表的第 3 个位置插入数据 6
数据查找
在链表查找数据为 7 的节点
节点删除
删除链表中位序为 5 的节点
删除指定数据节点
此操作结合查找和删除,暂无GIF
整体复现
#include<stdio.h>
#include<malloc.h>
#define DEFAULT_SIZE 5
typedef struct StaticLinkedNode {
char data;
int next;
} *NodePtr;
typedef struct StaticLinkedList {
NodePtr nodes; // 这是一个指针
int *used; // 这里声明的是一个数组 储存节点是否被使用的信息
} *ListPtr;
ListPtr InitLinkedList() {
ListPtr TemPtr = (ListPtr) malloc(sizeof(struct StaticLinkedList));
TemPtr->nodes = (NodePtr) malloc(sizeof(struct StaticLinkedNode) * DEFAULT_SIZE); // 分配默认大小个node的空间
TemPtr->used = (int *) malloc(sizeof(int) * DEFAULT_SIZE); // 分配默认大小个int的空间 储存节点是否被使用的信息
// 初始化静态表头节点
TemPtr->nodes[0].data = '\0';
TemPtr->nodes[0].next = -1; // 相当于单链表中的NULL
// 初始化used数组
TemPtr->used[0] = 1;
for(int i = 1; i < DEFAULT_SIZE; i++) {
TemPtr->used[i] = 0;
}
return TemPtr;
}
void PrintList (ListPtr ParaList) {
int p = ParaList->nodes[0].next;
while(p != -1) {
printf("%c", ParaList->nodes[p].data);
p = ParaList->nodes[p].next;
}
printf("\n");
}
// 查找空余节点
// 查找空余节点的前置节点
// 对空余节点进行赋值
// 更新前置节点标记
void InsertNode(ListPtr ParaList, char ParaData, int ParaPos) {
int p,q;
int i;
p = 0;
for(i = 0; i < ParaPos; i++) {
p = ParaList->nodes[p].next;
if(p == -1) {
printf("The position %d is beyond the scope of the list.\n", ParaPos);
return;
}
}
// 寻找未使用的节点
for(i = 1; i < DEFAULT_SIZE; i++) { // 注意这里从 1 开始
if(ParaList->used[i] == 0) {
printf("Space at %d allocated.\n", i);
ParaList->used[i] = 1;
q = i;
break;
}
}
if (i == DEFAULT_SIZE){
printf("No space.\n");
return;
}
ParaList->nodes[q].data = ParaData;
printf("Linking ~~~\n");
ParaList->nodes[q].next = ParaList->nodes[p].next;
ParaList->nodes[p].next = q;
}
// 定位删除节点
// 判断后置节点是否存在
// 更新前置节点信息
// 重置目标节点信息
void DeleteNode(ListPtr ParaList, char ParaData) {
int p,q;
p = 0;
while((ParaList->nodes[p].next != -1) && (ParaList->nodes[ParaList->nodes[p].next].data != ParaData)) {
p = ParaList->nodes[p].next;
}
if(ParaList->nodes[p].next == -1) {
printf("The data %c is not in the list.\n", ParaData);
return;
}
q = ParaList->nodes[p].next;
ParaList->nodes[p].next = ParaList->nodes[q].next;
// “初始化节点”
ParaList->used[q] = 0;
}
// 不能用这样的方式打印,因为链表内元素不是靠指针进行链接
// void PrintMemory(ListPtr ParaList) {
// int i;
// int p = 0;
// printf("Begin printing the memory :\n");
// for(i = 0; i < DEFAULT_SIZE; i++) {
// printf("The node %d is %c and the next node is %d", p,ParaList->nodes[p].data,ParaList->nodes[p].next);
// printf("\n");
// p = ParaList->nodes[p].next;
// }
// printf("\n");
// }
void PrintMemory(ListPtr paraListPtr) {
int i;
printf("Now output the memory.\n");
printf("The address of the list: %ld\n", paraListPtr);
printf("The address of nodes: %ld\n", paraListPtr->nodes);
printf("The address of used: %ld\n", paraListPtr->used);
printf("The contents the memory: [data, next, used]\n");
for (i = 0; i < DEFAULT_SIZE; i ++) {
printf("[%c, %d, %d]\n", paraListPtr->nodes[i].data, paraListPtr->nodes[i].next, paraListPtr->used[i]);
}
}
void TestAll(){
ListPtr tempList = InitLinkedList();
PrintList(tempList);
PrintMemory(tempList);
InsertNode(tempList, 'H', 0);
PrintMemory(tempList);
InsertNode(tempList, 'e', 1);
PrintMemory(tempList);
InsertNode(tempList, 'l', 2);
PrintMemory(tempList);
InsertNode(tempList, 'l', 3);
PrintMemory(tempList);
InsertNode(tempList, 'o', 4);
PrintMemory(tempList);
printf("Deleting 'e'.\n");
DeleteNode(tempList, 'e');
PrintMemory(tempList);
printf("Deleting 'a'.\n");
DeleteNode(tempList, 'a');
printf("Deleting 'o'.\n");
DeleteNode(tempList, 'o');
PrintList(tempList);
InsertNode(tempList, 'x', 2);
PrintList(tempList);
PrintMemory(tempList);
}
int main(){
TestAll();
return 0;
}
输出结果
PS D:\a_study\code_vs\DataStructure\StaticLinkedNode> gcc -o kernel code.cpp
PS D:\a_study\code_vs\DataStructure\StaticLinkedNode> ./kernel
Now output the memory.
The address of the list: 12393024
The address of nodes: 12393056
The address of used: 12393104
The contents the memory: [data, next, used]
[, -1, 1]
[P, 0, 0]
[s, 1331450709, 0]
[n, 6649449, 0]
[O, 1279348297, 0]
Space at 1 allocated.
Linking ~~~
Now output the memory.
The address of the list: 12393024
The address of nodes: 12393056
The address of used: 12393104
The contents the memory: [data, next, used]
[, 1, 1]
[H, -1, 1]
[s, 1331450709, 0]
[n, 6649449, 0]
[O, 1279348297, 0]
Space at 2 allocated.
Linking ~~~
Now output the memory.
The address of the list: 12393024
The address of nodes: 12393056
The address of used: 12393104
The contents the memory: [data, next, used]
[, 1, 1]
[H, 2, 1]
[e, -1, 1]
[n, 6649449, 0]
[O, 1279348297, 0]
Space at 3 allocated.
Linking ~~~
Now output the memory.
The address of the list: 12393024
The address of nodes: 12393056
The address of used: 12393104
The contents the memory: [data, next, used]
[, 1, 1]
[H, 2, 1]
[e, 3, 1]
[l, -1, 1]
[O, 1279348297, 0]
Space at 4 allocated.
Linking ~~~
Now output the memory.
The address of the list: 12393024
The address of nodes: 12393056
The address of used: 12393104
The contents the memory: [data, next, used]
[, 1, 1]
[H, 2, 1]
[e, 3, 1]
[l, 4, 1]
[l, -1, 1]
No space.
Now output the memory.
The address of the list: 12393024
The address of nodes: 12393056
The address of used: 12393104
The contents the memory: [data, next, used]
[, 1, 1]
[H, 2, 1]
[e, 3, 1]
[l, 4, 1]
[l, -1, 1]
Deleting 'e'.
Now output the memory.
The address of the list: 12393024
The address of nodes: 12393056
The address of used: 12393104
The contents the memory: [data, next, used]
[, 1, 1]
[H, 3, 1]
[e, 3, 0]
[l, 4, 1]
[l, -1, 1]
Deleting 'a'.
The data a is not in the list.
Deleting 'o'.
The data o is not in the list.
Hll
Space at 2 allocated.
Linking ~~~
Hlxl
Now output the memory.
The address of the list: 12393024
The address of nodes: 12393056
The address of used: 12393104
The contents the memory: [data, next, used]
[, 1, 1]
[H, 3, 1]
[x, 4, 1]
[l, 2, 1]
[l, -1, 1]
过程记录
- 复现代码
- 发现and解决问题
- 迁移博客
- 实现想法
优化记录
重写代码,优化结构体形式:用 -2、-1代替空余、结尾状态,减少结构体的创建
具体代码:
// 第一次复现 - 节点插入出现问题,报错Position is out of range! 以及 No such node!
// 问题是寻找前置节点的判断条件中错写成了 forwardNode != -1
// 问题是 List[forwardNode].next = -1; 被反复使用,造成循环节点问题
#include<stdio.h>
#include<malloc.h>
#define DEFAULT_SIZE 5 + 1
typedef struct StaticLinkedList {
char data;
int next;
// 下一个结点的索引
// -1 表示当前结点是链表的尾结点
// -2,则表示当前结点是空闲结点
} *NodePtr;
NodePtr InitList() {
NodePtr templist = (NodePtr)malloc(sizeof(struct StaticLinkedList) * DEFAULT_SIZE);
templist[0].data = '0';
templist[0].next = -1;
for (int i = 1; i < DEFAULT_SIZE; i++) {
templist[i].next = -2;
}
if(!templist) {
printf("InitList failed!\n");
} else {
printf("InitList success!\n");
}
return templist;
}
void InsertNode (NodePtr List, char Data, int Pos) {
if(Pos > DEFAULT_SIZE || Pos < 0) {
printf("Position is out of range! - Pos\n");
return;
}
int freeNode = -1;
int forwardNode = 0;
// 检查是否有空余节点
for(int i = 1; i < DEFAULT_SIZE; i++) {
if(List[i].next == -2) {
freeNode = i;
break;
}
}
if(freeNode == -1) {
printf("No free node!\n");
return;
}
// 转到前置节点
forwardNode = 0;
for(int i = 0; i < Pos; i++) {
if(List[forwardNode].next == -1) {
printf("Position is out of range!\n");
return;
}
forwardNode = List[forwardNode].next;
}
List[freeNode].data = Data;
List[freeNode].next = List[forwardNode].next;
List[forwardNode].next = freeNode;
}
void DeleteNode (NodePtr List, char Data) {
// 寻找前置节点
int forwardNode = 0;
while((List[forwardNode].next != -1) && (List[List[forwardNode].next].data != Data)) {
forwardNode = List[forwardNode].next;
}
if(List[forwardNode].next == -1) {
printf("No such node!\n");
return;
}
int tmp = List[forwardNode].next;
// 判断是否存在后置节点
if(List[List[forwardNode].next].next == -1) {
List[forwardNode].next = -1;
} else {
List[forwardNode].next = List[List[forwardNode].next].next;
}
// 初始化节点
List[tmp].next = -2;
}
void PrintList (NodePtr List) {
int forwardNode = List[0].next;
while(forwardNode != -1) {
printf("%c ", List[forwardNode].data);
forwardNode = List[forwardNode].next;
}
printf("\n");
}
void PrintInformation (NodePtr List) {
printf("List information:\n");
printf("Single node size: %d\n", sizeof(struct StaticLinkedList));
printf("List head: %ld\n", List);
printf("The contents of the list: [stata, data, next]\n");
for (int i = 0; i < DEFAULT_SIZE; i ++) {
printf("[%d %c, %d]\n", i,List[i].data, List[i].next);
}
}
void Test() {
NodePtr List = InitList();
printf("Begin Initing...\n");
PrintList(List);
PrintInformation(List);
printf("Begin Inserting...\n");
InsertNode(List, 'A', 0);
InsertNode(List, 'B', 1);
InsertNode(List, 'C', 2);
InsertNode(List, 'D', 3);
InsertNode(List, 'E', 4);
PrintList(List);
PrintInformation(List);
printf("Begin Deleting...\n");
DeleteNode(List, 'A');
DeleteNode(List, 'C');
DeleteNode(List, 'E');
PrintList(List);
PrintInformation(List);
printf("End!\n");
}
int main() {
Test();
return 0;
}
输出结果
PS D:\a_study\code_vs\DataStructure\StaticLinkedNode> gcc -o kernel optimized.cpp
PS D:\a_study\code_vs\DataStructure\StaticLinkedNode> ./kernel
InitList success!
Begin Initing...
List information:
Single node size: 8
List head: 11344448
The contents of the list: [stata, data, next]
[0 0, -1]
[1 P, -2]
[2 n, -2]
[3 O, -2]
[4 C, -2]
[5 =, -2]
Begin Inserting...
A B C D E
List information:
Single node size: 8
List head: 11344448
The contents of the list: [stata, data, next]
[0 0, 1]
[1 A, 2]
[2 B, 3]
[3 C, 4]
[4 D, 5]
[5 E, -1]
Begin Deleting...
B D
List information:
Single node size: 8
List head: 11344448
The contents of the list: [stata, data, next]
[0 0, 2]
[1 A, -2]
[2 B, 4]
[3 C, -2]
[4 D, -1]
[5 E, -2]
End!
问题记录:
第一次复现 – 节点插入出现问题
报错:Position is out of range!
以及 No such node!
问题一是寻找前置节点的判断条件中错写成了 forwardNode != -1
问题二是 List[forwardNode].next = -1; 被反复使用,造成循环节点问题
Points
静态链表的声明
在示例中,静态链表的使用依托于 StaticLinkedNode 与 StaticLinkedList 两个结构体,显得比较臃肿
我们可以看到,静态链表的主体是 data 与 next 组成的结构体数组,多余的结构体实现了结构体数组与空置信息的控制功能。但这两个功能完全可以放在一个结构体中:
typedef struct StaticLinkedNode {
char data;
int next;
} *NodePtr;
结构体主体内容不变,但是在声明和使用中与示例有所区别:
ListPtr InitLinkedList() {
NodePtr TemPtr = (NodePtr)malloc(sizeof(struct StaticLinkedNode)*DEFAULT_SIZE);
TemPtr[0]->data = '\0';
TemPtr[0]->next[0] = -1;
for(int i = 1; i < DEFAULT_SIZE; i++) {
TemPtr[i]->next[i] = -2;
}
return TemPtr;
}
用 -2 来表示节点未被使用、-1 表示当前结点是链表的尾结点,其它数字反映后置节点相对位置
静态链表创建大小
由于头节点存在,静态链表创建大小应为预计使用大小 + 1
信息反馈方式的确定
反馈程序运行状态和最终结果的不一定是地址,要根据具体需求来确定
例如在这个静态链表程序中,由于其相对链接的特性(不是通过指针链接),不太好直接顺序打印链表节点的地址,即使打印了也无法直观反馈静态链表的特性。此时,直接打印数据能获得更好更直观的结果反馈
Thinking
what – 什么是静态链表?
My Thoughts:
提前分配固定空间,利用相对索引进行链接操作的链表
利用相对索引模拟单链表特性与操作的链表
AI`s Explanation:
静态链表就是用数组来模拟链式结构,通过下标来代替指针实现结点之间的“连接”。
一句话解释:
静态链表就是用数组来模拟链式结构,通过下标来代替指针实现结点之间的“连接”。
为什么要有静态链表?
在一些编程语言中(尤其是像 C 语言),链表通常需要用到动态内存分配(如 malloc
),但有时:
- 不能或不方便使用指针(比如在某些教学场景或限制内存分配的环境里);
- 想要更加可控的内存使用(如提前开辟一块连续内存);
此时就可以用静态链表。
静态链表的基本结构:
用数组 Node[]
存储每个“结点”,每个结点包括两个信息:
c复制编辑typedef struct {
int data; // 存储的数据
int next; // 指向下一个元素的“下标”
} Node;
然后像链表一样连接这些结点,只不过不是通过指针,而是通过数组下标 next
。
举个简单例子:
假设我们用一个数组表示链表 3 -> 5 -> 7
:
c复制编辑Node list[5]; // 假设最多5个元素
list[0] = {3, 1}; // 第一个节点,值是3,下一节点在下标1
list[1] = {5, 2}; // 值是5,下一节点在下标2
list[2] = {7, -1}; // 值是7,-1表示结尾
这样就构成了一个“静态链表”。
静态链表 vs 动态链表
特点 | 静态链表 | 动态链表 |
---|---|---|
内存分配 | 提前分配好数组 | 运行时动态分配 |
使用结构 | 数组 + 下标模拟“指针” | 结构体 + 真正的指针 |
内存连续性 | 是 | 否 |
插入/删除操作 | 一般效率较低(需要移动空闲下标) | 效率高(只需修改指针) |
应用场景 | 不方便用指针的环境,教学模拟等 | 普通实际开发中更常见 |
总结一句话:
静态链表是用数组加下标来模拟链表结构的一种方法,适合在不支持或不方便用指针的场景下实现链式结构。
why – 有了单链表, 为什么还要引入静态链表?
My Thoughts:
其固定空间分配以及相对索引操作能够帮助理解操作系统内存分配操作以及链表操作的共通性质
静态链表与操作系统的内存分配机制
一句话核心解释:
静态链表通过数组和下标管理空闲和已用空间的方式,和操作系统管理内存的方式非常相似,尤其是在早期的内存分配方法中。
操作系统如何管理内存(简化版)?
操作系统要处理的问题是:
- 有一大块内存(比如 1GB),怎么分配给不同的程序?
- 哪些内存是空闲的,哪些是已经使用的?
- 需要支持动态分配和回收内存。
其中一种经典的管理方法是:
使用“空闲链表”(Free List):
- 操作系统用一个链表记录所有空闲内存块;
- 每个内存块除了数据,还存有指向下一个空闲块的指针(或下标);
- 申请内存时,从空闲链表中拿一个块;
- 释放内存时,把块插回空闲链表。
是不是和静态链表的做法一模一样?
静态链表的结构就像操作系统的内存:
c复制编辑typedef struct {
int data; // 数据域
int next; // 指向下一个节点的下标,相当于“指针”
} Node;
Node list[MAX_SIZE];
- 数组
list[]
就像是一大块预分配的“内存”; - 每个元素包含数据 + next;
- 我们维护两个链表:
- 一个是“已使用链表”(就是我们的数据链);
- 一个是“空闲链表”(就像操作系统的空闲内存列表)。
这样理解的好处:
- 形象地模拟了内存分配过程
- 把“内存”看成一个数组,像静态链表一样按下标管理;
- 分配和释放都是“从链表里拿”和“插回链表”。
- 加深对 malloc/free 等函数机制的理解
- 实际上,
malloc
也会从空闲链表中找一块可用的内存; free
就是把释放的内存插入空闲链表。
- 实际上,
- 帮助理解内存碎片、空闲块合并等操作
- 静态链表操作时也可能留下“空洞”,需要合并;
- 这与操作系统处理外部碎片的思想一致。
- 训练内存分配算法的思维
- 静态链表可以模拟首次适应法、最佳适应法、循环首次适应法等分配策略。
举个简单类比:
概念 | 静态链表中的对应 | 操作系统中的对应 |
---|---|---|
数组 | 模拟内存块 | 物理内存 |
节点的下标 | 模拟指针 | 地址 / 偏移量 |
空闲结点表 | 空闲链表 | 空闲内存块的管理 |
插入节点 | 占用内存 | 分配内存(malloc) |
删除节点 | 回收内存 | 释放内存(free) |
总结一句话:
静态链表通过数组模拟链表、管理空闲空间的方式,就像操作系统早期实现的空闲链表内存分配机制,非常适合用来教学和理解操作系统中内存的分配和回收原理。
如果你将来学习操作系统课程,比如学到“内存管理”、“页表”、“段式/分页式管理”,静态链表的思想会帮助你打好基础!
适用于对内存空间需要精确分配/不能使用malloc动态分配的场景
运行稳定性高,不易出现因操作失误而出现的野指针等相关问题
更易进行缓存优化(内存是连续的)
AI`s Explanation:
首先回顾一下:
- 单链表:用结构体 + 指针实现,灵活高效,需要动态内存分配(如
malloc
)。 - 静态链表:用数组 + 下标模拟链表,不用指针,不需要动态内存分配。
引入静态链表的理由:
1. 某些场景下不能使用指针
比如:
- 有些学校的考试/竞赛限制不能用指针;
- 某些嵌入式系统、早期机器、受限编程环境中,不支持或不允许使用动态内存分配。
静态链表在这些“不能用指针”的环境里,是唯一能模拟链表行为的方法。
2. 方便教学和理解链表原理
- 静态链表更容易可视化和调试;
- 新手更容易理解“指针连接”的思想,用“下标”来代替指针;
- 用来作为链表的模拟实验工具,更安全、不会出现野指针或内存泄露。
3. 内存管理更加可控
- 静态链表使用的是固定大小的数组,内存是连续且预分配的;
- 没有碎片化问题;
- 系统运行稳定性更高(特别是在实时系统或嵌入式系统中)。
4. 可实现类似链表的行为,又兼具数组的一些优势
- 数据存在数组里,可以随机访问;
- 更容易缓存优化(因为数组是连续的);
- 结构上更规整,有时候更方便移植或迁移数据。
5. 避免动态分配带来的问题
- 使用
malloc
时要记得free
,不然会内存泄漏; - 指针用错可能导致程序崩溃;
- 静态链表不会出现野指针,安全性更高。
🆚 总结对比:
特点 | 单链表(动态链表) | 静态链表 |
---|---|---|
是否用指针 | ✅ 是 | ❌ 否(用下标) |
是否支持动态内存分配 | ✅ 是(malloc/free ) | ❌ 否(固定数组) |
内存使用是否灵活 | ✅ 灵活 | ❌ 固定大小 |
程序安全性 | ⚠️ 可能内存泄漏或野指针 | ✅ 更安全 |
是否易于教学或模拟 | ❌ 相对较难理解 | ✅ 简单直观 |
应用场景 | 常规开发、需要动态性 | 教学、嵌入式、受限环境 |
结论:
静态链表不是为了替代单链表,而是为了解决在不能用动态内存或指针的特殊场景下,也能实现链表这种“灵活插入删除”的数据结构。
what – 静态链表与前面的单链表有什么联系与区别?
My Thoughts:
静态链表可以看作是特殊的单链表,唯二的区别在于静态链表空间/节点数量在初始化时候就固定分配,无法二次改变,并且静态链表节点之间不是使用指针进行直接连接,而是用相对索引进行间接连接。
AI`s Explanation:
一、概念界定
- 单链表(Singly Linked List):是一种通过结构体指针将若干个非连续存储的结点动态连接而成的线性表。每个结点包含数据域与指针域,指针域用于存储后继结点的地址。
- 静态链表(Static Linked List):是在不使用指针的前提下,利用数组的下标模拟链式存储结构的线性表。其每个“结点”由结构体组成,结构体内部包括数据域和指向下一个“结点”的数组下标。
二、结构特征的共性
- 链式存储模型的抽象一致性
静态链表与单链表均体现了线性结构的“动态连接”思想,即各结点之间的逻辑关系通过“地址”或“索引”来显式维护,而非依赖物理地址的连续性。 - 支持局部插入与删除操作的时间效率
两者均可在已知插入/删除位置的前提下,借由局部指针或下标的修改,实现常数时间复杂度 O(1)O(1)O(1) 的插入与删除。 - 逻辑结构与物理结构分离
这两种链表均突破了顺序存储结构在物理存储上的线性限制,增强了数据元素在物理空间上的分布自由度。
三、实现机制上的差异
对比维度 | 单链表 | 静态链表 |
---|---|---|
存储结构 | 动态内存分配(heap),基于指针的结点连接 | 静态数组分配,基于数组下标模拟指针关系 |
空间使用 | 空间动态扩展,不预设上限 | 空间固定,初始化时需指定数组容量 |
空闲空间管理 | 操作系统负责分配与回收内存(如 malloc/free ) | 需手动维护空闲链表以跟踪未使用数组下标 |
存储连续性 | 存储位置非连续,由系统分配 | 所有结点存储于同一数组中,物理地址连续 |
编程复杂性 | 操作灵活但需注意指针操作及内存管理 | 操作相对安全,但管理逻辑更复杂 |
应用场景 | 通用性强,适用于大多数实际开发情境 | 教学、嵌入式开发或无动态内存分配场景 |
四、在计算机系统设计中的关联性
静态链表的设计思想与操作系统早期的**空闲内存块管理机制(free list)**高度一致。在无动态内存分配机制的环境中,操作系统常以数组记录各内存块的使用状态,并通过“下标”维护空闲链表,从而模拟 malloc/free
的内存管理功能。因此,静态链表不仅在教学中作为链式结构的过渡形式,也为理解诸如“首次适应算法”、“空闲块合并”等操作系统内存分配策略提供了理论基础。
五、总结
静态链表与单链表本质上是同一抽象数据类型的两种不同实现。前者适用于不支持指针或动态内存分配的受限环境,强调实现上的可控性与安全性;后者则充分发挥链式结构的灵活性,广泛应用于各种实际编程任务中。在教学与系统模拟中,静态链表具有重要的启发性和实践价值,有助于从更底层的角度理解数据结构与内存管理之间的内在联系。
what – 对静态链表进行操作的时候, 容易出现哪些错误?
My Thoughts:
链表操作的共通易错:重复使用一个节点的链接部分(next),导致单个节点形成循环(尤其在 Delete 操作中容易出现)
特殊情况的判断:容易忘记操作范围越界、尾节点等特殊情况的处理
what – 地址与数组的下标有什么联系与区别?
My Thoughts:
联系:都是反馈数据相对位置的索引
区别:地址是在硬件空间内的相对位置,而数组下标则是在数组内的相对位置,后者抽象层级更高
AI`s Explanation:
在计算机系统中,地址(Address)与数组下标(Index)是两个密切相关但本质不同的概念。前者是底层内存管理的物理/逻辑单位,后者则是高级语言中访问数据结构的抽象机制。为了系统性地阐明二者之间的联系与区别,可从以下几个维度展开分析:
一、概念定义
- 地址(Address):指计算机内存中某个存储单元的标识,是对物理或逻辑存储空间中单元的唯一定位标识。现代计算机系统中,地址多为逻辑地址(virtual address),在程序运行时由硬件和操作系统映射为实际物理地址。
- 数组下标(Index):是高层程序设计语言中访问数组元素的一种语法形式,表示与数组起始位置的相对偏移量。下标通常是非负整数,反映数组元素在线性序列中的位置。
二、内在联系
- 数组下标通过地址偏移实现访问 在高级语言中对数组元素
a[i]
的访问,实质上是基于地址偏移的运算过程。设数组a
的首地址为Base(a)
,且每个元素大小为sizeof(T)
,则: Address(a[i])=Base(a)+i×sizeof(T)\text{Address}(a[i]) = \text{Base}(a) + i \times \text{sizeof}(T)Address(a[i])=Base(a)+i×sizeof(T) 因此,数组下标是对地址的线性映射,其本质是以固定步长对内存地址进行顺序访问。 - 数组是地址线性连续的一段内存空间 数组的存储模型要求元素在内存中是连续排列的。正因如此,使用下标时可以等价转换为偏移地址的访问,而无需使用指针运算。
三、主要区别
对比维度 | 地址(Address) | 数组下标(Index) |
---|---|---|
所在层级 | 底层内存模型(硬件/操作系统) | 高级语言抽象层(编译器层) |
表示方式 | 整型(通常以十六进制表示) | 整数(非负,通常从 0 开始) |
功能 | 精确定位内存单元 | 表示元素在数组中的相对位置 |
适用范围 | 所有内存访问(包括堆、栈、静态区等) | 仅适用于数组结构(顺序存储) |
可否控制 | 程序员可通过指针直接操作地址 | 编译器控制映射,不能直接操作底层地址 |
安全性 | 不当使用易导致野指针或内存破坏 | 较安全,超界访问可被检测 |
四、实例分析
c复制编辑int a[5] = {10, 20, 30, 40, 50};
printf("%p\n", &a[0]); // 输出 a[0] 的地址
printf("%p\n", &a[1]); // 输出 a[1] 的地址(偏移一个 sizeof(int))
这里,a[1]
对应的地址等于 a[0]
的地址加上一个 int
大小的偏移。也可以通过指针运算验证:
c复制编辑int *p = a;
printf("%d\n", *(p + 1)); // 输出 20,与 a[1] 等价
说明数组下标 i
实质上等价于 *(p + i)
,其中 p
是数组首地址。
五、在静态链表与内存管理中的意义
在静态链表中,使用下标代替地址指针,本质上是将内存控制权从操作系统移交给程序员,由用户通过数组下标手动模拟结点之间的连接。这种设计使得:
- 可以在无指针或动态内存分配机制的环境中实现链式结构;
- 模拟操作系统中的内存管理逻辑,例如通过数组模拟空闲链表;
- 提高了程序在特定场景下的安全性和可预测性。
六、总结
地址与数组下标之间存在紧密的映射关系,但从计算系统抽象层次的角度来看:
- 地址是物理性的、全局性的、绝对的标识单位;
- 数组下标是逻辑性的、局部性的、相对的访问手段。
两者之间通过编译器的转换机制建立联系,但在不同的编程范式与系统结构中,其适用范围与操作语义均存在本质差异。深入理解此关系,有助于掌握编译原理、内存管理、操作系统实现等核心计算机科学知识体系。
how – 如果要在一片空间内支持多个静态链表, 应该怎么做?
My Thoughts:
首先划分一个大的固定空间(domain)
然后按照需要划分出对应空间和数量的子空间(subdomain)
最后在静态链表的角度进行链表操作
AI`s Explanation:
在同一片预分配的静态内存空间中支持多个静态链表(static linked lists),实质上是在一个统一的数据区域上,实现多条逻辑链表的共存与相互隔离。为此,必须建立系统的存储管理机制以维护链表之间的边界,并协调空闲结点的统一调度。
下面从数据结构设计、操作机制、管理策略三个层面详细说明:
一、数据结构设计
1. 共享结点数组
设有一个固定大小的结点数组 Node S[MAX_SIZE]
,其中每个结点定义如下:
c复制编辑typedef struct {
int data; // 数据域
int next; // 下一个结点在数组中的下标(-1 表示无后继)
} Node;
所有静态链表的结点均从该数组中取用,彼此复用相同的存储空间。
2. 多链表的头指针集
为了支持多个链表,需引入一个头指针数组或链表头结构数组:
c复制编辑int heads[K]; // K 个链表的头指针,heads[i] 表示第 i 条链表的逻辑起点下标
若需保留头结点作为哨兵结点,可令 heads[i]
指向对应链表的头结点(即哨兵),实际数据从其 next
开始。
3. 全局空闲链表
统一管理空闲空间,设一个变量 free_head
指向空闲链表的首结点:
c复制编辑int free_head; // 指向空闲链表的头(初始为 0)
整个空闲链表通过结点数组中的 next
字段进行连接。
二、操作机制设计
1. 初始化
- 将所有结点连接为空闲链表;
- 初始化所有
heads[i] = -1
或者heads[i] = 分配的头结点
。
c复制编辑void init() {
for (int i = 0; i < MAX_SIZE - 1; ++i)
S[i].next = i + 1;
S[MAX_SIZE - 1].next = -1;
free_head = 0;
for (int i = 0; i < K; ++i)
heads[i] = -1; // 空链表起始
}
2. 分配结点
从空闲链表中取出一个结点用于任意一条链表:
c复制编辑int malloc_node() {
if (free_head == -1) return -1; // 无空闲结点
int p = free_head;
free_head = S[free_head].next;
return p;
}
3. 释放结点
回收某条链表中删除的结点,将其归还空闲链表:
c复制编辑void free_node(int p) {
S[p].next = free_head;
free_head = p;
}
4. 插入与删除操作
所有插入、删除等逻辑操作均在指定链表 heads[i]
上进行,完全独立且互不干扰。例如,在第 k
条链表中插入数据:
c复制编辑void insert(int k, int data) {
int p = malloc_node();
if (p == -1) return;
S[p].data = data;
S[p].next = heads[k];
heads[k] = p;
}
三、多链表共存的存储管理策略
为了实现多链表共存的空间利用最优化,应注意:
- 空闲结点共享而非分区
所有链表动态从统一空闲表中申请结点,实现“谁需要,谁申请”的策略,避免静态划分造成空间浪费。 - 链表之间逻辑隔离
虽然底层结点共享数组,但逻辑上通过heads[i]
隔离了不同链表的访问范围,从而保证结构正确性。 - 空闲链表始终维护完整性
所有删除操作必须显式调用free_node()
以避免内存“泄漏”。 - 必要时支持链表编号与结点标记映射
为增强链表识别性,可在结点结构中添加字段如int list_id
表示其归属,便于调试或扩展。
四、理论意义与应用场景
这种机制本质上是一种手动内存管理模型(manual memory allocator),在以下场景中具有重要意义:
- 操作系统级数据结构模拟(如文件系统索引节点、空闲块表);
- 嵌入式系统或内存受限环境;
- 无动态分配函数的语言环境;
- 教学目的下对链表和内存分配关系的演示;
- 实现用户态内存池(memory pool)中的数据组织结构。
五、总结
在一片共享空间中支持多个静态链表的关键在于:
操作互不干扰,结构逻辑独立。
统一分配:使用一个全局结点数组作为统一的存储池;
多头并行:通过多个头指针管理不同链表;
空闲协调:通过空闲链表动态调度资源;