数据结构之线性表(C语言版)

线性表

线性表的定义

线性表(List):零个或多个数据元素的有限序列
首先它是一个序列,也就是说,元素之间是有序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后驱,其它每个元素都有且只有一个前驱和后驱;
然后,线性表强调是有限的。

线性表的元素个数n(n≥0)定义为线性表的长度,当n=0时,称为空表
在较复杂的线性表中,一个数据元素可以由若干个数据项组成

线性表的抽象数据类型

Operation
InitList(*L): 初始化操作。创建一个空的线性表L;

ListEmpty(L): 若线性表为空,返回true,否则返回false;

ClearList(*L): 将线性表清空;

GetElem(L,i,*e): 将线性表L中的第i个位置元素值返回给e;

LocateElem(L,e): 在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则返回0表示失败

ListInert(*L,i,e): 在线性表L中的第i个位置插入新元素e;

ListDelete(*L,i,*e): 删除线性表L中第i给位置元素,并用e返回其值

ListLength(L): 返回线性表L的元素个数;

线性表的顺序存储结构

顺序存储定义

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

顺序存储方式

因为线性表的每个数据元素的类型都相同,所以可以用一维数组来实现顺序存储结构,即把第一个元素数据存到数组下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。

为了建立线性表,要在内存中找一块地,于是这一块地的第一个位置就非常关键,它是存储空间的起始位置。

随着数据的插入,线性表的长度开始表达,不过线性表的当前长度不能超过存储容量,即数组的长度

#define MAXSIZE 20
typedef int ElemtType;
typedef struct{
    Elemtype data[MAXSIZE];
    int length;
}SqList;

数据长度与线性表长度区别

数据长度:数据长度时内存放线性表的存储空间长度,存储分配后这个量一般是不变的。
线性表长度:是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。

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

地址计算方法

线性表的第i个元素要存储在数组的第i-1个位置上

存储器中的每个存储单元都有自己的编号,这个编号称为地址

假设每个数据元素占用c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据满足如下关系:

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

​ 通过这个公式,我们可以随时算出线性表中任意位置的地址,都是相同的时间。那么我们对每个线性表位置的存入或者取出数据对于计算机来都是相等的时间,也就是一个常数O(1)。我们通常把具有这一特点的存储结构称为随机存取结构。

顺序存储结构的插入与删除

获得元素操作

#define OK 1
#define ERROR 0
#define TURE 1
#define FALSE 0
typedef int Status;
/*Status是函数的类型,其值是函数结果状态代码,如OK等*/
/*初始条件:顺序表L已经存在,1≤i≤ListLength(L)*/
/*操作结果:用e返回L中第i个元素数据的值*/
Status GetElem(SqList L,int i,ElemType *e){
    if(i<1 || L.length == 0 || i > L.length)
        return ERROR;
    *e = L.data[i-1];
    return OK;
}

插入操作

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(l).*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加一*/
Status ListInsert(SqList *L,int i.Elemtype e){
    int k;
    if(L->length == MAXSIZE)//线性表已满
        return ERROR;
    if(i < 1 || i > L->length+1)//当i不在范围内时
        return ERROR;
    if(i<=L->length){
        for(k = L->length;k>i;k--)
            L->data[k+1] = L->data[k];
    }
    l->data[i-1] = e;
    l->length++;
    return OK;
}

