数据结构初阶入门之顺序表

今天我们正式进入了数据结构的相关知识的学习,今天学习的是有关顺序表的相关知识,我们来了解一下。

本文章在从C语言语法基础过渡到数据结构与算法的学习。通过通讯录项目作为实际应用案例,展示为什么需要学习数据结构,以及如何将理论知识应用于实际项目开发。

预备知识

要实现通讯录项目,需要掌握两个关键技术:

  1. C语言语法基础:
  • 结构体、指针、动态内存管理

  • 函数封装、模块化编程:

  1. 数据结构之顺序表/链表
  • 线性表的基本概念

  • 顺序表的实现与应用

  • 链表的实现与应用

1. 顺序表的概念及结构

1.1 线性表

线性表(linear list) 是n个具有相同特性的数据元素的有序序列。线性表是一种在实际中广泛使用的数据结构。

常见的线性表:

  • 顺序表

  • 链表

  • 队列

  • 字符串

逻辑结构 vs 物理结构:

  • 逻辑结构:线性结构,即连续的一条直线

  • 物理结构:存储时不一定是连续的,通常以数组或链式结构存储

    类比理解:

c 复制代码
蔬菜分类:绿叶类、瓜类、菌菇类
线性表:具有部分相同特性的一类数据结构的集合

2. 顺序表分类

2.1 顺序表和数组的区别

数组:基础数据结构,固定大小的连续内存空间

顺序表:基于数组的封装,实现了常用的增删改查等接口

2.2 顺序表分类

2.2.1. 静态顺序表

概念:使用定长数组存储元素

c 复制代码
// 静态顺序表实现
typedef int SLDataType;  // 数据类型,方便后续修改
#define N 7              // 固定容量

typedef struct SeqList {
    SLDataType a[N];     // 定长数组
    int size;            // 有效数据个数
} SL;

内存布局示例:

c 复制代码
索引:  0    1    2    3    4    5    6
数据: [ ]   [ ]   [ ]   [ ]   [ ]   [ ]   [ ]
      ↑
    数组a[N],N=7

静态顺序表的缺陷:

  • 空间给少了不够用

  • 空间给多了造成浪费

  • 无法动态调整大小

2.2.2 动态顺序表(重要)

概念:使用动态分配的数组,可根据需要调整大小

c 复制代码
// 动态顺序表实现
typedef int SLDataType;

typedef struct SeqList {
    SLDataType* a;    // 指向动态开辟的数组
    int size;         // 有效数据个数
    int capacity;     // 容量大小
} SL;

3. 动态顺序表的实现

3.1基本操作接口设计

c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLDataType;

typedef struct SeqList
{
	SLDataType* arr;
	int size;
	int capacity;
}SL;
void SLCheck(SL* ps);
void SLPrint(SL ps);
//顺序表初始化
void SLInit(SL* ps);
//顺序表的销毁
void SLDestroy(SL* ps);
//头部插入删除 / 尾部插入删除
void SLPushBack(SL* ps, SLDataType x);

void SLPushFront(SL* ps, SLDataType x);

void SLPopBack(SL* ps);
void SLPopFront(SL* ps);

//void SLBackAdd(SL* ps, int pos, SLDataType x);
//void SLDel(SL* ps, int pos);

静态顺序表适用于数据量已知且固定的场景,而动态顺序表适用于数据量变化较大的场景。

动态顺序表更加灵活,但实现复杂一些,需要处理内存分配和释放。静态顺序表简单,但不够灵活。

在我们的代码中,实现的是动态顺序表,它通过一个结构体来管理数组指针、大小和容量,并提供了扩容机制(当大小等于容量时,容量扩大为原来的2倍)。

代码中各个函数的功能:

c 复制代码
SLInit:初始化顺序表,将数组指针置为空,大小和容量置为0。
SLDestroy:销毁顺序表,释放动态分配的数组,并将指针置空,大小和容量置0。
SLCheck:检查顺序表容量,若已满则扩容。
SLPushBack:在顺序表尾部插入一个元素,先检查容量,然后将元素放入数组末尾,大小加1。
SLPushFront:在顺序表头部插入一个元素,先检查容量,然后将所有元素后移一位,在头部放入新元素,大小加1。
SLPopBack:删除顺序表尾部元素,只需将大小减1(注意:这里并不释放内存,只是逻辑删除)。
SLPopFront:删除顺序表头部元素,将头部之后的所有元素前移一位,然后大小减1。
SLBackAdd:在指定位置插入元素,先检查位置是否合法,然后检查容量,将该位置及之后的元素后移,插入新元素,大小加1。
SLDel:删除指定位置元素,检查位置合法性,然后将该位置之后的元素前移,大小减1。

注意:在删除操作中,我们并没有实际释放内存,只是通过减小size来标记元素数量减少。真正的内存释放会在销毁顺序表时进行,或者扩容时重新分配。

接下来我们来对这些分装函数一一进行讲解由来:

3.2函数功能实现

首先,由于我们这是一个项目,所以我们应该有属于这个项目的文件,我们这里是关于顺序表的,所以头文件就可以命名为SeqList.h,再然后把函数的声明都放在这个头文件当中,把函数的代码实现放在SeqList.c文件中,再然后我们在test.c文件中去测试就可以了。

如下图:

在开始之前我们再说明一下一下前提流程,为什么我们这里把int 用typedef给变成SLDataType呢?这是因为我们这里如果假设全是int,那么数据类型是不是就定死了呀,但如果我们以后想用这个代码不只处理整型数据,那么我是不是就不用一个一个去修改int,而是直接一键改SLDataType就可以了呢?

因为其他的.c文件只需要包含我们自己创建的这个头文件就行,所以我们在这个头文件中先把我们需要的包含的头文件写出来,这样就会方便也美观许多。

3.2.1顺序表的初始化
c 复制代码
void SLPrint(SL ps);
//顺序表初始化

这是我们在头文件中需要去声明的,接下来我们到SeqList.c文件中去完成代码的处理:

c 复制代码
void SLInit(SL* ps)
{
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

在这里我们需要说明一点,我们想要进行初始化等后续操作,这里必须为传址而不能为传值,也就是我们这里需要传指针,这个代码很好理解对吧,对arr赋空指针,接着把空间与个数的值赋为0,到此,顺序表的初始化就完成了。

3.2.2检查扩容函数

c 复制代码
void SLCheck(SL* ps);

//头文件

c 复制代码
void SLCheck(SL* ps)
{
	if (ps->size == ps->capacity)
	{
		int Newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, Newcapacity * sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("realloc");
			exit(1);
		}
		ps->arr = tmp;
		ps->capacity = Newcapacity;
	}
}

我们来解释下这个函数的形成,因为我们在进行函数操作时,可能会遇到ps->size == ps->capacity的情况,即空间不足,此时就需要我们去扩容,不过我们这里考虑到有一个问题,那就是我扩容该扩多少才合理呢?

假如我原本空间为8,因为这次空间不足了,我直接下次给它扩容到800000,那我问你,这和静态顺序表有什么区别呢?不是还是会浪费空间嘛,那如果我下次扩容到9呢?是不是太少了呀,每次都要扩容,太过于浪费时间。

所以,当我们发现ps->size == ps->capacity相等时,我去用Newcapacity 接受这个三目表达式的值,如果ps->capacity等于0,那么值为4要不然为2 * ps->capacity,接下来应该没什么好说的了

3.2.3尾插

c 复制代码
void SLPushBack(SL* ps, SLDataType x);
c 复制代码
void SLPushBack(SL* ps, SLDataType x)//尾插
{
	assert(ps);

	SLCheck(ps);
	ps->arr[ps->size++] = x;

}

这里为了防止传过来的为空指针,去用assert去断言一下,如果为空指针,那么就会直接报错,这里我们就是调用了上面的函数,扩容成功后就在最后位置插入我们想要的值即可,插入后不要忘了ps->size++

3.2.4头插

