CSDN小白一文详细解读顺序表

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

学完C语言的基础后,我们会用C语言的知识进行初步的应用。面对海量的数据,利用程序来对数据进行规律地管理,可以减少数据的丢失,数据使用和增删的混乱等问题,这也就是我们学习数据结构的原因所在。

【百度百科】
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。

浅说学习数据结构:

而对于初学者而言,最先接触的数据结构就是顺序表和链表,它们是我们接触以后更复杂结构的基础结构,比如,栈结构的底层结构较优且常用的结构是顺序表,队列的底层结构较优且常用的是链表,而在二叉树,我们会在不同情况下选择更优的结构去实现。

一、顺序表是什么?

前言:我们生活中常见的信息管理系统,管理每一个人的信息,每个人的信息块一个一个按顺序拼接存放在一起,看起来就像一个表结构,故称之为顺序表。
举一个简单
举上面这样一个简单的例子理解顺序表,在正式“请出”顺序表之前,我们先了解一类更广泛的表结构,也就是线性表。

【百度百科】
线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。
线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储,但是把最后一个数据元素的尾指针指向了首位结点))。

对于非线性表,比如说二叉树结构,他一个节点对应的可能是多个或一个节点(就是前文提到的信息块),数据元素之间不是首尾相接的。如下图(每一个绿色圆圈代表一个信息块)
在这里插入图片描述
由此我们发现,线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,比如说链表
在这里插入图片描述
顺序表在物理和逻辑上都是连续的
线性表在存储时,通常以数组和**链式结构(链表)**的形式存储。

二、利用数组实现顺序表

0.文件设置

为了方便代码的管理,我们将创建测试文件,函数功能文件和头文件(VS2019)
在这里插入图片描述
Test.c中主函数用来实现功能测试,SqeList.c实现函数的功能,SqeList.h分装头文件,里面装函数的声明

1.静态顺序表和动态顺序表

ps:从简单的先开始,我们的信息块先只存储一个数字,到以后的复杂结构,如通讯录,我们会新设一个结构体,去存放每个联系人的不同类型的信息

typedef int SLDataType;
struct Seqlist {
	SLDataType a[10];
	int size;
};

这里typedef是因为我们在以后,我们可能因为各种需求,顺序表中存的数据会变成其他类型,比如,哪天需要把字符存进顺序表的时候,如果没有typedef,那么在我们大的.c文件中,就要一个个地去改,很麻烦,不利于我们维护自己写的代码,而一个typedef成SLDataType,我们以后就只要改这一行的int就可以了。
同理,我们也没必要每次使用结构体的时候都用struct Seqlist,直接用缩写SL是一个好的选择,所以,优化代码后,即是

typedef int SLDataType;
typedef struct Seqlist {
	SLDataType a[10];
	int size;
}SL;

a[]是我们要存入数据要存入我们的数组(假设我们存10个数据),而size表示顺序表的大小。

但是,大家是否想过一个问题:如果我们哪天只要存储一个数据,那是不是浪费了9个位置空间,开了又不用,而如果哪天我们要存12个数据,那又放不下了。

所以,为了改变这种写死大小的状态,我们便采用一种新的动态的结构去适配我们需要的大小

typedef int SLDataType;
typedef struct Seqlist {
	SLDataType* a;
	int size;
}SL;

等会我们初始化的时候,就可以把这个指针设定指向我们要管理的动态数组,从而调整出最合适我们存放数据的数组的大小。

2.实现动态顺序表

2.0总体顺序表的实现功能(头文件)

//Sqelist.h
#define _CRT_SECURE_NO_WARNINGS
#pragma once//防止重复编译
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>//包好所有头文件
typedef int SLDataType;//重命名int类型,名为顺序表数据类型
typedef struct SeqList {
	SLDataType* arr;//存储顺序表的底层结构,数组,定义指向数组的指针
	int capacity;//定义结构体大小,顺序表的空间大小
	int size;//定义数组大小
}SL;
//初始化函数(注意,头文件中包含的是所有调用函数的声明,相当于转化成
//库函数可以在主函数中直接调用)
void SLInit(SL* ps);
//销毁函数
void SLDestroy(SL* ps);
//打印函数
void SLPrint(SL* ps);

