数据结构——顺序表专题


乐观学习,乐观生活,才能不断前进啊!!!

我的主页:optimistic_chen
我的专栏:c语言
点击主页:optimistic_chen专栏:c语言
创作不易,大佬们点赞鼓励下吧~

前言

什么是数据结构呢?
· 数据结构是由“数据”和“结构”两词组合⽽来。
· 概念:数据结构是计算机存储、组织数据的⽅式。
· 数据结构是指相互之间存在⼀种或多种特定关系的数据元素的集合。数据结构反映数据的内部构成,即数据由那部分构成,以什么⽅式构成,以及数据元素之间呈现的结构。
总结:
1)能够存储数据(如顺序表、链表等结构)
2)存储的数据能够⽅便查找

顺序表概念

线性表( linear list )是n个具有相同特性的数据元素的有限序列。线性表是⼀种在实际中⼴泛使⽤的数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

顺序表的分类

顺序表的底层结构是数组,对数组的封装,实现了常⽤的增删改查等接⼝

静态顺序表

struct SeqList
{
	int arr[100];//开少了不够,开多了浪费
	int size;//有效数据个数
};

typedef int SLDataType;

静态顺序表是一种在数据结构中常见的线性表实现方式,它使用定长数组来存储数据元素。这种数据结构在初始化时就确定了存储空间的大小,并在运行期间保持不变。

动态顺序表

typedef struct SeqList
{
	SLDataType* arr;//按需申请空间
	int size;//有效数据个数
	int capacity;//空间大小
}SL;

动态顺序表是一种可以动态调整其大小的顺序表,它克服了静态顺序表在大小固定方面的限制。

对比

分类静态顺序表动态顺序表
空间分配创建时就分配了固定的内存空间,这避免了在运行时频繁地申请或释放内存根据数据的实际需求动态地扩展或缩小存储空间,从而避免了静态顺序表在存储空间分配上可能出现的浪费或不足的问题
灵活性不灵活灵活性强
效率由于数据元素在内存中是连续存放的,因此可以通过下标直接计算出元素在内存中的位置,实现快速存取尽管动态顺序表能够动态调整大小,但在插入或删除元素时,仍可能需要移动其他元素以保持数据的连续性。这尤其在元素数量较大或需要频繁进行插入/删除操作时可能导致效率降低
管理静态顺序表的实现相对简单,因为数组的操作是基础的编程技能,所以理解和使用都比较容易动态顺序表需要维护额外的元数据(如当前大小、最大容量等),并需要实现相应的内存管理操作(如分配、释放等)。这增加了实现的复杂性和出错的可能性

总结:
静态顺序表在数据规模确定且不需要频繁改变的情况下表现良好,但在面对动态变化的数据规模时,其缺点就显得尤为突出。动态顺序表在处理动态变化的数据规模时具有较大的优势,但也需要考虑其带来的额外开销和实现的复杂性。在选择使用顺序表时,需要根据具体的应用场景和需求进行权衡。

顺序表的实现

我们以动态顺序表为例,完成顺序表的实现:

我们首先将顺序表的所有需求定义在一个头文件SeqList.h中。

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType
#define INIT_CAPACITY 4
/动态顺序表
typedef struct SeqList
{
	SLDataType* arr;
	int size;
	int capacity;//空间大小
}SL;

//顺序表初始化
void SLInit(SL* ps);

//顺序表的销毁
void SLDestroy(SL* ps);

//顺序表的扩容
void SLCheckCapacity(SL* ps);

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

//头部插入 / 尾部插入
void SLPushBack(SL* ps, SLDataType x);
void SLPushPront(SL* ps, SLDataType x);

//头部删除 / 尾部删除
void SLPopBack(SL* ps);
void SLPopPront(SL* ps);

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

//查找数据
int SLFind(SL* ps, SLDataType x);

我们后期的一系列操作都是根据头文件中的需求实现的。

一、顺序表的初始化

void SLInit(SL* ps)
{
	assert(ps);
 
	ps->arr = (SLDataType*)malloc(sizeof(SLDataType)* INIT_CAPACITY);
	if (ps->arr == NULL)
	{
		perror("malloc fail");
		return;
	}
 
	ps->size =0; 
	ps->capacity = INIT_CAPACITY;
}

