C语言实现动态数组、链表、栈与队列

C语言实现动态数组、链表、栈与队列

在数据结构的世界里,线性结构是构建复杂算法的基石。动态数组、链表、栈和队列作为最经典的线性结构,各自拥有独特的存储方式与操作特性,适用于不同的业务场景。本文将结合C语言实现代码,从结构定义、核心操作到实际应用,讲述这四种数据结构的设计思想与使用技巧。

一、动态数组(Dynamic Array):灵活扩容的连续存储

动态数组是对静态数组的优化,它解决了静态数组容量固定的痛点,支持在元素数量超过容量时自动扩容,同时保留了数组随机访问的高效性。

1.1 动态数组的结构设计

动态数组的核心是通过指针指向一片连续的内存空间,并记录当前的容量(capacity)长度(length)。容量表示当前内存可容纳的最大元素数,长度表示实际存储的元素数。

typedef int element_t; // 数据类型别名,便于后续修改

typedef struct {
    element_t *data;    // 指向连续内存空间的指针
    size_t capacity;    // 数组总容量(可容纳的最大元素数)
    size_t length;      // 数组当前长度(实际存储的元素数)
} DynamicArray;

1.2 核心操作实现

动态数组的核心操作包括初始化、扩容、插入、删除和销毁,下面逐一解析关键代码逻辑。

(1)初始化:分配初始内存

初始化函数为动态数组分配指定大小的初始内存,并将长度初始化为0。

void init_dynamic_array(DynamicArray *array, size_t initCapacity) {
    array->data = (element_t*)malloc(initCapacity * sizeof(element_t)); // 分配内存
    array->capacity = initCapacity; // 初始容量
    array->length = 0; // 初始长度为0(无元素)
}

(2)扩容:自动调整内存大小

当元素数量(length)等于容量(capacity)时,调用resizeDynamicArray将容量翻倍,通过realloc实现内存重分配。

void resizeDynamicArray(DynamicArray *array, size_t newCapacity) {
    array->data = (element_t*)realloc(array->data, newCapacity * sizeof(element_t));
    array->capacity = newCapacity; // 更新容量
}

(3)插入:指定位置插入元素

插入元素时需先判断下标合法性(不能超过当前长度),若容量不足则先扩容,再将插入位置后的元素向后移动一位,最后插入新元素并更新长度。

bool insert(DynamicArray *array, size_t index, element_t element) {
    if (index > array->length) return false; // 下标非法(如长度为5,下标不能>5)
    
    // 容量不足时扩容(翻倍)
    if (array->length == array->capacity) {
        resizeDynamicArray(array, array->capacity * 2);
    }
    
    // 元素后移(从最后一个元素开始,避免覆盖)
    for (size_t i = array->length; i > index; i--) {
        array->data[i] = array->data[i-1];
    }
    
    array->data[index] = element; // 插入新元素
    array->length++; // 更新长度
    return true;
}

// 末尾插入(简化版,调用insert指定下标为当前长度)
bool endinsert(DynamicArray *array, element_t element) {
    return insert(array, array->length, element);
}

(4)删除:指定位置删除元素

删除元素时需先判断下标合法性(不能大于等于当前长度),保存待删除元素的值,再将删除位置后的元素向前移动一位,最后更新长度。

bool delet(DynamicArray *array, size_t index, element_t *deleted_element) {
    if (index >= array->length) return false; // 下标非法
    
    *deleted_element = array->data[index]; // 保存删除的元素值
    
    // 元素前移(从删除位置开始,覆盖前一个元素)
    for (size_t i = index; i < array->length - 1; i++) {
        array->data[i] = array->data[i+1];
    }
    
    array->length--; // 更新长度
    return true;
}

// 末尾删除(简化版,调用delet指定下标为当前长度-1)
bool enddelet(DynamicArray *array, element_t *deleted_element) {
    return delet(array, array->length - 1, deleted_element);
}

(5)销毁:释放内存

销毁函数释放data指向的内存,并将指针置空,避免野指针问题。

void destory(DynamicArray *array) {
    free(array->data); // 释放内存
    array->data = NULL; // 指针置空
    array->capacity = 0;
    array->length = 0;
}

1.3 测试代码与运行结果

通过main函数测试动态数组的所有操作,验证功能正确性:

