数据结构之“线性表(数组)”_顺序表

前言:

线性表:几个具有相同特性的数据元素的有限序列,线性表在逻辑上是线性结构,也就是连续的一条直线

顾名思义“线性表”成一条线的表,在IT领域的数据结构中也有很多能看到的线性表,如“人员花名册”,“网络商品”,“图书名单系统”等等,都是一个个信息紧跟着排好供我们选择浏览等等~但这些结构的顺序是如何实现的呢?

接下来该文章主要针对线性表其一的“顺序表”进行主要讲解🔊


一,顺序表定义

其本质就是数组,必须从头开始一个接一个挨个存放,不能跳跃间隔。

也就是说这些数据就是一连串的在一起就像一条线一样,中间任意位置都不能有间隙断开的情况。

如何定义一个良好的多功能的顺序表呢?(C语言为例)

1,静态顺序表

在头文件中,首先做好以下几项基础工作:

  • #pragma once//避免以下各种定义被重复声明
  • #define N 100//定义一个常量表示符,主要用于我们定义数组大小能够方便修改
  • #typedef int SLDataType//重命名各种数据类型(这里以int为例),同样方便我们修改存储数据的类型
  • //下面创建一个静态顺序表(特点:根据“N”确定顺序表能够存储空间的大小)
  • typedef struct seqlist//定义结构体并重命名为'SL'(方便书写使用)
  • {
  • SLDataType [N];//"SLDataType"就呼吁了上方的数据类型的重命名“[N]“也能够达到修改大小的便利
  • int size;//记录数组元素个数
  • }SL;

以上就是静态顺序表的结构创建

为什么称为静态顺序表呢?

因为该顺序表初始定义的大小由#define 定义的标识符常量来决定其大小的,且常量所开辟的空间是存在于静态区的,所 以该顺序表即可称之为静态顺序表。

但是静态顺序表也有弊端:它所规定的空间大小是死的,如果空间太小则不够更多数据的存放,如果空间太大便会造成许多空间的浪费。

对此问题下面介绍:

2,动态顺序表
  • typedef struct seqlist
  • {
  • SLDataType *pf;//"pf"该类型指针,用于指向动态开辟的空间大小
  • int size;//记录数据个数
  • int capacity;//记录空间容量
  • }SL;

该动态顺序表结构就更好的解决了静态顺序表的空间问题,以一个指针来指向一块动态开辟的内存空间也就是”堆空间“,它能根据其所需的大小进行扩容到一定大小来存储,就很好的避免了空间大量浪费的问题。

总的代码实现:

//头文件中
//动态顺序表结构
#pragma once 
//基本的头文件声明
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//数据类型重命名
typedef int SLDataType;
//动态顺序表结构
typedef struct SeqList
{
	SLDataType *pf;
  int size;
  int capacity;
}

以上的就是动态顺序表结构的定义代码。



二,接口函数

根据这个结构来完成我们所需的一些基本普遍操作:”增“,”删“,”查“,”改“。


这些操作分别对应着每个接口函数来进行实现。

注意:我们针对各种接口函数的命名风格都要整齐统一。

1.接口声明

首先针对该数据结构的各种功能的接口函数声明于头文件之中。

//头文件中
//动态顺序表结构
#pragma once 
//基本的头文件声明
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//数据类型重命名
typedef int SLDataType;
//动态顺序表结构
typedef struct SeqList
{
	SLDataType *pf;//指向一块动态空间来存放的数据
  int size;//记录空间数据个数
  int capacity;//记录空间大小
}SL;
//接口函数声明
void SeqListInit(SL *ps); //初始化
void SeqListPushBack(SL *ps, SLDataType x); //数据尾插
void SeqListPopBack(SL *ps); //数据尾删
void SeqListPushFront(SL *ps, SLDataType x); //数据头插
void SeqListPopFront(SL *ps); //数据头删
void SeqListIsert(SL *ps, int pos, SLDataType x); //数据插入
void SeqListDelete(SL *ps, int pos); //数据删除
int SeqListFind(SL *ps, SLDataType x); //数据查找
void SeqListchange(SL *ps, int pos, SLDataType x); //数据更改
void SeqListDestroy(SL *ps); //数据表销毁

以上就是”动态顺序表“的基本操作(”增“,”删“,”查“,”改“)接口函数的声明。

注意:当我们对这个结构体进行操作时应注意以下几点:

1,当我们创建了该结构体变量,首先进行变量的初始化

2,无论实现哪一个接口函数,都应该进行传址调用,只有传址调用才能改变该结构体变量中的数据

(重点)(这也是就每个接口函数的形参都为指针变量的原因)

3,注意空间内存泄漏和野指针的问题,”动态顺序表“是在堆上来进行开辟空间,一旦某个空间数据不需要了,或者对整空间进行销毁,都需要我们亲自对其空间进行释放,并且把指向该空间的指针进行置空。

4,断言“assert”,无论实现哪个接口函数都需要考虑各种特殊的因素,例如:开辟堆空间未成功,空间数据异常,空间数据删过头等等...这些情况都需要进行断言,以便于我们进行调试修改。