代码分析:

断言(assert):

assert(ps);

断言用于检查传入的指针 ps 是否为 NULL。如果 ps 是 NULL,程序会在调试模式下终止执行,并显示一条错误消息。提前检查错误,确保ps 指向了一个有效的内存地址。
开辟内存空间

ps->arr = (SLDataType*)malloc(sizeof(SLDataType)* INIT_CAPACITY);

malloc 函数分配了 INIT_CAPACITY 个 SLDataType 大小的内存块,并将其地址赋值给 ps->arr。
这里SLDataType 是顺序表中存储的数据类型,而 INIT_CAPACITY 是一个常量,表示顺序表初始的容量
内存开辟失败:

if (ps->arr == NULL)  
 {  
 perror("malloc fail");  
 return;  
 }

如果malloc函数没有顺利开辟内存,那么就会打印错误信息,不执行后面的代码。
初始化顺序表:

    ps->size =0; 
	ps->capacity = INIT_CAPACITY;

size 表示顺序表中当前存储的元素数量,初始化为0,表示顺序表是空的。
capacity 表示顺序表的容量,即它可以存储的最大元素数量,初始化为 INIT_CAPACITY。

二、顺序表的销毁

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

代码分析:

释放内存:

if (ps->arr)
	{
		free(ps->arr);
	}

if 的判断条件为ps->arr不为NULL则释放arr所占用的空间
这是一个非常重要的步骤,因为如果不释放已分配的内存,会导致内存泄漏
重置指针和属性

ps->arr = NULL;
ps->size = ps->capacity = 0;

释放内存之后,将 ps->a 设置为 NULL 是一个好习惯,这可以避免在之后的代码中意外地使用已释放的内存。,避免野指针的出现。
此外,将 ps->capacity 和 ps->size 重置为0,确保顺序表的状态正确地反映了它已经被销毁。

三、顺序表的打印

void SLPrintf(SL* ps)//打印
{
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n");
}

代码分析:

for 循环,用于遍历顺序表中的每一个元素。
循环变量 i 从0开始,一直到 ps->size - 1(即顺序表中最后一个元素的索引)。
ps->size 表示顺序表中当前存储的元素数量

四、顺序表容量的检查

void SLCheckCapacity(SL* ps)//申请空间单独成一个函数
{
	if (ps->capacity == ps->size)
	{
		//申请空间(多大?一般2倍增容)
		int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * 2 * sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		//申请空间成功
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
}

SLCheckCapacity 函数用于检查顺序表的当前容量是否足够。如果顺序表已满(即 ps->size 等于 ps->capacity),则该函数会尝试扩大顺序表的容量

代码分析

检查容量:

if (ps->capacity == ps->size)

如果 ps->capacity==ps->size 说明顺序表已经没有额外的空间来存储新的元素了。

扩大容量:

SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * 2 * sizeof(SLDataType));

使用realloc函数重新分配内存,我们将顺序表的容量扩大两倍(ps->capacity*2)
realloc 函数尝试在内存中找到足够大的连续空间来容纳扩大后的顺序表,如果成功,它返回指向新内存块的指针;如果失败,它返回 NULL。

内存分配失败:

if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}

分配失败,返回值为NULL,满足 if 条件,我们使用 perror 函数打印一个错误消息(“realloc fail”),然后函数返回,不执行后续的代码。

更新指针和容量:

ps->arr = tmp;
ps->capacity = newCapacity;

分配内存成功,我们将 ps->arr 更新为指向新的内存块,并将 ps->capacity 更新为新的容量(newCapacity)。这样,顺序表就有了更多的空间来存储新的元素。

五、顺序表的头插