int main() {
    DynamicArray dy;
    init_dynamic_array(&dy, 8); // 初始容量8
    printf("初始长度:%zu\n", getLength(&dy)); // 输出:0

    // 末尾插入9个元素(触发一次扩容,容量从8→16)
    for (int i = 1; i <= 9; i++) {
        endinsert(&dy, i * 100);
    }
    insert(&dy, 4, 450); // 在第5个位置(下标4)插入450
    printf("插入后长度:%zu\n", getLength(&dy)); // 输出:10
    printf("插入后数组:");
    printDynamicArray(&dy); // 输出:100 200 300 400 450 500 600 700 800 900

    // 删除操作
    element_t de;
    delet(&dy, 2, &de); // 删除下标2的元素(300)
    printf("%d 被删除\n", de); // 输出:300
    enddelet(&dy, &de); // 删除末尾元素(900)
    printf("%d 被删除\n", de); // 输出:900
    printf("删除后数组:");
    printDynamicArray(&dy); // 输出:100 200 400 450 500 600 700 800

    destory(&dy); // 销毁数组
    return 0;
}

1.4 动态数组的优缺点

优点 缺点
支持随机访问(通过下标访问,时间复杂度O(1)) 插入/删除元素时需移动大量元素(时间复杂度O(n))
内存连续,缓存命中率高 扩容时可能产生内存碎片(realloc可能迁移内存)
实现简单,API直观 预分配过多内存会造成浪费

二、单向链表(Singly Linked List):灵活增减的离散存储

链表与数组的核心区别是离散存储——元素(节点)不占用连续内存,而是通过指针串联。单向链表的每个节点仅存储下一个节点的地址,结构灵活,适合频繁插入/删除的场景。

2.1 链表的结构设计

链表由节点(Node)链表管理结构体(LinkedList) 组成:节点存储数据和下一个节点的指针;链表管理结构体存储头节点地址和链表长度。

typedef int element_t;

// 节点结构体
typedef struct Node {
    element_t data; // 节点存储的数据
    struct Node *next; // 指向后一个节点的指针
} Node;

// 链表管理结构体
typedef struct {
    Node *head; // 头节点地址(链表的入口)
    size_t length; // 链表长度(节点个数)
} LinkedList;

2.2 核心操作实现

链表的核心操作围绕节点的“查找、插入、删除”展开,由于离散存储,操作时无需移动元素,只需修改指针指向。

(1)初始化:空链表

初始化时头节点为NULL,长度为0,表示空链表。

void initLinkedList(LinkedList *list) {
    list->head = NULL;
    list->length = 0;
}

(2)查找前驱节点:插入/删除的关键

插入或删除节点时,需先找到目标位置的前驱节点(前一个节点)。例如,要在第3个节点(下标2)插入,需找到第2个节点(下标1)作为前驱。

Node *getPrevNode(LinkedList *list, size_t index) {
    Node *prev_node = list->head;
    // 从头部开始遍历,找到第index-1个节点
    for (size_t i = 1; i < index; i++) {
        prev_node = prev_node->next;
    }
    return prev_node;
}

(3)插入节点:分“头插”和“中间/尾插”

  • 头插:新节点的next指向原头节点,再将链表的head指向新节点。
  • 中间/尾插:前驱节点的next指向新节点,新节点的next指向原前驱节点的next
bool insertnode(LinkedList *list, size_t index, element_t element) {
    if (index > list->length) return false; // 下标非法
    
    // 为新节点分配内存
    Node *new_node = (Node*)malloc(sizeof(Node));
    new_node->data = element;

    if (index == 0) { // 头插
        new_node->next = list->head; // 新节点指向原头节点
        list->head = new_node; // 头节点更新为新节点
    } else { // 中间/尾插
        Node *prev_node = getPrevNode(list, index); // 找前驱节点
        new_node->next = prev_node->next; // 新节点指向原后继
        prev_node->next = new_node; // 前驱节点指向新节点
    }

    list->length++;
    return true;
}

// 尾插(简化版,下标为当前长度)
bool endinsert(LinkedList *list, element_t element) {
    return insertnode(list, list->length, element);
}

(4)删除节点:分“头删”和“中间/尾删”

  • 头删:直接将head指向原头节点的next,释放原头节点。
  • 中间/尾删:前驱节点的next指向待删除节点的next,释放待删除节点。
bool deletelink(LinkedList *list, size_t index, element_t *deleted_element) {
    if (index >= list->length) return false;
    
    Node *deleted_node = NULL;
    if (index == 0) { // 头删
        deleted_node = list->head; // 待删除节点是头节点
        list->head = list->head->next; // 头节点更新为下一个节点
    } else { // 中间/尾删
        Node *prev_node = getPrevNode(list, index); // 找前驱节点
        deleted_node = prev_node->next; // 待删除节点是前驱的后继
        prev_node->next = prev_node->next->next; // 前驱跳过待删除节点
    }

    *deleted_element = deleted_node->data; // 保存删除的元素
    free(deleted_node); // 释放节点内存(避免内存泄漏)
    list->length--;
    return true;
}

