数据结构入门 (一):线性表的基石 —— 顺序表详解

目录

  • 一、基本概念与特性
    • 什么是线性表?
      • 1.线性表的定义
      • 2.线性表的特征
      • 3.核心概念总结
  • 二、线性表的顺序存储
    • 1.从逻辑结构到物理存储
    • 2. 顺序表的核心结构
    • 3. 内存分配策略对比
      • 静态分配实现
      • 动态分配实现
  • 三、核心机制:动态内存管理与扩容策略
    • 1. 为什么必须是堆内存?
    • 2. 扩容策略:性能与空间的平衡艺术
  • 四、顺序表操作实现(C语言为例)
    • 1. 定义表头
    • 2. 创建顺序表
    • 3. 销毁顺序表
    • 4. 查找元素
    • 5. 删除元素
    • 6. 顺序表扩容
    • 7. 从表尾插入元素
    • 8. 在指定位置插入元素
    • 9. 查看顺序表
  • 五、顺序表操作性能分析
  • 六、优缺点总结
  • 七、顺序表的适用场景
  • 八、总结:顺序表的核心思想

一、基本概念与特性

什么是线性表?

1.线性表的定义

线性表是由 n (n ≥ 0) 个具有相同数据类型的元素组成的有限序列。当 n = 0 时,称为空表。

2.线性表的特征

线性表满足以下几个特征:

  1. 元素同质:

    • 表中的所有元素都属于同一种数据类型。这意味着它们具有相同的含义和存储需求(例如,都是整数、都是学生记录、都是字符等)。
  2. 有限集合:

    • 元素的数量 n有限的。这个 n 被称为线性表的长度
  3. 严格有序:

    • 元素之间存在着严格的顺序关系,它们在逻辑上排列成一条“线”。
    • 存在唯一的第一个元素(称为表头元素首元)。
    • 存在唯一的最后一个元素(称为表尾元素)。
    • 除表头元素外,每个元素都有且仅有一个直接前驱元素(紧挨在它前面的元素)。
    • 除表尾元素外,每个元素都有且仅有一个直接后继元素(紧挨在它后面的元素)。
  4. 一对一关系:

    • 元素之间的逻辑关系是线性的、一对一的,就像一串珠子,每个珠子只和它前后的一个珠子相连。

3.核心概念总结

  • 逻辑结构: 线性表定义的是元素之间前驱和后继逻辑关系
  • 独立于物理存储: 这个定义只关心元素的逻辑顺序,不关心它们在计算机内存中具体如何存放(是用连续的数组?还是用指针链接的链表?这是后续要讨论的“存储结构”)。
  • 基础性: 它是理解栈、队列、字符串、数组等更复杂数据结构的基础。

二、线性表的顺序存储

1.从逻辑结构到物理存储

我们知道,线性表定义了数据元素之间严格的前驱后继关系(一对一)。那么如何在计算机内存中具体存放这些元素呢?这就涉及到“顺序存储结构”。

顺序表的本质定义

  • 使用连续的内存空间来存储线性表中的所有元素。
  • 通过物理位置的相邻性来体现逻辑上的“前驱-后继”关系。

2. 顺序表的核心结构

顺序表的核心在于如何管理连续的内存空间,这通常需要封装一个表头结构体/类来管理连续存储空间和当前状态:

typedef int ElementType;  // 为int型变量定义一个新的别名ElementType,便于后期修改

typedef struct {
    ElementType *data;   // 指向动态分配连续存储空间首地址的指针(关键!在堆上)
    int length;          // 当前线性表中实际存储的元素个数(有效长度)
    int capacity;        // 当前分配的存储空间总容量(最多可容纳元素个数)
} SeqList; 

表头成员解析

  • data (核心指针):指向在上动态申请的一块连续内存空间的首地址,用于存储实际数据。
  • length (当前长度):记录当前线性表中实际包含的有效元素个数。
  • capacity (当前容量):记录 data 指针指向的连续空间最多能容纳的元素个数。

