c语言实现顺序表(初阶数据结构)

Posted 红豆饭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c语言实现顺序表(初阶数据结构)相关的知识,希望对你有一定的参考价值。


前言

这是小编第一篇数据结构的文章,从今天开始更新数据结构相关的博客,希望大家多多关注,一起学习。

今天小编主要说的是顺序表的相关内容。


顺序表的介绍

说到顺序表,就需要介绍一下线性表了,线性表是什么呢?

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…

现在知道了顺序表是线性表的一种,线性表还包括链表,栈、队列和字符串。

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组链式结构的形式存储。

这里的数组也就是顺序表的基本结构,链式结构也就是链表的基本结构。这里我们先谈顺序表,以后再谈链表。


还需要补充一下逻辑结构物理结构,这个没理解好,后面是不太好学的

逻辑结构:简单地理解,就是指的数据之间的逻辑关系。
物理结构:数据在物理存储空间上选择集中存放还是分散存放

单看这个可能不太好理解,举个简单的例子

人物的关系就是逻辑结构,我是爸爸的儿子,爸爸是爷爷的儿子,正是这种关系才把我们串联在一起,我们可以不在一起,但是我们在逻辑上相当于有一条线将我们串在一起。

排队时就是物理结构,我们必须站在一列,是什么把我们串联在一起,是排队这种物理结构,相当于有一条实质性的线,而且这条一旦断了,物理结构也就没了。

顺序表在物理和逻辑上都是连续的,而链表在逻辑是连续的,物理上是不连续的


接下来正式谈顺序表

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
何为顺序表?简单说就是数组

顺序表的分类

静态顺序表:使用定长数组存储。
动态顺序表:使用动态开辟的数组存储。

这里我们主要讲解动态版本,静态在c语言中也学过,就是一个普通数组,实际意义不大。

动态开辟是什么意思?也就是数组的容量不是确定的,是可以动态增长的,这样其实更有利于存储数据,支持增删等操作的。


接下来我们就开始实现

项目分配


小编是使用的是vs2019作为开发工具,搭建顺序表工程

  • SeqList.h作为头文件,顺序表的创建、储存函数声明和引用函数所需的头文件
  • SeqList.c作为函数接口文件,即需求函数的定义
  • test 7_29_.c作为测试文件,看函数实现是否正确


顺序表的定义


typedef int SQDataType;
//动态顺序表的定义
typedef struct SeqList
{
	SQDataType* a;//数组的名称
	int size; //有效数据的个数
	int capacity;//空间总容量大小
}SLT;

看这段代码,其实还是有一些小技巧,解决了一些重要的问题

在一个项目中我们需要在很多地方用到数据的类型名,如int、char。如果我们想把原本储存数据类型为int改为char,就需要改很多的地方,那么有没有什么一劳永逸的方法呢?

答案是有的,我们可以用到typedef,直接将int定义为SQDataType,后面全用SQDataType,以后要改的话,就只需将int改为char或者其他类型,结构体也是如此,定义为了SLT。

这个小问题就解决了。
定义完成后将这段程序放在SeqList.h中。


顺序表的初始化


//初始化
void SeqListInit(SLT* psl)
{
	assert(psl);//断言psl一定不能为空,要引头文件assert.h
	psl->a = NULL;
	psl->size = psl->capacity = 0;
}

初始化需要将数组置为空,有效数据个数和大小均置为0,如果不进行初始化,将全部为随机值,可能会影响后续操作。
这里非常重要一点是传址,不是传值,记住一般需要修改数值时都是传址。

其实直接将main函数中创建的顺序表s直接s = {0};来初始化也是可以的,只是上面这样写是为了让大家了解具体是如何实现的。

我们可以通过调试来查看是否成功。

成功!!!


顺序表的打印


已经建立了一个顺序表,如何才能清楚的看到呢?这就需要我们将他打印在屏幕上了

//打印函数
void SeqListPrint(SLT* psl)
{
	assert(psl);
	for (int i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->a[i]);
	}
	printf("\\n");
}

打印就是简单的循环来完成就可以,需要注意的是传址

等下讲尾插的时候可以检测是否正确。