// 尾删(简化版,下标为当前长度-1)
bool enddeltelink(LinkedList *list, element_t *delted_element) {
    return deletelink(list, list->length - 1, delted_element);
}

(5)销毁:逐个释放节点

链表销毁需遍历所有节点,逐个释放内存,避免内存泄漏。

void destoryLinkList(LinkedList *list) {
    Node *current_node = list->head;
    while (current_node != NULL) {
        Node *free_node = current_node; // 临时保存待释放节点
        current_node = current_node->next; // 移动到下一个节点
        free(free_node); // 释放当前节点
    }
    list->head = NULL;
    list->length = 0;
}

2.3 测试代码与运行结果

int main() {
    // 声明链表
    LinkedList ll;
    initLinkedList(&ll);
    printf("初始链表长度:%zu\n", getLength(&ll)); // 输出:0

    // 尾插5个元素
    endinsert(&ll, 100);
    endinsert(&ll, 200);
    endinsert(&ll, 300);
    endinsert(&ll, 400);
    endinsert(&ll, 500);
    // 中间插入(下标2)和头插(下标0)
    insertnode(&ll, 2, 250);
    insertnode(&ll, 0, 50);

    printf("插入后长度:%zu\n", getLength(&ll)); // 输出:7
    printf("遍历链表: ");
    printLinkedList(&ll); // 输出:50 100 200 250 300 400 500

    // 尾删和头删
    element_t dn;
    enddeltelink(&ll, &dn);
    printf("\n%d 被删除\n", dn); // 输出:500
    deletelink(&ll, 0, &dn);
    printf("%d 被删除\n", dn); // 输出:50

    // 修改节点值(下标2)
    element_t val;
    getNodeValue(&ll, 2, &val); // 获取下标2的原值(250)
    setNodeValue(&ll, 2, val - 1); // 修改为249
    printf("修改后遍历:");
    printLinkedList(&ll); // 输出:100 200 249 300 400

    // 输出指定下标节点值
    printIndexLinkedList(&ll, 2); // 输出:索引为2 的值为 249

    destoryLinkList(&ll); // 销毁链表
    return 0;
}

2.4 链表的优缺点

优点 缺点
插入/删除节点只需修改指针(时间复杂度O(1),前提是找到前驱) 不支持随机访问(访问第n个节点需遍历,时间复杂度O(n))
内存利用率高(无需预分配内存,按需分配) 每个节点额外存储指针,占用更多内存
无扩容问题(不会产生内存碎片) 缓存命中率低(节点离散存储,不连续)

三、栈(Stack):先进后出的“桶”式结构

栈是一种受限线性结构,仅允许在一端(栈顶)进行插入(入栈/PUSH)和删除(出栈/POP)操作,遵循“先进后出(LIFO,Last In First Out)”原则,类似生活中的“桶”——先放进去的东西最后才能拿出来。

3.1 栈的结构设计

栈的实现基于动态数组(也可基于链表),通过length记录栈顶位置(length-1即为栈顶下标),无需额外维护栈顶指针。

typedef int element_t;

typedef struct {
    element_t *data;    // 动态数组存储栈元素
    size_t length;      // 栈的长度(栈顶位置=length-1)
    size_t capacity;    // 栈的容量(最大可容纳元素数)
} Stack;

3.2 核心操作实现

栈的操作极其简单,仅需关注“入栈”和“出栈”,且均在栈顶进行。

(1)初始化与销毁

与动态数组类似,初始化时分配初始内存,销毁时释放内存。

void initStack(Stack *stack, size_t initcapacity) {
    stack->data = (element_t*)malloc(initcapacity * sizeof(element_t));
    stack->capacity = initcapacity;
    stack->length = 0; // 栈空时长度为0
}

void destoryStack(Stack *stack) {
    free(stack->data);
    stack->data = NULL;
    stack->capacity = 0;
    stack->length = 0;
}

(2)入栈(PUSH):栈顶添加元素

入栈前判断容量,不足则扩容,然后将元素放入length位置(栈顶),并更新length

void pushStack(Stack *stack, element_t element) {
    if (stack->length == stack->capacity) {
        resizeStack(stack, stack->capacity * 2); // 扩容(同动态数组)
    }
    stack->data[stack->length] = element; // 元素放入栈顶
    stack->length++; // 栈顶上移
}

(3)出栈(POP):栈顶删除元素

