【C语言】线性表之顺序表、单链表、双向链表详解及实现

🎬 博主名称键盘敲碎了雾霭
🔥 个人专栏 : 《C语言》《数据结构》

⛺️指尖敲代码,雾霭皆可破


文章目录

一、线性表概述

线性表 (Linear List)是最基本、最简单的一种数据结构。一个线性表是n个数据元素的有限序列,它们之间的关系构成一个线性序列。常见的线性表有顺序表、链表等。

线性表具有以下特点:

  • 存在唯一的第一个元素和最后一个元素;
  • 除第一个元素外,每个元素有且仅有一个直接前驱;
  • 除最后一个元素外,每个元素有且仅有一个直接后继。

根据存储方式的不同,线性表可以分为顺序存储结构 (顺序表)和链式存储结构(链表)。本文将详细介绍这两种结构,并使用C语言实现它们的基本操作。


二、顺序表

顺序表是用一段连续的存储单元依次存储线性表的数据元素。在C语言中通常使用数组实现。根据数组的分配方式,顺序表可分为静态顺序表 (定长数组)和动态顺序表(动态开辟的数组)。动态顺序表更灵活,本文将重点介绍动态顺序表的实现。

2.1 结构定义

c 复制代码
typedef int SLDataType;  // 可根据需要修改元素类型

typedef struct SeqList {
    SLDataType* arr;      // 指向动态开辟的数组
    int size;             // 当前有效元素个数
    int capacity;         // 当前容量
} SL;

2.2 基本操作

初始化
c 复制代码
void SeqListInit(SL* ps) {
    ps->arr = NULL;
    ps->size = ps->capacity = 0;
}
容量检查与扩容
c 复制代码
void CheckCapacity(SL* ps) {
    if (ps->size == ps->capacity) {
        int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
        SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
        if (tmp == NULL) {
            perror("realloc");
            return;
        }
        ps->arr = tmp;
        ps->capacity = newCapacity;
    }
}
尾插
c 复制代码
void SeqListPushBack(SL* ps, SLDataType x) {
    assert(ps);
    CheckCapacity(ps);
    ps->arr[ps->size++] = x;
}
头插
c 复制代码
void SeqListPushFront(SL* ps, SLDataType x) {
    assert(ps);
    CheckCapacity(ps);
    // 将所有元素后移一位
    for (int i = ps->size; i > 0; --i) {
        ps->arr[i] = ps->arr[i - 1];
    }
    ps->arr[0] = x;
    ps->size++;
}
尾删
c 复制代码
void SeqListPopBack(SL* ps) {
    assert(ps && ps->size > 0);
    ps->size--;
}
头删
c 复制代码
void SeqListPopFront(SL* ps) {
    assert(ps && ps->size > 0);
    for (int i = 0; i < ps->size - 1; ++i) {
        ps->arr[i] = ps->arr[i + 1];
    }
    ps->size--;
}
指定位置插入
c 复制代码
void SeqListInsert(SL* ps, int pos, SLDataType x) {
    assert(ps);
    assert(pos >= 0 && pos <= ps->size);
    CheckCapacity(ps);
    for (int i = ps->size; i > pos; --i) {
        ps->arr[i] = ps->arr[i - 1];
    }
    ps->arr[pos] = x;
    ps->size++;
}
指定位置删除
c 复制代码
void SeqListErase(SL* ps, int pos) {
    assert(ps);
    assert(pos >= 0 && pos < ps->size);
    for (int i = pos; i < ps->size - 1; ++i) {
        ps->arr[i] = ps->arr[i + 1];
    }
    ps->size--;
}
查找
c 复制代码
int SeqListFind(SL* ps, SLDataType x) {
    assert(ps);
    for (int i = 0; i < ps->size; ++i) {
        if (ps->arr[i] == x)
            return i;
    }
    return -1;
}
修改
c 复制代码
void SeqListModify(SL* ps, int pos, SLDataType x) {
    assert(ps && pos >= 0 && pos < ps->size);
    ps->arr[pos] = x;
}
打印
c 复制代码
void SeqListPrint(SL* ps) {
    for (int i = 0; i < ps->size; ++i) {
        printf("%d ", ps->arr[i]);
    }
    printf("\n");
}
销毁
c 复制代码
void SeqListDestroy(SL* ps) {
    if (ps->arr) {
        free(ps->arr);
        ps->arr = NULL;
    }
    ps->size = ps->capacity = 0;
}

三、链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的。每个节点包含数据域和指针域。