删除操作

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(l).*/
/*操作结果:删除L中第i个位置的数据元素,并用e返回其值,L的长度减一*/
Status ListDelete(SqList *L,int i.Elemtype *e){
    int k;
    if(L->length == 0)
    	return ERROR; 
    if(i < 1 || i > L->length)
        return ERROR;
    if(i<L-.length){
    L->length--;
    return OK;
}

分析

如果元素要插入或删除最后一个位置,此时时间复杂度为O(1);
最坏的情况是,要插入或删除第一个位置的元素,那就意味着要移动所有的元素向后或向前,所以这个时间复杂度为O(n);
最终平均移动次数和最中间的那个元素移动次数相等,为(n-1)/2。
根据复杂度的推导,可以得出,平均时间复杂度还是O(n)。

着表明,顺序存储比较适合元素个数不太变化,而更多是存取数据的应用。

优点缺点
无须为表示表中元素之间的逻辑关系二增加额外的存储空间插入和删除操作需要运动大量元素
可以快速地存取表中任意位置的元素当线性长度变化较大时,难以确定存储空间的容量
···造成存储空间的“碎片”

线性表的链式存储结构

​ 对于顺序存储结构,我们可以发现,其在每次创建时都要确定他的长度,并且在插入和删除时要移动大量元素。我们有没有一种存储结构可以不在初始化时就确定长度,在插入和删除时不用移动大量元素呢?这就是链式存储结构

​ 线性表的链式存储结构的特点是用 一组任意的存储单元存储线性表的数据元素,这组存储单元在内存中的位置可以是连续的,也可以是是不连续的。这些数据可以存在内存未被占用的任意位置。

链式存储结构的一些概念

​ 在顺序存储结构中,每个数据元素只需要存储数据元素信息就可以了,但是在链式存储结构中,除了要存储数据元素信息外,还要存储它的后继元素的存储地址。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域为指针域。着两部分信息组成数据元素的存储映像,称为结点Node)。

​ n个结点链接成一个链表,即为线性表的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表

​ 链表中第一个结点的存储位置叫做头指针,线性链表的最后一个结点指针为“空”(NULL),有时,我们为了方便起见,会在链表的第一个结点前设置一个结点,称为头结点。头结点的指针域存储指向第一个结点的指针。

​ 注意头指针的头结点的不同,头指针是必须存在的,而头结点是可以不设置的。如果一个链表有头结点,则头指针指向头结点;若没有,则指向第一个结点

线性表链式存储结构代码

typedef struct Node{
    ElemType data;
    struct Node *next;
}Node;
typedef struct Node *LinkList;

设 p 是指向线性表第 i 个元素的指针,则该节点的数据域可以用 p->data 来表示,指针域可以用 p->next 表示,p->next 是一个指针,她指向第 i + 1 个元素。

单链表的读取

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:用e返回L中第i个元素的值*/
Status GetElem(LinkList L,int i,ElemType *e){
    int j;
    LinkList p;
    p = L->next; /*让p指向第一个结点*/
    j = 1; /*j为计数器*/
    while(p && j<i){
        p = p->next;
        j++;
    }
    if(!p || j>i)
        return ERROR;
    *e = p->data;
    return OK;
}

其实就是从第一个开始找,直到第i个结点为止。所以他的时间复杂度为O(n)。其主要核心思想就是“工作指针后移”,在链表的很多地方都可以看到这种思想。

单链表的插入与删除

插入
Status ListInsert(LinkList *L,int i,ElemType e){
    int j;
    LinkList p;
    p = *L;
    j = 1;
    while(p && j < i){
        p = p->next;
        j++;
    }
    if(!p || j > i)
        return ERROR;
    LinkList s = (LinkList)malloc(sizeof(Node));
    s->data = e;
    s->next = p->next; /*注意此处的应该先把p->next付给s->next*/
    p->next = s;
    return OK;
}
删除
Status ListDelete(LinkList *L,int i,ElemType *e){
    int j;
    LinkList p;
    j = 1;
    p = *L;
    while(p->next && j<i){
        p = p->next;
        j++;
    }
    if(!(p->next) || j>i)
        return ERROR;
    LinkList s = p->next;
    p->next = s->next;
    *e = s->data;
    free(s);
    return OK;
}

​ 整体来看,插入和删除的时间复杂度都为O(n),因为在操作中,都有一个查找的步骤,要先找到第 i 个位置上的元素,再进行操作,所以单链表数据结构在插入和删除方面,与顺序存储结构没有太大的优势。但是如果要在位置 i 插入10个元素,则链表就更有优势了。所以,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。

单列表的整表删除

Status ClearList(LinkList *L){
    LinkList p,q;
    p = (*L)->next;
    while(p){
        q= p->next; /*注意此处的步骤不能错*/
        free(p);
        p = q;
    }
    (*L)->next = NULL;
    return OK;
}

删除操作的核心思想就是利用循环把链表的空间释放。

双向列表和循环列表

双向列表就是一个结点中存在两个指针域,一个指向它的前驱,一个指向它的后驱。

循环列表则是在链表末尾的指针域指向表头。

总结

可以看出,每一种存储结构都有其各自的特点,应根据实际情况来判断使用哪一种存储结构。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值