出栈前判断栈是否为空,非空则取出length-1位置(栈顶)的元素,更新length(栈顶下移)。

bool popStack(Stack *stack, element_t *deleted_element) {
    if (stack->length == 0) return false; // 栈空,无法出栈
    
    *deleted_element = stack->data[stack->length - 1]; // 取出栈顶元素
    stack->length--; // 栈顶下移(逻辑删除,无需实际清除数据)
    return true;
}

3.3 栈的应用场景

  • 函数调用栈(保存函数返回地址)
  • 表达式求值(如后缀表达式计算)
  • 括号匹配(判断代码中括号是否成对)
  • undo/redo 操作(文本编辑器的撤销/重做)

3.3 测试代码与运行结果

int main() {
    // 定义栈并初始化(初始容量8)
    Stack st;
    initStack(&st, 8);

    // 入栈9个元素(触发扩容:容量从8→16)
    pushStack(&st, 100);
    pushStack(&st, 200);
    pushStack(&st, 300);
    pushStack(&st, 400);
    pushStack(&st, 500);
    pushStack(&st, 600);
    pushStack(&st, 700);
    pushStack(&st, 800);
    pushStack(&st, 900);

    printf("栈的长度:%zu\n", getStackLength(&st)); // 输出:9
    printf("栈内元素(从栈底到栈顶):\n");
    printStack(&st); // 输出:100 200 300 400 500 600 700 800 900

    // 出栈操作
    element_t del;
    popStack(&st, &del);
    printf("\n%d 出栈\n", del); // 输出:900(栈顶元素)
    popStack(&st, &del);
    printf("%d 出栈\n", del); // 输出:800

    printf("出栈后栈的长度:%zu\n", getStackLength(&st)); // 输出:7
    printf("出栈后栈内元素:\n");
    printStack(&st); // 输出:100 200 300 400 500 600 700

    destoryStack(&st); // 销毁栈
    return 0;
}

3.4 栈的优缺点与应用场景

优点 缺点
操作高效(入栈、出栈时间复杂度O(1)) 仅能访问栈顶元素,不支持随机访问
基于动态数组实现时,缓存命中率高 扩容时可能产生内存碎片(同动态数组)
逻辑简单,易于实现 若基于链表实现,缓存效率较低

典型应用场景

  • 函数调用栈(保存函数返回地址、局部变量)
  • 括号匹配校验(如代码中(){}[]是否成对)
  • 后缀表达式计算(如3 4 + 2 * 计算结果为14)
  • 撤销/重做功能(文本编辑器中记录操作历史)

四、队列(Queue):先进先出的“排队”结构

队列是另一种受限线性结构,仅允许在一端(队尾)插入元素(入队/Enqueue),在另一端(队首)删除元素(出队/Dequeue),遵循“先进先出(FIFO,First In First Out)”原则,类似生活中“排队买票”——先排队的人先得到服务。

4.1 队列的结构设计(循环数组实现)

队列的实现方式有两种:数组实现链表实现。本文采用“循环数组”实现(避免普通数组出队时元素整体移动的低效问题),核心通过三个变量管理:

  • front:队首指针,指向待删除元素的位置;
  • rear:队尾指针,指向待插入元素的位置;
  • length:队列当前长度(避免通过frontrear计算长度时的歧义)。
typedef int element_t;

typedef struct {
    element_t *data;    // 循环数组,存储队列元素
    size_t length;      // 队列当前长度(元素个数)
    size_t capacity;    // 队列总容量(最大可容纳元素数)
    size_t front;       // 队首指针(待删除位置)
    size_t rear;        // 队尾指针(待插入位置)
} Queue;

4.2 核心操作实现

循环数组的关键是通过取模运算(%)frontrear指针在数组范围内循环移动,避免指针越界。

(1)初始化:分配内存并初始化指针

void initQueue(Queue *queue, size_t initCapacity) {
    // 为循环数组分配内存
    queue->data = (element_t*)malloc(initCapacity * sizeof(element_t));
    queue->capacity = initCapacity; // 初始容量
    queue->length = 0; // 初始长度为0(空队列)
    queue->front = 0; // 队首指针初始化为0
    queue->rear = 0;  // 队尾指针初始化为0
}

(2)入队(Enqueue):队尾插入元素

入队前需判断队列是否已满(length == capacity),若未满则将元素插入rear位置,更新rear指针(rear = (rear + 1) % capacity)和队列长度。

bool enqueue(Queue *queue, element_t element) {
    if (queue->length == queue->capacity) {
        return false; // 队列已满,无法入队
    }
    queue->data[queue->rear] = element; // 元素插入队尾
    queue->rear = (queue->rear + 1) % queue->capacity; // 队尾指针循环后移
    queue->length++; // 长度+1
    return true;
}