void SLPushPront(SL* ps, SLDataType x)//头插
{
	assert(ps);
	void SLCheckCapacity(SL * 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++;
}

代码分析

assert(断言):

assert(ps);

断言用于检查传入的指针 ps 是否为 NULL。如果 ps 是 NULL,程序会在调试模式下终止执行,并显示一条错误消息。提前检查错误,确保ps 指向了一个有效的内存地址。

开辟插入节点的空间

void SLCheckCapacity(SL * ps);

调用SLCheckCapacity函数开辟空间

顺序表中原有的所有元素向后移动一位:

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++;

将元素插入第一个(头部)位置,链表大小加一。

六、顺序表的尾插

void SLPushBack(SL* ps, SLDataType x)//顺序表的尾插
{
	assert(ps);
	//插入数据之前先判断空间够不够
    void SLCheckCapacity(SL * ps);
	//ps->arr[ps->size] = x;
	//++ps->size;
	ps->arr[ps->size++] = x;
}

代码分析

(断言与开辟新内存和前面的情况一样,接下来不进行阐述。)
尾部插入元素

ps->arr[ps->size] = x;
++ps->size;
或者
ps->arr[ps->size++] = x;

二者代码没有区别,只是第一种代码可读性较好,第二种更加体现逻辑性
我们以第一种为例:
新元素 x 被添加到顺序表的末尾,并更新顺序表的大小。

七、 顺序表的头删

void SLPopFront(SL* ps)
{
	assert(ps);
	assert(ps->size》0);
 
	int pos = 1;
	while (pos < ps->size)
	{
		ps->arr[pos - 1] = ps->arr[pos];
		++pos;
	}
 
	ps->size--;
}

代码分析

前移元素

while (pos < ps->size)
	{
		ps->arr[pos - 1] = ps->arr[pos];
		++pos;
	}

它使用一个 while 循环将顺序表中从第二个元素开始的所有元素向前移动一位,以便删除第一个元素。

更新顺序表大小

ps->size--;

八、顺序表的尾删

void SLPopBack(SL* ps)//尾删
{
	assert(ps);
	assert(ps -> size);//顺序表不为空
	//ps->arr[ps->size - 1] = -1;
	--ps->size;//直接减去空间
}

代码分析

删去数据,直接减去空间,最后记得更新顺序表大小

九、顺序表指定位置之前插入数据

void SLInsert(SL* ps, int pos, SLDataType x) 
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
	//插入数据:空间够不够
	void SLCeckCapacityCapacity(ps);
	//让pos之后的数据整体往后移动一位
	for (int i = ps->size; i>pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];//arr[pos+1]=arr[pos]
	}
	ps->arr[pos] = x;
	ps->size++;
}

代码示例:

assert(断言)

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

确保pos的位置在顺序表中

检查容量

void SLCeckCapacityCapacity(ps);

调用SLCeckCapacityCapacity函数开辟内存空间。

移动元素

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

这一部分代码负责将位置 pos 及之后的元素向后移动一位,以便为新元素腾出空间。

插入新元素

ps->arr[pos] = x;

在移动完元素后,将新元素 x 插入到位置 pos。

更新链表大小

ps->size++;

需要注意的是,由于顺序表在内存中是连续存储的,插入操作的时间复杂度是 O(n),其中 n 是顺序表的大小。如果频繁进行插入操作,可能会影响程序的性能

十、顺序表指定位置之前删除数据

void SLFrase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size );
	for (int i = pos; i < ps->size-1; i++)
	{
		ps->arr[i] = ps -> arr[i + 1];
	}
	ps->size--;
}

代码示例:

移动元素

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

这一部分代码负责将位置 pos + 1 及之后的元素向前移动一位,以覆盖掉位置 pos 的元素

更新链表大小

ps->size--;

需要注意的是,SLFrase 函数并没有真正地释放被删除元素所占用的内存空间。它只是通过移动后续元素来覆盖掉被删除的元素,并将顺序表的大小减1

十一、顺序表的数据查找

//查找数据
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;
}

代码示例:

遍历顺序表

for (int i = 0; i < ps->size; i++)

使用一个 for 循环遍历顺序表中的每一个元素。
循环变量 i 从0开始,直到 ps->size-1 (顺序表中最后一个元素的索引)

查找元素

if (ps->arr[i] == x)
		{
			//找到了
			return i;
		}

