线性表Linear List:一种最基本/最常用/结构简单的数据结构

线性表

线性表(Linear List)是一种最基本、最常用且结构简单的数据结构。

它是由 n(n≥0)个数据元素组成的有限序列。在线性表中,除了第一个元素没有前驱,最后一个元素没有后继之外,每个元素都有一个且只有一个直接前驱和一个直接后继。

线性表在 C 语言中有两种主要的实现方式:

  1. 顺序存储结构(Sequential Storage Structure):通常使用数组实现。
  2. 链式存储结构(Chained Storage Structure):通常使用指针和结构体实现,即链表。

1. 顺序存储结构(Sequential Storage Structure)

概念:
顺序存储的线性表是将数据元素存放在一组地址连续的存储单元中,其逻辑次序与物理次序是一致的。就像排队一样,每个人挨着一个人站好。

C 语言实现:
在 C 语言中,通常使用 数组 来实现顺序存储的线性表。为了使其更具通用性,我们通常会结合结构体来封装数组、当前长度以及最大容量。

#include <stdio.h>
#include <stdlib.h> // 用于 malloc, free

// 假设我们存储整型数据
#define MAX_SIZE 100 // 线性表的最大容量

typedef struct {
    int* data;      // 存储元素的数组指针,可以动态分配
    int length;     // 当前线性表的长度(即元素个数)
    int capacity;   // 线性表的最大容量
} SqList;           // SqList 是结构体类型名

// --- 常见操作示例 (仅定义,不实现具体逻辑) ---

// 初始化顺序表
void InitList_Sq(SqList* L) {
    L->data = (int*)malloc(sizeof(int) * MAX_SIZE); // 动态分配内存
    if (!L->data) {
        exit(EXIT_FAILURE); // 内存分配失败
    }
    L->length = 0;
    L->capacity = MAX_SIZE;
}

// 插入元素
int ListInsert_Sq(SqList* L, int i, int e) {
    // 假设 i 是有效索引 (1 <= i <= L->length + 1)
    // 假设 L->length < L->capacity
    // 逻辑:将 i 之后的元素后移,然后插入 e
    // ...
    return 1; // 成功
}

// 删除元素
int ListDelete_Sq(SqList* L, int i, int* e) {
    // 假设 i 是有效索引 (1 <= i <= L->length)
    // 逻辑:取出要删除的元素 e,将 i 之后的元素前移
    // ...
    return 1; // 成功
}

// 查找元素
int GetElem_Sq(SqList L, int i, int* e) {
    // 假设 i 是有效索引 (1 <= i <= L->length)
    // 逻辑:直接访问 L.data[i-1]
    // ...
    return 1; // 成功
}

// 销毁线性表 (释放内存)
void DestroyList_Sq(SqList* L) {
    free(L->data);
    L->data = NULL;
    L->length = 0;
    L->capacity = 0;
}

特点:

  • 优点:
    • 存取速度快: 可以通过下标以 O(1) 的时间复杂度随机访问任何元素。
    • 存储密度大: 数据元素本身紧密存储,没有额外的空间开销。
    • 内存利用率高: 在已知最大容量的情况下,内存是连续分配的。
  • 缺点:
    • 插入和删除效率低: 在表的中间进行插入或删除操作时,需要移动大量的元素,平均时间复杂度为 O(n)。
    • 容量固定或需要扩容: 数组的大小在定义时需要确定,如果空间不够需要重新分配更大的内存空间并复制所有元素(动态数组),这会带来额外开销。
    • 容易造成存储空间的 碎片化浪费 (如果预设容量过大)。

2. 链式存储结构(Chained Storage Structure)

概念:
链式存储的线性表(通常称为链表)通过一组任意的存储单元来存储线性表中的数据元素。每个数据元素除了存储其本身的信息外,还额外存储一个指向其直接后继的指针。这些存储单元可以不连续,因此逻辑上相邻的元素在物理存储上不一定相邻。

C 语言实现:
在 C 语言中,通常使用 结构体 来定义链表的节点,每个节点包含数据域和指向下一个节点的指针域。

#include <stdio.h>
#include <stdlib.h> // 用于 malloc, free

// 定义链表节点结构
typedef struct LNode {
    int data;           // 数据域,存储元素本身
    struct LNode* next;  // 指针域,指向下一个节点
} LNode, *LinkList;     // LNode是节点类型,LinkList是指向节点的指针类型(常用于表示头指针)

