第二章 数据结构与算法 —— 线性表

2.1 线性表的介绍

目录

2.1 线性表的介绍

 2.1.1、线性表的定义

2.1.2、线性表的相关性质

2.2 线性表的顺序存储结构——顺序表

2.2.1、顺序表的概念

2.2.2、顺序表的优缺点

2.2.3、顺序表的分类及代码定义(静态和动态)

2.2.4、顺序表的基本操作(重点)


 2.1.1、线性表的定义

(1)、线性表是n(n>=0)个具有相同特性的数据元素的有限序列,其中n表示线性表的长度,即数据元素个数。如下图:

2.1.2、线性表的相关性质

(1)、n=0时表示空表,n>0时通常记为(a1,a2,a3......an),a1表示线性表首元素,an表示尾元素

(2)、其中,a(m-1)是a(m)的直接前驱,a(m)是a(m-1)的直接后继。

(3)、除了a1只有唯一后后继,an只有唯一前驱外,其余元素均只有唯一的前驱和后继。

(4)、线性表的逻辑结构示意图:

 (5)、每个数据元素的具体含义在不同的线性表中各不相同,它可以是一个数,或是一个符号,也可以是一个记录,甚至是其他更为复杂的信息。

2.2 线性表的顺序存储结构——顺序表

2.2.1、顺序表的概念

(1)、在内存中开辟一段连续的存储空间,用一组连续的存储单元依次存放数据元素,这种存储方式叫做线性表的顺序存储结构,简称顺序表。一般用数组存储,在数组上完成数据的增删查找等操作。

(2)、顺序存储结构的特点:在逻辑上相邻的数据元素,它们的物理位置也是相邻的。

(3)、由于线性表中的数据元素具有相同的特性,所以很容易确定表中第i个元素的存储地址

假设表中每个元素占用m个存储单元,并以所占的第一个存储单元的存储地址作为数据元素的存储地址,则表中第i个数据的存储位置是:

                                              LOC(ai)=LOC(a1)+(i-1)*m

其中:  LOC(a1)是表的第一个数据元素a1的存储位置,通常称为线性表的起始位置或基地址.

注:因为线性表只要确定了基地址和一个数据元素的大小m,那么表中任一元素的存储地址都可根据上述公式计算,这样就可以随机存取顺序表中的任意元素。因此,线性表的顺序存储结构是一种随机存取的存储结构

(4)、顺序表与数组的区别:数组是一片连续空间,但可以随便在某一空间放值,但顺序表不同,顺序表也是一片连续的空间,但存放数据必须依次存放

2.2.2、顺序表的优缺点

(1)、优点:

①:随机存取元素容易实现,根据上述定位公式容易确定表中每个元素的存储位置,所以要指定i个结点很方便。

②:简单、直观。

(2)、缺点:
①:插入和删除结点困难:由于表中的结点是依次连续存放的,所以插入和删除一个结点是时,必须将插入点以后的结点依次向后移动,或者删除点以后的结点依次向前移动。

②:扩展不灵活(静态):建立表时,若估计不到表的最大长度,就难以确定的分配空间,影响扩展。

③:容易造成浪费:分配空间过大时,会造成预留空间浪费。

2.2.3、顺序表的分类及代码定义(静态和动态)

顺序表一般分为两类,如下:

(1)、静态顺序表:

代码定义如下:

#define N 100
typedef int SLDatatype;
struct Seqlist
{
	SLDatatype q[N];//开辟的连续空间
	int size;//有效数据个数
};

解释:

①:N为所开辟的总空间,为使之方便改变,所以用define宏定义。

②:为使方便存放不同类型的数据,所以用了typedef对类型重命名。

③:数组q是所开辟的一份连续空间。

④:size为有效数据个数,即当前表中所存在的元素个数。

注:静态顺序表有几个缺点:像静态顺序表这样定义就写死了,N给100可能不够用。N给200又可能只用到10,严重浪费空间。所以我们一般使用下面一种定义方式——动态顺序表。

(2)、动态顺序表:

代码定义如下:

typedef int SLDatatype;
typedef struct Seqlist
{
	SLDatatype* q;//指向动态开辟的数组
	int size;//有效数据个数
	int capicity;//容量空间大小
}SL;

解释:

①:类型重命名同上。

②:这里定义的指针q是用来指向动态开辟的数组

③:size是有效数据个数,即顺序表现有的数据个数。

