数据结构(一):顺序表

  • Post author:
  • Post category:其他





一、概念及结构

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

顺序表一般可以分为:



1.静态顺序表

即使用定长数组存储数据。


代码如下(示例):

#define N 100
typedef int SeqListDataType;//这里将int定义为SeqListDataType方便使用
typedef struct SeqList
{
	SeqListDataType a[N];//存储数据的数组
	int size;//顺序表中当前有效数据的个数
}SeqList;



2.动态顺序表

即使用动态开辟的数组存储数据。


代码如下(示例):

typedef int SeqListDataType;
typedef struct SeqList
{
	SeqListDataType* a;//存储数据的数组(动态开辟)
	int size;//顺序表中当前有效数据的个数
	int capacity;//顺序表中最大存储的个数
}SeqList;

静态顺序表的缺点在于存储数据的数组是定长的,所以在使用是可能造成空间浪费,也可能会出现顺序表存满的情况。而动态开辟可以很好地解决这一问题。下面的代码以动态开辟的顺序表为例来介绍。




二、顺序表功能的实现



1.顺序表的初始化、销毁


代码如下(示例):

//初始化
void SeqListInit(SeqList* p)
{
	assert(p);//用assert来防止传入空指针时程序崩溃

	p->a = NULL;
	p->size = 0;
	p->capacity = 0;
}

//销毁
void SeqListDestroy(SeqList* p)
{
	assert(p != NULL);//写法不同,功能同上
	
	free(p->a);//释放动态开辟的空间
	p->a = NULL;
	p->size = 0;
	p->capacity = 0;
}

这里为了方便观察,再实现一个打印顺序表中数据的函数。


代码如下(示例):

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



2.数据的增加



(1)头部插入数据

在头部插入数据前,需要将所有数据向后挪一位,腾出第一个数据的位置来插入新数据。

要注意这里必须从后向前依次挪动数据,否则前面一位的数据会覆盖后面一位的数据,导致数据的丢失。


代码如下(示例):

void SeqListPushFront(SeqList* p, SeqListDataType x)
{
	assert(p);
	
	CheckCapacity(p);//该函数见下文
	
	int end = p->size;
	for (; end > 0; end--)
	{
		p->a[end] = p->a[end - 1];//从后向前挪动数据
	}
	p->a[0] = x;
	p->size++;//注意此处要让有效数据的个数+1
}

头插的函数实现后,会发现代码中有一个问题:如果此时顺序表中的数据已满,即当p->size和p->capacity相等时,那么在插入数据时就会出现越界访问的问题。所以需要在插入数据前判断当前顺序表是否已满,如果未满则正常插入数据,否则进行扩容,这里把这一功能封装为一个函数。


代码如下(示例):

//判断线性表是否已满
void CheckCapacity(SeqList* p)
{
	if (p->size == p->capacity)
	{
		int newcapacity = (p->capacity == 0) ? 4 : (p->capacity * 2);
		//如果容量为0(第一次插入数据),默认开辟4个SeqListDataType的空间
		//如果是在插入数据过程中顺序表已满,则将空间变为2倍(将空间变为2倍也可能会出现空间浪费的情况)
		//这里扩容的倍数需要具体情况具体分析,此处以2倍为例
		SeqListDataType* newA = (SeqListDataType*)realloc(p->a, sizeof(SeqListDataType)*newcapacity);
		if (newA == NULL)//开辟失败
		{
			printf("newcapacity fail\n");
			return;
		}
		p->a = newA;
		p->capacity = newcapacity;
	}
}

有了CheckCapacity函数后,只需在挪动数据前调用该函数,后面的插入数据即可正常实现。




(2)尾部插入数据

尾插数据与头插数据相比更加简单,因为不需要挪动数据,直接在数组末尾插入数据即可。


代码如下(示例):

//尾插
void SeqListPushBack(SeqList* p, SeqListDataType x)
{
	assert(p);
	
	CheckCapacity(p);

	p->a[p->size] = x;
	//p->size表示当前顺序表中有效的数据个数
	//a[p->size]访问当前最后一个数据的下一个位置
	p->size++;
}



