基础顺序表

顺序表分类

        顺序表分为动态顺序表和静态顺序表,他们的基本结构是怎么展现的呢?

#define N 100
//静态顺序表
struct Seqlist
{
	int arr[N];//存放数组大小
	int size;//有效存储数据个数
};

        静态顺序表只有两个成员,首先是存放数据数组,其次我们还要知道有效的数据个数,其存放数据大小是一开始便已经确定的,并不灵活!

再看动态顺序表

struct Seqlist
{
	int* arr;//存放数组大小
	int size;//有效存储数据个数
	int capacity;//开辟空间大小
};

        为啥这里是int* ?当然是因为我们需要动态开辟内存空间,既然是动态,当然要用到动态内存管理的知识 ,让我们的arr依据存储数据size需求量,对数组空间进行扩容或缩容。这里多了一个capacity表示开辟空间的大小。为啥要这样设计,因为有效存储数据size的大小并不一定等于空间capacity的大小,这点我们接下来就会明白。

        其实动态和静态都能解决问题,可是对于静态顺序表来说,一开始我们也不知道到底需要存储多少数据,就像抖音的老板一样,他也不知道自己的产品需要存储多少用户信息。万一初始N给小了,就放不了太多,容易导致数据丢失;万一初始N给大了,会占用很大的内存空间!

        因此,对比动态和静态顺序表,我们可以清楚的知道动态顺序表更有优势,接下来就是实现一个简单的动态顺序表。

动态顺序表的具体实现

初始化

        初始化其实很简单,用一个封装函数,让arr指针为空,size和capacity都为0。也可以用malloc初始扩充一定的空间,不多赘述。

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

        这里改良一下,将int重定义位SLDataType是为了以后方便修改数据

        万一我们以后要存储字符串咋办,很多涉及到int这个关键字的地方肯定不能全用VS的替换吧,当然不可能啊,以后代码万一增多,手动修改出现错误或误操作,代码可能直接崩溃,这是很可怕的事情。

        同时为了避免每次传结构体都要加上struct,把它重新命名为SL,这样会很方便!

        那我初始化函数这样写对不对?

        相信你们判断得没错,当然是不对的,我们渴望修改结构体里的数据,从而初始化。根据函数调用中实参和形参的关系可知,形参只是形参的临时拷贝,改变形参不会改变实参,所以传值调用是不合法的(建议可以了解一下函数栈帧的知识),对于用函数交换两个变量,我们是不是只能传两个变量的地址啊,这里也一样。

typedef int SLDataType;
typedef struct Seqlist
{
	
	SLDataType* arr;//存放数组大小
	int size;//有效存储数据个数
	int capacity;//开辟空间的大小
}SL;

这样写是不是没有问题了,我们经过调试,发现初始化成功了。

也可用malloc初始化先开辟小块空间,这里就不再赘述。

销毁

        有初始化必然就有销毁!销毁就必然要将开辟的内存空间重新还给操作系统,并将size和capacity置0。