顺序表的销毁


顺序表在使用完毕后,由于是动态开辟的内存空间,是需要free掉的,不然会造成内存泄漏,所以我们需要销毁这块已经使用过的空间。

//销毁函数
void SeqListDestory(SLT* psl)
{
	assert(psl);
	if (psl->a)//判断动态开辟的数组是否为空
	{
		free(psl->a);//不为空就释放掉
	}
	psl->size = psl->capacity = 0;
}

接下来都是些重要的函数接口了

顺序表的尾插

尾插其实就是从尾部插入数据,组成一个新的顺序表。

增容函数的实现

但是需要注意的是我们需要判断原有顺序表是否为空或者已满,如果为空或者已满我们就需要开辟新空间来储存插入的数据,所以需要写一个判断代码,再开辟新空间,由于还有其他的插入接口,所以我们可以直接内部封装一个增容函数SeqListCheclCapacity

//封装一个增容函数
void SeqListCheclCapacity(SLT* psl)
{
	if (psl->size == psl->capacity)//判断是否满了
	{
		//如果为空,大小就置为4,否则就以2倍递增
		size_t newcapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		//动态开辟空间
		psl->a = realloc(psl->a, newcapacity * sizeof(SQDataType));
		psl->capacity = newcapacity;
	}
}

这个增容代码需要注意的是调用realloc函数时,第二个参数是新的存储大小,单位为字节,所以传的应该是sizeof(存储的数据类型) * newcapacity(新大小),不能直接传新大小

再就是解释为什么以二倍递增?二倍其实是一种折中的考虑,避免造成空间的浪费,要是以3倍、4倍也行


尾插

//尾插
void SeqListPushBack(SLT* psl, SQDataType x)
{
	assert(psl);
	SeqListCheclCapacity(psl);
	psl->a[psl->size] = x;
	psl->size++;
}

先判断是否满了,满了就增容,然后就直接插入到size的位置,然后size++,有效个数加一。尾插就是这么简单。

我们来通过测试文件来看一下效果

完美创建,增容成功,打印也没有问题!!!


顺序表的头插


讲完尾插后,就是头插了,头插也就是从头部插入。

头部插入要比尾部插入麻烦一丢丢啦, 只需要将数据整体向后移动一位就可以,再在下标为0的位置插入就可以了,因为是插入,所以也需要考虑增容问题。

//头插
void SeqListPushFront(SLT* psl, SQDataType x)
{
	assert(psl);
	SeqListCheclCapacity(psl);//判断是否已满
	int end = psl->size - 1;//找到尾部
	while (end >= 0)
	{
		psl->a[end + 1] = psl->a[end];//整体向后移动一位
		end--;
	}
	psl->a[0] = x;
	psl->size++;
}

看一下效果


成功!!!


顺序表的任意位置插入数据


知道尾插和头插后一定就更想知道如何在任意位置插入了,其实任意位置插入也就和头插类似,也就是在将指定位置的后面所有数值均向后移动一位,而头插是固定的移动所有数值,这个移动要看要求,也就是传过来的pos值,pos值也就是下标值

函数声明为

//在指定位置插入数值
void SeqListInsert(SLT* psl, size_t pos, SQDataType x);//pos为指定的下标位置,x为传过来的数值

//指定位置插入数值(可以代替头插和尾插)
void SeqListInsert(SLT* psl, size_t pos, SQDataType x)
{
	assert(psl);
	assert(pos <= psl->size);//插入的位置必须小于等于含有的数据数
	SeqListCheclCapacity(psl);//判断是否需要增容

	size_t end = psl->size;
	//指定位置后的数据全部向后移动一位
	while (end > pos)
	{
		psl->a[end] = psl->a[end - 1];
		end--;
	}
	psl->a[pos] = x;
	psl->size++;
}

这个代码是可以代替头插和尾插的
看一下效果

成功!!!


顺序表的尾删


讲完插入就要讲删除了,一些不要的数据是需要删除的,踢出我们的顺序表。删除也和插入一样,我们先来看尾删,其实尾删是非常简单的,我们只需要把有效数字的个数减一就行了,这样就访问不到这个数据了。

