Cyber骇客的数据系统有序化 ——【初阶数据结构与算法】线性表之顺序表

「C++ 40 周年」主题征文大赛(有机会与C++之父现场交流!) 10w+人浏览 457人参与

在这里插入图片描述
在这里插入图片描述

点击下面查看作者专栏
🔥🔥C语言专栏🔥🔥
🌊🌊编程百度🌊🌊
🌠🌠如何获取自己的代码仓库🌠🌠

前言

本章将在前面C语言的基础上(主要是 结构体、数组与指针、动态内存管理),进入初阶数据结构的专题:
顺序表

在讲解顺序表之前,先引入一个新概念:线性表

一、线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列
常见的线性表:顺序表、链表、栈、队列、字符串…

线性表在逻辑上是线性结构,也就说是连续的一条直线
但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储

在这里插入图片描述



二、顺序表

  • 概念及结构
    顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储
    在数组上完成数据的增删查改
顺序表
静态顺序表
动态静态表

2.1)静态顺序表:使用定长数组存储元素

#define N 7;

typedef int SLDataType;

typedef struct SeqList {
	SLDataType data[N];//定长数组

	size_t size;  		//有效数据个数
}SL;
  • 特点
    内存分配: 通常在上分配(如果是全局变量则在静态区),不需要手动管理内存释放。

  • 容量限制:
    必须预估最大需求。一旦定义了MAX_SIZE100,哪怕你只存1个数据,它也占100个数据的空间;如果你想存101个数据,程序就会报错(溢出)



2.2)动态顺序表

/*动态顺序表*/
typedef int SLDataType;

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

静态顺序表只适用于确定知道需要存多少数据的场景
静态顺序表的定长数组导致N定大了,空间开多浪费,开少不够用
所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表



接口实现(动态顺序表的各种功能实现)

我们采取分文件编写将代码分为三个部分:

  • SeqList.h (头文件):存放结构体定义、函数声明、宏定义和库文件的引用
  • SeqList.c (源文件):存放函数的具体实现逻辑
  • Test.c (测试文件):存放 main 函数,用于调用和测试功能


1. 头文件:SeqList.h(存放结构体定义、函数声明、宏定义和库文件的引用)

相当于“目录”或“说明书”,告诉使用者有哪些功能可用

完整代码
#pragma once // 防止头文件被重复引用
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

// 定义数据类型,方便后续修改
typedef int SLDataType;

// 结构体定义
typedef struct SeqList {
    SLDataType* data;   // 指向动态开辟的数组
    size_t size;        // 当前有效元素个数
    size_t capacity;    // 当前顺序表的容量
} SL;

// --- 函数声明 (Declaration) ---

// 顺序表初始化
void SeqListInit(SL* psl);

// 顺序表销毁
void SeqListDestroy(SL* psl); 
// 注:图片中拼写为 Destory,建议使用正确的英文 Destroy

// 检查空间,如果满了,进行增容
void CheckCapacity(SL* psl);

// 顺序表打印
void SeqListPrint(SL* psl);

// --- 核心增删查改接口 ---

// 尾插
void SeqListPushBack(SL* psl, SLDataType x);
// 尾删
void SeqListPopBack(SL* psl);

// 头插
void SeqListPushFront(SL* psl, SLDataType x);
// 头删
void SeqListPopFront(SL* psl);

// 查找 (返回下标,找不到返回 -1)
int SeqListFind(SL* psl, SLDataType x);

// 在 pos 位置插入 x
void SeqListInsert(SL* psl, size_t pos, SLDataType x);
// 删除 pos 位置的值
void SeqListErase(SL* psl, size_t pos);


2. 功能实现文件:SeqList.c

具体的“干活”代码
这里必须包含头文件 SeqList.h



1)顺序表初始化

将一个刚刚定义的顺序表结构体置为一个由于未存储数据,所以不占用任何堆区空间的空状态

void SeqListInit(SL* psl) {
	assert(psl);			//断言 psl 不为空指针

	/*采取“懒加载模式”*/
	psl->data = (SLDataType*)malloc(sizeof(SLDataType) * 4);
	psl->size = 0;			//有效数据个数置为0
	psl->capacity = 4;		//容量初始值设为4
}
  • SL* psl为什么是指针?
    如果传递的是结构体变量本身(传值调用) 函数内部会创建一个副本;修改副本的成员变量(如 size)不会影响外部原本的结构体。
    为了修改外部定义的结构体,必须传递它的地址(指针
  • 对顺序表初始化,这里有个头文件中的宏定义#define INIT_CAPACITY 4,目的是为了方便修改初始化时的大小,不然每次修改要改多处,定义之后就只需要修改一个地方即可,刚开始capacity也要给一定的容量,而不是0


2)顺序表销毁
/*顺序表销毁*/ 
void SeqListDestroy(SL* psl) {
	assert(psl);			//断言 psl 不为空指针

	free(psl->data);		//释放动态数组空间

	// 即使 free 了 NULL,再赋值 NULL 也是安全的,且必须保留以防 data 原本非空
	psl->data = NULL;
	psl->size = 0;
	psl->capacity = 0;
}

