Bootstrap

数据结构-顺序表-详解

1.是什么

顺序表是一种基本的数据结构,它使用一组连续的内存空间来存储数据元素,这些元素在逻辑上也是连续的
顺序表中,每个元素都占据一个特定的位置,可根据位置找到元素。

在生活中常见于各种联系人列表,如QQ群的群成员列表。

2.静态顺序表

2.1实现

根据顺序表的要求,可使用数组存储成员,并添加一个变量表个数:

struct SeqList
{
	int a[20];
	int size;
};

在这个简单的实现中,定义了一个结构体SeqList,其中,包含了一个固定大小的数组a和一个整型变量size
数组a用于存储实际的数据,而size则记录了当前数组中的数据数量。

上面的顺序表比较简陋,可以加点东西修饰:

  • 表示成员个数
#define N 20
  • 表示成员类型
typedef int SLDataType

改版:

struct SeqList
{
	SLDataType a[N];
	int size;
};

这样的改进可使成员类型和最初数量的修改更加高效。

关于命名

  • SeqListsequencelist的缩写,表示顺序表
  • SLDataTypeSeqListDataType的缩写,表示顺序表的成员类型

2.2缺点

静态顺序表的缺点十分明显,即它的成员数量是固定的。
使用静态顺序表永远无法准确满足需求,如果预先分配的空间过大,会造成内存浪费;如果分配的空间过小,可能会导致空间不足。
综上,我们需要一种更灵活的方法,即动态顺序表。

3.动态顺序表

3.1总览

  • 动态顺序表是一种更加灵活的顺序表实现方式,可根据需要动态地调整存储空间大小。
  • 数据结构中最主要的就是对数据进行管理,而管理的需求一般分为四类:增、删、查、改。这同样是实现动态顺序表时必须完成的部分。
  • 为了更好地组织代码,动态顺序表在实现时建议放入三个文件中:
    在这里插入图片描述
SeqList.hSeqList.ctest.c
头文件源文件测试文件
引用头文件,函数声明,宏定义,结构体定义具体的函数实现验证功能是否正确

.c文件都只需引用SeqList.h,可避免头文件的重复引用。
注:引用自建的头文件使用双引号

#include "SeqList.h"

3.2动态顺序表的创建

动态顺序表中,不能再用数组,而改为指针
且静态顺序表只准备了size来存储当前数量,此时,还需一个变量存储当前最大数量,以免溢出。
SeqList.h

struct SeqList
{
	SLDataType* a;
	int size;
	int capacity;
};

其中,当空间容量capacity与有效数据个数size相等,需扩容。
可重命名简化类型:

typedef struct SeqList SeqList

3.3初始化

初始化函数应该确保结构体的成员被适当地设置,并且为数据分配足够的初始空间。
SeqList.h

void SeqListInit(SeqList* ps);
#define INIT_CAPACITY 3

SeqList.c
有两种方法,法一:

void SeqListInit(SeqList* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->size = ps->capacity = 0;
}

这种方法不推荐,也意义不大,因为结构体变量创建后自动初始化为0
法二:

void SeqListInit(SeqList* ps)
{
	assert(ps);
	ps->a = (SLDataType*)malloc(sizeof(SLDataType)*INIT_CAPACITY);
	if (!ps->a)
	{
		perror("SeqListInit::malloc");
		return;
	}
	ps->size = 0;
	ps->capacity = ;INIT_CAPACITY
}

使用malloc函数为数组a分配初始容量大小的空间。
如果内存分配失败,将输出错误信息并返回。
最后,它将size设置为0,并将capacity设置为初始容量。

关于命名
SeqListInitSeqListinitialize的缩写,表示初始化顺序表
INIT_CAPACITYinitializecapacity的缩写,表示初始空间容量

3.4销毁

销毁函数负责释放分配给顺序表的内存。
SeqList.h

void SeqListDestroy(SeqList* ps);

SeqList.c

void SeqListDestroy(SeqList* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->size = ps->capacity = 0;
}

使用free函数释放之前通过malloc分配的内存,并将指针设置为NULL,以防止野指针的产生。
最后,将sizecapacity设置为0,表示顺序表已经被清空。

注:多个等号,从右向左赋值。

3.5打印

打印函数用于输出顺序表中的所有元素。
SeqList.h

void SeqListPrint(const SeqList* ps);

SeqList.c

void SeqListPrint(const SeqList* ps)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
}

遍历所有元素,并打印。
这里建议用const SeqList* ps作为参数类型,表示这个函数不会修改顺序表的内容。

3.6插入

尾插

SeqList.h

void SeqListPushBack(SeqList* ps, SLDataType x);

SeqList.c
要在顺序表尾部插入一个数据特别简单,因为,当前数量size就是最后一个数据的下一个位置,即,插入元素的下标。
可以很快写出:

void SeqListPushBack(SeqList* ps, SLDataType x)
{
	assert(ps);
	ps->a[ps->size] = x;
	ps->size++;
}//有bug

