顺序表专题

发布于:2025-06-25 ⋅ 阅读:(16) ⋅ 点赞:(0)

1.为什么需要顺序表

顺序表的底层是数组,在数据结构(相关概念见算法的时间复杂度和空间复杂度-CSDN博客)的学习中,顺序表是最基础的线性结构之一。它如同一个 “数据容器”,能高效地组织和管理数据。以通讯录项目为例,若没有合适的数据结构,实现联系人的增删改查会变得复杂且低效(编写代码时需要找到数组中以有元素的个数,再进行增删改查操作)。顺序表作为数组的封装,不仅提供了基础的数据存储能力,还通过优化操作接口,让复杂的数据管理变得可行。

顺序表say:“虽然我大底层是数组,但是我提供了很多现成的方法,开箱即用,我就变成了一个新的很厉害的数据结构。其实也就是在数组的基础上,增加了增删改查等方法。”

在之前的学习中,对于C语言文件而言,分为.c,文件以及.h头文件

在.h 文件中包含:顺序表结构,声明顺序表的方法

在.c文件中包含:实现顺序表的方法

2.线性表与顺序表的定义

顺序表是线性表的一种!

1)线性表

  • 线性表:由 n 个相同类型数据元素组成的有限序列,逻辑上呈线性结构(如一条直线),物理存储可以是数组或链表。常见的线性表包括顺序表、链表、栈、队列等。

线性表指的是物理结构、逻辑结构具有相同特征的数据结构的集合。

线性表的物理结构:不一定连续

线性表的逻辑结构:连续的

比如:

蔬菜:小白菜,莲藕,莴笋……

水果:香蕉,草莓,苹果……

2)顺序表

  • 顺序表:线性表的一种实现方式,其物理存储基于数组,逻辑上相邻的元素在内存中也连续存储。

顺序表的物理结构:是连续的

顺序表的逻辑结构:一定是连续的

3.静态顺序表与动态顺序表的对比

int arr[10] = {0}

这是一个定长的数组

如果刚开始不知道数组的长度呢?

int* arr,这个时候就需要动态内存开辟,在确定大小之后再去动态申请。

顺序表分为静态顺序表以及动态顺序表。

1)静态顺序表

定义:

struct SeqList

{

      int arr[100];//定长数组

      int size;//顺序表当前有效的数据个数

};

2)动态顺序表

定义:

struct SeqList

{

      int* arr;

      int size;//有效的数据个数

      int capacity;//空间大小

};

顺序表为什么要叫SeqList:

因为Sequence表示顺序的,List表示列表。

3)对比

类型 实现方式 优点 缺点
静态顺序表 定长数组(如#define N 100 实现简单,访问效率高 空间固定,易造成浪费或不足
动态顺序表 动态分配数组(malloc/realloc 空间可按需扩展,灵活应对数据量变化 增容需拷贝数据,存在性能消耗

对比一下哪种顺序表更好?

按照相较而言,动态顺序表更加好,因为静态顺序表会面临数组大小如果给小的话,空间会不够用的情况。如果说当数组给大的话,会造成空间浪费的情况。而动态顺序表,可以动态增容。

4.动态顺序表的核心实现

动态顺序表的关键在于通过动态内存管理实现灵活扩容,以下是核心功能模块:

1)初始化

初始化:分配初始空间,设置初始容量和数据个数。

代码:

.h文件:

#pragma once

#include <stdio.h>
#include <stdlib.h>
//定义顺序表的结构
//#define N 100
 
静态顺序表
//struct SeqList
//{
//	int arr[N];
//	int size;
//	//有效个数
//};

//定义数据类型
typedef int SLDataType;

//动态顺序表
typedef struct SeqList
{
	int* arr;
	int size;
	//有效个数
	int capacity;
	//空间大小
}SL;
//定义结构体名称为SL

//顺序表初始化
void SLInit(SL ps);

.c文件:

SeqList.c:

#define _CRT_SECURE_NO_WARNINGS 1
#include"test.h"
void SLInit(SL s)
{
	s.arr = NULL;
	s.size = s.capacity = 0;

}

test.c:

#define _CRT_SECURE_NO_WARNINGS 1
#include"test.h"

//测试初始化对不对
void SLTest01()
{
	SL sl;
	SLInit(sl);
}
int main()
{
	SLTest01();
	return 0;
}

我们测试一下:

发现报错了,为什么?

要区别一下,传值与传地址。

形参传给实参,在这个过程中,实际上是传值,即值的拷贝。

此时此刻sl没有初始化,所以会报错,所以此时此刻需要,对sl初始化就应该传地址。[用取地址操作符,用指针来操作地址]

调试一下:

2)销毁