// --- 常见操作示例 (仅定义,不实现具体逻辑) ---

// 初始化链表 (创建一个空的头结点)
void InitList_L(LinkList* L) {
    *L = (LNode*)malloc(sizeof(LNode)); // 创建头结点
    if (!(*L)) {
        exit(EXIT_FAILURE);
    }
    (*L)->next = NULL; // 头结点的next指向NULL表示空链表
}

// 插入元素 (在第 i 个位置插入元素 e)
int ListInsert_L(LinkList L, int i, int e) {
    // 逻辑:找到第 i-1 个节点,创建新节点,修改指针指向
    // ...
    return 1; // 成功
}

// 删除元素 (删除第 i 个位置的元素,并将其值返回给 e)
int ListDelete_L(LinkList L, int i, int* e) {
    // 逻辑:找到第 i-1 个节点和第 i 个节点,保存第 i 个节点数据,修改指针,释放第 i 个节点
    // ...
    return 1; // 成功
}

// 查找元素 (获取第 i 个位置的元素值)
int GetElem_L(LinkList L, int i, int* e) {
    // 逻辑:从头开始遍历,找到第 i 个节点
    // ...
    return 1; // 成功
}

// 销毁链表 (释放所有节点内存)
void DestroyList_L(LinkList* L) {
    LNode* p = *L;
    LNode* q;
    while (p) {
        q = p->next;
        free(p);
        p = q;
    }
    *L = NULL;
}

特点:

  • 优点:
    • 插入和删除效率高: 在知道插入或删除位置的前一个节点时,时间复杂度为 O(1)。在表的任何位置进行插入和删除都只需要修改少量指针,不需要移动大量元素。
    • 容量灵活: 不需要预先分配存储空间,链表的长度可以根据需要动态增长或缩短,只要内存允许即可。
    • 没有存储溢出的问题(除非系统内存耗尽)。
  • 缺点:
    • 存取速度慢: 访问任意元素需要从表头开始遍历,时间复杂度为 O(n),不能随机存取。
    • 存储密度低: 每个节点都需要额外的空间来存储指针域,导致存储密度比顺序表低。
    • 实现相对复杂: 涉及指针操作,容易出错(如内存泄漏、野指针)。

3. 两种存储结构的比较与选择

特性顺序存储结构(数组)链式存储结构(链表)
存储方式连续的内存单元离散的内存单元,通过指针连接
逻辑顺序与物理顺序一致与物理顺序可以不一致
存取效率O(1) 随机访问,通过下标直接存取O(n) 只能顺序访问,需从头遍历
插入/删除O(n) 平均需要移动大量元素O(1)(已知前驱节点)或 O(n)(需查找)
空间利用存储密度大,但可能浪费或不足存储密度小(额外指针开销),但空间利用更灵活
容量固定或动态扩容(有开销)动态可变
实现难度相对简单相对复杂,涉及指针操作

选择建议:

  • 如果线性表的长度相对固定,或者对随机访问效率要求很高,且插入/删除操作不频繁,顺序存储结构(数组)是更好的选择。
  • 如果线性表的长度变化较大,插入/删除操作频繁,且对随机访问效率要求不高,链式存储结构(链表)是更好的选择。

4. 链表的变种

除了上面介绍的 单链表,链表还有几种常见的变种:

  • 双向链表(Doubly Linked List):每个节点除了指向后继的指针 next 外,还增加一个指向前驱的指针 prev。这使得在知道某个节点后,可以 O(1) 地找到其前驱和后继,方便双向遍历和删除操作。

    typedef struct DNode {
        int data;
        struct DNode* prev; // 指向前驱
        struct DNode* next; // 指向后继
    } DNode, *DuLinkList;
    
  • 循环链表(Circular Linked List):最后一个节点的 next 指针指向头结点(或第一个节点),形成一个环。这使得从任何一个节点开始都可以遍历整个链表,常用于实现循环缓冲区或约瑟夫环等问题。

    • 单向循环链表:尾节点的 next 指向头结点。
    • 双向循环链表:尾节点的 next 指向头结点,头结点的 prev 指向尾节点。

理解线性表的这两种基本实现方式及其优缺点,是学习更复杂数据结构(如栈、队列、树、图等)的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值