线性表的链式表示

2.3 线性表的链式表示

2.3.1 单链表的定义

​ 线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息之外,还需要存放一个指向其后继的指针。单链表结点结构,其中 data 为数据域,存放数据元素;next为指针域,存放其后继结点的地址。

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

typedef struct LNode{  //定义单链表结点类型
    ElemType data; //数据域
    struct LNode *next; //指针域
}LNode,*linkList;

​ 利用单链表可以解决顺序表需要大量连续存储单元的缺点,但附加的指针域,也存在浪费有储空间的缺点。由于单链表的元素离散地分布在存储空间中,因此是非随机存取的存储结构,即不能直接找到表中某个特定结点。查找特定结点时,需要从表头开始遍历,依次查找。通常用头指针工(或 head 等)来标识一个单链表,指出链表的起始地址,头指针为 NULL时表示一个空表。此外,为了操作上的方便,在单链表第一个数据结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,但也可以记录表长等信息。单链表带头结点时,头指针工指向头结点,如图2.4(a)所示。单链表不带头结点时,头指针工指向第一个数据结点如图 2.4(b)所示。表尾结点的指针域为 NULL(用“^”表示)。

image-20240621170356972

头结点和头指针的关系:

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

引入头结点后,可以带来两个优点:
① 由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。

​ ② 无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一

2.3.2 单链表的基本操作的实现

