【数据结构】顺序表实现

0. 前言

小伙伴们大家好,从今天开始,我们就开始学习《数据结构》这门课程~

首先想给大家讲讲什么是数据结构?

0.1 数据结构是什么?

数据结构是由“数据”“结构”两词组合⽽来。

什么是数据?

比如常⻅的数值1、2、3、4..... ;平时我们学校教务系统⾥保存的用户信息(姓名、性别、年龄、学历等等)凡是我们在网页在肉眼可以看到的信息(⽂字、图⽚、视频等等),这些都是数据。

那何为结构呢?

当我们想要使⽤⼤量使⽤同⼀类型的数据时,通过⼿动定义⼤量的独⽴的变量对于程序来说,可读性⾮常差,我们可以借助数组这样的数据结构将⼤量的数据组织在⼀起,结构也可以理解为组织数据的⽅式。

就好像:

在草原上,想找到名叫“咩咩”的⽺很难,

但是如果我们从⽺圈⾥找到1号⽺就很简单,⽺圈这样的结构有效将⽺群组织起来。

概念:

数据结构是计算机存储、组织数据的⽅式。数据结构是指相互之间存在⼀种或多种特定关系的数据元素的集合。数据结构反映数据的内部构成,即数据由那部分构成,以什么⽅式构成,以及数据元素之间呈现的结构。

总结:

1)能够存储数据(如顺序表、链表等结构)

2)存储的数据能够⽅便查找

0.2 为什么需要数据结构? 

如图中所⽰,我们生活中无论去公共场所,或者在火车站买票等等场景,我们都需要排队。

如果不借助排队的⽅式来管理客户,会导致客户感受差、等待时间⻓等情况。

同理,程序中如果不对数据进⾏管理,可能会导致数据丢失、操作数据困难、野指针等情况。

通过数据结构,能够有效将数据组织和管理在⼀起。按照我们的⽅式任意对数据进⾏

增删改查等操作。

最基础的数据结构:数组。

我们思考一下,有了数组,为什么还要我们学习其他的数据结构呢?

比如你遇到这样一个问题:

假定数组有10个空间,已经使⽤了5个,向数组中插⼊数据步骤:

求数组的⻓度,求数组的有效数据个数,向下标为数据有效个数的位置插⼊数据(注意:这⾥是否要判断数组是否满了,满了还能继续插⼊吗).....

假设数据量⾮常庞⼤,频繁的获取数组有效数据个数会影响程序执⾏效率。

结论:最基础的数据结构能够提供的操作已经不能完全满⾜复杂算法实现。

🌟🌟有什么办法可以完成数组完不成的任务呢?

这就是我们要学习的第一个数据结构——顺序表 

1、顺序表的概念及结构

1.1 线性表

线性表(linear list)n个具有相同特性的数据元素的有限序列

线性表是⼀种在实际中⼴泛使⽤的数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串...

线性表在逻辑上是线性结构,也就说是连续的⼀条直线但是在物理结构上并不⼀定是连续的,

线性表在物理上存储时,通常以数组和链式结构的形式存储。

2、顺序表分类

2.1 顺序表和数组的区别:

顺序表实质上就是对数组的封装,完成了对数组的增删改查的操作。

下面有一张图,可以帮助大家理解他俩的关系哦:

我们可以把数组和顺序表想象成两家餐厅,

一家是苍蝇馆子这样的普通餐厅,一家是米其林餐厅这样高档的五星级餐厅。

在普通餐厅能吃到炒西蓝花、玉米羹等等这样的菜,在高档的五星级餐厅也能吃到。

只不过五星级餐厅的厨师会把同样的菜做的更加细致,无论从食材的选择,料汁的调配,加上好看的摆盘,配上好听的名字,让这些菜变得更加档次。

所以简单来说,数组经过增加数据、删除数据、修改数据、查找数据等等的操作,摇身一变,就变成了顺序表。

