顺序表详解

目录

[1 · 线性表](#1 · 线性表)

[2 · 顺序表](#2 · 顺序表)

[2 - 1 · 顺序表与数组的区别](#2 - 1 · 顺序表与数组的区别)

[2 - 2 · 静态顺序表](#2 - 2 · 静态顺序表)

[2 - 3 · 动态顺序表](#2 - 3 · 动态顺序表)

[3 · 接口及实现](#3 · 接口及实现)

[3 - 1 · 接口总览](#3 - 1 · 接口总览)

[3 - 2 · 初始化,检查容量,销毁](#3 - 2 · 初始化,检查容量,销毁)

[3 - 2 - 1 · 初始化](#3 - 2 - 1 · 初始化)

[3 - 2 - 2 · 检查容量](#3 - 2 - 2 · 检查容量)

[3 - 2 - 3 · 销毁](#3 - 2 - 3 · 销毁)

[3 - 2 - 4 · 测试](#3 - 2 - 4 · 测试)

[3 - 3 · 头插,头删,打印](#3 - 3 · 头插,头删,打印)

[3 - 3 - 1 · 头插](#3 - 3 - 1 · 头插)

[3 - 3 - 2 · 头删](#3 - 3 - 2 · 头删)

[3 - 3 - 3 · 打印](#3 - 3 - 3 · 打印)

[3 - 3 - 4 · 测试](#3 - 3 - 4 · 测试)

[3 - 4 · 尾插,尾删](#3 - 4 · 尾插,尾删)

[3 - 4 - 1 · 尾插](#3 - 4 - 1 · 尾插)

[3 - 4 - 2 · 尾删](#3 - 4 - 2 · 尾删)

[3 - 4 - 3 · 测试](#3 - 4 - 3 · 测试)

[3 - 5 · 查找,指定位置插入,指定位置删除](#3 - 5 · 查找,指定位置插入,指定位置删除)

[3 - 5 - 1 · 查找](#3 - 5 - 1 · 查找)

[3 - 5 - 2 · 指定位置插入](#3 - 5 - 2 · 指定位置插入)

[3 - 5 - 3 · 指定位置删除](#3 - 5 - 3 · 指定位置删除)

[3 - 5 - 4 · 测试](#3 - 5 - 4 · 测试)

[4 · 顺序表的缺点](#4 · 顺序表的缺点)

总结


1 · 线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
数组相信大伙都不陌生,链式结构如下图:

可以看出,链式结构在物理结构上并不是连续的,而我们熟知的数组在物理结构上是连续的,这两种结构各有优劣。简单来说,数组可以随机访问,而链式结构方便增加与删除,后续会详细介绍。


2 · 顺序表

2 - 1 · 顺序表与数组的区别

简单来说,顺序表的底层结构是数组,顺序表在此基础上进行封装,实现了增删查改多种功能。


2 - 2 · 静态顺序表

静态顺序表是用定长数组来进行存储:

如下:

复制代码
#define MAX 100
typedef int SLDataType;
typedef struct SeqList
{
	SLDataType a[MAX];
	int size;
}SeqList;

静态顺序表有个很明显的缺陷,就是数组的大小要给多少,给少了可能不够用,给多了可能又会浪费。

因此,本篇将着重介绍动态顺序表。


2 - 3 · 动态顺序表

动态顺序表就是按需申请,不够用的时候就进行扩容:

复制代码
typedef int SLDataType;

//动态顺序表
typedef struct SeqList
{
	SLDataType* a;
	int size;//有效数据总个数
	int capacity;//总容量
}SeqList;

这里用到了 typedef ,最上面的是方便进行存储类型的修改,在代码实现中用 SLDataType,到时候如果想要修改存储的类型,只需要改这里一处即可。

下面在结构体这里的 typedef 是方便后续使用,可以少写 struct。


3 · 接口及实现

3 - 1 · 接口总览

如下:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLDataType;

//动态顺序表
typedef struct SeqList
{
	SLDataType* a;
	int size;//有效数据总个数
	int capacity;//总容量
}SeqList;

//初始化
void SeqListInit(SeqList* p);
//检查容量,不够就扩容
void CheakCapacity(SeqList* p);
//头插
void SeqListPushFront(SeqList* p, SLDataType x);
//头删
void SeqListPopFront(SeqList* p);
// 尾插
void SeqListPushBack(SeqList* p, SLDataType x);
// 尾删
void SeqListPopBack(SeqList* p);
//查找
int SeqListFind(SeqList* p, SLDataType x);
//指定位置插入
void SeqListInsert(SeqList* p, int pos, SLDataType x);
//指定位置删除
void SeqListErase(SeqList* p, int pos);
//销毁
void SeqListDestroy(SeqList* p);
//打印
void SeqListPrint(SeqList* p);

下面我们一个个介绍:


3 - 2 · 初始化,检查容量,销毁

3 - 2 - 1 · 初始化

代码如下:

复制代码
void SeqListInit(SeqList* p)
{
	assert(p);
	p->a = NULL;
	p->size = p->capacity = 0;
}

先进行assert断言,防止接收的参数为空指针,保证代码的健壮性。

随后正常初始化,这里可以初始化的时候就给一个初始的容量,上面写的是初始不给容量的,这两种写法关系到检查容量的写法。


3 - 2 - 2 · 检查容量

代码如下:

复制代码
void CheakCapacity(SeqList* p)
{
	assert(p);
	int newCapacity = 0;
	if (p->size == p->capacity)
	{
		newCapacity = p->capacity == 0 ? 4 : p->capacity * 2;
		SLDataType* ptr = (SLDataType*)realloc(p->a,sizeof(SLDataType) * newCapacity * 2);
		if (ptr == NULL)
		{
			perror("realloc");
			exit(1);
		}
		p->a = ptr;
		p->capacity = newCapacity;
	}
}

如果容量不够就需要扩容,那么就产生了一个问题,一次扩容应该扩多少?

我们扩容需要用到realloc,如果频繁使用,会产生较大的消耗。

因此我们决定扩容一次将原有容量翻倍。

那么触发扩容的条件是表满,即 size == capacity

如果初始化的时候我们没有给初始容量的话,此时的capacity 是0,而0*2 仍会等于0,所以我们用一个三目操作符来确定新的容量,再进行扩容,最后更新容量。

如果初始化的时候给了初始容量,此时就没有capacity 为0的情况了。


3 - 2 - 3 · 销毁

代码如下:

复制代码
void SeqListDestroy(SeqList* p)
{
	if(p->a)
	{
		free(p->a);
	}
	p->a = NULL;
	p->size = p->capacity = 0;
}

将开辟的空间释放,然后指针置空,size 与 capacity 置0


3 - 2 - 4 · 测试

我们对上面的功能测试一下:

复制代码
#include "SeqList.h"

void Test1()
{
	SeqList s;
	SeqListInit(&s);
	CheakCapacity(&s);
	SeqListDestroy(&s);
}

int main()
{
	Test1();
	
	return 0;
}

通过调试窗口:

初始化之后:

检查容量之后:

销毁:


3 - 3 · 头插,头删,打印

3 - 3 - 1 · 头插

代码如下:

复制代码
void SeqListPushFront(SeqList* p, SLDataType x)
{
	assert(p);
	CheakCapacity(p);
	//如果空表,直接给
	if (p->size == 0)
	{
		p->a[0] = x;
		p->size++;
	}
	else
	{
		//非空,集体后移再给
		int i = 0;
		for (i = p->size; i > 0; i--)
		{
			p->a[i] = p->a[i - 1];
		}
		p->a[0] = x;
		p->size++;
	}
}

头插就是将数据插入到表头的位置。

如果表满,显然是不能插入的,因此我们先检查容量,如果表是空的,那么就可以直接插入数据,如果非空,为了保持顺序与原有数据,就需要先将原有元素向后移动1位,再在表头插入数据,最后,当插入数据成功,更新size的值,也就是+1。


3 - 3 - 2 · 头删

代码如下:

复制代码
void SeqListPopFront(SeqList* p)
{
	assert(p);
	//空表不能删
	assert(p->size);
	//集体前移,size--
	int i = 0;
	for (i = 0; i < p->size - 1; i++)
	{
		p->a[i] = p->a[i + 1];
	}
	p->size--;
}

头删就是删除表头位置的数据。

空表显然是不能进行删除的,因此我们用assert断言,size不能为0。

我们只需要让除了表头位置的数据向前移动,覆盖原有值,最后更新size的值,也就是-1即可。

至于最后一个多出来的数据,可处理也可不处理,我们更新了size 也就更新了有效数据的总个数,原size 处的值(-1之前)对我们是无影响的。


3 - 3 - 3 · 打印

代码如下:

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

打印就是将顺序表中的元素打印出来。

注意:这里的形参可以不用使用指针,因为无需修改我们顺序表的成员。

但是还是建议使用指针,为了保持接口一致性,我们的接口当需要访问顺序表的时候统一使用的是指针,如果有的接口使用指针,有的接口不使用指针,就会造成混乱,不知道什么时候传参传什么,增加了使用时的记忆成本,因此,将参数统一使用指针,就是保持了接口一致,方便使用。


3 - 3 - 4 · 测试

我们对上面的功能测试一下:

复制代码
void Test2()
{
	SeqList s;
	SeqListInit(&s);
	
	SeqListPushFront(&s, 1);
	SeqListPrint(&s);

	SeqListPushFront(&s, 2);
	SeqListPrint(&s);

	SeqListPushFront(&s, 3);
	SeqListPrint(&s);

	SeqListPushFront(&s, 4);
	SeqListPrint(&s);

	SeqListPopFront(&s);
	SeqListPrint(&s);

	SeqListPopFront(&s);
	SeqListPrint(&s);

	SeqListPopFront(&s);
	SeqListPrint(&s);

	SeqListPopFront(&s);
	SeqListPrint(&s);

	/*SeqListPopFront(&s);
	SeqListPrint(&s);*/
	SeqListDestroy(&s);
}

int main()
{
	//Test1();
	Test2();

	return 0;
}

我们进行了4次头插,每次头插完进行一次打印,再进行4次头删,每次删完进行一次打印。

运行一下:

在此基础上,如果我们再次进行头删,就会触发我们的assert断言了。

将最后一个头删的注释取消后再运行:


3 - 4 · 尾插,尾删

3 - 4 - 1 · 尾插

代码如下:

复制代码
void SeqListPushBack(SeqList* p, SLDataType x)
{
	assert(p);
	CheakCapacity(p);
	p->a[p->size] = x;
	p->size++;
}

先检查容量,然后在顺序表尾插入数据,最后更新size 即可。


3 - 4 - 2 · 尾删

代码如下:

复制代码
void SeqListPopBack(SeqList* p)
{
	assert(p);
	//空表不能删
	assert(p->size);
	p->size--;
}

空表显然不能删,断言之后直接让size-1即可。

至于被删除的位置的值,可修改也可不修改,对我们是无影响的。


3 - 4 - 3 · 测试

我们测试一下上面的功能:

复制代码
void Test3()
{
	SeqList s;
	SeqListInit(&s);

	SeqListPushBack(&s, 1);
	SeqListPrint(&s);

	SeqListPushBack(&s, 2);
	SeqListPrint(&s);

	SeqListPushBack(&s, 3);
	SeqListPrint(&s);

	SeqListPopBack(&s);
	SeqListPrint(&s);

	SeqListPopBack(&s);
	SeqListPrint(&s);

	SeqListPopBack(&s);
	SeqListPrint(&s);

	/*SeqListPopBack(&s);
	SeqListPrint(&s);*/

	SeqListDestroy(&s);
}

int main()
{
	//Test1();
	//Test2();
	Test3();

	return 0;
}

我们进行三次尾插再三次尾删,每次执行完就打印一次

运行一下:

在此基础上,如果再次进行一次尾删,就会触发我们的assert断言了。


3 - 5 · 查找,指定位置插入,指定位置删除

3 - 5 - 1 · 查找

代码如下:

复制代码
int SeqListFind(SeqList* p, SLDataType x)
{
	assert(p);
	int i = 0;
	for (i = 0; i < p->size; i++)
	{
		if (p->a[i] == x)
		{
			return i;
		}
	}
	//没找到就返回一个无效的下标
	return -1;
}

查找就是找到一个值在顺序表中的位置,我们这里找的是下标

遍历顺序表并比较,找到了就返回,如果没找到就返回一个无效的下标。


3 - 5 - 2 · 指定位置插入

代码如下:

复制代码
void SeqListInsert(SeqList* p, int pos, SLDataType x)
{
	assert(p);
	//位置要在表内
	assert(pos >= 0 && pos <= p->size);
	CheakCapacity(p);
	int i = 0;
	for (i = p->size - 1; i >= pos; i--)
	{
		p->a[i + 1] = p->a[i];
	}
	p->a[pos] = x;
	p->size++;
}

由于顺序表的底层结构是数组,数据是连续存放的,因此可以做到随机访问。

这里指定的位置是下标位置。

首先要确保指定的位置在顺序表内,所以使用assert断言。pos是可以等于size的,虽然下标为size的位置是不在顺序表实际范围中的,但是是可以插入的,相当于尾插。

随后,既然是插入数据,必然是要先检查容量的,之后就是pos及pos后的元素先向后移动1位,再将数据插入到pos位置,最后更新size。

当然,如果指定位置为表头,那就等同于头插,指定位置为表尾,等同于尾插。


3 - 5 - 3 · 指定位置删除

代码如下:

复制代码
void SeqListErase(SeqList* p, int pos)
{
	assert(p);
	//空表不能删
	assert(p->size);
	//位置要在表内
	assert(pos < p->size);

	int i = 0;
	for (i = pos; i < p->size - 1; i++)
	{
		p->a[i] = p->a[i + 1];
	}
	p->size--;
}

这里的指定位置是下标位置。

空表显然不能删,所以用了assert断言。

确保指定位置要在表内,所以用了assert断言,与指定位置插入不同的是:pos不能等于size,因为下标为size的位置是不在顺序表中的。

只需让pos位置后的元素向前覆盖1位,再更新size即可。

当然,如果指定位置为表头,那就等同于头删,指定位置为表尾,等同于尾删。


3 - 5 - 4 · 测试

我们对上面的功能测试一下:

复制代码
void Test4()
{
	SeqList s;
	SeqListInit(&s);

	SeqListPushBack(&s, 1);
	SeqListPushBack(&s, 2);
	SeqListPushBack(&s, 3);
	SeqListPushBack(&s, 4);
	SeqListPrint(&s);

	SeqListInsert(&s, 0, 5);
	SeqListPrint(&s);

	SeqListInsert(&s, 5, 6);
	SeqListPrint(&s);

	SeqListErase(&s, 0);
	SeqListPrint(&s);

	SeqListErase(&s, 4);
	SeqListPrint(&s);

	int find = SeqListFind(&s, 4);
	printf("find == %d\n", find);

	find = SeqListFind(&s, 1);
	printf("find == %d\n", find);

	find = SeqListFind(&s, 6);
	printf("find == %d\n", find);

	SeqListDestroy(&s);
}

int main()
{
	//Test1();
	//Test2();
	//Test3();
	Test4();

	return 0;
}

运行一下:


4 · 顺序表的缺点

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

总结

以上简单介绍了顺序表有关内容,关于数据结构其余内容,请期待后续更新


以上内容如有错误或不准确之处,欢迎指出,或者你有更好的想法,也欢迎交流。

相关推荐
Lucky_ldy1 小时前
C语言学习:数据在内存中的存储
c语言·开发语言·学习
qeen871 小时前
【算法笔记】各种常见排序算法详细解析(上)
c语言·数据结构·c++·学习·算法·排序算法
青山师1 小时前
数组与链表深度解析:从内存布局到工业级实践
数据结构·算法·链表·数组·算法与数据结构
YangWeiminPHD2 小时前
金水32051编译器下的AI8051U单片机入门:从点亮LED到“你好,世界,我来了!”
c语言·汇编·51单片机·编译器
2301_789015622 小时前
Linux:基础指令(二)
linux·运维·服务器·c语言·开发语言·c++·算法
AI机器学习算法4 小时前
机器学习基础知识
数据结构·人工智能·python·深度学习·算法·机器学习·ai学习路线
坚果派·白晓明10 小时前
【鸿蒙PC三方库移植适配框架解读系列】第八篇:扩展lycium框架使其满足rust三方库适配
c语言·开发语言·华为·rust·harmonyos·鸿蒙
刀法如飞12 小时前
Ontology本体论是什么数据结构?Palantir 技术原理介绍
数据结构·人工智能·ai编程·图论
平行侠12 小时前
024多精度大整数 - 突破硬件精度限制的任意精度运算
数据结构·算法