当前位置:首页 > 技术 > 正文内容

C语言单链表实现原理与操作详解

访客 技术 2026年8月3日 1

链表基本概念

链式存储是线性表的一种重要实现方式,与顺序表不同,它通过指针将分散的内存单元串联起来。每个存储单元称为结点,包含两部分:存储实际数据的数据域和记录后继位置的指针域

单链表的结点结构如下:

typedef int DataType;

// 链表结点定义
typedef struct ListNode {
    DataType value;           // 数据域
    struct ListNode* link;    // 指针域,指向下一个结点
} ListNode;

核心术语

  • 头指针:指向链表首结点的指针变量
  • 头结点:附加在表头的空结点,统一操作边界
  • 尾结点:最后一个有效结点,其指针域为空
  • 空表:头指针指向头结点且头结点的指针域为空

链表初始化

创建带头结点的空链表:

ListNode* createList(void)
{
    ListNode* header = (ListNode*)malloc(sizeof(ListNode));
    if (header == NULL) {
        return NULL;  // 内存分配失败
    }
    header->value = 0;   // 头结点数据域可存储长度等元信息
    header->link = NULL;
    return header;
}

遍历输出

从头结点后第一个有效结点开始访问:

void displayList(ListNode* L)
{
    ListNode* current = L->link;  // 跳过头结点
    while (current != NULL) {
        printf("%d ", current->value);
        current = current->link;
    }
    printf("\n");
}

插入操作

前插法(头插)

新结点插入到首元素之前,时间复杂度 O(1):

int pushFront(ListNode* L, DataType item)
{
    ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
    if (newNode == NULL) return 0;
    
    newNode->value = item;
    newNode->link = L->link;  // 新结点指向原首结点
    L->link = newNode;        // 头结点指向新结点
    return 1;
}

前插法会使数据呈现逆序特性,最后插入的元素位于链表最前端。

后插法(尾插)

新结点追加到链表末尾,需先定位尾结点:

// 辅助函数:获取尾结点
ListNode* getLastNode(ListNode* L)
{
    ListNode* p = L;
    while (p->link != NULL) {
        p = p->link;
    }
    return p;
}

// 尾插实现
int pushBack(ListNode* L, DataType item)
{
    ListNode* tail = getLastNode(L);
    ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
    if (newNode == NULL) return 0;
    
    newNode->value = item;
    newNode->link = NULL;
    tail->link = newNode;  // 原尾结点指向新结点
    return 1;
}

指定位置插入

在第 pos 个位置(从1开始计数)插入元素:

int insertAt(ListNode* L, int pos, DataType item)
{
    ListNode* prev = L;  // 定位到第pos-1个结点
    
    for (int i = 1; i < pos; i++) {
        prev = prev->link;
        if (prev == NULL) return 0;  // pos超出范围
    }
    
    ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
    newNode->value = item;
    newNode->link = prev->link;  // ① 新结点连接后继
    prev->link = newNode;        // ② 前驱结点连接新结点
    return 1;
}

删除操作

删除第 pos 个位置的结点,需先找到其前驱:

int eraseAt(ListNode* L, int pos)
{
    ListNode* prev = L;
    
    // 定位前驱结点
    for (int i = 1; i < pos; i++) {
        prev = prev->link;
        if (prev == NULL) return 0;
    }
    
    ListNode* target = prev->link;  // 待删除结点
    if (target == NULL) {
        printf("删除位置无效\n");
        return 0;
    }
    
    prev->link = target->link;  // 绕过待删除结点
    free(target);               // 释放内存
    return 1;
}

查找操作

按值查找

ListNode* searchByValue(ListNode* L, DataType key)
{
    ListNode* p = L->link;
    while (p != NULL) {
        if (p->value == key) {
            return p;  // 返回结点地址
        }
        p = p->link;
    }
    return NULL;  // 未找到
}

求表长

int getLength(ListNode* L)
{
    int count = 0;
    ListNode* p = L->link;  // 从首元结点开始
    while (p != NULL) {
        count++;
        p = p->link;
    }
    return count;
}

销毁链表

释放所有结点内存,防止内存泄漏:

void destroyList(ListNode* L)
{
    ListNode* p = L->link;
    ListNode* temp;
    
    while (p != NULL) {
        temp = p->link;  // 保存下一个结点
        free(p);         // 释放当前结点
        p = temp;
    }
    L->link = NULL;      // 头结点指针域置空
}

完整测试示例

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

typedef int DataType;
typedef struct ListNode {
    DataType value;
    struct ListNode* link;
} ListNode;

// 函数声明(上述实现省略)

int main()
{
    ListNode* myList = createList();
    
    // 尾插建立有序链表
    pushBack(myList, 10);
    pushBack(myList, 20);
    pushBack(myList, 30);
    printf("初始链表:");
    displayList(myList);
    
    // 中间插入
    insertAt(myList, 2, 15);
    printf("插入15后:");
    displayList(myList);
    
    // 删除操作
    eraseAt(myList, 2);
    printf("删除后:");
    displayList(myList);
    
    // 查找验证
    ListNode* found = searchByValue(myList, 20);
    if (found) {
        printf("找到元素:%d\n", found->value);
    }
    
    printf("当前长度:%d\n", getLength(myList));
    
    destroyList(myList);
    return 0;
}

存储结构对比

特性单链表顺序表
存储空间动态分配,不连续预分配,连续
访问方式顺序访问 O(n)随机访问 O(1)
插入删除修改指针 O(1)移动元素 O(n)
内存开销额外指针域可能预留空间浪费
适用场景频繁增删,数据规模不定频繁查询,数据规模固定

链表变体

循环链表

尾结点的指针域指向头结点而非空,形成环状结构。判空条件变为 header->link == header,可从任意位置开始遍历整个链表。

双向链表

结点增加前驱指针,支持双向遍历:

typedef struct DListNode {
    DataType value;
    struct DListNode* prior;
    struct DListNode* next;
} DListNode;

双向链表删除结点无需遍历找前驱,直接通过 p->prior->next = p->next 即可完成,但空间开销增加。

返回列表

上一篇:RK3399平台Android 9.0出厂重置机制解析

没有最新的文章了...

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。