[c语言日记] 数组的一种死法和两种用法

发布于:2025-09-11 ⋅ 阅读:(11) ⋅ 点赞:(0)

在这里插入图片描述

警告:

  1. 下文出现的「数组版链表」在生产代码里出现,会被同事打;
  2. 代码仅用于加深对「索引」与「指针」本质的理解;
  3. 作者不提供心理咨询服务。

1. 用数组实现链表 —— 史上最没用的炫技

1.1 思路吐槽

普通链表:
malloc 一把梭,节点散落在堆的各个角落,缓存局部性稀烂。

数组版链表:
一次 malloc 一个数组,所有节点整整齐齐排好队,用「下标」假装「指针」。
优点:

  • 没有野指针,没有段错误,妈妈再也不担心我越界。
    缺点:
  • 插入/删除还是要搬数据,时间复杂度没变;
  • 代码可读性下降 300%;
  • 面试官看完简历后,把「会 C」改成了「会搞笑」。

1.2 结构定义

#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 基本操作

// 初始化:所有节点串成空闲链表
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 主函数跑一遍

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

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))

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 小测试

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 结构定义

typedef struct {
    int *arr;   // 动态申请,可复用
    int  head, tail;
    int  cap;   // 容量
    int  count; // 当前元素个数
} CircleQueue;

3.2 基本操作

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. 课后作业(挨骂预警)

  1. 给「数组版链表」加上 eraseinsert,体会数据搬移的痛苦。
  2. mmap 把「数组版链表」映射到共享内存,实现跨进程通信,然后提交 MR,观察朋友表情。

网站公告

今日签到

点亮在社区的每一天
去签到