数据结构初阶——顺序表专题

前言

本篇博客将为大家介绍数据结构中的顺序表的内容,它数据数据结构中的基础内容,希望所写内容能够为大家带来帮助,也希望大家多多支持;下面进入正文部分

1.数据结构的概念

数据结构::数据结构是计算机存储、组织数据的方式 

我们前面学过数组,数组就是一种数据结构,它是最基础的数据结构。

我们今天所要讨论的顺序表,其底层就是数组

2. 顺序表

顺序表是线性表的一种,什么是线性表呢?

线性表是相同特性的数据类型的集合,它在物理结构上不一定连续,在逻辑结构上是连续的;

而我们今天要说的顺序表是线性表的一种;

顺序表的特性:物理结构连续,逻辑结构连续。

顺序表可以分为两种:一种是静态顺序表,还有一种是动态顺序表;

2.1 静态顺序表

大家可以看到,上面就是静态顺序表的结构,包括一个定长数组、还有顺序表当前有效的数据个数。

2.2 动态顺序表

上面展示了动态顺序表的结构,大家发现,它与静态顺序表相比,多了一个成员,并且将定数组换成了指针的形式。

那么,请大家思考,静态顺序表和动态顺序表哪个更好呢?

其实这个问题显而易见,当然是动态顺序表更加实用。如果我们使用静态顺序表去存储一组不确定的信息,那么我们很难去精确地得知要存储多少,一旦我们定长数组的大小给的小了,那就会导致空间不够用,从而丢失信息;反之,如果我们害怕空间小了,一上来就给了一个很大的空间,那么这样也存在很大的问题,就是如果我们需要的空间远小于我们申请的空间,那将造成空间的浪费。

所以推荐大家使用动态顺序表,它可以动态增容,我们想要多少就申请多少,这样就不会担心空间不够或者空间大量浪费的问题

3. 顺序表的实现

关于顺序表的实现,我们需要三个文件,分别是头文件,顺序表的方法文件,测试文件;具体请大家仔细看下面的代码!!!

我将把整个代码拆解成各个小的部分来说明,最后会为大家展示完整代码,下面请大家认真阅读下面的内容。

首先,我们来将顺序表的结构写入头文件,大家看下面的代码

typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* arr;
	int size;
	int capacity;
}SL;

 这里大家要注意,我将int定义成了SLDataType,这么做是为了我们后面方便修改代码,如果我们需要把int改为char,那么我们只需要把第一行的int改为char,这样后面的代码中的int就统一改成了char,实现了“一劳永逸”的效果。

3.1顺序表的初始化

首先我们需要在头文件中声明一下

void SLInit(SL* ps);

这里声明完之后,下面我们就需要在方法文件中写出具体的初始化的方法了

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

这里大家需要注意,我们必须得用指针去接受,也就是传址调用;当我们写完上面的代码后,下面我们需要在测试文件中测试一下,看看写的代码是否正确。

void SLtest01()
{
	SL sl;
	SLInit(&sl);
}
int main()
{
	SLtest01();
	return 0;
}

 这里就是测试代码,我们通过调试,可以得到下面的结果

大家可以发现,这里我们就初始化成功了,与我们在方法文件中的要求一样。 

3.2 顺序表的销毁

与初始化相同,我们需要在头文件中进行声明;

void SLDestroy(SL* ps);

声明完成后,我们在方法文件中写具体的销毁方法

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

这里大家发现,顺序表销毁的代码与初始化的代码比较类似,我们首先需要判断顺序表中的数组是否为空,如果有空间,那我们就需要进行销毁,这里用到free;然后我们需要将其置为空指针,并且将size和capacity都置为0。 

上面我们讨论完了初始化和销毁,下面来进行具体的增删查改的操作

3.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(int));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(1);
		}
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
}

这里大家需要注意几点,首先,我们需要使用realloc来进行动态增容,并且判断空间是否足够,这里当size等于capacity时,就说明空间已经满了;

其次,我们要创建临时的指针变量,因为我们申请空间可能会失败,为了不让原始数据受到影响,我们需要创建临时的指针变量去保存我们申请的空间;同理,我们还创建了newCapacity来保护原始空间;

在申请空间的过程中,我们要进行强制类型转换,并将申请到的空间存放在临时的指针变量中;

中间加上一句判断,一旦申请失败,程序会直接报错并退出;

最后,我们将申请完的空间交给顺序表即可。

3.4 顺序表的尾插

首先,还是先进行声明

void SLPushBack(SL* ps, SLDataType x);

声明完后,我们来写具体的尾插方法

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

大家可以看到,尾插的代码很简单,这里大家需要注意,在我们尾插之前,需要进行assert断言。防止传入的是NULL,然后检查空间是否足够并进行增容操作;最后我们进行尾插。尾插完成后。我们需要让size自增。

3.5 顺序表的头插

第一步,先声明

void SLPushFront(SL* ps, SLDataType x);

 下面写出头插的具体代码

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[0] = x;
	ps->size++;
}

这里头插的过程就是将顺序表的每个数据都向后移动一位,并且是从后往前进行移动,最终会将首位空出来,这个时候我们将我们想要插入的数据插入到首位,这样就实现了头插的效果,最后,大家一定不要忘了让size自增,我们插入了数据,size就得跟着增大。

下面我们要进行测试;这里我们可以再写一个打印函数,将尾插和头插的结果全部打印出来;

我们在头文件中声明一下打印函数

void SLPrintf(SL s);

 然后我们写出具体代码

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