这里就有同学会问了,你这样不是数据还在吗?没有删除掉啊。

这里我想说的是,我们完全没有必要去把这个数据free掉,回想一下我们插入是怎么插入的,我们除了尾插其他都是通过后移数据来操作的,其实这个有点像的就是覆盖,所以如果要重新插入数据,也就是覆盖,所以这个删除数据在不在也就不影响,只是覆盖换值,我们所要做的也就是让顺序表访问不到就行了。

//尾删
void SeqListPopBack(SLT* psl)
{
	assert(psl);
	//assert(psl->size > 0); 不太温和的方式
	//温和一点
	if (psl->size > 0)
	{
		psl->size--;//有效数据个数减一
	}
}

这里温和和不太温和的方式意思其实是一样的,就是防止顺序表为空时还要进行删除。不温合就直接报错,温和就是判断一下。

看一下效果

成功!!!


顺序表的头删


讲完尾删就是头删了,头删的主要操作也就是覆盖,除了下标为0的所有数据全部向前移动一位,将第一个数据覆盖掉,有效数据个数减一即可

将2、3、4、5依次向前覆盖,再将size减一,所得到的的就是头删后的顺序表。

//头删
void SeqListPopFront(SLT* psl)
{
	assert(psl);
	int head = 1;
	while (head < psl->size)//从下标为1的位置开始前移
	{
		psl->a[head - 1] = psl->a[head];
		head++;
	}
	//assert(psl->size > 0); //不太温和的方式
	//温和一点
	if (psl->size > 0)
	{
		psl->size--;
	}
}

看一下效果

成功!!!


顺序表的任意位置删除


讲完尾删和头删,接下来就是任意位置的删除,这个的主要思想也是覆盖,插入是往后覆盖,删除则是往前覆盖,在给定位置后的所有数据依次向前覆盖,再size减一

//指定位置删除数据(可以代替头删和尾删)
void SeqListErase(SLT* psl, size_t pos)
{
	assert(psl);
	assert(pos < psl->size);
	size_t begin = pos + 1;
	while (begin < psl->size)
	{
		psl->a[begin - 1] = psl->a[begin];
		begin++;
	}
	psl->size--;
}

看一下效果

成功!!!


顺序表的查找

查找就是看顺序表是否存在指定的数值,直接遍历判断即可,存在就返回下标,不存在就返回-1,所以函数的返回值为int类型。

//查找
int SeqListFind(SLT* psl, SQDataType x)
{
	assert(psl);
	for (int i = 0; i < psl->size; i++)
	{
		if (psl->a[i] == x)
		{
			return i;
		}
	}
	return -1;
}

这个代码有个缺陷就是如果存在两个相同的数值,就只会返回下标小的那个,不会全部返回。这里只需要要求我们判断存不存在就够了。
看一下效果

成功!!!


顺序表的元素个数


直接将size打印即可,由于个数一定是大于等于0的,所以函数的返回值类型可以为size_t。

/求出顺序表中的元素数目
size_t SeqListSize(SLT* psl)
{
	assert(psl);
	return psl->size;
}
看一下效果


成功!!!


修改指定位置的数值

直接赋值替换就行。

//修改指定位置的值
void SeqListAt(SLT* psl, size_t pos, SQDataType x)
{
	assert(psl);
	assert(pos < psl->size);//位置要小于现有的数值个数
	psl->a[pos] = x;
}

看一下效果

成功!!!


结语

这就是顺序表所要掌握的所有函数接口,相信你肯定对顺序表有了一定了解,整体来看,顺序表的缺点还是很明显的,就是空间的浪费,实际使用的空间是常常小于所开辟的空间的,并且增容拷贝数据也是消耗很大的,所以链表就出现了。小编将在不久出一篇关于链表的博客

不过当下为了巩固大家顺序表的理解,小编大概在两天后更新一篇关于顺序表的几题力扣oj题,欢迎大家关注,共同学习,如果在阅读时发现错误,可以私信我修改,如果不理解,也可以来讨论,私信必回哦。感谢大家参考学习。


SeqList.h文件内容

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


typedef int SQDataType;