//销毁函数,用完手动销毁防止内存泄露
void SLDestroy(SL* ps);
//1.头插函数
void SLPushFront(SL* ps, SLDataType x);
// 2.尾插
void SLPushBack(SL* ps, SLDataType x);
// 3.头删
void SLPopFront(SL* ps);
// 4.尾删
void SLPopBack(SL* ps);
// 5.指定位置插入(三组测试,三种结果)
void SLInsert(SL* ps, SLDataType pos, SLDataType x);
//6.删除指定数据
void SLErase(SL*ps, SLDataType pos);
//7.查找指定数据
int SLFind(SL* ps, SLDataType x);
//8.修改指定位置的数据
void SLChange(SL* ps, SLDataType pos, SLDataType x);

2.1顺序表测试文件

//test.c
void slTest01() {
	SL s1;//顺序表的定义
	SLInit(&s1);//顺序表初始化

	SLPushBack(&s1, 1);
	SLPushBack(&s1, 2);
	SLPushBack(&s1, 3);
	SLPushBack(&s1, 4);//插入四个值达到测试的目的
	SLPrint(&s1);
//顺序表的功能:增删查改,改很简单,就是赋值,重点在前三者
//1.头插
	SLPushFront(&s1,5);
	SLPushFront(&s1,7);
	SLPushFront(&s1,6);
	SLPrint(&s1);
// 2.尾插
	SLPushBack(&s1,300);
	SLPrint(&s1);
// 3.头删
	SLPopFront(&s1);
	SLPrint(&s1);
// 4.尾删
	SLPopBack(&s1);
	SLPrint(&s1);
// 5.指定位置插入(4组测试,4种结果)
	SLInsert(&s1, 0, 100);//头插
	SLPrint(&s1);
	SLInsert(&s1, s1.size, 200);//尾插
	SLPrint(&s1);
	SLInsert(&s1, 2, 500);//中间插入
	SLPrint(&s1);
	SLInsert(&s1, 100, 300);//超范围插入
	SLPrint(&s1);
//6.删除指定数据,两组测试,两个结果
	SLErase(&s1, 0);
	SLPrint(&s1);
	SLErase(&s1, 1);
	SLPrint(&s1);
//7.查找指定的数据
	if (SLFind(&s1, 1)==-1)
	{
		printf("没找到");
	}
	else
		printf("找到了,在下标为%d的位置", SLFind(&s1, 1));
//8.修改指定位置的数据
	SLChange(&s1, 2, 100);
	SLPrint(&s1);
//9.销毁
	SLDestroy(&s1);
}

2.2顺序表的初始化和销毁

//初始化函数
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->size = ps->capacity = 0;
}

初始化:
总体思路即是将指向数组的指针置空初始化,并把顺序表元素的个数和占用的空间大小初始化为0。
销毁:
ps:加上断言保证结构体指针不为空,有利于代码的健壮性。
销毁只需指针释放和置空,size和capacity置为0即可。

2.3顺序表的增删查改操作

2.3.1头插操作

//1.头插函数
void SLPushFront(SL* ps, SLDataType x) {
	assert(ps);
	//判断是否扩容
	SLCheckCapacity(ps);

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

这里的一个重要操作是挪动数据,利用循环迭代移动,注意控制初末条件,有时需要正移有时需要逆向移动,以防移动数据后原数据被覆盖。头插函数中包含的扩容函数,扩容的思路是满空间就将原来的空间大小×2,初始空间为4,由于是在现有的空间基础上扩容,故用realloc很合适

//扩容函数
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;
	}
}

头插原理演示:(如在1,2,3中头插4)
在这里插入图片描述

2.3.2尾插操作

	void SLPushBack(SL* ps, SLDataType x) {
	assert(ps);
	SLCheckCapacity(ps);
	ps->arr[ps->size++] = x;
}

2.3.3 指定位置插入

// 5.指定位置插入(三组测试,三种结果)
void SLInsert(SL* ps, SLDataType pos, SLDataType x) {
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);//保证插入位置有效

	SLCheckCapacity(ps);

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

指定位置插入的原理和头插原理是类似的,指定位置插入数据涵盖了头插和尾插的操作,以上三种插入方法,包括下面删除类的函数,记得维持size大小,保证size是正确的值

2.3.4头删函数

//3.头删函数
void SLPopFront(SL* ps) {
	assert(ps);
	for (int i=0;i<ps->size-1;i++)
	{
		ps->arr[i] = ps->arr[i+1];
	}
	ps->size--;
}

这里的思路是让后一个数据不断去覆盖前面的数据(类似头插,不过是前往后,i++.头插是后往前移动和覆盖,i–)
同样,我们用图来描述头删到底是个什么过程,仍然用上面的例子
在这里插入图片描述
我们注意到,最后一步中原来的3并未被覆盖,仍然在我们的内存空间中,直接将size-。,也就是我们再进行其他操作的时候,并不会涉及到原来的3的位置。

