数据结构线性表

本文详细介绍了线性表的概念,包括逻辑结构和两种存储方式:顺序存储(顺序表)和链式存储(链表)。顺序表通过数组实现,支持随机访问但插入删除操作可能涉及大量元素移动;链表分为单链表和双链表,不支持随机访问但插入删除操作更高效。此外,还探讨了循环链表和静态链表的特点。文章列举了各种线性表操作的C/C++实现,如初始化、查找、插入和删除等。
摘要由CSDN通过智能技术生成

了解线性表

什么是线性表

线性表是具有相同数据类型数据元素(数据元素由数据项组成)有限序列

线性表是一种逻辑结构,表示元素间一对一相邻关系

线性表具有顺序存储链式存储两种存储方式,因此有顺序表及链表两种存储结构

线性表的特点:

  • 表中元素个数有限
  • 表中元素具有逻辑上的顺序性,元素具有先后次序
  • 表中元素都是数据元素,每个元素都是单个元素
  • 表中元素的数据类型都相同,即每个元素占有相同大小的存储空间
  • 表中元素具有抽象性,即仅讨论元素间的逻辑关系而不考虑元素究竟表示什么内容

线性表的基本操作

  • 初始化:InitList(&L)
  • 求表长:Length(L)
  • 按值查找:LocateElem(L,e)
  • 按位查找:GetElem(L,i)
  • 插入操作:ListInsert(&L,i,e)
  • 删除操作:ListDelete(&L,i,&e)
  • 输出操作:PrintList(L)
  • 判空操作:Empty(L)
  • 销毁操作:DestroyList(&L)

线性表的顺序表示

什么是顺序表

线性表的顺序存储即顺序表,利用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻

顺序表的特点

  • 随机访问-通过首地址和元素序号可在时间O(1)内找到指定的元素
  • 存储密度高(每个结点只存储数据元素)
  • 顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除需要移动大量元素

顺序表的存储类型

假设线性表的元素类型位ElemType,则线性表的顺序存储类型描述为:

#define MAXSIZE 50	//定义线性表的最大长度

typedef struct{
	ElemType data[MAXSIZE];		//顺序表的元素
	int length;					//顺序表的当前长度
}SQList;						//顺序表的类型定义

优化:动态分配

#define InitSize 100	//表长度的初始定义

typedef struct{
	ElemType *data;		//指示动态分配数组的指针
	int length;			//数组的当前个数
	int MaxSize;		//数组的最大容量
}SeqList;				//动态分配数组顺序表的类型定义

C的初始化动态分配语句

L.data=(ElemType *)malloc(sizeof(ElemType)*InitSize);

C++的初始化动态分配语句

L.data=new ElemType[InitSize];

顺序表基本操作的实现

  • 初始化InitList(&L)

    void InitList(SQList &L){
    	L.length=0;
    }
    
  • 求表长:Length(L)

    int Length(SQList L){
        return L.length;
    }
    
  • 按值查找:LocateElem(L,e)

    int LocateElem(SQList L,ElemType e){
        for(int i=0;i<L.length;i++){
    		if(L.data[i]==e){
                return i+1;
            }
        }
        printf("输入值有误!");
        return -1;
    }
    
  • 按位查找:GetElem(L,i)

    ElemType GetElem(SQList L,int i){
        return L.data[i];
    }
    
  • 插入操作:ListInsert(&L,i,e)

    bool ListInsert(SQList &L,int i,ElemType e){
        if(i<1||i>L.length+1){
            return false;
        }
        if(L.length>=MAXSIZE){
            return false;
        }
        for(int j=L.length;j>=i;j--){
            L.data[j]=L.data[j-1];
        }
        L.data[i-1]=e;
        L.length++;
        return true;
    }
    

    注意数组下标从0开始,而顺序表的下标从1开始

  • 删除操作:ListDelete(&L,i,&e)

    bool ListDelete(SQList &L,int i,ElemType &e){
        if(i<1||i>L.length){
            return false;
        }
        e=L.data[i-1];
        for(i;i<L.length;i++){
            data[i-1]=data[i];
        }
        L.length--;
        return true;
    }
    

线性表的链式表示

什么是单链表

线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。

为了建立数据元素之间的线性关系,对每个链表结点,除了存放自身的信息外,还需要存放一个指向其后继的指针。

单链表中的结点类型的描述如下:

typedef struct LNode{
	ElemType data;
	struct LNode *next;  //指针指向下一个节点
	 
}LNode,*LinkList;

单链表的特点

  • 不需要大量连续存储单元
  • 附加指针域,存在浪费存储空间的缺点
  • **非随机存取的存储结构-**即不能直接找到表中某个特定的结点,在查找某个特定的结点时,需要从表头开始遍历,依次查找

通常用头指针来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。

为了操作方便,在单链表的第一个结点之前附加一个结点,称之为头结点,头结点的数据域可以不设任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点

区分头结点和头指针:

不管带不带头结点,头指针始终指向链表的第一个结点,结点内通常不存储信息。

引入头结点后可以带来两个优点:

  • 由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理
  • 无论链表是否为空,其头指针都指向头结点的非空指针(空表中的头结点的指针域为空),因此空表和非空表的处理也得到了统一

在这里插入图片描述

单链表的基本操作

1.利用头插法建立单链表

该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点,如下图所示:

在这里插入图片描述

LinkList List_HeadInsert(LinkList &L){
    LNode *s;int x;
    L=(LNode *)malloc(sizeof(LNode));
    L->next=NULL;
    scanf("%d",&x);
    while(x!=9999){
        s=(LNode*)malloc(sizeof(LNode));
        s->data=x;
        s->next=L->next;
        L->next=s;
        scanf("%d",&x);
    }
    return L;
}

采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的。

每个结点的插入时间为O(1),设单链表长为n,则总时间的复杂度为O(n)

2.用尾插法建立单链表

头插法建立单链表虽然简单,但生成的链表中结点的次序和输入数据的顺序不一致。

若希望两者次序一致,则可以尝试尾插法,将新节点插入到当前链表的表尾,因此需要增加一个尾指针r,使其始终指向当前链表的尾结点

在这里插入图片描述

尾插法建立单链表的算法如下:

LinkList List_TailInsert(LinkList &L){
	int x;
    L=(LNode *)malloc(sizeof(LNode));
    LNode *s,*r=L;		//r为表尾指针
    scanf("%d",&x);		//输入结点的值
    while(x!=9999){		//输入9999表示结束
        s=(LNode *)malloc(sizeof(LNode));
        s->data=x;
        r->next=s;
        r=s;			//r指向新的表尾结点
        scanf("%d",&x);
    }
    r->next=NULL;		//表尾结点置空
    return L;
}

3.按序号查找结点

由于链表结构存储的特点,因此不能实现随机存取

LNode *GetELem(LinkList ,int i){
    int j=1;
    LNode *p=L->next;
    if(i==0) return L;
    if(i<1) return NULL;
    while(p&&j<i){
        p=p->next;
        j++;
    }
    return p;
}

时间复杂度为O(n)

4.按值查找表结点

LNode *LocateElem(LinkList L,ElemType e){
    LNode *p=L->next;
    while(p!=NULL&p->data!=e){
        p=p->next;
    }
    return p;
}

时间复杂度为O(n)

5.插入结点操作

1.后插操作
  • 先检查插入位置的合法性
  • 找到待插入位置的前驱结点,即第i-1个结点
  • 插入新节点,先令新节点 *s的指针域指向 *p的的后继节点,在令结点 *p的指针域指向新插入的结点 *s

在这里插入图片描述

p=GetElem(L,i-1);
s->next=p->next;
p->next=s;

时间开销主要在于查找第i-1个元素,时间复杂度为O(n)

若在给定结点后面插入新结点,则时间复杂度仅为O(1)

2.前插操作

前插操作是指在某结点的前面插入一个新的结点,后插操作的定义与之刚好相反。在单链表插入操作中,通常采用后插操作

以上面的算法为例

  • 首先调用GetElem()找到第i-1个结点,即插入结点的前驱结点
  • 对插入结点的前驱结点执行后插操作

此外,还可以采用另一种方式转化为后插操作(若给定插入结点*p,则时间复杂度为O(1))

  • 先调用后插操作将 *s插入到 *p后面
  • 然后将p->data与s->data交换
s->next=p->next;
p->next=s;
temp=s->data;
s->data=p->data;
p->data=temp;

6.删除结点操作

  • 同样,先检查删除位置的合法性
  • 查找表中第i-1个结点,即被删除结点的前驱结点
  • 将要删除的结点删除

在这里插入图片描述

p=GetElem(L,i-1);
q=p->next;
p->next=q->next;
free(q);