void Seqlist_Destroy(SL* ps)
{
	if (ps->arr) //等价于  if(ps->arr != NULL)
	{
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

        这里不要忘了将arr手动置NULL。为啥啊?我们用free函数将内存释放只是失去了访问之前开辟空间的能力,首地址还是那个地址没有变,可是已经没有明确的指向,构成野指针,而且free不会自动置空。

        补充一点,例如return 返回值,我们调用一个函数来返回一个结果,我们是不是一般都是返回一个值啊,万一返回一个地址咋办,而且这个地址是正常的吗?

以一道题目为例:

char* GetMemory()
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

        可以运行吗?

抱歉,编译器直接报错。原因嘛和上面一样啊,这个p地址还是那个地址,但是GetMemory函数执行完后p指向的空间还给操作系统了,没有访问权限,就会指向不明确,所以p为野指针,不能正确执行程序。就算有些编译器可以成功运行此程序,那也是非法访问。

插入数据

        有很多种插入方法,我们以头插和尾插为例:

尾插

        

//尾插
void SLPushBack(SL* ps, SLDataType x)//x为插入数据
{
	assert(ps);
	SLCheckOut(ps);
	ps->arr[ps->size++] = x;
}

        直接这样写行不行?试想,万一capacity和size相等,即空间大小等于有效个数大小,是不是没有空间了,需要扩容。还有如果capacity为0,arr的地址是无效的哦,也不能插入数据吧!

        因此需要写个判断函数:

void SLCheckOut(SL* ps)
{	
	if (ps->capacity == ps->size)
	{
		int NEWCapacity = (!ps->capacity) ? 4 : 2 * ps->capacity;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, NEWCapacity * sizeof(SLDataType));
		//空间开辟失败
		if (!tmp)
		{
			perror("realloc");
			exit(1);
		}
		//空间开辟成功
		ps->arr = tmp;
		ps->capacity = NEWCapacity;
	}
}

注意:

1、capacity == size怎么办?说明此时需要先扩容再插入数据。

2、capacity为0怎么办?所以要单独先判断capacity,为0就给个初始空间为4(随便给,别太离谱),不为0,扩容两倍。

3、realloc万一开辟失败怎么办?需要报错,来终止程序,开辟内存成功后,才能进行插入数据操作。因为一旦realloc开辟失败,不光不能解决问题,同时还有极大可能把我原来的数据删掉,这不是帮倒忙吗,因此我们使用realloc必须考虑这一点。

4、万一ps接收到空指针怎么办?那就无法实现插入数据,因此要用assert断言一下(可以去网上找一下assert的用法)其他插入删除数据同理

5、"exit(1)":只是一个简单的退出程序语句。

6、size表示有效数据个数,其初始值为0,所以插入完要++,因为是后置++,在插入数据前不会影响,也可以分开写,无所谓。

 我们测试一下:

插入成功。

头插

老规矩还是先整个图,方便理解:

看到这里是不是就明白了:

void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);
	SLCheckOut(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 PrintSeqlist(SL* ps)
{
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n");
}

        是不是很简单。

        还记得刚才在头插中ps->size为啥要加啊,其实与尾插一样,既然你插入了一个数据,那么对于有效数据size来说,肯定要增加1才符合逻辑。

无“ps->size++”:

有“ps->size++”:

删除数据

        显然对应前面插入数据有尾删和头删两种情况。

尾删

        

据图知:

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

下面是测试结果:

注意:

1、不需要考虑arr里面的数据是啥,所以我们只需要打印有效数据就可以。并不需要对要删除的数据做处理。

2、还是要断言一下,如果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--;
}

注意:

1、assert(ps->size)是判断顺序表是否为空,要是size为0,虽然打印有一个数据,但是此时无效数据为0。

下面是测试:

任意位置的插删数据

插入

        因此,就是要将pos位之后的数据像右移动一位,再把要放入的数据赋值给数组第pos位,最后有效数据个数加一

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

那我们还是测试一下:

由此可知,头插尾插也是这种特殊情况。

删除

据图知,将pos位之后的数据向前移动,移动位置不在pos位前,最后有效数据个数减一。

//任意位置删除
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	SLCheckOut(ps);
	for (int i = pos; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];//ps->arr[ps->size - 2] = ps->arr[ps->size - 1]
	}
	ps->size--;
}

测试一下:

注意:

1、用assert断言pos,让pos在安全且合法的范围内。

2、插入数据时,可以把数据插入到下标size上,而删除数据时,不能删除下标size-1后面的数据。

 还是用图来说明一下:

相信看完这个图,你就会明白了。

查找

//查找
int FindSL(SL* ps, SLDataType x)
{
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			//找到了
			return i;
		}
	}
	//没有找到
	return -1;
}

        这里我用下标i来遍历数组中的元素,如果找到数据返回下标,如果没找到返回-1,其中-1是随便给的数,目的是为了与数组下标区分开。

