数据结构0002(顺序表)

一,顺序表的概念 

顺序表是使用一段连续地址来依次存储数据元素的线性结构,一把情况下采用数组存储。

举个不恰当的例子,假设你所在的班有30人,而每个宿舍可以住6人。一般来说,在分宿舍时,学院会将同一个班的学生分在一起,即比如将A,B,C,D,E五个宿舍分给了你们班,这五个宿舍是挨着的,就好比顺序表一样,使用一段连续空间来存放数据。

 二,顺序表的分类

顺序表可以分为静态顺序表和动态顺序表。

(一)静态顺序表:使用定长数组来存储数据

#define N 7
typedef int SLDataType

//定长数组,不方便使用
typedef struct SeqList
{
    SLDataType array[N];
    size_t size;
}SList;

(二)动态顺序表:使用动态开辟的数组来存储

//动态顺序表

typedef int SLDataType

typedef struct SeqList
{
    SLDataType * array;  //这个是指向数组的指针
    size_t size;
    size_t capacity;     //这个是容量,当不够时我们需要扩容
}SL;

三,动态顺序表的实现

每一种结构的实现,我们都需要完成增删查改几个功能。

顺序表要包含以下的功能:

// 创建/销毁顺序表
void SLInit(SL* ps);
void SLDestory(SL* ps);

// 一个数据结构的四大功能:增删查改
// 增删数据:头插/头删/尾插/尾插/任意位置插入删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);

// 查找和修改
int SLFind(SL* ps, SLDataType x);
void SLModify(SL* ps, int pos, SLDataType x);

//辅助功能:检查容量,扩容操作/打印操作    
void SLCheckCapacity(SL* ps);
void SLPrint(SL* ps);

(一)初始化顺序表

设计函数SLInit实现对顺序表的初始化

由于初始化时,这个顺序表中还没有存储任何的数据,所以我们需要使顺序表中的指针指向NULL的同时让size和capacity也同时等于0。

初始化时,应当使用传址调用。若传值调用,由于形参的改变不会影响实参,所以无法将NULL和0初始化给相对应的数据。后面的几乎所有的代码都是要传地址的。

void SLInit(SL * ps)
{
    assert(ps); 
    ps -> a = NULL;
    ps -> size = ps -> capacity = 0;
}

(二)顺序表的销毁

设计函数SLDestory实现对顺序表的销毁

在扩容的时候由于使用了realloc函数,所以我们应该使用free函数来释放空间。

void SLDestory(SL * ps)
{
    assert(ps);                      //判空检查
    //如果不为空的话就释放空间并将指针置为空,size,capacity全部置为0
    if(ps->a != NULL)      
    {
        free(ps->a);
        ps->a=NULL;
        ps->size=ps->capacity=0;
    }
}

(三)顺序表的扩容

设计函数CheckCapacity实现对顺序表的扩容

对容量空间进行检查:没空没满啥也不干,若空则开辟,若满则扩容

当为空时,size = capacity == 0 ;当顺序表装满时,此时size = capacity != 0。所以,当size==capacity时,我们就需要扩容,size==capacity即是我们判断是否需要扩容的条件。

假设当其为空时,我们安排开辟四个,如果不为空时,每次空间使用完之后都扩容成为上一次空间的两倍。

void CheckCapacity(SL * ps)
{
    assert(ps);
    //这里的作用是检查容量,由上面的分析可知
    //判断是否申请或扩容的标准是size是否等于capacity
    if(ps->size==ps->capacity)
    {
        //这个代码用于确定顺序表需要的容量
        int newcapacity = (ps->capacity==0?4:ps->capacity*2);
        //当size==capacity==0时,我们需要的是开辟数据
        //当size==capacity!=0时,我们需要的是在原有空间上扩大二倍
        //这个语句也是可以用if语句写的,但是三目写起来是更简便的

        //这个代码用于动态开辟空间
        SLDataType* tmp =(SLDataType*)realloc(ps->a,newcapacity*sizeof(SLDataType));
        //void* realloc (void* ptr, size_t size)
        //我们需要的空间大小就是 对象个数*对象大小,其中对象个数就是我们的capacity

        //这个代码用于判断我们的动态开辟是否成功。如果不成功的话就会直接结束。
        if(tmp==NULL)
        {
            printf("realloc fail!!");
            return;
        }

        //如果成功开辟了,我们就把新空间给到ps->a,新容量给到ps->capacity
        ps->a=tmp;
        ps->capacity=newcapacity;
    }
}

(四)顺序表的打印

设计函数SLPrint实现对顺序表的打印

由size可以知道顺序表存储数据的多少,所以我们用size来判断终止的标志

void SLPrint(SL * ps)
{
    assert(ps);
    for(int i=0 ; i<ps->size ; i++)
    //定义i从0开始,只要i小于size,就打印a[i]的内容
    {
        printf("%d ",ps->a[i]);
    }
    printf("\n");
}

(五)顺序表的尾插

设计函数SLPushBack实现对顺序表的尾插

顺序表尾插的时间复杂度为O(1),是比较快速的

尾插时,我们要讨论三种情况:

1,若初始化后马上尾插,此时的size和capacity均为0

2,size==capacity!=0时,说明此时顺序表已经满了,要扩容

以上两种情况告诉我们,在进行插入操作之前,我们要对容量进行检查。

3,没满,这时正常插入即可

void SLPushBack (SL* ps,SLDataType x)
{
    assert(ps);
    //对容量空间进行检查,如果为空或满,就会自动扩容
    SLCheckCapacity(ps);

    //当尾插时,我们想插入位置的下标一定是size
    ps->a[ps->size]=x;

    //扩容之后要给size++
    ps->size++;
}