和插入操作一样,时间开销主要在于查找第i-1个元素,时间复杂度为O(n)

注:若给出要删除的结点*p

可采用删除结点*p的后继结点操作实现,实际上就是将其后继结点的值赋予自身,然后删除后继结点,则时间复杂度为O(1)

q=p->next;
p->data=p->next->data;
p->next=q->next;
free(q);

7.求表长操作

求表长操作就是计算单链表中数据结点(不含头结点)的个数,需要从第一个结点开始顺序依次访问表中每个结点,因此需要设置一个计数器变量,每访问一个结点,计数器加1,直到访问到空姐点为止,算法的时间复杂度为O(n)

注:单链表的长度不包括头结点,因此不带头结点和带头结点的单链表求表长操作会略有不同,对不带头结点的单链表,当表为空时,需要单独处理

什么是双链表

单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历,要访问某个结点地前驱结点(插入、删除操作时),只能从头开始遍历,访问后继结点地时间复杂度为O(1),访问前驱结点地时间复杂度为O(n)

为了克服单链表地上述缺点,引入双链表,双链表结点中有两个指针prior和next,分别指向前驱结点和后继结点,如下图所示:

在这里插入图片描述

双链表中结点类型地描述如下:

typedef struct DNode{
    ElemType data;
    struct DNode *prior,*next;
}DNode,*DLinkList;

双链表的特点

双链表在插入和删除操作的实现上,与单链表有较大的不同,这是因为“链”变化时也需要对prior指针做出修改,其关键是保证在修改的过程中不断链。以外,双链表可以很方便的找到其前驱结点,因此,插入、删除操作的时间复杂度仅为O(1)

双链表的基本操作

1.双链表的插入操作

在双链表中p所指的结点之后插入结点*s:

在这里插入图片描述

s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;

注:为了保证*p的后继结点的指针不能丢掉,因此第一步与第二部必须在第四步之前

2.双链表的删除操作

删除双链表中结点 *p的后继结点 *q:

在这里插入图片描述

p->next=q->next;
q->next->prior=p;
free(q);

什么是循环链表

1.循环单链表

循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而是改为指向头结点,从而整个链表形成一个环

  • 循环单链表的判空操作

    在循环单链表中,表尾的结点*r的next指向L,故表中没有指针域为NULL的结点,因此循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针

    在这里插入图片描述

  • 循环单链表的插入、删除操作

    循环单链表的插入、删除操作与单链表的几乎一样,所不同的是若操作是在表尾进行,则执行的操作不同,以让单链表继续保持着循环的性质

    因为循环单链表是一个”环“,因此在任何一个位置上的插入和删除操作都是等价的,无需判断是否是表尾

在单链表中只能从表头结点开始往后顺序遍历整个链表,而循环单链表可以从表中的任意一个结点开始遍历整个链表。

有时对单链表常做的操作是在表头和表尾进行的,此时对循环单链表不设头指针而设尾指针,从而使得操作效率更高。其原因是,若设的是头指针,对表尾进行操作需要O(n)的时间复杂度,而若设的是尾指针r,r->next即为头指针,对表头与表尾进行操作都只需要O(1)的时间复杂度

2.循环双链表

循环单链表的定义不难推出循环双链表,不同的是在循环双链表中,头结点的prior指针还要指向表尾结点

在这里插入图片描述

在循环双链表L中,某结点*p为尾结点时,p->next==L;当循环双链表为空表时,其头结点的prior域和next域都等于L

什么是静态链表

静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与前面的链表中的指针不同的是,这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间

在这里插入图片描述

静态链表结构类型描述如下:

#define MaxSize 50
typedef struct{
    ElemType data;
    int next;
}SLinkList[MaxSize];

静态链表以next==-1作为其结束的标志。静态链表的插入、删除操作与动态链表的相同,只需要修改指针,而不需要移动元素。总体来说,静态链表没有单链表使用起来方便,但在一些不支持指针的高级语言中,这是一种非常巧妙地设计方法。

顺序表与链表的比较

1.存取(读写)方式

顺序表可以顺序存取,也可以随机存取,

链表只能从表头顺序存取元素

2.逻辑结构与物理结构

采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。

采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的

3.查找、插入和删除操作

对于按值查找,顺序表无序时,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O(log2n)

对于按序号查找,顺序表支持随机访问,时间复杂度为O(1),而链表的平均时间复杂度为O(n),顺序表的插入,删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需要修改相关结点的指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值