Bootstrap

数据结构——线性表

线性表概念

线性表(List):零个或多个数据元素的有限序列

线性表的抽象数据类型

ADT 线性表
Data
    线性表的数据对象集合为(a1,a2,.....an),每个元素的类型均为DataType。其中,除第一个元素a1外
每个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素都有且只有一个直接后继元素。数据元素
之间的关系是一对一的关系。
Operation
    InitList(*L)        初始化操作,建立一个空的线性表
    ListEmpty(L)        若线性表为空,返回true,否则返回false
    ClearList(*L)       将线性表清空
    GetElem(L,i,*e)     将线性表中的第i个位置元素返回给e
    LocateElem(L,e)     在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序号
                        表示成功,否则返回0表示失败
    ListInsert(*L,i,e)  在线性表中的第i个位置插入新元素e
    ListDelete(*L,i,*e) 删除线性表L中第i个位置元素,并用返回值e返回其值
    ListLength(L)       返回线性表L的元素个数
endADT

线性表的顺序存储结构

顺序存储定义

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。

线性表(a1,a2,......an)的顺序存储结构如下:

举个例子,有个同学,人特别老实,热心,经常帮室友去图书馆占座,他总是答应,如果一个宿舍九个人,他每一次吃完早饭,就冲去图书馆,挑一个好地,把他书包里的书一本一本的按座位放好,这里连续的九个位置就相当于是一个顺序存储结构。

顺序存储方式

线性表的顺序存储结构,说白了和上面的例子一样,就是在内存中找了块地方,通过占位的形式,把一定内存空间给占了,然后把相同数据类型的数据元素依次存放在这块空地中,既然线性表的每个数据元素类型都相同,所以可以用C语言(其他语言也相同)的一维数组来实现顺序存储结构,即把第一个数据元素存到数组下标为0的位置中,接着把线性表相邻的元素存储到数组中相邻的位置。

可以看下,线性表的顺序存储结构代码

#define MAXSIZE 20        /*存储空间初始分配量*/
typedef int Elemtype     /*Elemtype类型根据实际情况而定,这里假设为int*/
typedef struct
{
    Elemtype data[MAXSIZE];    /*数组存储数据元素,最大值为MAXSIZE*/
    int length;                /*线性表当前长度*/
}Sqlist;

这里,我们可以发现描述顺序存储结构需要三个属性

  • 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置

  • 线性表的最大存储容量:数组长度MaxSize

  • 线性表的当前长度:length

数据长度和线性表长度的区别

数组的长度:是存放线性表的存储空间的长度,存储分配后这个量一般是不变的,其实数组长度也可以变化,动态分配一维数组和柔性数组可以是不定长的数组。

线性表的长度:是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。

在任意时刻,线性表的长度小于等于数组的长度。

地址计算方法

由于我们的数都是从1开始数的,线性表的定义也不能免俗,起始也是1,可C语言的数组却是从0开始第一个下标的,于是线性表的第i个元素是要存储在数组下标为i-1的位置,即数据元素的序号和存放它的数组下标之间存在对应关系:

用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以插入和删除操作,因此分配的数组空间要大于当前线性表的长度。

其实,内存中的地址,就和图书馆里或者电影院里的座位一样,都是有编号的。存储器中的每个存储单元都有自己的编号,这个编号称之为地址。

LOC(ai+1) = LOC(ai) + c

所以对于第i个数据元素ai可以由a1推算得出:

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

顺序存储结构的操作

线性表的动态分配顺序存储结构

#define LIST_INIT_SIZE 10  //线性表存储空间的初始分配量
#define LIST_INCREMENT 2   //线性表的存储空间的分配增量
struct SqList
{
    Elemtype *elem;  //存储空间基址
    int length;     //当前长度
    int listsize;  //当前分配的存储容量(以sizeof(Elemtype)为单位
}

其实,可以不采用固定数组作为线性表的存储结构,而是采用动态分配的存储结构,这样可以合理的利用空间,使长表占用的存储空间多,短表占用的存储空间少,这样内存的分配更加灵活。

初始化线性表