注意: 释放完动态数组a之后要记得将指针置为空,不然会导致野指针的出现



3)顺序表打印
/*顺序表打印*/
void SeqListPrint(SL* psl) {
	assert(psl);			//断言 psl 不为空指针

	//增加空表判断,提升用户体验
	if (psl->size = 0) {
		printf("顺序表为空(Empty List)\n");
		return;				//如果表为空,后面的 for 循环(0次到size-1)实际上不会执行任何有效操作
	}

	for (size_t i = 0; i < psl->size; i++) {
		printf("%d ", psl->data[i]);
	}
	printf("\n");
}	

值得一提的是:
if语句中的return语句用于提前结束函数执行,避免不必要的操作

如果表为空,后面的 for 循环(0次到size-1)实际上不会执行任何有效操作
通过 return 提前退出,节省了不必要的循环判断



4)顺序表容量检查与扩容函数

该内容需要用到动态内存开辟,不懂的可以看这个
传送卷轴:动态内存开辟

/*容量检查并增容*/
void CheckCapacity(SL* psl) {
	assert(psl);

	//只有当 size 等于 capacity 时才扩容
	if (psl->size = psl->capacity) {
		// 首次扩容给 4 个空间,之后按 2 倍扩容
		size_t newCapacity = (psl->capacity) ? 4 : (psl->capacity * 2);

		// 使用临时指针接收 realloc 的结果,防止扩容失败导致原数据丢失
		SLDataType* temp = (SLDataType*)realloc(psl->data, newCapacity * sizeof(SLDataType));
		if (temp == NULL) {
			// 扩容失败,保持原有数据不变,提示错误信息
			fprintf(stderr, "顺序表扩容失败(Reallocation Failed)\n");

			/*
			// 程序崩溃前确保错误信息输出
				fprintf(stderr, "致命错误:内存分配失败\n");
			// 立即显示,即使程序随后崩溃

				printf("程序正在运行...");
			// 如果程序崩溃,可能不会显示(缓冲未刷新)
			*/

			return;
		}
		psl->data = temp;	//扩容成功,更新数据指针
		psl->capacity = newCapacity;
	}

🔥下面罗列出这段代码中需要强调的点🔥

  • realloc 函数功能
    重新分配内存块的大小
  • newCapacity * sizeof(SLDataType)

newCapacity:新的元素个数(比如从4个扩展到8个)
sizeof(SLDataType):每个元素的大小(字节)
结果:新内存块的总字节大小

  • (SLDataType*)
    realloc 返回 void* 类型
  • fprintf输出流
    程序崩溃前确保错误信息输出
    fprintf(stderr, "致命错误:内存分配失败\n");
    // 立即显示,即使程序随后崩溃

    printf("程序正在运行...");
    // 如果程序崩溃,可能不会显示(缓冲未刷新)
  • return
    优雅返回,程序继续运行
  • stderr是什么
    传送卷轴:输出流


5)通用位置增删 (最核心的底层接口)
/*在 pos 位置插入 数值*/

// 时间复杂度: O(N)
	void SeqListInsert(SL * psl, size_t pos, SLDataType x) {
		assert(psl);
		// pos 可以等于 size (表示尾插),但不能大于 size
		assert(pos <= psl->size);

		// 1. 检查容量
		CheckCapacity(psl);

		// 2. 挪动数据 (从后往前挪)
		// 注意:使用 size_t (无符号) 进行倒序循环时需非常小心溢出问题
		// 这里使用 end > pos 作为终止条件是安全的,因为我们操作的是 end-1
		size_t end = psl->size;
		while (end > pos) {
			psl->a[end] = psl->a[end - 1];
			end--;
		}

		// 3. 放入数据
		psl->a[pos] = x;
		psl->size++;
	}

🔥下面罗列出这段代码中需要强调的点🔥

  • 参数:
    psl:顺序表指针
    pos:插入位置(0-based索引)
    x:要插入的数据
  • 检查插入的位置是否合法
    pos == psl->size 表示尾插(允许)
    pos > psl->size 会造成"空洞",不允许
  • CheckCapacity(psl); 容量检查
    如果表已满,自动进行扩容
  • 数据挪动(核心逻辑)
    为什么从后往前挪?从前往后挪会覆盖前面的数据
  • 循环开始前:
    在这里插入图片描述
  • 第一次循环:
    在这里插入图片描述
  • 第二次循环
    在这里插入图片描述
  • 第三次循环
    在这里插入图片描述
  • 循环结束后
    此时位置1就可以随意插入数据了