在循环体内部,检查当前元素 ps->arr[i] 是否等于要查找的元素 x。
如果相等,说明找到了元素 x,函数立即返回该元素在顺序表中的位置 i。

没有找到

//没找到
return -1;

如果循环结束后都没有找到元素 x,函数返回 -1,表示在顺序表中未找到该元素。

完整代码

SeqList.h

//动态顺序表
typedef struct SeqList
{
	SLDataType* arr;
	int size;
	int capacity;//空间大小
}SL;

//typedef struct SeqList SL;

//顺序表初始化
void SLInit(SL* ps);
//顺序表的销毁
void SLDestroy(SL* ps);
  
void SLPrintf(SL s);
//头部插入 / 尾部插入
void SLPushBack(SL* ps, SLDataType x);
void SLPushPront(SL* ps, SLDataType x);

//头部删除 / 尾部删除
void SLPopBack(SL* ps);
void SLPopPront(SL* ps);

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

//查找数据
int SLFind(SL* ps, SLDataType x);

SeqList.c

#include"SeqList.h"
void SLInit(SL* ps)//初始化
{
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

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

void SLPushBack(SL* ps, SLDataType x)//顺序表的尾插
{
	assert(ps);
	//插入数据之前先判断空间够不够
	if (ps->capacity == ps->size)
	{
		//申请空间(多大?一般2倍增容)
		int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDataType*tmp = (SLDataType*)realloc(ps->arr,newCapacity * 2 * sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		//申请空间成功
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
	//ps->arr[ps->size] = x;
	//++ps->size;
	ps->arr[ps->size++] = x;
}

void SLCheckCapacity(SL* ps)//申请空间单独成一个函数
{
	if (ps->capacity == ps->size)
	{
		//申请空间(多大?一般2倍增容)
		int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * 2 * sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		//申请空间成功
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
}

void SLPushPront(SL* ps, SLDataType x)//头插
{
	assert(ps);
	void SLCheckCapacity(SL * 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++;
}

void SLPrintf(SL s)//打印
{
	for (int i = 0; i < s.size; i++)
	{
		printf("%d ", s.arr[i]);
	}
	printf("\n");
}

void SLPopBack(SL* ps)//尾删
{
	assert(ps);
	assert(ps -> size);//顺序表不为空
	//ps->arr[ps->size - 1] = -1;
	--ps->size;//直接减去空间
}

void SLPopPront(SL* ps)
{
	assert(ps);
	assert(ps->size);
	
	//数据整体往前一位
	for (int i = 0; i < ps->size-1;i++)
	{
		ps->arr[i] = ps->arr[i + 1];//arr[size-2]=arr[size-1]
	}
	ps->size--;
}

//在指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDataType x) 
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
	//插入数据:空间够不够
	void SLCeckCapacityCapacity(ps);
	//让pos之后的数据整体往后移动一位
	for (int i = ps->size; i>pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];//arr[pos+1]=arr[pos]
	}
	ps->arr[pos] = x;
	ps->size++;
}

//在指定位置之前删除数据
void SLFrase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size );
	for (int i = pos; i < ps->size-1; i++)
	{
		ps->arr[i] = ps -> arr[i + 1];
	}
	ps->size--;
}

//查找数据
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;
}

test.c

void TestSeqList1()
{
	SL s;
	SLInit(&s);
	SLPushBack(&s, 1);
	SLPushBack(&s, 2);
	SLPushBack(&s, 3);
	SLPushBack(&s, 4);
	SLPrint(&s);
 
	SLPopBack(&s);
	SLPopBack(&s);
	SLPrint(&s);
 
	SLPopBack(&s);
	SLPopBack(&s);
	SLPopBack(&s);
	SLPopBack(&s);

	//SLPopBack(&s);
	SLPrint(&s);
 
	SLPushBack(&s, 10);
	SLPushBack(&s, 20);
	SLPrint(&s);
 
	SLDestroy(&s);
}
 
int main()
{
	TestSeqList1();
 
	return 0;
}

完结

本次博客到此结束
祝开心每一天~~~
最后觉得博客有帮助,可以点点关注,支持一下,期待下次博客~~~

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值