数据结构与算法——顺序表 (完整代码)

数据结构与算法——顺序表 (完整代码)_第1张图片

数据结构——顺序表

  • 概念及结构
    • 概念
    • 分类
      • 静态的顺序表(不做实现)
      • 动态的顺序表(重点实现)
  • 初始化和销毁
  • 打印
  • 接口
    • 插入
      • 尾插
      • 头插
      • 在pos的位置插入x
    • 删除
      • 尾删
      • 头删
      • 删除pos位置的值
    • 查找
      • 查找可以和指定位置查找相结合
    • 修改
  • 顺序表的缺点

概念及结构

概念

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构(不能跳跃着存放),一般情况下采用数组存储。在数组上完成数据的增删查改。

分类

  1. 静态顺序表:使用定长数组存储元素
  2. 动态顺序表:使用动态开辟的数组存储

静态的顺序表(不做实现)

#define N 10 //宏设置数组大小,增加灵活性
typedef int SLDataType;//重命名需要使用的类型,这里是int类型
struct SeqList
{
	SLDataType a[N];//用数组来存储数据
	int size;//存储数据的个数
};

缺陷:大小是固定的,不好确定开辟数组的大小

动态的顺序表(重点实现)

typedef int SLDataType;//重命名需要使用的类型,这里是int类型
typedf struct SeqList
{
	SLDataType* a;//指向动态开辟的数组
	int size;//存储数据的个数存储空间的大小
	int capacity;//容量空间的大小
}SL;

这里又会出现空间满了怎么扩容的问题,扩容太少会使得内存空间碎片化,有效率的损失,扩容太多又造成空间浪费。
那多少扩容合适呢,一般2倍比较合适

不建议进行缩容:realloc原则上可以给比原来空间要小的数据空间,那样的话会对开辟的容量进行缩小,也分两种情况原地缩和异地缩,异地缩的时候会对性能造成影响,并且顺序表需要插入数据,缩容之后添加数据发现空间不够又需要扩容,这时候又会出现原地扩,异地扩的问题,对性能造成影响。
缩容:以时间换空间
不缩容:以空间换时间

原则:不要直接去操作改变原始的结构体,需要使用函数来对结构体进行操作

初始化和销毁

void SLInit(SL* psl);//初始化

void SLDestory(SL* psl);//销毁

初始化函数实现

-----------------------------------------------------------
void SLInit(SL* psl)
{
	assert(psl);
	psl -> a = NULL;
	psl -> capacity = psl -> size = 0; 
}

-------------------------------------------------------------
void TestSeqList()
{
	SL s;//创建了类型为SL的结构体变量s
	SLInit(&s);//调用初始化函数,初始化s
}

int main()
{
	TestSeqList1();//调用测试函数来完成测试,方便调试
}

注意:这里需要传递指针,才能达到初始化的效果。

空间销毁函数实现

----------------------------------------------------
void SLDestory(SL* psl)
{
	assert(psl);
	if(psl -> a)
	{
		free(psl->a);
		psl -> a = NULL;
		psl -> capacity = psl -> size = 0;
	}
}
------------------------------------------------------
void TestSeqList()
{
	SL s;//创建了类型为SL的结构体变量s
	SLInit(&s);//调用初始化函数,初始化s
	SLDestory(&s);//调用空间销毁函数,置空s
}

int main()
{
	TestSeqList1();//调用测试函数来完成测试,方便调试
}

打印

void SLPrint(SL* psl)
{
	assert(psl);
	for(int i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->a[i]);
	}
	printf("\n");
]

此处是为了保持接口一致性,所以使用了传指针的方式,理论上是可以传值的

接口

头插头删,尾插尾删

void SLPushBack(SL* psl, SLDataType x);//插入,x为插入的值
void SLPushFront(SL* psl, SLDataType x);
void SLPopBack(SL* psl);//删除
void SLPopBack(SL* psl);

插入

对于插入数据来说会出现容量满了需要扩容的情况,这里我们将扩容写成一个函数独立出来

