《数据结构与算法》-顺序表:算法落地的第一个线性结构

hello大家好 欢迎来到小四季豆的博客


在上一篇博客中我们学习了算法的基础概念与复杂度分析,今天,我们就来认识数据结构中最基础、最核心的成员之一 ------ 顺序表(Sequential List)

一、什么是线性表

在学习顺序表之前,我们需要先理解线性表的概念。

线性表(Linear List):是n个具有相同特性的数据元素组成的有限序列。

  • 有序性:数据是一个挨着一个排列的,有先后顺序。
  • 一对一关系:除了第一个元素和最后一个元素,中间的每个元素,都有且仅有一个前驱和后继
  • 逻辑结构:一定是线性的(思维层面 )
  • 物理结构:不一定是线性的(数据在内存中存储是否连续)

我们可以把线性表想象成排队时的队伍,除了第一个人和最后一个人,每个人前面只有一个人(前驱),后面也只有一个人(后继)。

注意:线性表的 "线性",指的是逻辑关系上的一对一顺序结构,而不是指内存里的存储地址必须连续

二、什么是顺序表

1.顺序表的定义

顺序表是线性表逻辑关系的具体实现

顺序表:用一段连续的内存空间,依次存储相同类型数据元素的线性存储结构

  • 相同类型元素:一个顺序表中只能存储同一种数据类型
  • 依次存储:数据按照存入的先后顺序排列
  • 逻辑结构:线性(一对一关系)
  • 物理结构:线性(数据在内存中的存储是连续的,元素相邻)

数组就是顺序表的底层实现

**注意:**数组只是顺序表的底层语法,而顺序表是对数据元素存储和对数据操作(增删改查,扩容,判空)的封装,是一套成熟的数据结构模型。

根据内存分配方式不同,顺序表分为两种:

(1)静态顺序表:长度固定,容量固定

(2)动态顺序表:长度可变,可以灵活调整数据量

2.顺序表的结构定义

(1)静态顺序表

cpp 复制代码
#define MAXSIZE 10  // 定义最大容量
typedef int SLDataType; // 数据类型,方便修改

//结构体类型重命名
typedef struct SeqList
{
    SLDataType data[MAXSIZE];  // 定长数组,容量固定
    int size;       // 有效数据的个数(当前长度)
} SL;
  • 容量:固定不变
  • 优点:实现简单,适用于固定数据量的场景
  • 缺点:空间给小了容易溢出,空间给大了容易浪费

(2)动态顺序表

cpp 复制代码
typedef int SLDataType; // 数据类型,方便统一修改

//结构体类型重命名
typedef struct SeqList
{
    SLDataType* data;    // 指向动态数组的指针(底层是堆上的数组)
    int size;     // 有效数据的个数
    int capacity; // 当前数组的容量
} SeqList;
  • *data:指针指向动态申请的数组(与静态顺序表的核心区别)
  • 容量:可以根据实际需求进行扩容
  • 优点:空间利用率高,灵活性强。适用于数据量不确定的场景
  • 缺点:扩容时需要调用realloc,存在时间和空间的开销,可能在堆内存产生碎片

realloc动态内存函数

void * realloc(void*ptr,size_t size);

返回值:开辟成功返回新内存的起始地址(类型要转换成我们所需的结构体类型),开辟失败返回NULL

ptr:想要调整的内存地址

size:调整后新内存的大小(单位是字节)

三、顺序表的实现

三步走

  • **xxx.h 头文件:**结构体定义、宏、类型别名、函数声明(对外接口)
  • xxx.c 源文件:.h里所有函数的实现、static内部私有函数
  • **test.c 测试文件:**main,创建结构体变量,调用接口测试

1.静态顺序表的实现

项目结构:

StaticSeqlist.h:结构体、宏、函数声明

StaticSeqlist.c:所有功能函数实现

test.c:main 测试逻辑

(1)编写头文件

cpp 复制代码
//头文件保护 + 头文件引入
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
  • #pragma once:防止头文件重复包含
  • _CRT_SECURE_NO_WARNINGS:消除 VS 安全警告
  • assert.h:用于接口入参合法性断言
cpp 复制代码
//定义容量宏 + 数据类型重定义
#define MAX_SIZE 10
typedef int SLDataType;
  • MAX_SIZE:静态数组固定最大长度,改宏即可修改顺序表容量
  • SLDataType:类型封装,后续存其他类型只需改 typedef 后的类型
cpp 复制代码
//定义静态顺序表结构体
typedef struct
{
	SLDataType arr[MAX_SIZE];
	int size;
}StaSL;
  • arr[MAX_SIZE]:固定大小连续数组,存放数据
  • size:当前有效元素个数,空表size=0
cpp 复制代码
//声明所有对外功能接口