c 复制代码
void SLPushFront(SL* ps, SLDataType x);
c 复制代码
void SLPushFront(SL* ps, SLDataType x)//头插
{
	assert(ps);
	SLCheck(ps);
	for (int i = ps->size; i > 0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];//arr[1]=arr[0];
	}
	ps->arr[0] = x;
	ps->size++;
}

如下图:

我们只需要从最后一个元素开始不断的向后移动,直到把第一个位置空出来即可~

所以这里写一个for循环当空出来后,对第一个位置进行插入,然后ps->size++即可

3.2.5尾删

c 复制代码
void SLPopBack(SL* ps);
c 复制代码
void SLPopBack(SL* ps)//尾删
{
	assert(ps->size);
	assert(ps->arr);
	--ps->size;

}

我们这里还是检查问题,因为要进行尾删,实际上只需要--ps->size即可,因为我们打印时就是看ps->size

3.2.6头删

c 复制代码
void SLPopFront(SL* ps);
c 复制代码
void SLPopFront(SL* ps)//头删
{
	assert(ps->size);
	assert(ps->arr);
	for (int i = 0; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

我们想要进行头删,实际上就是从第二个开始把每个元素向前移动是不是就可以了?

移动完之后ps->size--即可

3.2.7在指定位置之前插入元素

c 复制代码
void SLBackAdd(SL* ps, int pos, SLDataType x);
c 复制代码
void SLBackAdd(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
	SLCheck(ps);
	for (int i = ps->size; i > pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	ps->size++;
}

这个其实也很好理解,既然是在在指定位置之前插入元素,那么我就让那个指定位置之后的元素都往后移动就可以了,然后插入元素,最后ps->size++即可

3.2.8删除指定位置的数据

c 复制代码
void SLDel(SL* ps, int pos);
c 复制代码
void SLDel(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
	SLCheck(ps);
	for (int i = pos; i < ps->size-1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;

}

这个代码的逻辑为,既然是删除指定位置的数据,那么只需要从要删除的这个数据开始,让后一个去覆盖前一个就好了,这样就达到了我们想要的效果,当删除后别忘了ps->size--

3.2.9打印

最简单的一集来了奥!

c 复制代码
void SLPrint(SL ps);
c 复制代码
void SLPrint(SL ps)
{
	for (int i = 0; i < ps.size; i++)
	{
		printf("%d ", ps.arr[i]);
	}
	
}

没事好说的,过!

3.2.10顺序表的销毁

c 复制代码
//顺序表的销毁
void SLDestroy(SL* ps);
c 复制代码
void SLDestroy(SL* ps)
{
	if (ps->arr != NULL)
	{
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

当传过来的不是空指针时那么就free掉,再然后置为空指针,都赋为0即可。

3.3测试

我们只需要在test.c文件中去测试我们代码的逻辑有没有问题即可,我们来举个例子,测试下尾插和指定位置删除:


本篇文章到此结束,敬请期待下一篇的到来

相关推荐
SHOJYS4 小时前
学习离线处理 [CSP-J 2022 山东] 部署
数据结构·c++·学习·算法
ada7_5 小时前
LeetCode(python)108.将有序数组转换为二叉搜索树
数据结构·python·算法·leetcode
仰泳的熊猫5 小时前
1084 Broken Keyboard
数据结构·c++·算法·pat考试
_w_z_j_6 小时前
最小覆盖字串(滑动窗口)
数据结构·算法
路弥行至6 小时前
FreeRTOS任务管理详解中: FreeRTOS任务创建与删除实战教程(动态方法)
c语言·开发语言·笔记·stm32·操作系统·freertos·入门教程
湖北师范大学2403w7 小时前
根据前序和中序遍历构建二叉树
数据结构·算法
了一梨7 小时前
外设与接口:input子系统
linux·c语言
2401_841495647 小时前
【LeetCode刷题】最大子数组和
数据结构·python·算法·leetcode·动态规划·最大值·最大子数组和
我是华为OD~HR~栗栗呀7 小时前
23届(华为od)-C开发面经
java·c语言·c++·python·华为od·华为·面试
liu****7 小时前
8.栈和队列
c语言·开发语言·数据结构·c++·算法