2.接口定义

基本的接口函数声明介绍完了,下面对这些接口函数的各个功能进行具体的实现。

void SeqListInit(SL *ps); //初始化

当有了该结构体变量首先对其进行初始化,如创建一个整型变量进行初始化int a=0;如果不初始化int a;其a的值会被赋为随机值,同样创建该结构体变量如果不初始化,其中的数据也会被置为随机值,就不便于对该结构体变量的数据进行操作使用。

则该接口函数的具体实现:

void SeqListInit(SL *ps) //初始化
{
	assert(ps);//断言“确保是有效指针”
  
	ps->pf=NULL;//“初始化为NULL”
  ps->size=0;//数据个数初始化为0
  ps->capacity=0;//空间大小初始化为0
}


void SeqListPushBack(SL *ps, SLDataType x); //数据尾插

该接口函数的实现功能:在数据的末尾插入一个数,相当于在现有的数据里的最后位置再填上一个数。

而实现这个功能,我们需要考虑以下几种因素:1.当前空间是否足够?,2.如果不够扩容多大空间?

则该接口函数的具体实现:

void SeqListPushBack(SL *ps, SLDataType x) //数据尾插
{
	assert(ps); //断言
  
  //在入数据之前先判断空间够不够
  //根据该结构中的size数据个数 与capacity 空间大小相比较
  //如果当 size 等于 capacity 说明空间已经满了 我们就需要扩容
  if(ps->size == ps->capacity)
  {
    	//扩容需要扩多大呢?如果每次只括一个数据的空间的话虽然能充分利用空间但是其效率太低。
    	//因为每入一次数据就需要扩容一次大大影响了代码效率。
    	//所以这里我采用了两倍扩容,每次空间满了就扩大目前空间的两倍,虽然可能会有些空间浪费,但这也是空间换时间效率的一种。
    	int newcapacity = capacity == 0 ? 4 : 2 * ps->capacity;//如果目前空间为0的话先给上4个数据空间大小,之后再扩容两倍
    	SLDataType *newnode=(SLDataType*)realloc(ps, sizeof(SLDataType)*newcapacity);
    	if(newnode==NULL) //防止空间扩容失败
        	assert(newnode);
    	ps=newnode;
  }
  //空间问题解决了,下面就可以安心尾插数据了
  ps->pf[ps->size] = x; //size 代表当前数据个数,同时也代表尾数据的下一个数据下标
  ps->size++; //成功尾插 size++
}


void SeqListPopBack(SL *ps); //数据尾删

该接口函数的实现功能:删除一个末尾数据,相当于把现有数据的最后一个数据进行删除

实现该功能,考虑的因素:1,空间是否有数据支持我们删除

该接口函数的具体实现:

void SeqListPopBack(SL *ps) //数据尾删
{
  	assert(ps); //断言
  	//删除数据则考虑空间中是否有数据供我们删除
  	assert(ps->size!=0); //再对此进行断言
  	
  	//如果有数据正常进行删除
  	//数组进行删除数据并不需要将其空间释放,只需改变size控制的下标即可
  	ps->size--;
}


void SeqListPushFront(SL *ps, SLDataType x); //数据头插

该接口函数的实现功能:在数据的首位置插入一个数,相当于在头位置插入一个数使其为下标为0的数。

实现该功能,考虑的因素:1,空间是否足够。2,不能打乱线性结构

该接口函数的实现:

void SeqListPushFront(SL *ps, SLDataType x) //数据头插
{
  	assert(ps); //断言
  	//插入前考虑增容
  	if(ps->size == ps->capacity)
  	{
    	int newcapacity = capacity == 0 ? 4 : 2 * ps->capacity;//如果目前空间为0的话先给上4个数据空间大小,之后再扩容两倍
    	SLDataType *newnode=(SLDataType*)realloc(ps, sizeof(SLDataType)*newcapacity);
    	if(newnode==NULL) //防止空间扩容失败
        	assert(newnode);
    	ps=newnode;
  	}
  	//因为是在头部位置进行数据的插入,且不能打乱之前数据的顺序。
 		//则在插入之前只能将所有的数往后挪一个位置,空出开头的位置,再将该数插入头位置。
  	//因为要满足线性结构,插入一个数据的时间复杂度为:O(n)
  	//且挪动数据只能从后往前挪动,如果从前往后挪动会将原本后面的数进行覆盖。
  	for(int i=ps->size; i>0; i--)
    {
        ps->pf[i] = ps->pf[i-1]; //数据挪动
    }
    
    ps->pf[0] = x; //头插
    ps->size++; //记录数据+
}


void SeqListPopFront(SL *ps); //数据头删

该接口函数的实现功能:在数据的首位置删除一个数,相当于删除当前数据的第一个数据。

实现该功能,考虑的因素:1,空间是否有数据。2,不能打乱线性结构

该接口函数的实现:

void SeqListPopFront(SL *ps); //数据头删
{
  assert(ps); //断言
  assert(ps->size); //判断空间是否有数据
  
  //该接口函数的功能是删除第一个数,且必须保持线性结构的规则,则将第二个数开始依次往前挪动一个位置
  for(int i=1; i<ps->size; i++)
  {
    ps->pf[i-1] = ps->pf[i]; //依次往前挪一个数
  }
  ps->size--; //记录数据-1
}


void SeqListIsert(SL *ps, int pos, SLDataType x); //数据插入

该接口函数的实现功能:在数据的pos位置插入一个数,相当于在当前数据的指定位置插入一个数据。

实现该功能,考虑的因素:1,空间是否足够。2,不能打乱线性结构,以及原有数据的顺序位置

该接口函数的实现:

void SeqListIsert(SL *ps, int pos, SLDataType x) //数据插入
{
  	assert(ps); //断言
  	assert(pos>0 && pos<=ps->size+1); //确保插入位置有效
  	//无论在哪插入数据,将该位置之后的数据往后挪动一位再进行插入
  	int tmp=ps->size; //保存当前数据个数
  	
  	for(tmp; tmp>=pos; tmp--)
    {
        ps->pf[tmp] = ps->pf[tmp-1];
    }
    
    ps->pf[pos-1] = x; //插入数据,注意下标
    ps->size++;
}


void SeqListDelete(SL *ps, int pos); //数据删除

该接口函数的实现功能:删除pos位置的数,相当于删除当前数据指定位置的数。

实现该功能,考虑的因素:1,数据是否足够。2,不能打乱线性结构,以及原有数据的顺序位置

该接口函数的实现:

void SeqListDelete(SL *ps, int pos) //数据删除
{
  assert(ps); //断言
  assert(ps->size); //判断是否有数据
  //若删除随机位置的数,则将该位置之后的数往前挪动一位。
  int tmp = ps->size;
  
  for(tmp; pos<tmp; pos++)
  {
    ps->pf[pos-1] = ps->pf[pos];
  }
  ps->size--;
}


int SeqListFind(SL *ps, SLDataType x); //数据查找

该接口函数的实现功能:查找一个数,相当于查找当前数据的一个数。

实现该功能,考虑的因素:1,是否有这个数。2,如果有,返回该数的位置,没有则返回0

该接口函数的实现:

int SeqListFind(SL *ps, SLDataType x) //数据查找
{
 	 assert(ps); //断言
   
   //实现查找,只需将数依次比较,找到了返回该数的位置,没找到返回0
   for(int i=0; i<ps->size; i++)
   {
      if(ps->pf[i] == x)
         return i+1 ;//注意i是下标位置,返回实时位置则需+1
   }
   //当循环出来说明没找到
   return 0;
}

void SeqListchange(SL *ps, int pos, SLDataType x); //数据更改

该接口函数的实现功能:更改pos位置的数,相当于更改当前数据的pos位置的数。

实现该功能,考虑的因素:1,pos的位置小于数据的个数。

该接口函数的实现:

void SeqListchange(SL *ps, int pos, SLDataType x) //数据更改
{
  	assert(ps); //断言
  	assert(pos>0 && pos<=ps->size); //确保pos位置正确
  	//因为pos位置的对应的下标需-1
  	ps->pf[pos-1] = x;
}

该接口函数配合“数据查找”的功能配合使用。

int pos =SeqListFind(ps, x);//查找某个数,获得该数的位置
SeqListchange(ps, pos, x);//且有了该数的位置就能对其进行更改


void SeqListDestroy(SL *ps, SLDataType x); //数据表销毁

该接口函数的实现功能:销毁该表的数据,将当前的所有数据进行销毁

实现该功能,考虑的因素:1,避免内存泄漏以及野指针的问题。

该接口函数的实现:

void SeqListDestroy(SL *ps) //数据表销毁
{
  assert(ps); //断言
  //该数据都存在于堆中,需要亲自进行空间释放
  free(ps->pf); //释放空间
  ps->pf=NULL; //将其置空,避免野指针
  ps->size=ps->capacity=0;//同时将 数据 以及空间都为0
}


三,顺序表的优缺点

优点:顺序表是支持随机访问的,且只要知道数据的位置就能很快的访问到我们想要的数据.

   可以看出顺序表访问的时间复杂度为O(1);

缺点:顺序表的满足条件必须是一块连续的空间才能形成此结构,但如果我们的数据比较庞大,则需要很大的连续空间才能进行存储,这样就使得空间利用度并不是很高,且可以发现顺序表每次存储数据或者删除数据的时间复杂度是O(n),只有在尾部增删的时间复杂度为O(1),这样使得效率就很不稳定。

完结 撒花

总结:

实现一个动态的顺序表还是比较简单的,只要注意其许多细节上的因素,但该结构所带来的弊端且大于利端,可以看出顺序表主要还是对于读数据比较方便,在写数据上还是稍差一筹,针对这个结构问题我们可以了解了解链式结构的组成,看看链式结构与顺序结构到底有何不同~


🛸拜拜~🛸

数据结构之“线性表(数组)”_ci_02