//初始化
void StaSlInit(StaSL* ps);
//尾插
void SlPushBack(StaSL* ps, SLDataType x);
//头插
void SlPushFront(StaSL* ps,SLDataType x);
//任意位置插入
void SlInsert(StaSL* ps,int pos, SLDataType x);
//尾删
void SlPopBack(StaSL* ps);
//头删
void SlPopFront(StaSL* ps);
//指定位置删除
void SlErase(StaSL* ps, int pos);
//查找元素
int SlFind(const StaSL* ps, SLDataType x);
//打印顺序表
void SlPrint(const StaSL* ps);
//清空顺序表
void StaSlClear(StaSL* ps);
  • 只声明需要给外部调用的函数,static内部工具函数不在头文件声明

(2)编写实现文件StaticSeqlist.c

  • 引入头文件 、实现内部私有辅助函数
cpp 复制代码
#include "StaticSeqlist.h"

//static修饰:函数仅本.c文件可用,外部无法调用(静态顺序表容量编译时就确定)
//不需要给外部文件调用的函数,就加 static,收拢访问权限、防重名、便于模块化

//判满:有效元素>=最大容量
static int SL_isFull(const StaSL* ps)
{
	return ps->size >= MAX_SIZE;
}
//判空:有效元素为0
static int SL_isEmpty(const StaSL* ps)
{
	return ps->size == 0;
}
  • 初始化函数StaSlInit
cpp 复制代码
void StaSlInit(StaSL* ps)
{
	assert(ps != NULL); //防止传入空指针
	ps->size = 0;      //有效元素置0,完成初始化
}
  • 尾插 SlPushBack
cpp 复制代码
void SlPushBack(StaSL* ps, SLDataType x)
{
	assert(ps != NULL);
	assert(!SL_isFull(ps)); //满了不能插入
	ps->arr[ps->size] = x;
	ps->size++;
}
  • 头插 SlPushFront
cpp 复制代码
void SlPushFront(StaSL* ps, SLDataType x)
{
	assert(ps != NULL);
	assert(!SL_isFull(ps));
	//所有元素整体后移一位,从最后一个有效元素开始挪
	for (int i = ps->size; i > 0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;
	ps->size++;
}
  • 任意位置插入 SlInsert
cpp 复制代码
void SlInsert(StaSL* ps, int pos, SLDataType x)
{
	assert(ps != NULL);
	assert(!SL_isFull(ps));
	//pos合法范围:1 ~ size+1(1头插,size+1等价尾插)
	assert(pos >=1 && pos <=ps->size + 1);
	//插入点之后数据全部后移
	for (int i = ps->size; i > pos - 1; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos - 1] = x; //pos转数组下标:pos-1
	ps->size++;
}
  • 尾删 SlPopBack
cpp 复制代码
void SlPopBack(StaSL* ps)
{
	assert(ps != NULL);
	assert(!SL_isEmpty(ps)); //空表不能删
	ps->size--; //有效长度-1,原末尾数据逻辑失效
}
  • 头删 SlPopFront
cpp 复制代码
void SlPopFront(StaSL* ps)
{
	assert(ps != NULL);
	assert(!SL_isEmpty(ps));
	//后续元素逐个向前覆盖
	for (int i = 0; i < ps->size; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}
  • 指定位置删除 SlErase
cpp 复制代码
void SlErase(StaSL* ps, int pos)
{
	assert(ps != NULL);
	assert(!SL_isEmpty(ps));
	assert(pos >= 1 && pos <= ps->size); //删除位置不能越界
	//从删除下标开始,后一个元素向前覆盖
	for (int i = pos - 1; i < ps->size-1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}
  • 按值查找 SlFind
cpp 复制代码
int SlFind(const StaSL* ps, SLDataType x)
{
	assert(ps != NULL);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			return i + 1; //找到返回位置(pos从1开始)
		}
	}
	return -1; //找不到返回-1
}
  • 顺序表打印 SlPrint
cpp 复制代码
void SlPrint(const StaSL* ps)
{
	assert(ps != NULL);
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d->", ps->arr[i]);
	}
	printf("\n");
}
  • 清空顺序表 StaSlClear
cpp 复制代码
void StaSlClear(StaSL* ps)
{
	assert(ps != NULL);
	ps->size = 0; //只需size置0,逻辑清空
}

(3)编写测试文件 test.c

cpp 复制代码
#include "StaticSeqlist.h"

int main()
{
    StaSL SL;
    StaSlInit(&SL);

    //增数据
    SlPushBack(&SL, 3);
    SlPushFront(&SL,1);
    SlInsert(&SL,2, 2);
    SlPushBack(&SL, 4);
    SlPrint(&SL);

    //查找测试
    int Num_pos = SlFind(&SL, 3);
    if (Num_pos!=-1)
        printf("找到这个元素了!在第%d个位置\n", Num_pos);
    else
        printf("未找到该元素!\n");

    //删数据
    SlPopBack(&SL);
    SlPopFront(&SL);
    SlErase(&SL, 2);
    SlPrint(&SL);

    //清空顺序表
    StaSlClear(&SL);
    return 0;
}