销毁:释放内存,避免内存泄漏。

代码:

test.c文件补充:

//销毁
SLDestory(&sl);

SeqList.c文件补充:

//顺序表销毁
void SLDestory(SL* ps)
{
	if (ps->arr)
	{
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

3)数据操作接口

插入操作:尾部插入(O (1))、头部插入(O (n))、指定位置插入(O (n))。

删除操作:尾部删除(O (1))、头部删除(O (n))、指定位置删除(O (n))。

查找与修改:按值查找(O (n))、按位置修改(O (1))。

//头部插入删除 / 尾部插入删除
void SLPushBack(SL * ps, SLDataType x);//尾部插入
void SLPopBack(SL * ps);
void SLPushFront(SL * ps, SLDataType x);//头部插入
void SLPopFront(SL * ps);

(1)尾插

顺序表ps:

如图所示原始:size = 4;capacity = 6;

现在插入x = 5 ;(尾插,在 size 处插入 x )

之后变为:size = 5;capacity = 6;

代码:

//顺序表尾插
void SLPushBack(SL* ps, SLDataType x)
{
	/*ps->arr[ps->size] = x;
	++ps->size;*/
	//可以写为
	ps->arr[ps->size++] = x;

}

测试一下:

	//尾插
	SLPushBack(&sl, 1);

 报错了,调试一下:

发现空间为0,所以需要我们在插入前保证是有可用空间的,修改代码如下:

//顺序表尾插
void SLPushBack(SL* ps, SLDataType x)
{
	//首先观测空间够不够
	if (ps->capacity == ps->size)
	{
		//三目表达式
		int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		//申请空间
		//使用realloc 
		SLDataType * tmp = (SLDataType*)realloc(ps->arr, newCapacity * 2 * sizeof(SLDataType));
		//动态的容易报错,申请失败,则可以补充SLDataType* tmp
		if (tmp == NULL)
		{
			//判断有没有动态申请成功
			perror("realloc 失败!");
			//直接退出程序,不再继续
			exit(1);
		}
		//申请成功
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
	//ps->arr[ps->size] = x;
	//++ps->size;
	//可以写为
	ps->arr[ps->size++] = x;
}

 调试一下:

补充:申请空间需要用下面哪个函数?

malloc  calloc realloc 

比如给一个空间:int arr[100] 需要增容用realloc

malloc 申请空间

calloc申请空间并且初始化

需要申请多大的空间呢?一次性增容多大?

初始有四个空间,但是现在空间不够了,所以我们应该增容一个空间变成五个空间,还是说增加很多很多个变成n个空间呢?

增容通常来说是成倍数的增加,一般情况下是两倍或者三倍,这是数学推理出来的,用二倍或三倍更加合理。

realloc 增容是怎么增的呢?

在一块内存中,arr 已经有一部分空间,我果需要增加空间,在后面继续申请,如果说它后面的这部分空间依然不够,则需要重新另辟蹊径找一个空间足够大的地址,继续申请空间,在下图中,黑色为原始空间,绿色为申请的空间。

在其他地址申请空间时,需要将原始的数据拷贝过来,并且将之前的空间释放掉,成为一个新的 arr [绿色的位置] ,代替原来的 arr [黑色的位置],的空间。

注意的是这个步骤非常的麻烦,如果要频繁的增容,则会造成程序的运行效率大大降低的现象。

如果一次给大呢?就会造成空间浪费的问题。

所以需要成倍数增加。

可以乱加数据吗?

SLPushBack(NULL, 5);

加一段代码,测试一下:

这里是因为NULL为空,传在指针的位置不可以对空指针进行解引用[野指针],所以此时的代码还是比较脆弱的,在进行以下补充,防止用户传输奇怪的值:

//加强代码
if (ps == NULL)
{
	return;
}

或者直接断言:

	//断言
	assert(ps);
	//等价于assert(ps != NULL)

断言需要添加头文件:

#include<assert.h>

 (2)头插

同样的,涉及到插入数据,我们就要思考空间够不够?够的话直接插入空间,不够的话需要增容。

所以我们将上述的判断空间够不够的一段代码进行封装函数:

//判断空间的封装函数
void SLChackCapacity()
{
	//首先观测空间够不够
	if (ps->capacity == ps->size)
	{
		//三目表达式
		int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;

		//申请空间
		//使用realloc 
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * 2 * sizeof(SLDataType));

		//动态的容易报错,申请失败,则可以补充SLDataType* tmp
		if (tmp == NULL)
		{
			//判断有没有动态申请成功
			perror("realloc 失败!");

			//直接退出程序,不再继续
			exit(1);
		}
		//申请成功
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
}

尾插是从size处开始插入数据,而头插则应当从下标为零的地方开始插入数据,也就是第一个位置开始插入数据。

需要将原始数据,从后向前依次全部向后挪,第一个位置空出来,再插入数据。

代码如下:

//顺序表头插
void SLPushFront(SL* ps, SLDataType x)
{
	//判断空间
	SLChackCapacity(ps);

	//断言
	assert(ps);
	//等价于assert(ps != NULL)

	//已有的数据向后移动
	for (int i = ps->size; i > 0; i--)
	{
		//arr[1] = arr[0]
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;
}

测试一下:

//头插
SLPushFront(&sl, 5);
SLPushFront(&sl, 6);

调试:

是一个一个向后挪动的:

 接下来插入6:

 打印一下(打印代码见下述):

我们发现,该代码也有问题,因为我们需要增加数据,所以在数据表中 size 也需要++,添加代码如下所示:

	ps->size++;

分割线:以下内容还较不全面,今天时间太晚啦,明天将会补充完整


(3)尾删

前提是数据表不能为空,所以需要一个断言。

assert(ps);

删完数数据以后要少一个,所以需要 size--。

关于删除数据,我们可以用这个办法:

ps->arr[size-1] = -1;

size--

注意观测顺序表是否为空?如果为空的话,我们则不能进行删除操作。所以此刻还需要一个断言及顺序表,不能为空。

assert(ps->size);

尾删代码如下:

 

尾删中size减减会影响后续的数据操作吗?

不会的,因为当删除时执行一次 size--,当增加数据时,在size的位置插入数据,并且执行 size++。当修改数据时,也在size内。查找数据时,循环条件为不能超过 size,所以对,后续是没有影响的。

(4)头删

怎么操作的呢?

是将第一个数据删除后,后面的数据一个接一个依次向前挪动一个位置,之后再执行 size--,代码如下:

 

测试一下:

 

打印:

 

4)打印

在上述描述中,每次测试时都需要通过调试去观测代码的编写是否正确,这样非常的麻烦。我们可以通过顺序表的打印,来观测结果,这样更加便捷。

//顺序表打印
void SLPrint(SL s)
{
	for (int i = 0; i < s.size; i++)
	{
		printf("%d", s.arr[i]);
	}
	printf("\n");
}

打印的话打印值,不需要指针。

 


网站公告

今日签到

点亮在社区的每一天
去签到