void InitList(SqList *L)
{
    L->elem = (Elemtype *)malloc(sizeof(Elemtype) * length);
    if(L->elem == NULL)
    {
        exit(OVERFLOW);
    }
    L->length = 0; //空表长度为0
    L->listsize = LIST_INIT_SIZE; //初始存储容量
}

销毁线性表

void Destroy(SqList *L)
{
    //初始条件:顺序线性表已经存在,操作结果:销毁顺序线性表
    free(L->elem);  //释放L->elem所指向的存储空间
    L->elem = NULL;    //L->elem不再指向任何存储单元
    L—>length = 0;
    L->listsize = 0;  

}

清空线性表

void ClearList(SqList *L)
{
    //初始条件:顺序线性表已经存在,操作结果:将L重置为空表
    L->length = 0;

}

判断线性表是否为空

Status ListEmpty(SqList *L)
{
    //初始条件:顺序线性表已经存在
    if(L->length = 0)
    {
        return TRUE;
    }
    else
        return FALSE;
}

求线性表的长度

int ListLength(SqList *L)
{
    //初始条件:顺序线性表L已存在,操作结果:返回L中数据元素个数
    return L->length;

}

获取指定位置的元素

Status Get_List_sq(List_sq List_sq, int pos, Elemtype *elem)
{
    if(is_empty_Listsq(List_sq))        //判断线性表是否为空
    {
        return ERROR;
    }
    if(pos<1||pos>List_sq->cnt)     //指定的位置是否合理
    {   
        return ERROR;
    }
   // elem->name = (char *)malloc(sizeof(char)*LENGTH);
    //elem->number = (char *)malloc(sizeof(char)* LENGTH);
    {
        elem->name = List_sq->elem[pos-1].name;
        elem->number = List_sq->elem[pos-1].number;
        elem->age = List_sq->elem[pos-1].age;    //这里我是元素的类型,是一个结构体,包含姓名和学号和年龄,前两者是char *后者是int
        return OK;
    }

}

查找某个元素在线性表中的位置

Status Locate_List_sq(List_sq List_sq, char **name) //根据元素的姓名成员查找
{
    if(is_empty_Listsq(List_sq))
    {
        return ERROR;
    }
    else
    {
        for(int i=0; i<List_sq->cnt; i++)
        {
            if(strcmp(List_sq->elem[i].name,*name) == 0)
            {
                return i+1;
            }

        }
        return ERROR;

    }
}

这下面是参考别人的代码
int LocateElem(SqList L, Elemtype e, Status (*compare)(Elemtype, Elemtype))
{
    //初始条件:顺序线性表L已存在,compare是数据元素判定函数(满足为1,否则为0)
    //操作结果:返回L中第一个与e满足关系compare()的 数据元素的位置
              如果不存在则返回0
    int i=1;
    Elemtype *p = L.elem;    //p的初值为第一个元素的存储位置
    while(i<= L.length!=compare(*p++,e))    //i未超出表的范围且未找到满足关系的数据元素
        i++;
    if(i<L.length)
    {
        return i;        //找到满足关系的数据元素
    }    
    else
    {
        return 0;    //未找到,返回0
    }


}

这里个人觉得,后者的代码精妙之处在于是用来函数指针,通过函数指针可以修改根据什么条件来查找,这是非常精妙的一个地方。

获取前一个元素

Status PriorElem(SqList L, Elemtype cur_e, Elemtype *pre_e)
{
    int i = 2;
    Elemtype *p = L.elem+1;        //p指向第二个元素
    while(i<=L.length&&*p!=cur_e)  //i未超出表的范围且未找到值为cur_e的元素
    {
        p++;        //p指向下一个元素
        i++;        //计数加1
    }
    if(i>L.length)        //到表结束处还未找到值为cur_e的元素
        return ERROR;
    else
    {                    //找到了
        pre_e = *--p;
        return OK;
    }
}

找元素的下一个元素

Status NextElem(SqList L,Elemtype cur_e,Elemtype *next_e)
{
    int i=1;
    Elemtype *p = L.elem;
    while(i<L.length&&*p!=cur_e)
    {
        p++;
        i++;
    }
    if(I==L.length)
        return ERROR;
    else
    {
        next_e = *++p;
        return OK;
    }

}