/*删除 pos 位置的值*/
	// 时间复杂度: O(N)
	void SeqListErase(SL* psl, size_t pos) {
		assert(psl);
		// 必须有数据才能删
		assert(psl->size > 0);
		// pos 必须在有效范围内 (0 到 size-1)
		assert(pos < psl->size);

		// 1. 挪动数据 (从前往后挪,覆盖掉 pos 位置)
		size_t begin = pos + 1;
		while (begin < psl->size) {
			psl->a[begin - 1] = psl->a[begin];
			begin++;
		}

		// 2. 减少尺寸
		psl->size--;
	}

🔥下面罗列出这段代码中需要强调的点🔥
我们定义一个局部变量begin,方便对数组下标的移动

  • 为什么不能size_t begin = pos;
    容易越界访问!!!!


6)顺序表头插和头删除&&尾插和尾删
/*顺序表头插和头删*/

	// 头插
// 时间复杂度: O(N)
	void SeqListPushFront(SL* psl, SLDataType x) {
		SeqListInsert(psl, 0, x);
	}

	// 头删
	// 时间复杂度: O(N)
	void SeqListPopFront(SL* psl) {
		SeqListErase(psl, 0);
	}


/*顺序表尾插和尾删*/

	// 尾插
// 时间复杂度: O(1) (摊还分析)
	void SeqListPushBack(SL* psl, SLDataType x) {
		SeqListInsert(psl, psl->size, x);
	}

	// 尾删
	// 时间复杂度: O(1)
	void SeqListPopBack(SL* psl) {
		// 检查是否为空在 Erase 中已经 assert 过了,这里可以不写,或者为了明确报错信息再写一次
		// assert(psl->size > 0); 
		SeqListErase(psl, psl->size - 1);
	}