(五)顺序表的尾删

设计函数SLPopBack实现对顺序表的尾删

尾插时,队后一个位置的元素设置为多少其实并不重要,只要直接--size就可以。

原因就是:因为顺序表是按顺序排列的,size表示有效元素的个数。只要--size,即令最后一个元素失效。就等效于删除数据。

由于容量不会改变,所以不必给capacity进行任何操作。

void SLPopBack(SL* ps)
{
    //尾删时要注意,当我们删到没有数据,再次进行尾删时,
    //会导致size<0,即越界访问!!
    //所以我们应当对size进行检查,只有size大于0时才可以进行尾删。
    assert(ps!=NULL);
    assert(ps->size>0);

    ps->size--;
}

(六)顺序表的头插

设计函数SLPushFront实现对顺序表的头插

头插时,当我们给第一个位置放入了一个新的数据时,原先第一个位置的顺序就会被顶到第二个位置,以此类推,所有的数据都会向后挪动一位。

并且我们要注意,再插入数据前,我们要判断顺序表是否是满的,如果顺序表是满的,则需要为新数据扩容。

顺序表头插的时间复杂度为O(n),相较尾插是比较慢的。

挪动数据是也是有讲究的,挪动数据时要从后往前挪,否则会遮盖原本的数据。

挪动的大体思路如图:

对应该图的代码如下: 

void SLPushFront(SL* ps,SLDataType x)
{
    assert(ps);
    //检查容量是否已满
    SLCheckCapacity(ps);
    //定义一个结尾位置end,便于我们从后向前挪动数据
    int end = ps->size-1;
    while(end>=0)
    {
        ps->a[end+1]=ps->a[end];
        end--;
    }
    //当循环结束后直接把想插入的数据给到ps->a[0];
    ps->a[0]=x;
    ps->size++;
} 

(六)顺序表的头删

设计函数SLPopFront实现对顺序表的头删

头删时,数据如同尾删一样,也是要挪动的。但是尾插是从后向前挪动,头插是从前向后挪动。

void SLPopFront(SL* ps)
{
    assert(ps);
    assert(ps->size>0)

    int begin = 0;
    while(begin<ps->size)
    {
        ps->a[i]=ps->a[i+1];
        begin++;
    }
    ps->size--;
}

(七)顺序表任意位置的插入

设计函数SLInsert实现对顺序表的任意位置的插入。

这个插入函数,可以服用在头插和尾插之中。

插入数据以后,就像尾插一样,数据要从后面向前挪动,再在pos位置处检查。并且对插入的位置也是有要求的!!如下图。

首先是插入位置1,该位置比capacity都大,一定不是合法位置。

其次是插入位置2,由于顺序表的定义是连续存储,所以这个位置也不是合法的。

之后是插入位置3,这个位置就相当于尾插,是可以的。

最后是插入位置4,这个位置不用问就是合法的。

所以,综上所述,可以插入的位置是:pos>=0并且pos<=ps->size。

代码写作:

void SLInsert(SL* ps,int pos,SLDataType x)
{
    assert(ps);
    assert(pos<=ps->sizee&&pos>=0);
    SLCheckCapacity(ps);

    //挪动数据
    int end=ps->size-1;
    while(end>=pos)
    {
        ps->a[end+1]=ps->a[end];
        --end;
    }

    ps->a[pos]=x;
    ps->size++;
}

实现这个代码之后,头插和尾插可以这样写:

void SLPushFront(SL* ps,SLDataType x)
{
    SLInsert(ps,0,x);
} 

void SLPushBack (SL* ps,SLDataType x)
{
    SLInsert(ps,ps->size,x);
}

(八)顺序表任意位置的删除

设计函数SLErase实现对顺序表的任意位置的删除。

同之前一样,删除时数据从前往后移,这个函数同时也可以实现头删,尾删。

void SLErase(SL* ps, int pos)
{
    assert(ps);
    assert(pos>=0&&pos<ps->size);
    //此时不可以等于size,这样写还顺便检查了size为空的情况

    int begin = pos+1;
    while(begin<ps->size)
    {
        ps->a[begin-1]=ps->a[begin];
        ++begin;
    }
    ps->size--;
}

实现这个代码之后,头删和尾删可以这样写:

void SLPopFront(SL* ps)
{
    SLErase(ps, 0);
}

void SLPopBack(SL* ps)
{
    SLErase(ps, ps->size-1);
}

(九)顺序表查找数据,返回查找到数据的下标

设计函数SLFind实现对顺序表数据的查找。

int SLFind(SL* ps,SLDataType x)
{
    assert(ps);

    for(int i=0;i<ps->size;i++)
    {
        //当找到值为x的元素时就返回下标
        if(ps->a[i]==x)
        return i;
    }

    //当没有找到时,就返回-1,由于一定没有下标为负的元素
    //所以当返回值为-1就证明没有找到
    return -1;
}

(十)顺序表修改pos位置的元素的值

设计函数SLModify实现对顺序表pos位置元素数据的修改。

void SLModify(SL* ps,int pos,SLDataType x)
{
    assert(ps);
    assert(pos>=0&&pos<ps->size);
    ps->a[pos]=x;
}

四,顺序表的优缺点

优点:作为数组,有着连续的物理空间,方便组织数据。

           支持下标的随机访问。

缺点:要求物理空间连续,可能会造成空间浪费,性能消耗。

          头插/头删与中间插入/删除效率低下O(N),仅尾插/尾删效率较高O(1)。

知识小结:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值