④:capicity是总容量空间大小,一是记录当前最大空间,二是方便后续扩充空间。

2.2.4、顺序表的基本操作(重点)

(1)、初始化函数

代码实现如下:

void SLInit(SL* ps)
{
	ps->capicity = 5;
	ps->size = 0;
	ps->q = (SLDatatype*)malloc(sizeof(SLDatatype) * 5);
	if (ps->q == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
}

解释:

①:形参是结构体指针,用于接收结构体对象的地址,因为形参是实参的临时拷贝,所以我们整个操作都是传址调用

②:初始化为空表,所以size置0,capicity初始化应该和初始开辟的空间等大,因为需要用于判断是否表满,然后还可以用来扩充顺序表空间大小,扩充完后,capicity又应该和新的空间等大。

③:动态顺序表顾名思义是动态开辟一块连续的空间,这里使用到malloc函数动态开辟一块空间,并使指针q指向它,初始空间可根据实际大致估算预留空间。

④:通常malloc函数使用后,我们会去检验一下开辟空间是否成功,malloc开辟失败会返回空,所以作为判断条件。具体小伙伴们可自行了解。

(2)、释放空间(销毁)函数:因为我们是动态开辟的空间,使用完后应该将空间还给操作系统。

代码实现如下:

void Sldestroy(SL* ps)
{
	free(ps->q);
	ps->q = NULL;
	ps->capicity = ps->size = 0;
}

接下来该是插入操作,这里插入我们分为头插法和尾插法,同理删除也为头删法和为尾删法。

这里值得注意,插入数据的时候,我们应当先判断顺序表有没有满的情况,又因为有尾插法和头插法都要判断,所以将此单独创建一个函数,如下:、

(3)、判断是否表满函数
代码实现如下:

void SLCheckCapicity(SL* ps)
{

	if (ps->size == ps->capicity)
	{
		SLDatatype* tmp = (SLDatatype*)realloc(ps->q, ps->capicity * 2 * sizeof(SLDatatype));
		if (tmp == NULL)
		{
			perror("realloc failed");
			exit(-1);
		}
		ps->q = tmp;
		ps->capicity *= 2;
	}
}

解释:

①:顺序表涉及数组,数组就涉及下标,所以关于线性表的个个知识,小编建议大家多多画图分析,如下图:

可以看出,顺序表表满的时候,size==capicity,所以可用作 if 判断条件。

 ②:当判断条件成立,即表满时,我们就需要扩充空间,这里用到realloc函数(建议小伙伴们自行去学习一下realloc函数,各个参数代表什么,有什么功能,返回值是什么)。

前面提到的capicity有扩充空间的功能就用在于此,一般我们扩充一次空间就扩充为两倍的capicity,即原来空间的两倍,同时应该capicity*=2,用于记录扩充后的空间大小。

③:至于这里为什么是新创建一个指针变量tmp用于扩充空间,然后将tmp直接赋值给指针q,这得益于realloc函数的功能,大家可自学一下realloc函数。

(4)、尾插法:顾名思义就是在尾部插入元素.

代码实现如下:

void SLPushBack(SL* pa, SLDatatype x)
{
	SLCheckCapicity(pa);
	pa->q[pa->size++] = x;
}

解释:

①:形参中指针pa同上,用于接收对象地址,x即要插入的元素。

②:不管什么插入方法,首先都要判断是否表满,所以调用上述我们实现的SLCheckCapicity函数进行判断。

③:接着就是插入,因为size是现有元素个数,而下标从0开始,所以如下图,我们只需要将x放在数组q的下标为size处,即q[size]=x,然后再将size自增一下,即可完成尾插法。

 (5)、有了尾插自然就有尾删法:顾名思义就是在尾部删除数据
代码实现如下:

void SLPopBack(SL* pa)
{
	//判断是否为空表
	if (pa->size == 0)
	{
		assert(pa->size > 0);
	}
	pa->size--;
}

解释:

①:不管是什么删除元素的方法,刚开始肯定要先判断表中是否有元素,有的话才能删除。这里判断的处理方法小编给有两种,一种是事例上面用一个名为“断言”的函数assert,形参为一个表达式,若表达式为假,则assert函数会直接报错。另一种是直接return,使删除函数结束。

②:思路:因为是顺序表,所以尾删法直接使size自减一下就行了,这样就访问不到尾元素了,下次再增加数据也可以直接覆盖掉原有数据。

(6)、头插法:顾名思义就是在头部插入数据

代码实现如下:

void SLPushFront(SL* pa, SLDatatype x)
{
	SLCheckCapicity(pa);
	int end = pa->size - 1;
	//从后往前移动数据
	while (end >= 0)
	{
		pa->q[end + 1] = pa->q[end];
		end--;
	}
	pa->q[0] = x;
	pa->size++;
}

解释:

①:同样是插入数据,所以开始先调用SLCheckCapicity函数判断是否表满。

②:思路;然后因为要在头部插入,所以在空间足够的情况下,我们只需将原表中的数据依次向后移动一个存储空间,然后再将数据x放入首元素q[0]的位置处,插入成功后size自增一下即可。

(7)、头删法顾名思义就是在头部删除数据。

代码实现如下:

void SLPopFront(SL* pa)
{
	if (pa->size == 0)
	{
		assert(pa->size > 0);
	}
	int left = 1;
	while (left <= pa->size - 1)
	{
		pa->q[left-1] = pa->q[left];
		left++;
	}
	pa->size--;
}

解释:

①:删除数据,同理先判断是否表空。

②:思路:因为是在删除元素,所以只需将首元素后面size-1个数据依次向前移动一个存储空间,将首元素覆盖掉,然后size自减一下即可。注意是从前往后向前移动,防止丢失数据。

(8).在下标为pos的位置插入数据x

代码实现如下:

void SLInsert(SL* ps, int pos, SLDatatype x)
{
	assert(pos <= ps->size && pos >= 0);//判断插入位置是否无效
	SLCheckCapicity(ps);
	int end = ps->size-1;
	while (end >=pos)
	{
		ps->q[end + 1] = ps->q[end];
		end--;
	}
	ps->q[pos] = x;
	ps->size++;
}

解释:

①:形参中指针ps用于接收对象地址,pos为要插入数据的位置(下标),x为要插入数据的值。

②:思路:将原pos位置及pos位置之后的值依次向后一个储存空间,切记从后向前移动,然后再把x放到下标为pos的位置。

③:用到库函数断言assert,用于判断插入下标pos是否合法,pos必须大于等于0且小于等于size.

④:依旧需要判断是否表满,调用函数SLCheckCapicity。

⑤:移动涉及循环,数组涉及下标,所以一定要画图分析各个下标,可以达到事半功倍的效果:

 最后再将x放到下标为pos的位置,然后size自增一下即可。

(9)、当有了第(9)种操作后,我们会发现如下:

①:pos==size时,即为尾插算法,所以尾插可复用此函数,即:


//尾插
void SLPushBack(SL* pa, SLDatatype x)
{
	//正常方法:
	/*SLCheckCapicity(pa);
	pa->q[pa->size++] = x;*/

	//复用函数SLInsert
	SLInsert(pa, pa->size, x);
}

②:pos==0时,即为头插算法,同理如下:

//头插
void SLPushFront(SL* pa, SLDatatype x)
{
	//正常方法:
	//SLCheckCapicity(pa);
	//int end = pa->size - 1;
	从后往前移动数据
	//while (end >= 0)
	//{
	//	pa->q[end + 1] = pa->q[end];
	//	end--;
	//}
	//pa->q[0] = x;
	//pa->size++;

	//复用函数SLInsert
	SLInsert(pa, 0, x);
}

大家会发现是不是大量减少了代码量。 

(10)、删除pos位置的数据

代码实现如下:

//删除pos位置的数据,下标为pos-1
void SLErase(SL* ps, int pos)
{
	assert(ps->size != 0 && pos >= 0 && pos < ps->size);
	//直接将pos位置覆盖
	int left = pos + 1;
	while (left < ps->size)
	{
		ps->q[left-1] = ps->q[left];
		left++;
	}
	ps->size--;
}

解释:

①:形参中。pos为要删除的数据的下标。

②:然后需要判断一下下标pos的值是否有效,这里依然用到库函数assert。

③:思路:只需要将下标为pos的位置及其pos之后的数据依次向前移动一个存储空间,将要删除的数据覆盖即可达到删除的目的,最后size自减一下即可。

④:依旧需要画图分析,小伙伴们可自行尝试。

(11)、当有了第10种操作后,我们会发现如下:

①:pos==0时,即为头插算法。

//本章知识尚未结束,后续小编会持续更新!

猜你喜欢

转载自blog.csdn.net/hffh123/article/details/131962368