补充

        关于扩容的规则,可以参考一下这篇博客,扩容规则的推导涉及到概率论的知识,作者还不会。

http://t.csdnimg.cn/BGiZa

总结

        顺序表的本质就是对一个数组增删查改,顺序表是属于线性表的一种。顺序表物理和逻辑结构都是线性的,而线性表不同的地方是其只在物理结构是非线性,逻辑结构依然是线性的!

源码

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "sqlist.h"
//测试顺序表
void test()
{
	//声明一下结构体
	SL sl;
	/*初始化*/
	Seqlist_Init(&sl);
	SLPushBack(&sl, 1);
	SLPushBack(&sl, 2);
	SLPushBack(&sl, 3);
	PrintSeqlist(&sl);
	SLInsert(&sl, 0, 9);
	SLInsert(&sl, 2, 8);
	//SLPushFront(&sl, 5);
	//SLPushFront(&sl, 6);
	//SLPushFront(&sl, 7);
	//SLPopBack(&sl);//尾删
	//SLPopFront(&sl);//头删
	SLInsert(&sl, sl.size, 7);
	PrintSeqlist(&sl);

	SLErase(&sl, 2, 8);
	SLErase(&sl, 0, 9);	
	SLErase(&sl, sl.size - 1, 7);
	PrintSeqlist(&sl);
	/*销毁*/
	Seqlist_Destroy(&sl);
	printf("\n");
	int ret = FindSL(&sl, 4);
	if (ret < 0)
	{
		printf("没找到");
	}
	else
	{
		printf("找到啦");
	}
}
int main()
{
	test();
	return 0;
}

sqlist.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "sqlist.h"

void Seqlist_Init(SL* ps)
{
	ps->arr = NULL;
	ps->capacity = ps->size = 0;
}
void Seqlist_Destroy(SL* ps)
{
	if (ps->arr) //等价于  if(ps->arr != NULL)
	{
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}
void SLCheckOut(SL* ps)
{	
	if (ps->capacity == ps->size)
	{
		int NEWCapacity = (!ps->capacity) ? 4 : 2 * ps->capacity;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, NEWCapacity * sizeof(SLDataType));
		//空间开辟失败
		if (!tmp)
		{
			perror("realloc");
			exit(1);
		}
		//空间开辟成功
		ps->arr = tmp;
		ps->capacity = NEWCapacity;
	}
}

//尾插
void SLPushBack(SL* ps, SLDataType x)//x为插入数据
{
	assert(ps);
	SLCheckOut(ps);
	ps->arr[ps->size++] = x;
}
//头插
void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);
	SLCheckOut(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 PrintSeqlist(SL* ps)
{
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n");
}
//尾删
void SLPopBack(SL* ps)
{
	assert(ps);
	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)
{
	assert(ps);
	//不能让顺序表为空
	assert(pos >= 0 && pos <= ps->size);
	SLCheckOut(ps);
	for (int i = ps->size; i >= pos + 1; i--)
	{
		ps->arr[i] = ps->arr[i - 1];//ps->arr[pos + 1] = ps->arr[pos]
	}
	ps->arr[pos] = x;
	ps->size++;
}
//任意位置删除
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	SLCheckOut(ps);
	for (int i = pos; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}
//查找
int FindSL(SL* ps, SLDataType x)
{
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			//找到了
			return i;
		}
	}
	//没有找到
	return -1;
}

sqlist.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//#define N 100
静态顺序表
//struct Seqlist
//{
//	int arr[N];//存放数组大小
//	int size;//有效存储数据个数
//};

//动态顺序表
typedef int SLDataType;
typedef struct Seqlist
{
	
	SLDataType* arr;//存放数组大小
	int size;//有效存储数据个数
	int capacity;//开辟空间的大小
}SL;

//初始化
void Seqlist_Init(SL* ps);

//销毁
void Seqlist_Destroy(SL* ps);
//打印
void PrintSeqlist(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, SLDataType x);
//查找
int FindSL(SL* ps, SLDataType x);

  • 34
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值