单链表的C语言实现详解

前言

大家好!今天我们来聊聊数据结构中的基础但非常重要的内容——单链表。我会用最详细的方式,从零开始教你如何用C语言实现一个单链表(无头结点、不循环)。即使你完全没有链表的概念,相信通过这篇博客也能完全理解!

什么是单链表?

单链表是一种常见的线性数据结构,它由一系列节点组成,每个节点包含两部分:

  1. 数据域 - 存储实际的数据

  2. 指针域 - 存储下一个节点的地址

这些节点通过指针连接起来,形成一个链式结构。单链表的特点是:

  • 不需要连续的内存空间

  • 插入和删除效率高(O(1)时间复杂度)

  • 访问效率相对较低(O(n)时间复杂度)

代码实现

头文件设计 (LinkedList.h)

首先我们创建头文件,定义数据结构和函数声明:

#ifndef __LINKEDLIST_H__
#define __LINKEDLIST_H__

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

// 定义数据类型,使用typedef可以方便地更改存储的数据类型
typedef int DataType;

// 定义链表节点结构体
typedef struct LinkedListNode {
    DataType data;              // 数据域
    struct LinkedListNode* next; // 指针域(指向下一个节点)
} LinkedListNode;

// 定义链表结构体(管理整个链表)
typedef struct LinkedList {
    LinkedListNode* head; // 指向链表的第一个节点
    size_t size;          // 记录链表中节点的数量
} LinkedList;

// 函数声明

// 初始化链表
void LinkedListInit(LinkedList* list);

// 创建新节点
LinkedListNode* BuyNode(DataType data);

// 在链表头部插入节点
void LinkedListPushFront(LinkedList* list, DataType data);

// 在链表尾部插入节点
void LinkedListPushBack(LinkedList* list, DataType data);

// 在指定位置插入节点
void LinkedListInsert(LinkedList* list, size_t pos, DataType data);

// 删除链表头部节点
void LinkedListPopFront(LinkedList* list);

// 删除链表尾部节点
void LinkedListPopBack(LinkedList* list);

// 删除指定位置节点
void LinkedListErase(LinkedList* list, size_t pos);

// 查找节点
LinkedListNode* LinkedListFind(LinkedList* list, DataType data);

// 获取链表长度
size_t LinkedListSize(const LinkedList* list);

// 检查链表是否为空
int LinkedListEmpty(const LinkedList* list);

// 清空链表
void LinkedListClear(LinkedList* list);

// 销毁链表
void LinkedListDestroy(LinkedList* list);

// 打印链表
void LinkedListPrint(const LinkedList* list);

#endif // __LINKEDLIST_H__

源文件实现 (LinkedList.c)

现在我们来逐一实现这些函数:

#include "LinkedList.h"

// 初始化链表
void LinkedListInit(LinkedList* list) {
    // 使用断言确保传入的指针有效
    assert(list != NULL);
    
    // 将头指针初始化为NULL,表示空链表
    list->head = NULL;
    list->size = 0;
}

// 创建新节点
LinkedListNode* BuyNode(DataType data) {
    // 为新节点申请内存空间
    LinkedListNode* newNode = (LinkedListNode*)malloc(sizeof(LinkedListNode));
    
    // 检查内存是否申请成功
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        exit(-1); // 分配失败则退出程序
    }
    
    // 初始化新节点的数据域和指针域
    newNode->data = data;
    newNode->next = NULL;
    
    return newNode;
}

// 在链表头部插入节点
void LinkedListPushFront(LinkedList* list, DataType data) {
    assert(list != NULL);
    
    // 创建新节点
    LinkedListNode* newNode = BuyNode(data);
    
    // 新节点指向原来的头节点
    newNode->next = list->head;
    
    // 更新头指针指向新节点
    list->head = newNode;
    
    // 链表节点数增加
    list->size++;
}

// 在链表尾部插入节点
void LinkedListPushBack(LinkedList* list, DataType data) {
    assert(list != NULL);
    
    // 创建新节点
    LinkedListNode* newNode = BuyNode(data);
    
    // 如果链表为空,新节点就是头节点
    if (list->head == NULL) {
        list->head = newNode;
    } else {
        // 找到最后一个节点
        LinkedListNode* cur = list->head;
        while (cur->next != NULL) {
            cur = cur->next;
        }
        
        // 将新节点连接到链表尾部
        cur->next = newNode;
    }
    
    // 链表节点数增加
    list->size++;
}