2.3.5尾删函数

//4.尾删函数
void SLPopBack(SL* ps) {
	assert(ps);
	assert(ps->size);
	ps->arr[ps->size - 1] = -1;
	ps->size--;
}

2.3.6删除指定位置的数据

//6.删除指定位置的数据
void SLErase(SL* ps, SLDataType pos) {
	assert(ps);
	assert(pos >= 0 && pos <= ps->size - 1);
	for (int i = pos; i <=ps->size-2 ; i++) {
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

2.3.7查找指定位置的数据

//7.查找指定位置的数据
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			return i;
		}
	}
	return -1;
}

基本方法就是简单的遍历数组,我们设定一个-1作为未找到的返回值(-1的判断方法),是因为要查找的原顺序表中的数据下标可能就是0,这样返回给主函数的时候无法判断是找到了在0下标处的数据还是查找失败返回了0。返回值后具体的条件判断在文章2.1

2.3.8修改指定位置的数据

void SLChange(SL* ps,SLDataType pos, SLDataType x) {
	assert(ps);
	assert(pos >= 0 && pos <= ps->size - 1);
	ps->arr[pos] = x;
}

2.4功能函数总代码

#include"SeqList.h"
//初始化函数
void SLInit(SL* ps) {
	ps->arr = NULL;
	ps->size = ps->capacity = 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);//相当于return 1
	}//成功扩容
		ps->arr = tmp;//动态内存开辟的内容,指针指向的空间赋给数组
		ps->capacity = newCapacity;
	}
}

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

//打印函数
void SLPrint(SL* ps) {
	for (int i = 0; i < ps->size; i++) {
		printf("%d ", ps->arr[i]);
	}
	printf("\n");
}
//1.头插函数
void SLPushFront(SL* ps, SLDataType x) {
	assert(ps);
	//判断是否扩容
	SLCheckCapacity(ps);

	//旧数据往后移动一位
	for (int i = ps->size;i>=1; i--) {
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;
	ps->size++;
}
	// 	   
//2.尾插函数
void SLPushBack(SL* ps, SLDataType x) {
	assert(ps);
	SLCheckCapacity(ps);
	ps->arr[ps->size++] = x;
}
//3.头删函数
void SLPopFront(SL* ps) {
	assert(ps);
	for (int i=0;i<ps->size-1;i++)
	{
		ps->arr[i] = ps->arr[i+1];
	}
	ps->size--;
}

//4.尾删函数
void SLPopBack(SL* ps) {
	assert(ps);
	assert(ps->size);
	ps->arr[ps->size - 1] = -1;
	ps->size--;
}
// 5.指定位置插入(三组测试,三种结果)
void SLInsert(SL* ps, SLDataType pos, SLDataType x) {
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);

	SLCheckCapacity(ps);

	for (int i = pos;i<=ps->size+1; i++) {
		ps->arr[i + 1] = ps->arr[i];
	}
	ps->size++;
	ps->arr[pos] = x;
}
//6.删除指定数据
void SLErase(SL* ps, SLDataType pos) {
	assert(ps);
	assert(pos >= 0 && pos <= ps->size - 1);
	for (int i = pos; i <=ps->size-2 ; i++) {
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}
//7.查找指定位置的数据
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			return i;
		}
	}
	return -1;
}

//8.修改指定位置的数据
void SLChange(SL* ps,SLDataType pos, SLDataType x) {
	ps->arr[pos] = x;
}

三、总结

本文介绍了数据结构的入门内容顺序表,顺序表又分静态和动态,是一种应用数组实现的底层线性结构,其基本操作有增删查改。涉及C语言的指针,数组,控制语句,函数,动态内存分配结构体知识。时间复杂度为O(1),空间复杂度为O(N),动态顺序表优点有大小可控,按需开辟空间,相比静态顺序表,浪费少,可以随机读取和访问数据,元素存储高效,适用于存储需要频繁访问的数据(相比链表来说的一大优势)。但是缺点也很明显,插入数据或删除数据挪动不方便,要同时改变后面所有元素的位置。

四、浅谈数据结构学习

数据结构在C语言的基础上应用和展开,是各类计算机考试和面试的重点板块,对于初学者而言具有一定的学习难度,无论是思维还是代码相对于C语言都有明显的跳跃和提升,前承C语言后接C++,不过不用担心,老铁们看到这里了,如果喜欢我的文章的三连一波再走吧。这是我在CSDN上的第一篇文章,CSDN小白将在这里继续打怪升级,我们一起在技术的道路上学习和成长吧,下次见!拜拜┏(^0^)┛

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值