插入元素

举个例子,本来我们在春运时去买火车票,大家都排队排的好好的。这时来了一个美女,对着队伍中排在第三位的你说,“大哥,求求你帮帮忙,我家母亲有病,我得急着回去看她,这队伍这么长,你可否让我排在你的前面?”你心一软,就同意了。这时,你必须得退后一步,否则她是没法进到队伍来的。这可不得了,后面的人像蠕虫一样,全部都得退一步。骂起四声。但后面的人也不清楚这加塞是怎么回事,没什么办法。

插入算法的思路:

  • 如果插入位置不合理,抛出异常

  • 如果线性表的长度大于等于数组长度,则抛出异常或动态增加容量

  • 从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一个位置

  • 将要插入元素填入位置i处

  • 表长加1

Status ListInsert(SqList *L,int i,Elemtype e)
{
    Elemtype *newbase,*p,*q;
    if(i<1||i>L->length+1)   //插入位置是否合法
        return ERROR;
    if(L->length == L.listsize)    //表是否已满
    {
        newbase = (Elemtype *)realloc(L->elem,(L->listsize + LIST_INCREMENT)*sizeof(Elemtype));
        if(!newbase)  
            exit(OVERFLOW);
        L->elem = newbase;        //新基址赋给L->elem
        L->listsize += LIST_INCREMENT;  //增加存储容量
    }
    q = L->elem + i -1; //q为插入位置
    for(p = L->elem + L->length -1;p>=q;--p) //插入位置及之后的元素右移(由表尾元素开始移)
        *(p+1) = p;
    *q = e;
    L->length++;
    return OK;
}

删除元素操作

可以接着刚才的例子,此时后面排队的人群意见都很大,都说怎么可以这样,不管什么原因,插队就是不行,有本事找火车站开后门去,就这时,远处跑来一胖子,对着这美女大喊,可找到你了,你这骗子,还我钱。只见这女子二话不说,突然就冲出了队伍,胖子追在其后,消失在人群中。哦,原来她是倒卖火车票的黄牛,刚才还装可怜。于是排队的人群,又像蠕虫一样开始往前移动一步,骂声渐渐消失,队伍又恢复了平静。

删除算法的思路:

  • 如果删除位置不合理,抛出异常

  • 取出删除元素

  • 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置

  • 表长减1

Status ListDelete(SqList *L,int i,Elemtype *e)
{
    int k;
    if(L->length == 0)    /*线性表为空*/
        return ERROR;
    if(i<1||i>L->length)
    {
        return ERROR;        /*删除位置不合理*/
    }
    *e = L->data[i-1];
    if(i<L->length)        /*如果删除位置不是最后一个位置*/
    {
        for(k = i;k<L->length;k++)
        {
            L->data[k-1] = L->data[k];    //将删除位置后继元素前移
        }
    }
    L->length--;
    return OK;
}

线性表顺序存储结构的优缺点

优点:

  • 无须为表示表中元素之间的逻辑关系而增加 额外的空间

  • 可以快速的存取表中任意位置的元素

缺点

  • 插入和删除操作需要移动大量的元素

  • 当线性表长度变化较大时,难以确定存储空间的容量

  • 造成存储空间的“碎片”

线性表的链式存储结构

为了表示每个数据元素ai与其直接后继ai+1之间的逻辑关系,对于数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继的域称作指针域。指针域中存储信息的称作指针或链,这两部分信息组成元素ai的存储映像,称作结点node。

n个结点(ai的存储映像)链接成一个链表,即为线性表(a1,a2,....an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。

对于线性表来说,总得有个头有个尾,链表也不例外,我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。最后一个,当然意味着直接后继不存在,通常结点指针为空,用NULL表示。

有时,为了方便对链表的操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以存储信息,也可以不存储任何信息,头结点的指针指向第一个结点的指针。

头结点和头指针的异同

头指针

  • 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针

  • 头指针具有标识作用,所以常用头指针冠以链表名字

  • 无论链表是否为空,头指针均不为空,头指针是链表的必要元素

头结点:

  • 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)

  • 有了头结点,对第一元素结点前插入结点和删除第一结点,其操作与其它节点的操作就统一了

  • 头结点不一定是链表必须要素