还能将函数最后的两句合并:

ps->a[ps->size++] = x;

现在可以写个程序测试一下:
test.c

#include "SeqList.h"
int main()
{
	SeqList s;
	SeqListInit(&s);
	for (int i = 0; i < 5; i++)
	{
		SeqListPushBack(&s, i);
		SeqListPrint(&s);
		printf("\n");
	}
	SeqListDestroy(&s);
	return 0;
}

运行结果:
在这里插入图片描述

内容打印出来了,但是报错了,为什么?可以调试一下,最终发现是free出了问题。

free出问题主要就两种情况:
1. 野指针,即从中间位置开始释放 1.野指针,即从中间位置开始释放 1.野指针,即从中间位置开始释放
2. f r e e 的空间有越界 2.free的空间有越界 2.free的空间有越界

在这里不可能是情况一,毕竟ps->a从未改过。
因此,空间越界,检查一下SeqListPushBack函数,可以发现,插入前,需检查是否需要扩容

修改后:

void SeqListPushBack(SeqList* ps, SLDataType x)
{
	assert(ps);
	if (ps->size == ps->capacity)
	{
		SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * 2 * ps->capacity);
		if (!tmp)
		{
			perror("SeqListPushBack::realloc");
			return;
		}
		ps->a = tmp;
		ps->capacity *= 2;
	}
	ps->a[ps->size++] = x;
}

在插入新元素之前,首先检查当前的size是否等于capacity。如果是,则说明需要扩容。
这里使用realloc函数重新分配内存,并将新分配的内存地址赋值给a。如果内存分配失败,将输出错误信息并返回。
最后,将新元素插入到数组的末尾,并将size加一。

头插

与尾插的实现基本类似。
SeqList.h

void SeqListPushFront(SeqList* ps, SLDataType x);

SeqList.c

void SeqListPushFront(SeqList* ps, SLDataType x)
{
	assert(ps);
	if (ps->size == ps->capacity)
	{
		SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * 2 * ps->capacity);
		if (!tmp)
		{
			perror("SeqListPushBack::realloc");
			return;
		}
		ps->a = tmp;
		ps->capacity *= 2;
	}
	for (int i = ps->size; i > 0; i--)
	{
		ps->a[i] = ps->a[i - 1];
	}
	ps->a[0] = x;
	ps->size++;
}

在头插操作中,首先检查是否需要扩容。如果需要扩容,则按照尾插的方式进行。
接着,使用一个循环将已有的元素向后移动一位,以便为新元素腾出头部位置。
最后,将新元素插入到数组的起始位置,并将size加一。
测试一下:
test.c

#include "SeqList.h"
int main()
{
	SeqList s;
	SeqListInit(&s);
	for (int i = 0; i < 5; i++)
	{
		SeqListPushFront(&s, i);
		SeqListPrint(&s);
		printf("\n");
	}
	SeqListDestroy(&s);
	return 0;
}

运行结果:
在这里插入图片描述

关于命名
PushBack,表尾插
PushFront,表头插
好像STL库中是这样命名的,我还没学,不清楚。

3.7删除

尾删

SeqList.h

void SeqListPopBack(SeqList* ps);

SeqList.c

void SeqListPopBack(SeqList* ps, SLDataType x)
{
	assert(ps);
	//assert(ps->size > 0);
	if (ps->size == 0)
		return;
	ps->a[ps->size-1] = 0;//不需要
	ps->size--;
}

上面写了句废话:

ps->a[ps->size-1] = 0;

回顾一下顺序表的要求:从开始位置连续储存,这里是size个数据。
我们是用size来遍历,因此size--就足够了,这使最后一个数据的空间不会被访问。
且这里将数据置为0也极不合理,因为并不能确定原数据会存什么。

这里,还需防止数据被删完了还在删,有两种检查方法:

  • 暴力检查
assert(ps->size > 0);

使用assert断言数据个数大于0,否则终止程序,并报错。

  • 温柔的检查
if (ps->size == 0)
	return;

当数据个数等于0直接返回,无其他处理。

头删

SeqList.h

void SeqListPopFront(SeqList* ps);

SeqList.c

void SeqListPopFront(SeqList* ps)
{
	assert(ps);
	for (int i = 0; i < ps->size - 1; i++)
	{
		ps->a[i] = ps->a[i + 1];
	}
	ps->size--;
}

在头删操作中,同样先检查size是否为0。如果不是,则使用一个循环将后面的元素向前移动一位,以覆盖掉原本位于头部的元素。
最后,将size减一,表示删除了一个元素。

关于命名
PopBack,表尾删
PopFront,表头删


在头插,头删时,可以发现操作较为繁琐,时间复杂度O(N),此时,顺序表不合适,可用链表。


希望本篇文章对你有所帮助,并激发您进一步探索数据结构和算法的兴趣!

本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

相关文章:
C语言实现简单的通讯录
C语言实现通讯录-动态版本与文件版本

;