//动态顺序表的定义
typedef struct SeqList
{
	SQDataType* a;
	int size; //有效数据的个数
	int capacity;//空间总容量大小
}SLT;

//增删查改

//初始化
void SeqListInit(SLT* psl);

//销毁函数
void SeqListDestory(SLT* psl);

//打印函数
void SeqListPrint(SLT* psl);

//封装一个增容函数
void SeqListCheclCapacity(SLT* psl);

//尾插
void SeqListPushBack(SLT* psl, SQDataType x);

//头插
void SeqListPushFront(SLT* psl, SQDataType x);

//尾删
void SeqListPopBack(SLT* psl);

//头删
void SeqListPopFront(SLT* psl);

//查找
int SeqListFind(SLT* psl, SQDataType x);

//在指定位置插入数值
void SeqListInsert(SLT* psl, size_t pos, SQDataType x);

//删除指定位置的数值
void SeqListErase(SLT* psl, size_t pos);

//求出顺序表中的元素数目
size_t SeqListSize(SLT* psl);

//修改指定位置的值
void SeqListAt(SLT* psl, size_t pos, SQDataType x);

test 7_29_.c文件内容

#pragma once
#include"SeqList.h"

//初始化
void SeqListInit(SLT* psl)
{
	assert(psl);//psl一定不能为空
	psl->a = NULL;
	psl->size = psl->capacity = 0;
}

//销毁函数
void SeqListDestory(SLT* psl)
{
	assert(psl);
	if (psl->a)
	{
		free(psl->a);
	}
	psl->size = psl->capacity = 0;
}

//打印函数
void SeqListPrint(SLT* psl)
{
	assert(psl);
	for (int i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->a[i]);
	}
	printf("\\n");
}

//封装一个增容函数
void SeqListCheclCapacity(SLT* psl)
{
	
	if (psl->size == psl->capacity)
	{
		size_t newcapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		psl->a = realloc(psl->a, newcapacity * sizeof(SQDataType));
		psl->capacity = newcapacity;
	}
}

//尾插
void SeqListPushBack(SLT* psl, SQDataType x)
{
	assert(psl);
	SeqListCheclCapacity(psl);
	psl->a[psl->size] = x;
	psl->size++;
}

//头插
void SeqListPushFront(SLT* psl, SQDataType x)
{
	assert(psl);
	SeqListCheclCapacity(psl);
	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->a[end + 1] = psl->a[end];
		end--;
	}
	psl->a[0] = x;
	psl->size++;
}

//尾删
void SeqListPopBack(SLT* psl)
{
	assert(psl);
	//assert(psl->size > 0); 不太温和的方式
	//温和一点
	if (psl->size > 0)
	{
		psl->size--;
	}
}

//头删
void SeqListPopFront(SLT* psl)
{
	assert(psl);
	int head = 1;
	while (head < psl->size)
	{
		psl->a[head - 1] = psl->a[head];
		head++;
	}
	//assert(psl->size > 0); 不太温和的方式
	//温和一点
	if (psl->size > 0)
	{
		psl->size--;
	}
}

//查找
int SeqListFind(SLT* psl, SQDataType x)
{
	assert(psl);
	for (int i = 0; i < psl->size; i++)
	{
		if (psl->a[i] == x)
		{
			return i;
		}
	}
	return -1;
}

//指定位置插入数值(可以代替头插和尾插)
void SeqListInsert(SLT* psl, size_t pos, SQDataType x)
{
	assert(psl);
	assert(pos <= psl->size);//插入的位置必须小于等于含有的数据数
	SeqListCheclCapacity(psl);

	size_t end = psl->size;
	while (end > pos)
	{
		psl->a[end] = psl->a[end - 1];
		end--;
	}
	psl->a[pos] = x;
	psl->size数据结构初阶:动态顺序表的功能实现(用C语言实现,附图分析)

数据结构顺序表(增删查改)的实现 [初阶篇_ 复习专用]

初阶数据结构——线性表——顺序表

数据结构初阶——顺序表

[ 数据结构--C语言 ]不收藏必后悔系列--二叉树初阶

[ 数据结构--C语言 ]不收藏必后悔系列--二叉树初阶