3. 内存分配策略对比

策略类型 特点 适用场景 关键问题
静态分配 编译时固定大小 元素数量已知且不变 空间浪费或溢出风险
动态分配 运行时按需申请/释放 元素数量变化频繁 需实现扩容/缩容机制

静态分配实现

#define MAX_SIZE 100  // 硬编码容量
typedef struct {
    ElementType data[MAX_SIZE];  // 通常存储于栈/数据区空间
    int length;  // 当前元素数量
} StaticList;
数据结构入门 (一):线性表的基石 —— 顺序表详解_第1张图片

动态分配实现

typedef struct {
    ElementType *data;   // 指向堆内存的指针,可以在栈/堆
    int length;          // 当前元素数量
    int capacity;        // 当前总容量
} DynamicSeqList;

数据结构入门 (一):线性表的基石 —— 顺序表详解_第2张图片

三、核心机制:动态内存管理与扩容策略

1. 为什么必须是堆内存?

内存区域 特点 是否适合顺序表
栈空间 自动生命周期、大小固定、不具备动态性 不适合
数据区 全局固定不可变 不适合
堆内存 动态申请/释放,支持运行时扩容 必须使用
  • 结论: data 指针指向的空间必须位于堆上。表头结构本身可以在栈或堆上,但其核心职责是管理堆上的那片连续数据区。

2. 扩容策略:性能与空间的平衡艺术

  • 扩容触发条件:length == capacity

  • 扩容核心步骤:

    1. 申请新空间
    2. 数据拷贝迁移
    3. 释放旧空间
    4. 更新容量
  • 扩容策略对比

策略 扩容公式 插入n元素总复制次数 均摊时间复杂度 空间利用率
固定增量 new = old + C O(n²) O(n) 95%~100%
倍增(推荐) new = old * 2 O(n) O(1) 50%~100%
1.5倍扩容 new = old * 3/2 O(n) O(1) 66%~100%

四、顺序表操作实现(C语言为例)

1. 定义表头

typedef int Element_t;  

typedef struct {
    Element_t *data;   //  存储表中数据空间的首地址
    int pos;          //  待插入的位置,也表示空间里数据的个数
    int capacity;        //  最大存储容量
} SeqTable_t;

2. 创建顺序表

SeqTable_t* createSeqTable(int n) {  // n表示顺序表的初始容量
	SeqTable_t* table = NULL;  // 防止指向不确定内存区域
	
	// 1.分配表头结构体
	table = malloc(sizeof(*table));  // 等价于sizeof(SeqTable_t),为结构体指针分配内存空间
	// 条件判断:表头
	if (table == NULL) {
		fprintf(stderr,"表头创建失败!\n");  
		return NULL;
	}
	
	// 2.分配数据空间
	table->data = malloc(sizeof(Element_t)* n);  // 为数据区分配内存空间
	// 条件判断:数据区分配
	if (table->data == NULL) {
		fprintf(stderr,"数据空间分配失败!\n");  
		free(table);  // 谨记,否则会造成内存泄漏
		return NULL;
	}

	// 3.初始化状态
	table->pos = 0;  // 初始化当前存储个数为0,表示是空表
	table->capacity = n;  // 初始化最大存储容量
}

3. 销毁顺序表

void destroySeqTable(SeqTable_t* table) {
	// 1.先释放空间!
	if (table && table->data) {
			free(table->data);
			table->data = NULL; // 避免悬空指针
		}
	}
	// 2.再释放表头
	free(table);
}

4. 查找元素

int findInSeqTable(SeqTable_t* table, Element_t value) {
	for (int i = 0; i < table->pos; ++i) {
		if (table->data[i] == value) {
			return i;
		}
	}
	return -1;
}

5. 删除元素