3.数据的删除



(1)头部删除数据

头删只需从头开始从后向前挪动数据即可,原来第一位的数据会被覆盖以达到删除的目的。


代码如下(示例):

//头删
void SeqListPopFront(SeqList* p)
{
	assert(p);
	assert(p->size > 0);
	int i = 0;
	for (i = 0; i < p->size - 1; i++)//从前向后遍历挪动数据
		p->a[i] = p->a[i + 1];
	p->size--;
}



(2)尾部删除数据

尾删同样也比头删简单,只需让数组中有效数据的个数-1即可,由于顺序表的size减小1,所以此时不会访问到被删除的数据,虽然没有真正的删除掉末尾的数据,但达到了删除的目的。


代码如下(示例):

//尾删
void SeqListPopBack(SeqList* p)
{
	assert(p);
	assert(p->size > 0);

	CheckCapacity(p);

	p->size--;
}



4.任意位置插入、删除数据

任意位置实现数据的插入只需将该位置及该位置之后的数据向后挪动一位,并将该位置改为要插入的数据即可。


代码如下(示例):

//中间插入数据
void SeqListInsert(SeqList* p, int pos, SeqListDataType x)
{
	assert(p);
	assert(pos >= 0 && pos <= p->size);
	CheckCapacity(p);

	int i = p->size;
	for (; i > pos; i--)
		p->a[i] = p->a[i - 1];//向后挪动数据
	p->a[i] = x;
	p->size++;
}

任意位置实现数据的删除只需将该位置及该位置之后的数据向前挪动一位即可。


代码如下(示例):

//中间删除数据
void SeqListErase(SeqList* p, int pos)
{
	assert(p);
	assert(pos >= 0 && pos < p->size);
	CheckCapacity(p);

	int i = pos;
	for (; i < p->size - 1; i++)
		p->a[i] = p->a[i + 1];//向前挪动数据
	p->size--;
}

不难发现,头插、尾插、头删、尾删是这两个函数的特殊情况,所以顺序表头部和尾部的操作可通过调用这两个函数实现。


代码如下(示例):

//头插
void SeqListPushFront(SeqList* p, SeqListDataType x)
{
	SeqListInsert(p, 0, x);
}

//尾插
void SeqListPushBack(SeqList* p, SeqListDataType x)
{
	SeqListInsert(p, p->size, x);
}

//头删
void SeqListPopFront(SeqList* p)
{
	SeqListErase(p, 0);
}

//尾删
void SeqListPopBack(SeqList* p)
{
	SeqListErase(p, p->size - 1);
}



5.查找数据

查找数据只需遍历顺序表即可。


代码如下(示例):

//查找(第一个)值为x的数据的下标
int SeqListFind(SeqList* p, SeqListDataType x)
{
	assert(p);
	int i = 0;
	for (i = 0; i < p->size; i++)
	{
		if (p->a[i] == x)
			return i;
	}
	return -1;//找不到返回-1
}



6.修改数据


代码如下(示例):

//将pos位置的值修改为x
void SeqListModify(SeqList* p, int pos, SeqListDataType x)
{
	assert(p);
	assert(pos >= 0 && pos < p->size);

	p->a[pos] = x;
}



三、顺序表的优劣

优点:

1.由于数据是连续存放的,所以空间利用率较高,访问时命中率高。

2.数据存放在数组中,可以直接通过下标访问,时间复杂度为O(1)。

3.对尾部进行操作时,时间复杂度为O(1)。

缺点:

1.在中间和头部的位置插入删除数据时,由于需要挪动数据,时间复杂度为O(N)。

2.增容时需要申请新空间,如果申请的新空间与原来不同,则需要将顺序表中的全部数据拷贝一次,这一过程会有不小的消耗。

3.一次扩容的大小难以确定。如果一次扩容较大,则会出现浪费空间的情况,如果一次扩容较小,又会由于频繁的扩容导致性能的下降。




感谢阅读,如有错误请批评指正



版权声明:本文为weixin_51983604原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。