这里就是一个简单的循环,我们去打印每个元素;

下面,统一为大家展示尾插和头插的运行结果

大家可以看到,上面我们尾插和头插都实现了理想的效果。

3.6 顺序表的尾删

 前面说完了插入。下面来进行删除操作,咱们先来看尾删

第一步,还是先在头文件中声明

void SLPopBack(SL* ps);

 然后,写出具体的尾删代码

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

大家会发现。尾删代码很简洁,我们在进行删除操作前。需要保证顺序表不为空。也就是第二个断言;最后一行代码其实比较巧妙,我们直接让size自减,这样就相当于将尾部最后一个数据删除。 

下面来测试尾删代码

  

大家可以看到,尾删代码是成功的。

3.7 顺序表的头删 

和前面一样,还是先在头文件中声明

void SLPopFront(SL* ps);

 下面来写具体头删代码

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

大家可以发现,头删的代码和头插有些类似,只不过头删是要求我们让后一位的数据覆盖其前一位的数据,其实就是将顺序表的每个数据都相前移动一位,并且是从前往后进行移动;当头删操作完成后,我们需要让size自减,因为我们删除了一个数据。

最后,我们来测试一下头删代码,大家请看

这里我们成功地实现了头删的效果,证明我们的头删代码是正确的。这里还需要给大家提醒一下,在删除的时候,我们不能将顺序表删为空! 

3.8 在指定位置之前插入数据

首先,还是进行声明

void SLInsert(SL* ps, int pos, SLDataType x);

下面,我们来写出具体代码

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

这里我们在进行指定位置插入时, 我们需要注意对pos的限制,首先“数组”的下标不能为负,其次pos也不能太大,不能超过size;前面说过,插入数据的时候,我们必须要判断顺序表的空间是否足够,所以我们在插入之前,需要先进行SLCheckCapacity;

再往下就与头删的代码很相似了,头删是将所有数据向后挪动一位;而现在我们需要将pos后的数据向后挪动一位,并且是从后往前进行挪动,这样我们就可以将pos这个位置空出来,将我们想要插入的数据进行插入,最后,我们要让size自增。因为我们增加了一个数据。

写完了方法,我们来测试一下;

大家可以看到,我们分别在不同位置实现了数据的插入,证明我们的代码是正确的.

3.9 删除指定位置的数据

上面说完了插入,现在来看删除数据

首先还是进行声明

void SLErase(SL* ps, int pos);

下面来实现删除代码

void SLErase(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--;
}

这里大家需要注意,在对pos限制时,与插入的代码有一点区别,就是pos不能等于size,因为size位置是没有数据的,所以也就不存在删除;然后具体删除本质上就是将pos后的数据向前移动一位,并且是从前往后进行移动最后,我们需要让size自减,因为我们删除了一个数据。

下面我们来进行测试

 大家可以发现,我删除了下标为1的数据,也就是第二个位置的数据;实现了想要的效果,证明我们的代码是正确的。

3.10 查找数据

 第一部还是先声明

int SLFind(SL* ps, SLDataType x);

下面来写查找代码

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(数组的下标不能为负)。

最后我们来进行测试

 

大家发现,我们可以找到对应的数据,说明代码没有问题。 

4. 顺序表的完整代码

OK,到这里,顺序表的内容就为大家介绍完毕了,下面为大家展示完整的代码,仅供参考

首先,来展示头文件SeqList.h

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* arr;
	int size;
	int capacity;
}SL;
void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrintf(SL s);
void SLCheckCapacity(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);
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)
{
	if (ps->arr)
	{
		free(ps->arr);
	}
	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(int));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(1);
		}
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
}
void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);
	SLCheckCapacity(ps);
	ps->arr[ps->size++] = x;
}
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[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->size--;
}
void SLPopFront(SL* ps)
{
	assert(ps);
	assert(ps->size);
	for (int i = 0; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i+1];
	}
	ps->size--;
}
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
	SLCheckCapacity(ps);
	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);
	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

#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);
	SLPrintf(sl);
	SLPushFront(&sl, 5);
	SLPushFront(&sl, 6);
	SLPrintf(sl);
	SLPopFront(&sl);
	SLPrintf(sl);
	SLPopFront(&sl);
	SLPrintf(sl);
	SLPopFront(&sl);
	SLPrintf(sl);
	SLPopBack(&sl);
	SLPrintf(sl);
	SLInsert(&sl, 0, 9);
	SLPrintf(sl);
	SLInsert(&sl, sl.size, 8);
	SLPrintf(sl);
	SLInsert(&sl, 1, 99);
	SLPrintf(sl);
	SLErase(&sl, 0);
	SLPrintf(sl);
	SLErase(&sl, 3);
	SLPrintf(sl);
	SLErase(&sl, 1);
	SLPrintf(sl);
	int find = SLFind(&sl, 4);
	if (find < 0)
	{
		printf("没找到\n");
	}
	else
	{
		printf("找到了,下标为%d\n", find);
	}
	SLDestroy(&sl);
}
int main()
{
	SLtest01();
	return 0;
}

5. 总结  

到这里,关于顺序表的内容就全部为大家介绍完了,希望大家认真去理解每一小部分的代码,注意其中的坑点,最终可以自己写出顺序表的所有内容;顺序表是数据结构中的基础内容,所以初学者一定要掌握好顺序表,为后面高阶数据的学习打好基础;最后,希望本篇博客的内容可以为大家带来帮助,如有错误,欢迎大家评论交流,谢谢!  

  • 55
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值