线性表链式存储结构代码描述

若线性表为空,则头结点的指针域为空,

线性表的单链表存储结构

struct LNode{
    Elemtype data;
    LNode *next;
}
typedef LNode *LinkList; 

以下操作都是针对有头结点的链表:

初始化链表

void InitList(LinkList *L)
{
    L = (LinkList)malloc(sizeof(LNode));
    if(!L)
        exit(OVERFLOW);
    L->next = NULL;  //头结点指针域为空
    
}

销毁链表

void Destroy(LinkList *L)
{
    LinkList q;    
    while(L)    //L指向
    {
        q = L->next; //q指向首元结点
        free(L); //释放头结点
        L=q; //L指向首元结点,现头结点
    }
}

清空链表

void ClearList(LinkList L)
{
    LinkList p = L->next;    //p指向第一个结点
    L->next = NULL;    //头结点指针域为空
    Destroy(p);    //销毁p所指的单链表
}

判断链表是否为空

Status ListEmpty(LinkList L)
{
    if(L->next) //非空
        return FALSE:
    else
        return TRUE;
}

求链表长度

int ListLength(LinkList L)
{
    int i=0;    //计数器初值
    LinkList p = L->next; //p指向第一个结点
    while(p)
    {
        i++;    //依次遍历,计数
        p = p->next;
    }
    return i;
}

获取元素

Status GetElem(LinkList L,int i,Elemtype *e)
{
    int j=1; //计数器初值为1
    LinkList p = L->next;    //p指向第一个结点
    while(p&&j<i)    //顺着指针往后查找,直到p指向第i个结点或p成为空(第i个结点不存在)
    {
        j++;    //计数器加1
        p=p->next;    //p指向下一个结点
    }
    if(!p||j>i)    //第i个结点不存在
        return ERROR;
    *e = p->data;    //取第i个元素的值赋给e
    return OK;

}

查找某个元素的位置

int LocateElem(LinkList L,Elemtype e, Status (*compare)(Elemtype,Elemtype))
{
    int i=0;
    LinkList p=L->next;
    while(p)
    {
        i++;
       if(compare(p->data,e))
            return i;
        p=p->next;
    }
    return 0;
}

插入新结点

Status ListInsert(LinkList L,int i,Elemtype e)
{
    int j=0;
    LinkList s,p=L;
    while(p&&j<i-1) //寻找第i-1个结点
    {
        j++;
        p=p->next;
    }
    if(!p||j>i-1)    //i小于1或者大于表长
    {
        return ERROR;
    }
    s = (LinkList)malloc(sizeof(LNode)); //生成新结点,以下将其插入L中
    s->data = e;    //将e赋值给新结点
    s->next = p->next; //新结点指向原第i个结点
    p->next=s;  //原第i-1个结点指向新结点
    return OK; 
}

删除结点

Status ListDelete(LinkList L,int i,Elemtype *e)
{
    int j=0;
    LinkList q,p=L;
    while(p->next&&j<i-1)    //寻找第i个结点,并令p指向其前趋
    {
        j++;
        p=p->next;
    }
    if(!p->next||j>i-1) //删除位置不合理
    {
        return ERROR;    //删除失败
    }
    q = p->next;    
    p->next = q->next;
    e = q->data;
    free(q);
    return OK;
}

个人总结

数据结构中最基本的线性表,分为顺序存储结构和链式存储结构,其中不同的存储结构,其相同操作具体实现是不一样的,更加充分证明了数据结构是研究数据类型和存储结构的一门课程,这里代码难点在于指针的灵活运用和函数指针的画龙点睛之用,具体操作是只有懂了原理不算特别难,自己手敲实现了顺序表和链表的一些简单操作:可以参考

(3条消息) 数据结构——线性表_~Old的博客-CSDN博客

具体操作难点就在于插入和删除操作

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;