7)顺序表查找下标
/*顺序表头插和头删*/
void SLPushFront(SL* psl){

/*查找下标*/
	// 时间复杂度: O(N)
int SeqListFind(SL* psl, SLDataType x){
	assert(psl);
	scanf_s("%d", &x);
	for (int i = 0; i < psl->size; ++i) {
		if (psl->a[i] == x) {
			return i;
		}
	}
	return -1;		//未找到
	// 函数在此结束,后面的代码不会执行
}


完整代码
#include "SepList.h"

// --- 函数定义 (Definition) ---

/*顺序表初始化*/
void SeqListInit(SL* psl) {
	assert(psl);			//断言 psl 不为空指针

	/*采取“懒加载模式”*/
	psl->data = (SLDataType*)malloc(sizeof(SLDataType) * 4);
	psl->size = 0;			//有效数据个数置为0
	psl->capacity = 4;		//容量初始值设为4
}

/*顺序表销毁*/ 
void SeqListDestroy(SL* psl) {
	assert(psl);			//断言 psl 不为空指针
	
	free(psl->data);		//释放动态数组空间

	// 即使 free 了 NULL,再赋值 NULL 也是安全的,且必须保留以防 data 原本非空
	psl->data = NULL;
	psl->size = 0;
	psl->capacity = 0;
}

/*顺序表打印*/
void SeqListPrint(SL* psl) {
	assert(psl);			//断言 psl 不为空指针

	//增加空表判断,提升用户体验
	if (psl->size == 0) {
		printf("顺序表为空(Empty List)\n");
		return;				//如果表为空,后面的 for 循环(0次到size-1)实际上不会执行任何有效操作
	}

	for (size_t i = 0; i < psl->size; i++) {
		printf("%d ", psl->data[i]);
	}
	printf("\n");
}

/*容量检查并增容*/
void CheckCapacity(SL* psl) {
	assert(psl);

	//只有当 size 等于 capacity 时才扩容
	if (psl->size == psl->capacity) {
		// 首次扩容给 4 个空间,之后按 2 倍扩容
		size_t newCapacity = (psl->capacity == 0) ? 4 : (psl->capacity * 2);

		// 使用临时指针接收 realloc 的结果,防止扩容失败导致原数据丢失
		SLDataType* temp = (SLDataType*)realloc(psl->data, newCapacity * sizeof(SLDataType));
		if (temp == NULL) {
			// 扩容失败,保持原有数据不变,提示错误信息
			fprintf(stderr, "顺序表扩容失败(Reallocation Failed)\n");

			/*
			// 程序崩溃前确保错误信息输出
				fprintf(stderr, "致命错误:内存分配失败\n");
			// 立即显示,即使程序随后崩溃

				printf("程序正在运行...");
			// 如果程序崩溃,可能不会显示(缓冲未刷新)
			*/

			return;
		}
		psl->data = temp;	//扩容成功,更新数据指针
		psl->capacity = newCapacity;
	}
}
/*在 pos 位置插入 数值*/

// 时间复杂度: O(N)
	void SeqListInsert(SL * psl, size_t pos, SLDataType x) {
		assert(psl);
		// pos 可以等于 size (表示尾插),但不能大于 size
		assert(pos <= psl->size);

		// 1. 检查容量
		CheckCapacity(psl);

		// 2. 挪动数据 (从后往前挪)
		// 注意:使用 size_t (无符号) 进行倒序循环时需非常小心溢出问题
		// 这里使用 end > pos 作为终止条件是安全的,因为我们操作的是 end-1
		size_t end = psl->size;
		while (end > pos) {
			psl->data[end] = psl->data[end - 1];
			end--;
		}

		// 3. 放入数据
		psl->data[pos] = x;
		psl->size++;
	}

/*删除 pos 位置的值*/
	// 时间复杂度: O(N)
	void SeqListErase(SL* psl, size_t pos) {
		assert(psl);
		// 必须有数据才能删
		assert(psl->size > 0);
		// pos 必须在有效范围内 (0 到 size-1)
		assert(pos < psl->size);

		// 1. 挪动数据 (从前往后挪,覆盖掉 pos 位置)
		size_t begin = pos + 1;
		while (begin < psl->size) {
			psl->data[begin - 1] = psl->data[begin];
			begin++;
		}

		// 2. 减少尺寸
		psl->size--;
	}

/*顺序表头插和头删*/

	// 头插
// 时间复杂度: O(N)
	void SeqListPushFront(SL* psl, SLDataType x) {
		SeqListInsert(psl, 0, x);
	}

	// 头删
	// 时间复杂度: O(N)
	void SeqListPopFront(SL* psl) {
		SeqListErase(psl, 0);
	}


/*顺序表尾插和尾删*/

	// 尾插
	void SeqListPushBack(SL* psl, SLDataType x) {
		SeqListInsert(psl, psl->size, x);
	}

	// 尾删
	// 时间复杂度: O(1)
	void SeqListPopBack(SL* psl) {
		// 检查是否为空在 Erase 中已经 assert 过了,这里可以不写,或者为了明确报错信息再写一次
		// assert(psl->size > 0); 
		SeqListErase(psl, psl->size - 1);
	}

/*查找下标*/
	// 时间复杂度: O(N)
int SeqListFind(SL* psl, SLDataType x){
	assert(psl);
	scanf_s("%d", &x);
	for (int i = 0; i < psl->size; ++i) {
		if (psl->data[i] == x) {
			return i;
		}
	}
	return -1;		//未找到
	// 函数在此结束,后面的代码不会执行
}


3. 测试文件:Test.c

用户入口,用来测试代码逻辑是否正确

完整代码
#include "SepList.h"

void TestSeqList() {
	SL s;
	SeqListInit(&s);

	printf("--- 开始测试尾插与自动扩容 ---\n");
	SeqListPushBack(&s, 1);
	SeqListPushBack(&s, 2);
	SeqListPushBack(&s, 3);
	SeqListPushBack(&s, 4);
	SeqListPushBack(&s, 5); // 触发扩容
	SeqListPrint(&s); // 期待输出: 1 2 3 4 5

	printf("--- 开始测试头插 ---\n");
	SeqListPushFront(&s, 10);
	SeqListPushFront(&s, 20);
	SeqListPrint(&s); // 期待输出: 20 10 1 2 3 4 5

	printf("--- 开始测试任意位置插入与删除 ---\n");
	SeqListInsert(&s, 1, 999); // 在下标1位置插入
	SeqListPrint(&s);

	SeqListErase(&s, 1); // 删除下标1位置
	SeqListPrint(&s);

	printf("--- 开始测试查找 ---\n");
	int result = SeqListFind(&s, 3);
	if (result != -1) {
		printf("找到了数字3,下标为: %d\n", result);
	}

	printf("--- 开始测试销毁 ---\n");
	SeqListDestroy(&s);
	printf("销毁完成。\n");
}

int main() {
	TestSeqList();

	return 0;
}


三、顺序表的优劣

  • 🌠🌠🌠🌠优势
  1. 尾部操作效率极高
  2. 支持随机访问
  3. CPU 高速缓存命中率高
  4. 存储密度高
  • 🌠🌠🌠🌠劣势
  1. 中部和头部插入/删除效率低
  2. 动态扩容的代价
  3. 对内存连续性的要求高


四、顺序表应用



希望读者多多三连

给小编一些动力

蟹蟹啦!

在这里插入图片描述

评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值