// 在指定位置插入节点
void LinkedListInsert(LinkedList* list, size_t pos, DataType data) {
    assert(list != NULL);
    
    // 检查位置是否有效
    if (pos > list->size) {
        printf("插入位置无效!\n");
        return;
    }
    
    // 如果插入位置是0,相当于头插
    if (pos == 0) {
        LinkedListPushFront(list, data);
        return;
    }
    
    // 如果插入位置是末尾,相当于尾插
    if (pos == list->size) {
        LinkedListPushBack(list, data);
        return;
    }
    
    // 创建新节点
    LinkedListNode* newNode = BuyNode(data);
    
    // 找到插入位置的前一个节点
    LinkedListNode* prev = list->head;
    for (size_t i = 0; i < pos - 1; i++) {
        prev = prev->next;
    }
    
    // 插入新节点
    newNode->next = prev->next;
    prev->next = newNode;
    
    // 链表节点数增加
    list->size++;
}

// 删除链表头部节点
void LinkedListPopFront(LinkedList* list) {
    assert(list != NULL);
    
    // 检查链表是否为空
    if (list->head == NULL) {
        printf("链表为空,无法删除!\n");
        return;
    }
    
    // 保存要删除的节点
    LinkedListNode* toDelete = list->head;
    
    // 更新头指针
    list->head = list->head->next;
    
    // 释放节点内存
    free(toDelete);
    
    // 链表节点数减少
    list->size--;
}

// 删除链表尾部节点
void LinkedListPopBack(LinkedList* list) {
    assert(list != NULL);
    
    // 检查链表是否为空
    if (list->head == NULL) {
        printf("链表为空,无法删除!\n");
        return;
    }
    
    // 如果只有一个节点
    if (list->head->next == NULL) {
        free(list->head);
        list->head = NULL;
    } else {
        // 找到倒数第二个节点
        LinkedListNode* prev = list->head;
        while (prev->next->next != NULL) {
            prev = prev->next;
        }
        
        // 释放最后一个节点
        free(prev->next);
        prev->next = NULL;
    }
    
    // 链表节点数减少
    list->size--;
}

// 删除指定位置节点
void LinkedListErase(LinkedList* list, size_t pos) {
    assert(list != NULL);
    
    // 检查位置是否有效
    if (pos >= list->size) {
        printf("删除位置无效!\n");
        return;
    }
    
    // 如果删除位置是0,相当于头删
    if (pos == 0) {
        LinkedListPopFront(list);
        return;
    }
    
    // 如果删除位置是末尾,相当于尾删
    if (pos == list->size - 1) {
        LinkedListPopBack(list);
        return;
    }
    
    // 找到要删除节点的前一个节点
    LinkedListNode* prev = list->head;
    for (size_t i = 0; i < pos - 1; i++) {
        prev = prev->next;
    }
    
    // 保存要删除的节点
    LinkedListNode* toDelete = prev->next;
    
    // 绕过要删除的节点
    prev->next = toDelete->next;
    
    // 释放节点内存
    free(toDelete);
    
    // 链表节点数减少
    list->size--;
}

// 查找节点
LinkedListNode* LinkedListFind(LinkedList* list, DataType data) {
    assert(list != NULL);
    
    // 遍历链表查找数据
    LinkedListNode* cur = list->head;
    while (cur != NULL) {
        if (cur->data == data) {
            return cur; // 找到返回节点指针
        }
        cur = cur->next;
    }
    
    return NULL; // 没找到返回NULL
}

// 获取链表长度
size_t LinkedListSize(const LinkedList* list) {
    assert(list != NULL);
    return list->size;
}

// 检查链表是否为空
int LinkedListEmpty(const LinkedList* list) {
    assert(list != NULL);
    return list->head == NULL;
}

// 清空链表
void LinkedListClear(LinkedList* list) {
    assert(list != NULL);
    
    // 逐个删除所有节点
    while (list->head != NULL) {
        LinkedListPopFront(list);
    }
    
    // 确保链表状态正确
    list->size = 0;
}

// 销毁链表
void LinkedListDestroy(LinkedList* list) {
    // 清空链表内容
    LinkedListClear(list);
    
    // 注意:这里不需要free(list),因为链表对象可能是在栈上分配的
    // 这个函数主要是为了语义上的完整性
}

// 打印链表
void LinkedListPrint(const LinkedList* list) {
    assert(list != NULL);
    
    printf("链表内容(长度=%zu): ", list->size);
    
    LinkedListNode* cur = list->head;
    while (cur != NULL) {
        printf("%d", cur->data);
        if (cur->next != NULL) {
            printf(" -> ");
        }
        cur = cur->next;
    }
    printf("\n");
}

测试代码 (test.c)

最后,我们编写一个测试程序来验证我们的链表实现:

#include "LinkedList.h"

void TestLinkedList() {
    // 创建链表
    LinkedList list;
    
    // 初始化链表
    LinkedListInit(&list);
    printf("初始化后: ");
    LinkedListPrint(&list);
    
    // 测试尾插法
    printf("\n=== 测试尾插法 ===\n");
    for (int i = 1; i <= 5; i++) {
        LinkedListPushBack(&list, i);
        printf("尾插 %d 后: ", i);
        LinkedListPrint(&list);
    }
    
    // 测试头插法
    printf("\n=== 测试头插法 ===\n");
    for (int i = 6; i <= 10; i++) {
        LinkedListPushFront(&list, i);
        printf("头插 %d 后: ", i);
        LinkedListPrint(&list);
    }
    
    // 测试指定位置插入
    printf("\n=== 测试指定位置插入 ===\n");
    LinkedListInsert(&list, 3, 99);
    printf("在位置3插入99后: ");
    LinkedListPrint(&list);
    
    LinkedListInsert(&list, 0, 88);
    printf("在位置0插入88后: ");
    LinkedListPrint(&list);
    
    // 测试查找
    printf("\n=== 测试查找 ===\n");
    LinkedListNode* found = LinkedListFind(&list, 99);
    if (found != NULL) {
        printf("找到节点99,它的下一个节点是: %d\n", found->next->data);
    } else {
        printf("未找到节点99\n");
    }
    
    // 测试删除
    printf("\n=== 测试删除 ===\n");
    LinkedListPopFront(&list);
    printf("删除头节点后: ");
    LinkedListPrint(&list);
    
    LinkedListPopBack(&list);
    printf("删除尾节点后: ");
    LinkedListPrint(&list);
    
    LinkedListErase(&list, 3);
    printf("删除位置3的节点后: ");
    LinkedListPrint(&list);
    
    // 测试清空
    printf("\n=== 测试清空 ===\n");
    LinkedListClear(&list);
    printf("清空链表后: ");
    LinkedListPrint(&list);
    
    // 测试空链表操作
    printf("\n=== 测试空链表操作 ===\n");
    LinkedListPopFront(&list); // 应该输出错误信息但不崩溃
    LinkedListPopBack(&list);  // 应该输出错误信息但不崩溃
}

int main() {
    TestLinkedList();
    return 0;
}

关键概念详解

1. 为什么使用typedef?

typedef int DataType;

使用typedef可以让我们轻松地更改链表存储的数据类型。如果以后想存储double类型的数据,只需要修改这一行:

typedef double DataType;

而不需要修改所有使用数据类型的地方。

2. 节点结构体的递归定义

struct LinkedListNode {
    DataType data;
    struct LinkedListNode* next; // 指向同类型结构体的指针
};

这里next指针指向的是另一个同类型的节点,这就是递归定义。它使得节点能够链接起来形成链式结构。

3. 为什么需要BuyNode函数?

BuyNode函数封装了节点创建的逻辑,使代码更加模块化和可维护。所有需要创建新节点的操作都可以调用这个函数,避免了代码重复。

4. 断言(assert)的作用

assert(list != NULL);

断言用于检查程序中的假设是否成立。如果条件不满足,程序会立即终止并输出错误信息。这帮助我们及早发现程序中的错误。

5. 内存管理

我们使用malloc动态分配内存,使用free释放内存。这是C语言中手动内存管理的典型应用。务必确保每个malloc都有对应的free,否则会导致内存泄漏。

常见问题解答

Q1: 什么是"无头结点"的单链表?

无头结点意味着链表的第一个节点直接存储数据,而不是一个不存储数据的辅助节点。我们的实现就是无头结点的。

Q2: 什么是"不循环"的单链表?

不循环意味着链表的最后一个节点的next指针指向NULL,而不是指向第一个节点形成循环。

Q3: 为什么需要记录链表大小(size)?

记录大小可以让我们在O(1)时间内获取链表长度,而不需要每次遍历整个链表来计算。

Q4: 头插法和尾插法有什么区别?

  • 头插法:新节点插入到链表头部,时间复杂度为O(1)

  • 尾插法:新节点插入到链表尾部,需要遍历到链表末尾,时间复杂度为O(n)

Q5: 如何选择使用头插法还是尾插法?

  • 如果需要保持插入顺序,使用尾插法

  • 如果不需要保持顺序或者想要逆序,使用头插法

  • 如果需要高效的插入操作,使用头插法

注意:在实现单链表各个功能时,建议采用写一测一,而不是一股脑写完再统一测试,当每个函数功能实现正常时,就可以整体调用尝试了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值