2.2 顺序表分类:

顺序表分为静态顺序表动态顺序表

2.2.1 静态顺序表

概念:使⽤定⻓数组存储元素

静态顺序表缺陷:空间给少了不够⽤,给多了造成空间浪费

2.2.2 动态顺序表 

3. 接口实现

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

基本增删查改接口

//对数据管理 --- 增删查改
void SLInit(SL* ps);			//初始化
void SLDestory(SL* ps);			//释放
void SLPrint(SL* ps);        	//打印
void SLCheakCapacity(SL* ps);	//检查容量 -- 扩容

//头插头删 尾插尾删
void SLPushBack(SL* ps, SLDateType x); //尾插
void SLPopBack(SL* ps);				   //尾删
void SLPushFront(SL* ps, SLDateType x);//头插
void SLPopFront(SL* ps);			   //头删

//返回下标,没找到返回-1
int SLFind(SL* ps, SLDateType);		   //查找元素,返回下标

//在pos位置插入x
void SLInsert(SL* ps, int pos, SLDateType x);	//任意位置插入
//在pos位置删除x
void SLErase(SL* ps, int pos);					//任意位置删除

void SLModify(SL* ps, int pos, SLDateType x);//修改

3.1 创建项目

由于在实际工程中,项目的实现都是采用模块化进行实现的。

所以在此处我也采用了模块化的方式进行实现。

3.2 定义动态顺序表结构 

#pragma once

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

动态顺序表
typedef int SLDataType;

typedef struct SeqList
{
	SLDataType* arr;//存储数据的底层结构 
	int capacity;//记录顺序表的空间大小 
	int size;//记录顺序表当前有效的数据个数 
}SL;

为了后续好修改类型数据,我们可以使用typedef将结构体类型struct SeqList 重新命名为SL
在后续对顺序表操作中,为了用户更好的输入数据,一般我们会将输入数据的数据类型重命名为SLDataType
采用typedef将其数据类型int重命名为SLDataType 

3.3 初始化与销毁

函数声明:

//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);

函数实现: 

