
警告:
- 下文出现的「数组版链表」在生产代码里出现,会被同事打;
- 代码仅用于加深对「索引」与「指针」本质的理解;
- 作者不提供心理咨询服务。
1. 用数组实现链表 ------ 史上最没用的炫技
1.1 思路吐槽
普通链表:
malloc
一把梭,节点散落在堆的各个角落,缓存局部性稀烂。
数组版链表:
一次 malloc
一个数组,所有节点整整齐齐排好队,用「下标」假装「指针」。
优点:
- 没有野指针,没有段错误,妈妈再也不担心我越界。
缺点: - 插入/删除还是要搬数据,时间复杂度没变;
- 代码可读性下降 300%;
- 面试官看完简历后,把「会 C」改成了「会搞笑」。
1.2 结构定义
c
#include <stdio.h>
#include <stdlib.h>
#define POOL_SIZE 100
typedef struct {
int data; // 真实 payload
int next; // "指针":下一个元素的下标,-1 表示 NULL
} Node;
typedef struct {
Node pool[POOL_SIZE]; // 一次性批发 100 个节点
int head; // 头指针(其实是头下标)
int free; // 空闲链表头,后面实现简单内存管理
} ArrayLinkedList;
1.3 基本操作
c
// 初始化:所有节点串成空闲链表
void init(ArrayLinkedList *list) {
for (int i = 0; i < POOL_SIZE - 1; ++i)
list->pool[i].next = i + 1;
list->pool[POOL_SIZE - 1].next = -1;
list->head = -1;
list->free = 0;
}
// 从空闲链表摘一个节点
int alloc_node(ArrayLinkedList *list) {
if (list->free == -1) return -1; // 池子用完
int id = list->free;
list->free = list->pool[id].next;
return id;
}
// 头插
void push_front(ArrayLinkedList *list, int val) {
int id = alloc_node(list);
if (id == -1) { puts("pool full"); return; }
list->pool[id].data = val;
list->pool[id].next = list->head;
list->head = id;
}
// 打印
void print(const ArrayLinkedList *list) {
for (int cur = list->head; cur != -1; cur = list->pool[cur].next)
printf("%d -> ", list->pool[cur].data);
puts("NULL");
}
1.4 主函数跑一遍
c
int main(void) {
ArrayLinkedList list;
init(&list);
for (int i = 0; i < 5; ++i) push_front(&list, i);
print(&list); // 4 -> 3 -> 2 -> 1 -> 0 -> NULL
return 0;
}
2. 动态大小线性队列 ------ 让链表做点正事
链表最正经的用途之一:
实现长度无上限的队列 ,不用事先 realloc
数组。
2.1 队列 ADT
c
typedef int QData;
typedef struct QNode {
QData data;
struct QNode *next;
} QNode;
typedef struct {
QNode *front, *rear;
int len; // 可选,O(1) 返回长度
} LinkedQueue;
2.2 核心操作(O(1))
c
void q_init(LinkedQueue *q) { q->front = q->rear = NULL; q->len = 0; }
int q_empty(const LinkedQueue *q) { return q->len == 0; }
void q_push(LinkedQueue *q, QData val) {
QNode *n = malloc(sizeof(QNode));
n->data = val; n->next = NULL;
if (q->rear) q->rear->next = n;
else q->front = n;
q->rear = n;
++q->len;
}
QData q_pop(LinkedQueue *q) {
if (q_empty(q)) { fputs("underflow\n", stderr); exit(1); }
QNode *n = q->front;
QData v = n->data;
q->front = n->next;
if (!q->front) q->rear = NULL;
free(n);
--q->len;
return v;
}
2.3 小测试
c
int main(void) {
LinkedQueue q; q_init(&q);
for (int i = 1; i <= 3; ++i) q_push(&q, i);
while (!q_empty(&q)) printf("%d ", q_pop(&q)); // 1 2 3
return 0;
}
3. 固定大小循环队列 ------ 把数组掰弯
链表好,但缓存局部性差;
很多时候我们只需要固定长度 的 FIFO,用数组就能搞定。
掰弯数组 → 循环队列。
3.1 结构定义
c
typedef struct {
int *arr; // 动态申请,可复用
int head, tail;
int cap; // 容量
int count; // 当前元素个数
} CircleQueue;
3.2 基本操作
c
void cq_init(CircleQueue *q, int size) {
q->arr = malloc(sizeof(int) * size);
q->cap = size;
q->head = q->tail = q->count = 0;
}
int cq_full(const CircleQueue *q) { return q->count == q->cap; }
int cq_empty(const CircleQueue *q) { return q->count == 0; }
int cq_push(CircleQueue *q, int val) {
if (cq_full(q)) return -1;
q->arr[q->tail] = val;
q->tail = (q->tail + 1) % q->cap;
++q->count;
return 0;
}
int cq_pop(CircleQueue *q, int *out) {
if (cq_empty(q)) return -1;
*out = q->arr[q->head];
q->head = (q->head + 1) % q->cap;
--q->count;
return 0;
}
void cq_free(CircleQueue *q) { free(q->arr); }
3.3 可视化
cap = 8
push 1 2 3 4 5
pop 1 2
push 6 7 8 9
数组实际布局: 3 4 5 6 7 8 9 _
head=3, tail=3, count=6
4. 总结:什么时候用谁?
场景 | 选型 | 理由 |
---|---|---|
长度未知、频繁入出队 | 链表队列 | 不会 realloc,O(1) |
长度已知、缓存友好 | 循环数组队列 | 局部性极佳,无 malloc 开销 |
想炫技/写玩具 | 数组版链表 | 写完立刻后悔,加深索引理解 |
5. 课后作业(挨骂预警)
- 给「数组版链表」加上
erase
与insert
,体会数据搬移的痛苦。 - 用
mmap
把「数组版链表」映射到共享内存,实现跨进程通信,然后提交 MR,观察朋友表情。