史上最强数据结构----顺序表的模拟实现(C语言版)

2.1概念及结构

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

顺序表一般可以分为:

  1. 静态顺序表:使用定长数组存储元素,元素的数目无法进行修改。

    //顺序表的静态存储
    #define N 7
    typedef int SLDataType;
    typedef struct SeqList
    {
    	SLDataType array[N];//定长数组
    	size_t size;//有效数据的个数
    }SeqList;
    
  2. 动态顺序表

    //顺序表的动态存储
    typedef struct SeqList
    {
    	SLDataType* array;//指向动态开辟的数组,空间不够可以增容
    	size_t size;//有效数据的个数
    	size_t capacity;//容量空间的大小
    };
    

2.2 接口实现

静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪
费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。

2.2.1 顺序表的动态存储

typedef int SLDataType;//顺序表中存储的数据,此处假设是int型
typedef struct SeqList
{
	int* a;//指向动态开辟的数组空间,空间可以随时增容
	int size;//存储数据个数
	int capacity;//存储空间大小
}SL,SeqList;

2.2.2 顺序表初始化

void SeqListInit(SeqList* psl);//声明
void SeqListInit(SeqList* psl)
{
	assert(psl);//进行断言是因为当psl为NULL时,下面的操作将无法进行,因为空指针是无法进行解引用的。
	psl->a = NULL;
	psl->size = 0;
	psl->capacity = 0;
}//函数实现

注意:进行断言是因为当psl为NULL时,下面的操作将无法进行,因为空指针是无法进行解引用的,后面也是如此。

2.2.3 顺序表的销毁

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

注意:free后面括号中的指针必须是malloc开辟出来的那块空间,且不能有任何的偏差(即指针不能发生移动)。

下面进行举例:

image-20220313121909217

扫描二维码关注公众号,回复: 13770495 查看本文章

像上面这样使用是完全没有问题的,但是像下面这样进行使用就出现了问题:

image-20220313122035082

tmp进行自增操作后,就指向了下图所示位置:

image-20220313122258326

free的位置是tmp++后的位置,这不符合C语言的规定,且即使正常的释放掉了,前面的那一块int空间也将引起内存泄漏问题,即动态开辟的内存忘记释放。

2.2.4 顺序表的尾插

void SeqListPushBack(SeqList* psl,SLDataType x);//声明
void SeqListPushBack(SeqList* psl, SLDataType x)
{
	assert(psl);
	//如果满了,就进行扩容
	SeqListCheckCapacity(psl);
	psl->a[psl->size] = x;
	psl->size++;
}

image-20220313123214723

2.2.5 顺序表的尾删

void SeqListPopBack(SeqList* psl);
void SeqListPopBack(SeqList* psl)
{
	assert(psl);
	if(psl->size > 0)
	{
		psl->size -= 1;
	}
}

2.2.6 顺序表的头插

void SeqListPushFront(SeqList* psl, SLDataType x);
void SeqListPushFront(SeqList* psl, SLDataType x)
{
	assert(psl);
	SeqListCheckCapacity(psl);
	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->a[end+1] = psl->a[end];
		--end;
	}
	psl->a[0] = x;
	psl->size++;
}

顺序表的头插会涉及到后续元素的移动,头插时要将顺序表中的元素从后面开始进行移动,因为从前面开始移动的话会出现覆盖现象。下面是图示:

image-20220313150759211

2.2.7 顺序表的头删

同理,如果想要元素不被覆盖,就只能从前向后进行移动。

void SeqListPopFront(SeqList* psl);
void SeqListPopFront(SeqList* psl)
{
	//挪出数据,腾出头部空间
	//方法一:从1开始移动
	/*assert(psl);
	if (psl->size > 0)
	{
		int begin = 1;
		while (begin < psl->size)
		{
			psl->a[begin - 1] = psl->a[begin];
			begin++;
		}
		psl->size--;
	}*/
	//方法二:从0开始移动
	assert(psl);
	if (psl->size > 0)
	{
		int begin = 0;
		while (begin < psl->size - 1)
		{
			psl->a[begin] = psl->a[begin + 1];
			begin++;
		}
		psl->size--;
	}
}

下图是两种移动方式的区别:

image-20220313152013487

问:为什么不可以直接将指针进行加1操作?

  1. free释放空间时的指针和malloc开辟空间的指针必须相同
  2. mallo开辟的空间存在浪费,即0的那块空间被浪费,无法进行使用,因为那块空间是被合法申请的。

2.2.8 顺序表容量的检查与扩容

void SeqListCheckCapacity(SeqList* psl);
void SeqListCheckCapacity(SeqList* psl)
{
	assert(psl);
	if (psl->capacity == psl->size)
	{
		size_t newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		else
		{
			psl->a = tmp;
			psl->capacity *= 2;
		}
	}
}

注意点1:此处考虑使用的是如果容量不够,就将容量扩容为原容量的两倍,但是最开始的容量是0,所以要考虑到最开始为0的情况。

注意点2:要对扩容是否成功进行检测,即判断刚申请的空间是否为空。

2.2.9 顺序表任意位置的插入

void SeqListInsert(SeqList* psl,size_t pos,SLDataType x);
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
	assert(psl);
	//较为温和的检查方式
	/*if (pos > psl->size)
	{
		exit(-1);
	}*/
	assert(pos <= psl->size);//较为暴力的检查方式
	SeqListCheckCapacity(psl);
	size_t end = psl->size;
	while (end > pos)
	{
		psl->a[end] = psl->a[end-1];
		--end;
	}
	psl->a[pos] = x;
	psl->size++;
}

注意:

问:为什么end从psl->size开始?

答:此处需要注意的是pos和end的类型,为什么呢?因为两者都是无符号类型,所以尤其注意当end到了-1的时候,就会变成一个很大的数字,此时如果以此作为下标进行访问,一定会出现越界访问内存的情况,考虑一种极端情况,当pos为0的时候,end最小的时候就是为0,所以此时不会出现越界的情况,但是如果end是从psl->size - 1开始的话,那么while循环体内的语句就变成下面这样:

while(end > pos)
{
	psl->a[end+1] = psl->a[end];
	--end;
}

最后end的最小值会变成-1,但是因为end是size_t类型,所以会变成一个很大的数字,在whle()循环条件判定时条件始终是满足的,同时在进入循环体内之后,会出现越界访问内存的操作。所以两种情况的图如下所示:

image-20220311164610452

2.2.10 顺序表任意位置的删除

void SeqListErase(SeqList* psl, size_t pos);
void SeqListErase(SeqList* psl, size_t pos)
{
	assert(psl);
	assert(pos <= psl->size);
	size_t begin = pos+1;
	while (begin < psl->size)
	{
		psl->a[begin-1] = psl->a[begin];
		++begin;
	}
	psl->size--;
}

2.2.11 顺序表的打印

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

2.2.12 顺序表元素的查找

int SeqListFind(SeqList* psl,SLDataType x);
int SeqListFind(SeqList* psl, SLDataType x)
{
	assert(psl);
	for (int i = 0; i < psl->size; i++)
	{
		if (psl->a[i] == x)
			return i;//找到了对应元素,返回相应的下标
	}
	return -1;//说明没有找到对应的元素
}

2.2.13 顺序表元素的修改

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

猜你喜欢

转载自blog.csdn.net/m0_57304511/article/details/123460490