注意

static函数只能在.c 定义,不能在.h 声明

删除不用擦除原数组数据,修改 size 就行


2.动态顺序表的实现

  • 项目结构

SeqList.h:结构体、宏、函数声明

SeqList.c:所有功能函数实现

test.c:main 测试逻辑

(1)编写头文件 SeqList.h

cpp 复制代码
//头文件保护  头文件引入
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
  • #pragma once:防止头文件被重复包含,避免结构体重复定义报错
  • _CRT_SECURE_NO_WARNINGS:消除 VS 下 scanf/realloc 等函数的安全警告
  • assert.h:提供assert断言,用于接口入参合法性校验、
  • string.h:提供memmove高效内存拷贝函数
cpp 复制代码
//数据类型重定义,方便后续更换存储类型
typedef int SLDataType;

//定义动态顺序表结构体
typedef struct SeqList
{
	SLDataType* arr;    // 动态堆内存开辟的数组
	int size;           // 当前有效数据元素个数
	int capacity;       // 当前数组总容量(已开辟空间大小)
}SL;
  • SLDataType:类型封装,如需存储 char/float,仅修改 typedef 后的基础类型即可
  • arr:不固定长度,运行时通过 realloc 动态扩容
cpp 复制代码
//声明所有对外功能接口


//初始化顺序表
void SLInit(SL* ps);
//判空
int SL_is_Empty(const SL* ps);
//扩容检查
void Check_Sl_Capacity(SL* ps);
//尾插
void SlPushBack(SL* ps, SLDataType x);
//头插
void SlPushFront(SL* ps, SLDataType x);
//任意位置插入(pos从1开始)
void SlInsert(SL* ps, int pos, SLDataType x);
//pos下标后插入新元素
void SlInsertback(SL* ps, int pos, SLDataType x);
//尾删
void SlPopBack(SL* ps);
//头删
void SlPopFront(SL* ps);
//指定下标删除(pos从0开始)
void SlErase(SL* ps, int pos);
//按值查找,返回下标,找不到返回-1
int SlFind(const SL* ps, SLDataType x);
//销毁顺序表
void SlDestory(SL* ps);
//打印遍历顺序表
void SlPrint(const SL* ps);

(2)编写实现文件 SeqList.c

  • 初始化顺序表
cpp 复制代码
#include "SeqList.h"


void SLInit(SL* ps)
{
	assert(ps != NULL);
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}
  • 判断顺序表是否为空
cpp 复制代码
int SL_is_Empty(const SL* ps)
{
	assert(ps);
	return ps->size == 0; //size==0返回 1(空表),否则返回 0
}
  • 检查是否满容
