【数据结构】2.顺序表(C语言)

【数据结构】——2.顺序表

一、线性表概念

线性表(linear list):具有相同特征的数据元素的有限序列。

​ 线性表在逻辑结构上时线性结构、是一条连续的线,但是物理结构上不一定是连续的。

常见的线性表:顺序表、链表、栈、队列、字符串等

二、顺序表概念及结构

1. 顺序表概念

顺序表(sequence list):用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般使用数组存储。在数组上完成增删查改

顺序表一般分为静态顺序表和动态顺序表

2. 顺序表结构

1.1 静态顺序表

​ 使用定长数组存储元素,容量不够时提醒用户不能存储

#define NUMBER 1000

typedef int SLDataType;
typedef struct SeqList
{
    SLDataType data[NUMBER];	 //定长数组
    size_t size;				//有效数据个数
} SeqList;

1.2 动态顺序表

​ 使用动态开辟数组的存储,容量不够时可以增容

typedef int SLDataType;
typedef struct SeqList
{
    SLDataType *data;	//指向动态开辟的数组
    size_t size;		//有效数据个数
    size_t capacity;	//容量空间的大小
}

三、顺序表的实现

1. 结构定义

​ 现实中基本使用动态顺序表,所以我们对动态顺序表进行操作

typedef int SLDataType;
typedef struct SeqList
{
    SLDataType *data;	//指向动态开辟的数组
    size_t size;		//有效数据个数
    size_t capacity;	//容量空间的大小
}

2. 初始化

  • 初始化时可以不分配空间,在插入时检查并分配空间,不够了就重新申请
void SeqListInit(SeqList* psl)
{
	assert(psl != NULL);
    
	psl->data = NULL;
	psl->size = 0;
	psl->capacity = 0;
}

3. 检查容量

  1. 在插入操作之前(头插、尾插、下标插入),都要进行容量检查,所以单独成为一个函数
  2. 检查顺序表是否满了,判断有效数据个数和最大容量是否相等
  3. 判断有效数据个数和最大容量相等时,还要判断空间是为0(未使用)时给空间一个初始值,不为0时让容量按指定值增长(这里是*2了)
  4. 使用realloc函数时,要注意返回值的判断和赋值
void CheckCapacity(SeqList* psl)
{
	assert(psl != NULL);

	if (psl->size == psl->capacity)
	{
        //初始值判断,从4开始,不够就*2
		int newCapacity = psl->capacity == 0 ? 4 : 2 * psl->capacity;
        
        //申请空间
		SLDataType* temp = (SLDataType*)realloc(psl->data, newCapacity * sizeof(SLDataType));
		if (temp == NULL)
		{
			perror("realloc fail\n");
			exit(-1);
		}
        
        //将重新申请的空间赋值给data,并改变最大容量
		psl->data = temp;
		psl->capacity = newCapacity;
	}
}

4. 插入数据

4.1 尾插

尾插:在线性表的末尾插入数据

  1. 插入之前检查容量
  2. 插入时直接将size位置的data赋值就行,size加一
void SeqListPushBack(SeqList* psl, SLDataType x)
{
	assert(psl != NULL);

	CheckCapacity(psl);

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

4.2 头插

头插:在线性表头部插入数据

  1. 插入之前检查容量
  2. 插入前要移动数据,从后面开始移动
  3. 插入时将0位置的data赋值,size加一
void SeqListPushFront(SeqList* psl, SLDataType x)
{
	assert(psl != NULL);

	CheckCapacity(psl);

	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->data[end + 1] = psl->data[end];
		--end;
	}

	psl->data[0] = x;
	psl->size++;
}

4.3 插入

​ 在指定下标的位置插入数据

  1. 插入数据的下标位置一般声明为无符号整型
  2. 从后往前移动数字,将指定位置空出来,插入指定数据
  3. 检查下标不用检查负数,使用的是无符号整型,下标可以是size,插入到末尾
  • 错误的移动方式:当从size-1开始移动时,每次使data[end+1] = data[end]来完成移动,会出现错误

如果pos为0,则end>=0时执行循环体,end=0时执行循环体end--end为无符号整型值,减一后为整型最大值,无限循环
若是将end声明为int型,end与无符号pos比较依然按照无符号来,所以是死循环

size_t end = psl->size - 1;	//end从size-1开始移动
while (end >= pos)		//end>=pos执行循环体
{
	psl->data[end + 1] = psl->data[end];
	--end;
}
  • 正确的移动方式:使移动从size开始,每次使data[end] = data[end-1]来完成移动,就不会出现错误

此时pos为0时,则end>0时执行循环体,end--后为0,循环停止,不会陷入死循环

void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
	assert(psl != NULL);
	assert(pos <= (size_t)psl->size);	//检查pos,可以等于size,就是尾插

	CheckCapacity(psl);

	size_t end = psl->size;		//从size开始,end>pos时执行,pos为0时就不会出现死循环了
	while (end > pos)
	{
		psl->data[end] = psl->data[end-1];
		--end;
	}

	psl->data[pos] = x;
	psl->size++;
}