(3)出队(Dequeue):队首删除元素

出队前需判断队列是否为空(length == 0),若非空则取出front位置的元素,更新front指针(front = (front + 1) % capacity)和队列长度。

bool dequeue(Queue *queue, element_t *deleted_element) {
    if (queue->length == 0) {
        return false; // 队列为空,无法出队
    }
    *deleted_element = queue->data[queue->front]; // 取出队首元素
    queue->front = (queue->front + 1) % queue->capacity; // 队首指针循环后移
    queue->length--; // 长度-1
    return true;
}

(4)遍历队列:按入队顺序输出元素

由于是循环数组,遍历需从front开始,通过(front + i) % capacity计算每个元素的实际下标,确保顺序正确。

void printQueue(Queue *queue) {
    if (queue->length == 0) {
        printf("队列为空\n");
        return;
    }
    printf("队列元素(队首→队尾):");
    for (size_t i = 0; i < queue->length; i++) {
        // 计算当前元素的下标(循环偏移)
        size_t index = (queue->front + i) % queue->capacity;
        printf("%d ", queue->data[index]);
    }
    printf("\n");
}

(5)销毁队列:释放内存

void destoryQueue(Queue *queue) {
    free(queue->data); // 释放循环数组内存
    queue->data = NULL; // 指针置空,避免野指针
    queue->length = 0;
    queue->capacity = 0;
    queue->front = 0;
    queue->rear = 0;
}

4.3 测试代码与运行结果

int main() {
    // 定义队列并初始化(初始容量6)
    Queue Q;
    initQueue(&Q, 6);

    // 入队操作(尝试入队8个元素,前6个成功,后2个失败)
    enqueue(&Q, 100);
    enqueue(&Q, 200);
    enqueue(&Q, 300);
    enqueue(&Q, 400);
    enqueue(&Q, 500);
    enqueue(&Q, 600);
    bool res1 = enqueue(&Q, 700); // 队列已满,返回false
    bool res2 = enqueue(&Q, 800); // 队列已满,返回false

    printf("前6个元素入队结果:%s\n", (res1 && res2) ? "全部成功" : "后2个失败");
    printQueue(&Q); // 输出:100 200 300 400 500 600
    printf("队列当前长度:%zu\n", getQueueLength(&Q)); // 输出:6

    // 出队操作(删除队首元素100)
    element_t del;
    dequeue(&Q, &del);
    printf("\n%d 出队\n", del); // 输出:100

    // 出队后再入队(此时队列有空闲位置,1100可成功入队)
    enqueue(&Q, 1100);
    printf("出队后入队1100,当前队列:\n");
    printQueue(&Q); // 输出:200 300 400 500 600 1100
    printf("队列当前长度:%zu\n", getQueueLength(&Q)); // 输出:6

    destoryQueue(&Q); // 销毁队列
    return 0;
}

4.4 队列的优缺点与应用场景

优点 缺点
操作高效(入队、出队时间复杂度O(1)) 不支持随机访问,仅能操作队首和队尾
循环数组实现避免元素移动,内存利用率高 固定容量,满队后无法继续入队(需手动扩容)
逻辑清晰,符合“排队”场景的直觉 若基于链表实现,每个节点需额外存储指针

典型应用场景

  • 任务调度(如操作系统的进程调度、打印机任务队列)
  • 消息队列(如分布式系统中服务间的异步通信)
  • 滑动窗口算法(如数组中“长度为k的最大子数组和”问题)
  • 缓冲区(如IO缓冲区,平衡数据生产和消费速度)

五、四种线性结构的核心对比

为了帮助大家快速选择合适的结构,下表总结了动态数组、链表、栈、队列的核心差异:

结构类型 存储方式 核心操作效率 随机访问 适用场景
动态数组 连续内存 插入/删除(O(n))、访问(O(1)) 支持(O(1)) 需频繁访问元素、较少插入删除(如数据存储、排序)
单向链表 离散内存 插入/删除(O(1),需找到前驱)、访问(O(n)) 不支持 需频繁插入删除、较少访问(如链表式队列、邻接表)
连续/离散 入栈/出栈(O(1)) 不支持(仅栈顶) 先进后出场景(如函数调用、括号匹配)
队列 连续/离散 入队/出队(O(1)) 不支持(仅队首/队尾) 先进先出场景(如任务调度、消息队列)
posted @ 2025-09-13 15:43  CreamyKitten  阅读(47)  评论(0)    收藏  举报