cpp 复制代码
void Check_Sl_Capacity(SL* ps)
{
	assert(ps != NULL);
	if (ps->size == ps->capacity)
	{
		//空表首次开辟4个空间,非空二倍扩容
		int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDataType* temp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
		if (temp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		ps->arr = temp;
		ps->capacity = newCapacity;
	}
}

**注意:**realloc开辟空间单位为字节

  • 尾插
cpp 复制代码
void SlPushBack(SL* ps, SLDataType x)
{
	assert(ps != NULL);
	Check_Sl_Capacity(ps);
	ps->arr[ps->size++] = x;
}
  • 头插
cpp 复制代码
void SlPushFront(SL* ps, SLDataType x)
{
	assert(ps != NULL);
	Check_Sl_Capacity(ps);
	//从后往前挪,防止数据被提前覆盖
	for (int i = ps->size; i > 0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;
	++ps->size;
}
/*
//memmove优化高效写法
Check_Sl_Capacity(ps);
memmove(ps->arr + 1, ps->arr, ps->size * sizeof(SLDataType));
ps->arr[0] = x;
ps->size++;
*/
  • 指定位置之后插入
cpp 复制代码
void SlInsertback(SL* ps, int pos, SLDataType x)
{
	assert(ps != NULL);
	assert(pos > 0 && pos <= ps->size);
	Check_Sl_Capacity(ps);
	for (int i = ps->size; i > pos - 1; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	++ps->size;
}
  • 指定逻辑位pos插入(pos:1~size+1)
cpp 复制代码
void SlInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps != NULL);
	assert(pos > 0 && pos <= ps->size + 1); //判断pos合法性
	Check_Sl_Capacity(ps);
	for (int i = ps->size; i > pos - 1; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos - 1] = x;
	++ps->size;
}
  • 尾删
cpp 复制代码
 
void SlPopBack(SL* ps)
{
	assert(ps != NULL);
	assert(!SL_is_Empty(ps));
	--ps->size;
}
  • 头删
cpp 复制代码
 
void SlPopFront(SL* ps)
{
	assert(ps != NULL);
	assert(!SL_is_Empty(ps));
	for (int i = 0; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	--ps->size;
}
/*
//memmove优化
memmove(ps->arr, ps->arr + 1, (ps->size - 1) * sizeof(SLDataType));
ps->size--;
*/

memmove内存拷贝函数

void *memmove(void *dest, const void *src, size_t n)

从 src 拷贝 n 字节内存到 dest,支持源、目标内存区域重叠

  • 指定位置删除
cpp 复制代码
 
void SlErase(SL* ps, int pos)
{
	assert(ps != NULL);
	if (pos < 0 || pos >= ps->size) return; 
	for (int i = pos; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	--ps->size;
}
/*
//memmove高效删除
if(pos < ps->size -1)
	memmove(ps->arr+pos, ps->arr+pos+1, (ps->size-pos-1)*sizeof(SLDataType));
ps->size--;
*/
  • 按值查找
cpp 复制代码
 
int SlFind(const SL* ps, SLDataType x)
{
	assert(ps != NULL);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
			return i;   //找到返回位置
	}
	return -1; //找不到返回-1
}
  • 销毁顺序表
cpp 复制代码
 
void SlDestory(SL* ps)
{
	assert(ps != NULL);
	free(ps->arr);  //必须 free 动态开辟的 arr 数组,避免内存泄漏
	ps->arr = NULL;
	ps->capacity = ps->size = 0;
}
  • 打印顺序表
cpp 复制代码
 
void SlPrint(const SL* ps)
{
	assert(ps != NULL);
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d", ps->arr[i]);
		if (i != ps->size - 1)   //防止多打印箭头
			printf("->");
	}
	printf("\n");
}

(3)编写测试文件 test.c

cpp 复制代码
#include "SeqList.h"

int main()
{
	SL slist;
	SLInit(&slist);

	//头插1
	SlPushFront(&slist, 1);
	//尾插2、3、4、6
	SlPushBack(&slist, 2);
	SlPushBack(&slist, 3);
	SlPushBack(&slist, 4);
	SlPushBack(&slist, 6);
	printf("初始插入:");
	SlPrint(&slist);

	//在5号逻辑位后插入5
	SlInsertback(&slist, 5, 5);
	printf("指定位置插入后:");
	SlPrint(&slist);

	//删除下标5元素
	SlErase(&slist, 5);
	printf("删除下标5:");
	SlPrint(&slist);

	//查找元素2
	int pos = SlFind(&slist, 2);
	if (pos != -1)
	{
		printf("找到了,在位置%d\n", pos);
	}

	//销毁释放内存
	SlDestory(&slist);
	return 0;
}

**注意:****顺序表的检查容量、按值查找和打印只是调用而不修改指针都应该加const---**表达"只读"

至此我们完整的学习了顺序表的实现,通过代码我们可以总结出一个问题:中间位置插入 / 删除需要大批量挪动元素,时间复杂度 O (N);动态扩容存在内存开销、易产生内存碎片。

那我们要如何解决这个问题?这就要用到即将学习的链表~~

此篇到这里就结束啦~~ 希望本篇内容能帮大家理清思路哦~~ 我们下期再见

如果觉得这篇文章对你有帮助,别忘了点赞收藏哦~~

相关推荐
8Qi81 小时前
LeetCode 96:不同的二叉搜索树(Unique Binary Search Trees)—— 题解 ✅
算法·leetcode·职场和发展·动态规划
189228048611 小时前
NV041固态MT29F16T08GSLCEM9-QBES:C
人工智能·算法·microsoft·缓存·性能优化
jimy11 小时前
C语言中使用“结构体 + 函数指针”来模拟面向对象编程(OOP
c语言
罗超驿2 小时前
15.LeetCode 30. 串联所有单词的子串(Java):滑动窗口+哈希表详解
算法·leetcode
Marianne Qiqi2 小时前
非hot100的力扣算法题
数据结构·算法·leetcode
三品吉他手会点灯2 小时前
C语言学习笔记 - 45.运算符和表达式 - 运算符3 - 逻辑运算符
c语言·笔记·学习
CC数学建模2 小时前
2026第八届中青杯全国大学生数学建模竞赛C题:情绪维度耦合约束的脑电信号情绪识别 (1)完整思路、代码、模型、文章,全网首发高质量分享!
python·算法·数学建模
玖玥拾2 小时前
C/C++ 基础笔记(五)
c语言·c++·指针
Dillon Dong2 小时前
【风电控制】双馈风机网侧高低穿控制策略——从VrtCal信号处理到状态机逻辑的完整解析
算法·变流器·风电控制·dfig