int deleteInSeqTable(SeqTable_t* table, int value) {
	// 1.查找value在数据结构中的索引位置
	int index = findInSeqTable(table, value);
	if (index = -1) {
		printf("没有找到第%d个元素!\n", value);
		return -1;
	}
	// 2.删除这个元素,把后面的元素依次往前移动覆盖前一个元素
	for (int i = index + 1; i < table->pos; ++i) {
		table->data[i - 1] = table->data[i];
	}
	// 3.记得把pos前移一位
	--table->pos;
	return 0;
}	

6. 顺序表扩容

satic int enlargeSeqTable(SeqTable_t* table) {
	// 1.再申请一个新空间并初始化
	SeqTable* tmp = malloc(sizeof(*table) * 2);
	if (tmp == NULL) {
		printf("顺序表扩容失败!\n");
		return -1;
	}
	// 2.拷贝老内容到新空间
	memcpy(tmp, table->data, sizeof(Element_t) * table->pos);
	// 3.更新表头,释放老空间
	free(table->data);
	table->data = tmp;

	return 0;
	

7. 从表尾插入元素

int appendSeqTable(SeqTable_t* table, Element_t value) {
	if (table == NULL) {
		fprintf(stderr, "顺序表指针为空,操作失败!\n");
		return -1;
	}
	// 顺序表已满,进行扩容
	if (table->pos >= table->capacity && enlargeSeqTable(table)) {
		fprintf(stderr, "顺序表扩容失败!\n");
		return -1;
	}
	table->data[table->pos] = value;
	++table->pos;
	return table->pos;	

8. 在指定位置插入元素

int insertSeqTable(SeqTable_t* table, Element_t value, int index) {
	// 1.空间有效性判断
	if (index < 0 || index > table->pos) {
		printf("索引无效!\n");
		return -1;
	}
	if (table->pos >= table->capacity && enlargeSeqTable(table)) {
			fprintf(stderr, "顺序表扩容失败!\n");
			return -1;
	}
	// 2.为新数据预留位置
	for (int i = table->pos - 1; i >= index; ++i) {
		table->data[i + 1] = table->data[i];
	}
	table->data[index] = value;
	++table->pos;
	return 0;

9. 查看顺序表

void showSeqTable(const SEQTable_t* table)
{
    for (int i = 0; i < table->pos; ++i)
    {
        printf("%d\t",table->data[i]);
    }
    printf("\n");
}

五、顺序表操作性能分析

操作 描述 时间复杂度 备注
访问 通过下标获取元素 O(1) ✅ 最大优势
插入 在指定位置插入 O(n) ⚠️ 需移动大量元素
删除 删除指定位置 O(n) ⚠️ 同样需移动
查找 按值查找 O(n) ⚠️ 可能需遍历整个表
扩容 动态调整容量 O(n) ⚠️ 高开销,需合理策略
创建 初始化结构 O(1)
销毁 释放资源 O(1)

六、优缺点总结

优点 缺点
✅ 支持随机访问(O(1)) ⚠️ 插入/删除效率低(O(n))
✅ 空间紧凑、缓存友好 ⚠️ 需要预估容量或实现扩容
✅ 局部空间利用率高 ⚠️ 扩容代价高
✅ 实现简单、易于理解 ⚠️ 不适合频繁增删的场景

七、顺序表的适用场景

  • 需要频繁随机访问元素(如通过索引查询)。
  • 已知或可预估元素最大数量,或增删操作非常少(主要是查询)。
  • 尾部操作相对高效(插入/删除尾部元素时不需要移动其他元素,接近O(1))。

八、总结:顺序表的核心思想

顺序表通过物理位置的连续性,完美映射了线性表元素间的逻辑相邻关系。

  • 优点:随机访问快(O(1))、空间利用率高(数据区无额外开销)、缓存友好。
  • 缺点:插入删除慢(O(n))、容量管理复杂(需扩容)、扩容代价高。
  • 关键结构:表头 (data, length, capacity) + 堆上的连续空间。

你可能感兴趣的:(数据结构入门 (一):线性表的基石 —— 顺序表详解)