//初始化顺序表
void SLInit((SL* ps)//传入链表地址便于修改
{
	ps->arr= NULL;
	ps->size = ps->capacity = 0;
}

动态顺序表是动态开辟的空间,结束时需要进行释放,避免造成内存泄漏

void SLDestroy(SL* ps)
{
	assert(ps);
	if (ps->arr) 
	{
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->capacity = ps->size = 0;
}

 代码解读:

assert(ps):这是一个断言语句,用于检查 ps 是否为非空指针。如果ps为空,
程序会在运行时中断并报错。 
if (ps->arr):检查 ps 结构体中的 arr 成员是否不为空。
free(ps->arr)`:如果 arr 不为空,使用 free 函数释放其占用的内存空间。 
ps->arr = NULL:将 arr 成员指针设置为空,以避免悬空指针。 
ps->capacity = ps->size = 0:将 capacity 和 size 成员都设置为 0。

3.4 顺序表容量检查

注意:

  1. 每当要增加数据时,都需要考虑空间是否使用完毕
  2. 如果使用完毕则需要考虑增容,增容为原来的两倍(避免频繁扩容)
  3. 增容后更新记录容量大小

注:这里我们考虑到有许多地方要检查是否增容,为了方便将它封装成一个函数 

函数声明:

//扩容
void SLCheckCapacity(SL* ps); 

函数实现: 

void SLCheckCapacity(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 fail!");
			exit(1);
		}
		//扩容成功
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
}

 代码解读:

判断结构体中的当前元素数量 size 是否等于容量 capacity 。如果相等,说明需要进行扩容操作。计算新的容量。如果当前容量为 0 ,则新容量设置为 4 ;否则新容量设置为当前容量的 2 倍。使用 realloc 函数尝试为数组重新分配内存,新的内存大小为新的容量乘以每个元素的大小。检查 realloc 是否成功。如果 realloc 失败(返回 NULL ),则:
打印错误信息。退出程序。如果扩容成功:更新结构体中的数组指针,使其指向新分配的内存。更新结构体中的容量值。

 3.5 打印

函数声明:

//顺序表打印
void SLPrint(SL* ps);

 上述函数定义完成后,我们通常需要测试打印以下相关数据,来判断相关函数定义是否成功.

代码实现:

void SLPrint(SL* ps)
{
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	printf("\n");
}

3.6 尾插

函数声明:

//顺序表的尾部插入
void SLPushBack(SL* ps, SLDataType x);

我们来分析一下:

尾插是在尾部插入一个数据。

但是在数据的尾部插入一个数据时,

我们需要考虑一个问题:原有空间是否可以容纳新的数据,是否需要扩容。
所以我们在插入数据时,要先调用 SLCheakCapacity函数来检查是否需要扩容。

代码实现:

//顺序表的尾部插入
void SLPushBack(SL* ps, SLDataType x)
{
    //对于顺序表为空,可以有两种判断方式
	//①断言--粗暴的解决方式
	//assert(ps != NULL);
	assert(ps);

	//②if判断--温柔的解决方式
	//if (ps == NULL)
    //{
	//	return;
	//}

	//空间不够,扩容
	SLCheckCapacity(ps);

	//空间足够,直接插入
	ps->arr[ps->size++] = x;
}

3.7尾删

函数声明:

//顺序表的尾部删除
void SLPopBack(SL* ps);

我们来分析一下:

尾删:删除尾部最后的一个元素。

但尾删同样也要考虑一个问题,空间中是否还有数据给我们删除。
所以在进行尾删时,我们可以采用assert函数断言空间中还有数据。

代码实现:

void SLPopBack(SL* ps)
{
	assert(ps);
	assert(ps->size >= 0);//断言空间中还有元素
	ps->size--;//下标减1
}

在删除数据时,我们不用将原有数据删除。只需要下标减1即可。
原因在于我们时根据下标来使用数据的,当下标减1后,尾部最后一个数据便无法进行访问。

 3.8 头插

函数声明:

//顺序表头部插⼊
void SLPushFront(SL* ps, SLDataType x);

我们来分析一下:

头插:在数据最开始地方插入数据。

比如,在0前面插入100

我们可以这样做: 

同样,头插也要调用 SLCheakCapacity函数来检查空间是否足够,是否需要扩容。

 

代码实现:

void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);

	//判断是否扩容 
	SLCheckCapacity(ps);

	//旧数据往后挪动一位
	for (int i = ps->size; i > 0 ; i--)
	{
		ps->arr[i] = ps->arr[i - 1];//ps->arr[1] = ps->arr[0]
	}
	ps->arr[0] = x;
	ps->size++;
}

3.9 头删

函数声明:

//顺序表的头部删除
void SLPopFront(SL* ps);

我们来分析一下:

头删:删除数据最开始的元素。

思路和头插类似,只要下标从1开始,所有数据依次向前移动1位,再把有限个数减1即可。
同时头删也需要使用assert函数断言原有空间中还有数据可以删除。 

 代码实现:

void SLPopFront(SL* ps)
{
	assert(ps);
	assert(ps->size);//空间中还有数据可以删除

	//后面的数据往前挪动一位
	for (int i = 0; i < ps->size; i++)
	{
		ps->arr[i-1] = ps->arr[i];//ps->arr[1] = ps->arr[0]
	}
	ps->size--;
}

3.10 指定位置之前插入数据

函数声明:

//指定位置之前插⼊
void SLInsert(SL* ps, int pos, SLDataType x);

我们分析一下:

看到插入两个字,我们就要考虑是否需要扩容。这一点很重要。还有我们要多pos这个参数进行判断,看是否在顺序表指定的范围中,因为顺序表是连续的,我们任意位置插入要合理,所以要对参数进行合理性判断:

assert(pos >= 0 && pos <= ps->size); 

先给大家来画个图分析:

代码实现:

void SLInsert(SL* ps, int pos, SLDataType x)
{
    //顺序表为空
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
    
    // 空间够不够?是否扩容?
	SLCheckCapacity(ps);
	
    //pos及之后的数据往后挪动一位

	for (int i = ps->size; i> pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	ps->size++;
}

代码解读:

assert(ps) :确保 ps 指针非空。
assert(pos >= 0 && pos <= ps->size) :确保指定的插入位置 pos 是有效的,即在 0 到当前元素数量 ps->size 的范围内。
SLCheckCapacity(ps) :在插入元素之前,检查并可能进行容量的扩展,以确保有足够的空间来插入新元素。
循环部分:
通过 for 循环方式,将位置 pos 及之后的元素向后移动一位,为新元素腾出位置。

ps->arr[pos] = x :将新元素 x 插入到指定位置 pos 。
ps->size++:增加元素数量,表示成功插入了一个新元素。 

3.11 删除任意位置数据

函数声明:

//删除指定位置数据 
void SLErase(SL* ps, int pos);

【代码思路】:和插入任何位置数据思想类似。首先我们要检查输入下标pos是否合法。之后从输入下标开始,后一个元素拷贝到前一个元素空间。

代码实现:

void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);

	//pos以后的数据往前挪动一位
	for (int i = pos; i < ps->size-1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
		//ps->arr[i-2] = ps->arr[i-1];
	}
	ps->size--;
}

3.12 查找

函数声明:

//在顺序表中查找x
int SLFind(SL* ps, SLDataType x);

【代码思路】:要查找某个元素。由于这里只是最简单的查找,我们直接暴力查找,遍历整个数组返回下标即可。更为复杂的数据查找,会有更高阶的数据结构来实现。

代码实现:

int SLFind(SL* ps, SLDateType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (x == ps->a[i])
			return i;
	}
	return -1;
}

4. 所有代码 

SeqList.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

//静态顺序表
//#define N 100
//typedef int SLDataType;
//
struct SeqList
{
	SLDataType a[N];
	int size;
};

//动态顺序表
typedef int SLDataType;

typedef struct SeqList
{
	SLDataType* arr;//存储数据的底层结构 
	int capacity;//记录顺序表的空间大小 
	int size;//记录顺序表当前有效的数据个数 
}SL;

//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);//保证接口的一致性


//顺序表头部 尾部插⼊
void SLPushBack(SL* ps, SLDataType x);
void SLPushFront(SL* ps, SLDataType x);

//顺序表的头部 尾部删除
void SLPopBack(SL* ps);
void SLPopFront(SL* ps);

//指定位置之前插入数据
//删除指定位置数据 
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);

//在顺序表中查找x
int SLFind(SL* ps, SLDataType x);

SeqList.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "SeqList.h"

void SLInit(SL* ps)
{
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

void SLDestroy(SL* ps)
{
	assert(ps);
	if (ps->arr) 
	{
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->capacity = ps->size = 0;
}


void SLCheckCapacity(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 fail!");
			exit(1);
		}
		//扩容成功
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
}

//顺序表的头部/尾部插入
void SLPushBack(SL* ps, SLDataType x) {
	//断言--粗暴的解决方式
	//assert(ps != NULL);
	assert(ps);

	//if判断--温柔的解决方式
	//if (ps == NULL) {
	//	return;
	//}

	//空间不够,扩容
	SLCheckCapacity(ps);

	//空间足够,直接插入
	ps->arr[ps->size++] = x;
	//ps->size++;
}

void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);

	//判断是否扩容 
	SLCheckCapacity(ps);

	//旧数据往后挪动一位
	for (int i = ps->size; i > 0 ; i--)
	{
		ps->arr[i] = ps->arr[i - 1];//ps->arr[1] = ps->arr[0]
	}
	ps->arr[0] = x;
	ps->size++;
}

//顺序表的头部 尾部删除
void SLPopBack(SL* ps)
{
	assert(ps);
	assert(ps->size);

	ps->size--;
}
void SLPopFront(SL* ps)
{
	assert(ps);
	assert(ps->size);

	//后面的数据往前挪动一位
	for (int i = 0; i < ps->size; i++)
	{
		ps->arr[i-1] = ps->arr[i];//ps->arr[1] = ps->arr[0]
	}
	ps->size--;
}

void SLPrint(SL* ps)
{
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	printf("\n");
}

//指定位置之前插入数据
//删除指定位置数据 

void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);

	SLCheckCapacity(ps);
	//pos及之后的数据往后挪动一位

	for (int i = ps->size; i> pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	ps->size++;
}

void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);

	//pos以后的数据往前挪动一位
	for (int i = pos; i < ps->size-1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
		//ps->arr[i-2] = ps->arr[i-1];
	}
	ps->size--;
}

//在顺序表中查找x

int SLFind(SL* ps, SLDataType x)
{
	//加上断言健壮性更好 
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (x == ps->arr[i])
			return i;
	}
	return -1;
}

测试代码:Test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "SeqList.h"

void SLTest01()
{
	SL sl;
	SLInit(&sl);
	//测试尾插
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushBack(&sl, 3);
	SLPushBack(&sl, 4);// 1 2 3 4
	SLPrint(&sl);
	//SLPushBack(&sl, 5);
	//SLPrint(&sl);


	//头插
	/*SLPushFront(&sl, 5);
	SLPushFront(&sl, 6);
	SLPushFront(&sl, 7);*///7 6 5 1 2 3 4

	//SLPrint(&sl);

	//尾删
	//SLPopBack(&sl);
	//SLPopBack(&sl);
	//SLPopBack(&sl);
	//SLPopBack(&sl);
	//SLPopBack(&sl);

	/*SLPopFront(&sl);
	SLPopFront(&sl);
	SLPrint(&sl);
	SLPopFront(&sl);

	SLPrint(&sl);*/

	//指定位置插入
	//SLInsert(&sl, 0, 100);
	//SLPrint(&sl);//100 1 2 3 4
	//SLInsert(&sl, sl.size, 200);
	//SLPrint(&sl);//100 1 2 3 4 200

	//SLInsert(&sl, 100, 300);
	//SLPrint(&sl);//100 1 2 3 4 200

	//SLErase(&sl, 0);
	//SLPrint(&sl);
	//SLErase(&sl, sl.size-1);
	//SLPrint(&sl);
	SLErase(&sl, 1);
	SLPrint(&sl);

}

void SLTest02()
{
	SL sl;
	SLInit(&sl);
	//测试尾插
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushBack(&sl, 3);
	SLPushBack(&sl, 4);// 1 2 3 4
	SLPrint(&sl);

	//测试查找 
	int ret = SLFind(&sl, 30);
	if (ret < 0) {
		printf("数据不存在,查找失败!\n");
	}
	else
	{
		printf("数据找到了, 在下标为%d位置\n", ret);
	}
}

void SLTest03()
{
	SL sl;
	SLInit(&sl);
	//测试尾插
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushBack(&sl, 3);
	SLPushBack(&sl, 4);// 1 2 3 4
	SLPrint(&sl);
	//测试销毁
	SLDestroy(&sl);
	SLPrint(&sl);

}

int main()
{
	//SLTest01();
	//SLTest02();
	SLTest03();

	return 0;
}

 大家可以根据上述思路和提供的源码,自己练习哦~

后续数据结构的学习,大家需要多画图,多练习代码,这样在实现中能够得心应手~

那么本期博客就讲到这里,如果对你有所帮助~ 别忘了收藏点赞哦

有疑问的,可以随时在评论区骚扰我哟~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值