1、单链表的初始化
带头结点和不带头结点的单链表的初始化操作是不同的。带头结点的单链表初始化时,需要创建一个头结点,并让头指针指向头结点,头结点的next域初始化为NULL。

 bool Initlist(LinkList &L){ //带头结点的单链表的初始化
    L=(LNode*)malloc(sizeof(INode)); //创建头结点!
    L->next=NULL; //头结点之后暂时还没有元素结点
    return true;

不带头结点的单链表初始化时,只需将头指针工初始化为 NULL。

bool InitList(LinkList &L){ //不带头结点的单链表的初始化
L=NULL;
return true;

2、求表长操作
求表长操作是计算单链表中数据结点的个数,需要从第一个结点开始依次访问表中每个结点,为此需设置一个计数变量,每访问一个结点,其值加1,直到访问到空结点为止。

int length(LinkList ){
    int len=0;
    LNode *p=;
    while(p->next!=NULL){
        //计数变量,初始为0
		p=p->next;
        len++;
    }
    return len;
}

3、按序号查找结点
从单链表的第一个结点开始,沿着next 域从前往后依次搜索,直到找到第i个结点为止,则返回该结点的指针;若i小于单链表的表长,则返回 NULL。

LNode *GetElem(Linklist int i){
    LNode *p=L; //指针p指向当前扫描到的结点
    int i=0; //记录当前结点的位序,头结点是第0个结点
    while(p !=NULL&&j<i){ //循环找到第i个结点
        p=p->next;
        j++; //返回第i个结点的指针或NULL
		return p;
   }

按序号查找操作的时间复杂度为 O(n)。
4、按值查找表结点
从单链表的第一个结点开始,从前往后依次比较表中各结点的数据域,若某结点的 data域等于给定值e,则返回该结点的指针:若整个单链表中没有这样的结点,则返回 NULL。

LNode *LocateElem(LinkListElemType e){
    LNode *p=->next;
    while(p!=NULL&&p->data!=e){ //从第一个结点开始査找数据域为e的结点
		p=p->next;
		return p; //找到后返回该结点指针,否则返回NULL
}

按值查找操作的时间复杂度为 O(n)。

5、插入结点操作
插入结点操作将值为x的新结点插入到单链表的第i个位置。先检查插入位置的合法性,然后找到待插入位置的前驱,即第i - 1个结点,再在其后插入。
首先查找第 i-1 个结点,假设第 i-1 个结点为**p,然后令新结点s的指针域指向*p的后继,再令结点*p的指针域指向新插入的结点s。

bool ListInsert(linklist &l,int i,ElemType e){
    LNode *p=L;
//指针p指向当前扫描到的结点
    int j=0;
    while(p!=NULL&&j<i-1){
        p=p->next;j++;
    }
if(p==NULL)
//记录当前结点的位序,头结点是第0个结点//循环找到第i-1个结点
//i值不合法
return false;
  LNode *s=(LNode*)malloc(sizeof(LNode));
  s->data=e;
  s->next=p->next;
  p->next=s;
return true;

插入时,①和②的顺序不能颠倒,否则,先执行p->next=s后,指向其原后继的指针就不存在了,再执行 s->next=p->next 时,相当于执行了s->next=s,显然有误。本算法主要的时间开销在于查找第 i-1个元素,时间复杂度为 O(n)。若在指定结点后插入新结点,则时间复杂度仅为 O(1)。需注意的是,当链表不带头结点时,需要判断插入位置i是否为1,若是,则要做特殊处理,将头指针工指向新的首结点。当链表带头结点时,插入位置i为1时不用做特殊处理。

扩展:对某一结点进行前插操作。前插操作是指在某结点的前面插入一个新结点,后插操作的定义刚好与之相反。在单链表插入算法中,通常都采用后插操作。以上面的算法为例,先找到第 i-1个结点,即插入结点的前驱,再对其执行后插操作。由此可知,对结点的前插操作均可转化为后插操作,前提是从单链表的头结点开始顺序查找到其前驱结点,时间复杂度为O(n)。此外,可采用另一种方式将其转化为后插操作来实现,设待插入结点为s,将s插入到*p的前面。我们仍然将*s插入到*p的后面,然后将p->data与s->data 交换,这样做既满足逻辑关系,又能使得时间复杂度为0(1)。该方法的主要代码片段如下:

//修改指针域,不能颠倒
s->next=p->next;
p->next=s;
temp=p->data;
//交换数据域部分
p->data=s->data;
s->data=temp;

6、删除结点操作
删除结点操作是将单链表的第i个结点删除。先检査删除位置的合法性,然后查找表中第i-1个结点,即被删结点的前驱,再删除第i个结点。

image-20240621171650884

7、采用头插法建立单链表
该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后。

image-20240621171734966

8、采用尾插法建立单链表
头插法建立单链表的算法虽然简单,但生成的链表中结点的次序和输入数据的顺序不一致。若希望两者次序一致,则可采用尾插法。该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点

image-20240621171811777

2.3.3 双链表

​ 单链表结点中只有一个指向其后继的指针,使得单链表只能从前往后依次遍历。要访问某个结点的前驱(插入、删除操作时),只能从头开始遍历,访问前驱的时间复杂度为O(n)。为了克服单链表的这个缺点,引入了双链表,双链表结点中有两个指针prior和next,分别指向其直接前驱和直接后继,表头结点的prior域和尾结点的next域都是 NULL。

image-20240621171906710

双链表在单链表结点中增加了一个指向其前驱的指针prior,因此双链表的按值查找和按位查找的操作与单链表的相同。但双链表在插入和删除操作的实现上,与单链表有着较大的不同。这是因为“链”变化时也需要对指针 prior 做出修改,其关键是保证在修改的过程中不断链。此外,双链表可以很方便地找到当前结点的前驱,因此,插入、删除操作的时间复杂度仅为O(1)。
1、双链表的插入操作
在双链表中p所指的结点之后插入结点*s

image-20240621172100117

2、双链表的删除操作
删除双链表中结点*p的后继结点*q

image-20240621172150401

2.3.4 循环链表

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

image-20240621172240664

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

2、循环双链表
由循环单链表的定义不难推出循环双链表。不同的是,在循环双链表中,头结点的 prior指针还要指向表尾结点,如图所示。

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

image-20240621172402087

2.3.5 静态链表

​ 静态链表是用数组来描述线性表的链式存储结构,结点也有数据域 data 和指针域 next,与前面所讲的链表中的指针不同的是,这里的指针是结点在数组中的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。
​ 静态链表以 next==-1作为其结束的标志。静态链表的插入、删除操作与动态链表的相同,只需要修改指针,而不需要移动元素。总体来说,静态链表没有单链表使用起来方便,但在一些不支持指针的高级语言(如 Basic)中,这是一种非常巧妙的设计方法。

2.3.6 顺序表和链表的比较

1、存取(读/写)方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头开始依次顺序存取。例如在第i个位置上执行存取的操作,顺序表仅需一次访问,而链表则需从表头开始依次访问i次。
2、逻辑结构与物理结构
采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素物理存储位置不一定相邻对应的逻辑关系是通过指针链接来表示的。
3、查找、插入和删除操作
对于按值查找,顺序表无序时,两者的时间复杂度均为 O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为 O(logn)。对于按序号查找,顺序表支持随机访问,时间复杂度仅为 O(1),而链表的平均时间复杂度为 O(n)。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需修改相关结点的指针域即可。
4、空间分配

​ 顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。此外,由于链表的每个结点都带有指针域,因此存储密度不够大。

5、基于存储的考虑
难以估计线性表的长度或存储规模时,不宜采用顺序表:链表不用事先估计存储规模,但链表的存储密度较低,显然链式存储结构的存储密度是小于1的。
6、基于运算的考虑
在顺序表中按序号访问a的时间复杂度为 O(1),而链表中按序号访问的时间复杂度为 O(n),因此若经常做的运算是按序号访问数据元素,则显然顺序表优于链表。在顺序表中进行插入、删除操作时,平均移动表中一半的元素,当数据元素的信息量较大且表较长时,这一点是不应忽视的;在链表中进行插入、删除操作时,虽然也要找插入位置,但探作主要是比较操作,从这个角度考虑显然后者优于前者。
7、基于环境的考虑
顺序表容易实现,任何高级语言中都有数组类型:链表的操作是基于指针的,相对来讲,前者实现较为简单,这也是用户考虑的一个因素。

配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。此外,由于链表的每个结点都带有指针域,因此存储密度不够大。

5、基于存储的考虑
难以估计线性表的长度或存储规模时,不宜采用顺序表:链表不用事先估计存储规模,但链表的存储密度较低,显然链式存储结构的存储密度是小于1的。
6、基于运算的考虑
在顺序表中按序号访问a的时间复杂度为 O(1),而链表中按序号访问的时间复杂度为 O(n),因此若经常做的运算是按序号访问数据元素,则显然顺序表优于链表。在顺序表中进行插入、删除操作时,平均移动表中一半的元素,当数据元素的信息量较大且表较长时,这一点是不应忽视的;在链表中进行插入、删除操作时,虽然也要找插入位置,但探作主要是比较操作,从这个角度考虑显然后者优于前者。
7、基于环境的考虑
顺序表容易实现,任何高级语言中都有数组类型:链表的操作是基于指针的,相对来讲,前者实现较为简单,这也是用户考虑的一个因素。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值