void SLCheckCapacity(SL* psl)
{
	if(psl->size == psl->capacity)//容量满了
	{
		int newCapcity = psl->capacity == 04 :psl->capacity * 2 ;
		SLDataType* tmp = (SLDataType*)realloc(psl->a,newCapcity*sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			//return;
			exit(-1)
		}
		psl->a = tmp;
		psl->capacity = newCapcity;
	}
}
  1. realloc 可以接收空指针来达到开辟空间的作用
  2. 对于realloc开辟的空间一定要进行检查避免开辟失败,在检查的时候记得使用临时变量,防止开辟失败将原来的指针置空
  3. 可以使用exit(-1)直接结束程序
  4. 在开辟空间时出现最常见的错误就是逻辑错误,越界访问

尾插

数据结构与算法——顺序表 (完整代码)_第2张图片
size表示的是存储数据的个数,图片中的数组存储了五个数据,所以size的大小为5,因为数组下标从0开始,所以size实际上是指向了最后一个数据的下一个位置
尾部插入数据就是将数据放到size指向的地方,然后给size进行++就行

--------------------------------------------------------------
void SLPushBack(SL* psl, SLDataType x)
{
	assert(psl);
//检查容量
		SLCheckCapacity(psl);

	psl->a[psl->size] = x;
	psl->size ++;
	  
}

--------------------------------------------------------------

void TestSeqList()
{
	SL s;//创建了类型为SL的结构体变量s
	SLInit(&s);//调用初始化函数,初始化s
	SLPushBack(&s,1);//调用插入函数,插入了数据1
	SLPrint(&s);//打印
	SLDestory(&s);//调用空间销毁函数,置空s
	
}

int main()
{
	TestSeqList1();//调用测试函数来完成测试,方便调试
}

头插

顺序表要求数据必须是连续的,因此头部插入需要移动数据,并且只能从后往前挪动(先挪动最后面的数据),如果从前向后挪动的话就会将后面的数据覆盖。
数据结构与算法——顺序表 (完整代码)_第3张图片
思路是定义一个end指向最后一个元素,然后将end的数据挪动到end+1的地方,最后end–,end为负数时停止,然后将想添加的数据x加入到数组下标为0的地方

void SLPushFront(SL* psl,SLDateType x)
{
	assert(psl);
	SLCheckCapacity(psl);

	//挪动数据
	int end = psl->size-1;
	while(end >= 0)
	{
		psl->a[end+1] = psl->a[end];
		end--;
	}
	psl->a[0] = x;
	size++;
}

在pos的位置插入x

数据结构与算法——顺序表 (完整代码)_第4张图片
假设需要在2的这里(前面)插入数据,也就是将数据变成1,x,2,3,4。
思路是设置end变量指向最后(size-1),从后往前挪动数据,当end移动到pos后停止
数据结构与算法——顺序表 (完整代码)_第5张图片

void SLInsert(SL* psl, size_t pos, SLDataType)
{
	assert(psl);
	assert(pos <= psl->size);//限定范围
	SLCheckCapacity(psl);
	
	size_t end = psl->size - 1;
	while(end >= pos)
	{
		psl->a[end + 1] = psl->a[end];
		--end
	} 
	psl->a[pos] = x;
	psl->size++;
}
  1. 这个代码存在无符号数的bug:pos=0时出现死循环问题。当我们想要在下标为0的数据之前添加数据就会发现出错:具体原因是在于当end变成0进入一次循环之后,end会–变成-1,但是end是一个无符号数,所以这时候end会变成一个巨大的正数,造成循环无法结束
  2. 如果将end的类型强制类型转换int类型的话也会出现这样的问题,因为当执行一个运算时,如果它的一个运算数是有符号的而另一个数是无符号的,那么C语言会隐式地将有符号数强制类型为无符号数(通过产生临时变量的方式)来进行比较
  3. 那如果一开始就使用int类型的话会出现与他人接口不相容的问题,在库中的实现都是使用无符号数来进行

解决方案一
使用int类型的end之后,强制转换pos为int

void SLInsert(SL* psl, size_t pos, SLDataType)
{
	assert(psl);
	assert(pos <= psl->size);//限定范围
	SLCheckCapacity(psl);
	
	int end = psl->size - 1;
	while(end >= (int)pos)
	{
		psl->a[end + 1] = psl->a[end];
		--end
	} 
	psl->a[pos] = x;
	psl->size++;
}