5. 删除数据

5.1 尾删

尾删:删除末尾数据

  1. 删除之前要先判断size是否已经为0,若为0,则终止程序或不做处理(下面是不做处理的,终止程序可以用断言)
  2. 删除数据直接将size减一即可
void SeqListPopBack(SeqList* psl)
{
	assert(psl != NULL);
    
	//不做处理,直接return
	if (psl->size == 0)
	{
		return;
	}
    
    //assert(psl->size != 0);	断言处理,终止程序

	psl->size--;
}

5.2 头删

头删:删除第一个数据

  1. 删除之前要先判断size是否已经为0,若为0,则终止程序或不做处理(下面是不做处理的,终止程序可以用断言)
  2. 从下标为1的数据从前往后向前移动一位即可,再size减一
void SeqListPopFront(SeqList* psl)
{
	assert(psl != NULL);

	if (psl->size == 0)
	{
		return;
	}

	int begin = 0;
	while (begin < psl->size-1)
	{
		psl->data[begin] = psl->data[begin + 1];
		++begin;
	}
    
	psl->size--;
}

5.3 删除

​ 删除指定下标位置的数据

  1. 从指定下标开始,从前往后将数据向前移动一格
  2. 下标不能为size,必须是有效的数据
void SeqListErase(SeqList* psl, size_t pos)
{
	assert(psl != NULL);
	assert(pos < (size_t)psl->size);

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

	psl->size--;
}

6. 查找数据

  1. 顺序查找指定值,返回下标,若是没有就返回-1
  2. 不建议使用二分查找,排序会影响效率
int SeqListFind(SeqList* psl, SLDataType x)
{
	assert(psl != NULL);
    
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		if (psl->data[i] == x)
		{
			return i;
		}
	}
    
	return -1;
}

7. 修改数据

  • 修改就直接将值赋值给该位置即可
//修改 Modify
void SeqListModify(SeqList* psl, size_t pos, SLDataType x)
{
	assert(psl != NULL);
	assert(pos < (size_t)psl->size);

	psl->data[pos] = x;
}

8. 销毁顺序表

  1. 顺序表销毁的是data申请的空间,断言时只需断言顺序表是否存在即可,NULL被free时不报错
  2. 释放完空间时,记得将data置为NULL,容量和有效个数置为0
void SeqListDestory(SeqList* psl)
{
	assert(psl != NULL);

	free(psl->data);
	psl->data = NULL;
	psl->size = 0;
	psl->capacity = 0;
}

四、顺序表的相关问题

1. 效率问题

  1. 中间、头部、的插入时间复杂度为 O ( n ) O(n) O(n),效率低
  2. 增容需要申请空间,拷贝数据,释放空间。消耗很大
  3. 若是数据少,申请的空间多,造成浪费

2. 缩容问题

顺序表删除数据时不建议缩容

  1. realloc函数可以实现缩容,所以不缩容与函数无关
  2. 缩容造成的时间浪费非常大,若是在扩容边界上反复插入、删除,就会导致空间反复扩容缩容,严重影响效率
  3. 不缩容是以空间换取时间,以现在的科技水平,空间成本低,空间上的要求并没有时间上的要求高

3. 插入和删除问题

3.1 头插和尾插

​ 头插法和尾插法可以调用插入函数实现

  1. 头插法就是下标为0的插入
//头插法
void SeqListPushFront(SeqList* psl, SLDataType x)
{
	SeqListInsert(psl, 0, x);
}
  1. 尾插法就是下标为size的插入,尾插要传参size,所以要检查psl
//尾插法
void SeqListPushBack(SeqList* psl, SLDataType x)
{
    assert(psl != NULL);
	SeqListInsert(psl, psl->size, x);
}

3.2 头删和尾删

​ 头删和尾删可以调用删除函数实现

  1. 头删就是下标为0的删除
//头删
void SeqListPopFront(SeqList* psl)
{
	SeqListErase(psl, 0);
}
  1. 尾删就是下标为size-1的删除,尾删要传参size,所以要检查psl
//尾删
void SeqListPopBack(SeqList* psl)
{
    assert(psl != NULL);
	SeqListErase(psl, psl->size-1);
}

五、顺序表的特点

1. 顺序表的优点

  1. 尾插尾删的效率很高
  2. 随机访问,用下标访问
  3. CPU高速缓存命中率更高

CPU访问数据:

  1. CPU不直接访问内存,而是先访问高速缓存
  2. 没有命中时,先加载到缓存,局部性原理(加载数据时会连带周围的数据)

2. 顺序表的缺点

  1. 头部和中部插入删除数据的效率低
  2. 扩容时的性能消耗和空间浪费

六、完整代码

代码保存在gitee中:点击完整代码参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值