链表有多种形式:单链表双向链表循环链表 等。本文将分别实现不带头结点的单链表带头结点的双向循环链表

3.1 单链表

结构定义
c 复制代码
typedef int SLTDataType;

typedef struct SListNode {
    SLTDataType data;
    struct SListNode* next;
} SLNode;
基本操作
创建新节点
c 复制代码
SLNode* BuySListNode(SLTDataType x) {
    SLNode* newNode = (SLNode*)malloc(sizeof(SLNode));
    if (newNode == NULL) {
        perror("malloc");
        exit(1);
    }
    newNode->data = x;
    newNode->next = NULL;
    return newNode;
}
尾插
c 复制代码
void SListPushBack(SLNode** pphead, SLTDataType x) {
    assert(pphead);
    SLNode* newNode = BuySListNode(x);
    if (*pphead == NULL) {
        *pphead = newNode;
    } else {
        SLNode* tail = *pphead;
        while (tail->next) {
            tail = tail->next;
        }
        tail->next = newNode;
    }
}
头插
c 复制代码
void SListPushFront(SLNode** pphead, SLTDataType x) {
    assert(pphead);
    SLNode* newNode = BuySListNode(x);
    newNode->next = *pphead;
    *pphead = newNode;
}
尾删
c 复制代码
void SListPopBack(SLNode** pphead) {
    assert(pphead && *pphead);
    if ((*pphead)->next == NULL) {
        free(*pphead);
        *pphead = NULL;
    } else {
        SLNode* prev = NULL;
        SLNode* tail = *pphead;
        while (tail->next) {
            prev = tail;
            tail = tail->next;
        }
        free(tail);
        prev->next = NULL;
    }
}
头删
c 复制代码
void SListPopFront(SLNode** pphead) {
    assert(pphead && *pphead);
    SLNode* next = (*pphead)->next;
    free(*pphead);
    *pphead = next;
}
查找
c 复制代码
SLNode* SListFind(SLNode** pphead, SLTDataType x) {
    assert(pphead && *pphead);
    SLNode* cur = *pphead;
    while (cur) {
        if (cur->data == x)
            return cur;
        cur = cur->next;
    }
    return NULL;
}
在指定位置之前插入
c 复制代码
void SListInsertBefore(SLNode** pphead, SLNode* pos, SLTDataType x) {
    assert(pphead && pos);
    if (*pphead == pos) {
        SListPushFront(pphead, x);
    } else {
        SLNode* prev = *pphead;
        while (prev->next != pos) {
            prev = prev->next;
        }
        SLNode* newNode = BuySListNode(x);
        newNode->next = pos;
        prev->next = newNode;
    }
}
在指定位置之后插入
c 复制代码
void SListInsertAfter(SLNode* pos, SLTDataType x) {
    assert(pos);
    SLNode* newNode = BuySListNode(x);
    newNode->next = pos->next;
    pos->next = newNode;
}
删除指定位置节点
c 复制代码
void SListErase(SLNode** pphead, SLNode* pos) {
    assert(pphead && pos);
    if (*pphead == pos) {
        SListPopFront(pphead);
    } else {
        SLNode* prev = *pphead;
        while (prev->next != pos) {
            prev = prev->next;
        }
        prev->next = pos->next;
        free(pos);
    }
}
销毁链表
c 复制代码
void SListDestroy(SLNode** pphead) {
    assert(pphead);
    SLNode* cur = *pphead;
    while (cur) {
        SLNode* next = cur->next;
        free(cur);
        cur = next;
    }
    *pphead = NULL;
}
打印
c 复制代码
void SListPrint(SLNode* phead) {
    SLNode* cur = phead;
    while (cur) {
        printf("%d->", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}

3.2 带头结点的双向循环链表

带头结点的双向循环链表每个节点有prenext两个指针,并且头结点的pre指向最后一个节点,最后一个节点的next指向头结点,形成一个环。带头结点使得操作更加统一,不需要单独处理空表情况。

结构定义
c 复制代码
typedef int LTDataType;

typedef struct ListNode {
    struct ListNode* pre;
    struct ListNode* next;
    LTDataType data;
} LTNode;
基本操作
创建新节点
c 复制代码
LTNode* BuyListNode(LTDataType x) {
    LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
    if (newNode == NULL) {
        perror("malloc");
        exit(1);
    }
    newNode->data = x;
    newNode->pre = newNode->next = newNode;  // 初始指向自己
    return newNode;
}
初始化(创建头结点)
c 复制代码
LTNode* ListInit() {
    return BuyListNode(-1);  // 头结点不存储有效数据
}
尾插
c 复制代码
void ListPushBack(LTNode* phead, LTDataType x) {
    assert(phead);
    LTNode* newNode = BuyListNode(x);
    LTNode* tail = phead->pre;  // 尾节点

    newNode->pre = tail;
    newNode->next = phead;
    tail->next = newNode;
    phead->pre = newNode;
}
头插
c 复制代码
void ListPushFront(LTNode* phead, LTDataType x) {
    assert(phead);
    LTNode* newNode = BuyListNode(x);
    LTNode* first = phead->next;

    newNode->pre = phead;
    newNode->next = first;
    phead->next = newNode;
    first->pre = newNode;
}
尾删
c 复制代码
void ListPopBack(LTNode* phead) {
    assert(phead && phead->next != phead);  // 不能为空表
    LTNode* tail = phead->pre;
    tail->pre->next = phead;
    phead->pre = tail->pre;
    free(tail);
}
头删
c 复制代码
void ListPopFront(LTNode* phead) {
    assert(phead && phead->next != phead);
    LTNode* first = phead->next;
    first->next->pre = phead;
    phead->next = first->next;
    free(first);
}
查找
c 复制代码
LTNode* ListFind(LTNode* phead, LTDataType x) {
    assert(phead);
    LTNode* cur = phead->next;
    while (cur != phead) {
        if (cur->data == x)
            return cur;
        cur = cur->next;
    }
    return NULL;
}
在指定位置之前插入
c 复制代码
void ListInsertBefore(LTNode* pos, LTDataType x) {
    assert(pos);
    LTNode* newNode = BuyListNode(x);
    newNode->next = pos;
    newNode->pre = pos->pre;
    pos->pre->next = newNode;
    pos->pre = newNode;
}
删除指定位置节点
c 复制代码
void ListErase(LTNode* pos) {
    assert(pos);
    pos->pre->next = pos->next;
    pos->next->pre = pos->pre;
    free(pos);
}
销毁链表
c 复制代码
void ListDestroy(LTNode* phead) {
    assert(phead);
    LTNode* cur = phead->next;
    while (cur != phead) {
        LTNode* next = cur->next;
        free(cur);
        cur = next;
    }
    free(phead);
}
打印
c 复制代码
void ListPrint(LTNode* phead) {
    assert(phead);
    LTNode* cur = phead->next;
    while (cur != phead) {
        printf("%d->", cur->data);
        cur = cur->next;
    }
    printf("\n");
}

四、顺序表与链表的比较

特性 顺序表 链表
存储空间 连续,预分配或动态扩展 非连续,按需分配
访问元素 随机访问,时间复杂度 O(1) 顺序访问,时间复杂度 O(n)
插入/删除 平均 O(n)(需移动大量元素) 已知位置时 O(1)(仅修改指针)
空间利用率 无额外指针开销,但可能有空间浪费 有指针开销,但无闲置空间
缓存友好性 高(连续内存,利用CPU缓存) 低(节点分散,缓存命中率低)
适用场景 频繁访问、较少插入删除 频繁插入删除、元素个数不确定

五、总结

线性表是数据结构的基础,顺序表和链表各有优劣。顺序表适合静态数据、频繁随机访问的场景;链表适合动态变化、频繁插入删除的场景。在实际开发中,应根据具体需求选择合适的结构。

本文通过C语言完整实现了顺序表、单链表和双向循环链表的核心操作,希望读者能通过代码加深对线性表的理解,并能够灵活运用。

(注:本文代码均在VS环境下测试通过,使用C语言标准库函数。)

相关推荐
m0_531237173 小时前
C语言-分支与循环语句练习2
c语言·开发语言·算法
Once_day3 小时前
GCC编译(3)常见编译选项
c语言·c++·编译和链接
程序员酥皮蛋4 小时前
hot 100 第三十三 33.排序链表
数据结构·算法·链表
爱编码的小八嘎4 小时前
第3章 Windows运行机理-3.1 内核分析(3)
c语言
嵌入式×边缘AI:打怪升级日志4 小时前
9.2.1 分析 Write File Record 功能(保姆级讲解)
java·开发语言·网络
天荒地老笑话么4 小时前
Bridged 与虚拟机扫描:合规边界与自测范围说明
网络·网络安全
祈安_4 小时前
深入理解指针(三)
c语言·后端
m0_531237174 小时前
C语言-函数练习2
c语言·开发语言
fly的fly5 小时前
RT-Thread消息队列源码机制讲解
c语言·stm32·物联网