解决方案2(推荐)
不使用>=,使用>,将前一个位置移到后面的位置

void SLInsert(SL* psl, size_t pos, SLDataType)
{
	assert(psl);
	assert(pos <= psl->size);//限定范围
	SLCheckCapacity(psl);
	
	size_t end = psl->size - 1;
	while(end > pos)
	{
		psl->a[end + 1] = psl->a[end];
		--end
	} 
	psl->a[pos] = x;
	psl->size++;
}

可以直接用这个函数来实现头插和尾插

SLInsert(psl, 0 ,x);
SLInsert(psl, psl->size ,x);

删除

顺序表的访问是以size为基准的

尾删

将size进行–,就无法访问到那个数据。(顺序表的访问是以size为基准的)

注意:

  1. 空间不可以释放:动态开辟之后的空间不能只对一部分free
  2. 数据不用修改,置空(置空成0,万一数据本身是0呢)
void SLPopBack(SL* psl)
{
	assert(psl);
	psl->size--;
}

不断–:会有一个bug出现,size会被减到负数,然后再进行插入数据就会导致越界访问。(没有数据了还在pop)

两种解决方式
方式一:温柔检查

void SLPopBack(SL* psl)
{
	assert(psl);
	
	if(psl-> == 0)
	{
		return 0;
	}
	psl->size--;
}

方式二:暴力检查

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

头删

数据结构与算法——顺序表 (完整代码)_第6张图片
这里要删除头部数据1,首先因为数据需要连续存放,删除之后2需要到前面来,然后按顺序存放。
其次这里删除之后不能对1的空间进行释放:在堆中动态开辟的空间不可以部分释放。
数据结构与算法——顺序表 (完整代码)_第7张图片

注意这里的挪动方式:从前往后挪动,将2挪到1的位置再将3挪到2的位置。(先挪动最前面的数据)

数据结构与算法——顺序表 (完整代码)_第8张图片
思路是先创建循环变量begin指向1这个位置,然后将后一个的值传入前一个里面,之后bagin指向3这个位置,也就是下标为siae-2的位置

void SLPopFront(SL* psl)
{
	assert(psl);
	assert(psl->size > 0);//暴力检查
	int begin = 0;
	while (begin < psl->size-1)//(begin <= psl->size-2)
	{
		psl->a[begin] = psl->a[begin + 1];
		++begin;//c++中前置会更好
	}
	--psl->size;
}

删除pos位置的值

数据结构与算法——顺序表 (完整代码)_第9张图片

设置begin变量,删除后将下一个数据移动到上一个数据的地方,最后移动begin,当begin移动到size-2这个位置时函数结束,即begin

void SLErase(SL* psl,size_t)
{
	assert(psl);
	assert(pos < psl->size);//size没有数据,删除没有意义

	size_t begin = pos;
	while(begin < psl->size -1)
	{
		psl->a[begin] = psl->a[begin + 1];
		++begin;
	}
	psl->size--;
}

可以直接用这个函数来实现头删和尾删

SLErase(psl, 0);//头
SLErase(psl, psl->size-1);//尾

查找

int SLFind(SL* psl, SLDataType x)//返回下标
{
	assert(psl);
	for(int i = 0; i < psl->size; i++)
	{
		if(psl->a[i] == x)
		{
			return i;
		}
	}
	return -1; 
}

查找可以和指定位置查找相结合

int x = 0;
scanf("%d",&x);//输入查找的数
int pos = SLFind(&s,x);//查找这个输入的数
if(pos != -1)
{
	SLInsert(&s,pos,x*10);//有的话在x这个数的下标之前插入这个数的10倍
}

修改

void SLModify(SL* psl, size_t pos,SLDataType x)
{
	assert(psl);
	assert(pos < psl->size);
	psl->a[pos] = x;
}

顺序表的缺点

  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
    200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间

为了解决这些问题,我们提出了链表来进行解决这些问题

你可能感兴趣的:(基于c语言的数据结构初阶,数据结构,算法,c++,c语言,开发语言)