【数据结构】2话:线性表1

目录

线性表:

线性表的定义:

线性表的特点:

案例引入:

①用顺序存储结构表示:

②用顺序存储结构表示非零项:

③用链表结构存储非零项:

线性表的抽象数据类型定义:

线性表的顺序表示和实现:

顺序表中基本操作的实现:

1表长:

2初始化:

3取值:

4查找:

5插入:

6删除:

线性表的链式表示和实现:

单链表的定义和表示:

头结点:

 typedef结构体指针:

单链表基本操作的实现:

1字长:

2初始化:

3取值:

4查找:

按序号查找:

按值查找:

5插入:

6删除:

插入和删除循环条件区别:

创建单链表:

术语补充

(1)前插法:

(2)后插法:

循环链表:

双向链表:

 双向链表的插入:

双向链表的删除:

广义表:

多重链表:

多重链表的定义:

多重链表图: 

Tag、Head、Term结点: 


线性表是最基本且最常用的一种线性结构,同时也是其他数据结构的基础。

线性表:

线性表的定义:

同一线性表中的元素必定具有相同的特性, 即属于同一数据对象, 相邻数据元素之间存在着序偶关系。诸如此类由n(n≥0)个数据特性相同的元素构成的有限序列,称为线性表

线性表中元素的个数n(n≥0)定义为线性表的长度,当n=0时称之为空表

线性表的特点:

对于非空的线性表或线性结构, 其特点是:
(1)存在唯一的一个被称作“第一个”的数据元素;
(2)存在唯一的一个被称作“最后一个”的数据元素;
(3)除第一个元素之外,结构中的每个数据元素均只有一个前驱;
(4)除最后一个元素之外,结构中的每个数据元素均只有一个后继。

案例引入:

一元多项式的运算:实现两个多项式的相关运算的前提是在计算机中有效地表示一元多项式,进而在此基础上设计相关运算的算法。

①用顺序存储结构表示:

②用顺序存储结构表示非零项:

在处理形如:S(x)=1+3x的10000次 +2x的20000次 的多项式时,

就要用一个长度为20001的线性表来表示,而表中仅有3个非零元素,将会造成存储空间的很大浪费,这种对空间的浪费是应当避免的。

由于线性表中的元素可以包含多个数据项,由此可改变元素设定,对多项式的每一项,可用“(系数,指数)”的形式唯一确定。

③用链表结构存储非零项:

可以看出,在工作和生活中的许多实际应用问题都会涉及这些基本操作。

这些问题中都包含n个数据特性相同的元素,即可以表示为线性表。

不同的问题所涉元素的数据类型不尽相同,可以为简单数据类型,也可以为复杂数据类型。

但这些问题所涉及的基本操作都具有很大的相似性,如果为每个具体应用都编一个程序显然不是一种很好的方法。

解决这类问题的最好方法就是从具体应用中抽象出共性的逻辑结构和基本操作(抽象数据类型),然后采用程序设计语言实现相应的存储结构和基本操作。

线性表的抽象数据类型定义:

线性表是一种相当灵活的数据结构,其长度可根据需要增长或缩短,即对线性表的数据元素不仅可以进行访问,而且可以进行插入和删除等操作。

为不失一般性,我们采用1.2节抽象数据类型格式对各种数据结构进行描述。下面给出线性表的抽象数据类型定义: 

 (1)抽象数据类型仅是一个模型的定义,并不涉及模型的具体实现,因此这里描述中所涉及的参数不必考虑具体数据类型。在实际应用中,数据元素可能有多种类型,到时可根据具体需要选择使用不同的数据类型。

(2)上述抽象数据类型中给出的操作只是基本操作,由这些基本操作可以构成其他较复杂的操作。

(3)对于不同的应用,基本操作的接口可能不同。

(4)由抽象数据类型定义的线性表,可以根据实际所采用的存储结构形式,进行具体的表示和实现。

线性表的顺序表示和实现:

线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,这种表示也称作线性表的顺序存储结构或顺序映像。

通常,称这种存储结构的线性表为顺序表(Sequential List)。其特点是,逻辑上相邻的数据元素,其物理位置也是相邻的。        

只要确定了存储线性表的起始位置,线性表中任一数据元素都可随机存取,所以线性表的顺序存储结构是一种随机存取的存储结构。      

由于高级程序设计语言中的数组类型也有随机存取的特性,因此,通常都用数组来描述数据结构中的顺序存储结构。在此,由于线性表的长度可变,且所需最大存储空间随问题不同而不同,则在C语言中可用动态分配的一维数组表示线性表。                                        

------------顺序表的存储结构------------
#define MAXSIZE 100        顺序表可能达到的最大长度
typedef struct{
    ElemType *elme;        存储空间的基地址
    int length;            当前长度
}SqList;                   顺序表的结构类型为SqList

(1)数组空间通过后面的算法2.1初始化动态分配得到,初始化完成后,数组指针elem指示顺序表的基地址,数组空间大小MAXSIZE。
(2)元素类型定义中的ElemType数据类型是为了描述统一而自定的,在实际应用中,用户可根据实际需要具体定义表中数据元素的数据类型,既可以是基本数据类型,如int、float、char等,也可以是构造数据类型,如struct结构体类型。
(3)length表示顺序表中当前数据元素的个数。因为C语言数组的下标是从0开始的,而位置序号是从1开始的,所以要注意区分元素的位置序号和该元素在数组中的下
标位置之间的对应关系,数据元素a1、a2、…、an,依次存放在数组elem[0]、elem[1]、…、
elem[length-1]中。

-------

-----------多项式的顺序存储结构--------------
#define MAXSIZE 100
typedef struct{            多项式非零项的定义
    float coef;            系数coefficient
    int expn;              指数
}Polynomial;
typedef struct{
    Polynomial *elem;
    int length;
}SqList;

由上述讨论可以看出,如果多项式属于非稀疏多项式,且只对多项式进行“求值”等不改变多项式的系数和指数的运算,可采用数组表示的顺序存储结构。

如果多项式属于稀疏多项式,虽然可以采用数组表示法,但这种顺序存储结构的存储空间分配不够灵活。因为事先无法确定多项式的非零项数,所以需要根据预期估计可能的最大值定义数组的大小。

这种分配方式可能会带来两种问题:一种是实际非零项数比较小,浪费了大量存储空间;另一种是实际非零项数超过了最大值,存储空间不够。另外在实现多项式的相加运算时,还需要开辟一个新的数组保存结果多项式,导致算法的空间复杂度较高。

改进方案是利用链式存储结构表示多项式的有序序列,这样灵活性更大些。

顺序表中基本操作的实现:

1表长:

因为表的长度是顺序表的一个“属性”,所以可以通过返回length的值实现求表长的操作,通过判断lengh的值是否为0判断表是否为空,实现这个操作的算法的时间复杂度都是O(1)。

2初始化:

Status InitList(SqList &L)
{构造一个空的顺序表
    L.elem=new ElemType[MAXSIZE];    为顺序表分配一个大小为MAXSIZE的数组空间
    if(!L.elem) exit(OVERFLOW);      存储分配失败退出
    L.length=0;                      空表长度为0
    return OK;
}

动态分配线性表的存储区域可以更有效地利用系统的资源,当不需要该线性表时,可以使
用销毁操作及时释放占用的存储空间。

3取值:

Status GetElem(SqList L,int i,ElemType &e)
{
    if(i<1||i>L.length)return ERROR;    判断i值是否合理,若不合理,
                                          返回ERROR
    e=L.elem[i-1];                        将第i个数据元素elem[i-1]赋给e
                                          通过e返回第i个数据元素的传值
    return OK;
}

取值操作是根据指定的位置序号i,获取顺序表中第i个数据元素的值。
由于顺序存储结构具有随机存取的特点,可以直接通过数组下标定位得到,elem[i-1]单元
存储第i个数据元素。

4查找:

int LocateElem(SqList L,ElemType e)
{顺序表L中查找值为e的数据元素,返回其序号
    for(int=i,i<L.length;i++)
        if(L.elem[i]==e) return i+1;    查找成功,返回序号i+1
    return 0;                             查找失败,返回0
}

查找操作是根据指定的元素值e,查找顺序表中第1个值与e相等的元素。
若查找成功,则返回该元素在表中的位置序号;若查找失败,则返回0。

5插入:

Status ListInsert(SqList &L,int i,ElemType e)
{在顺序表L中第i个位置插入新的数据元素e,i值的合理范围是1 ≤ i ≤ L.length+1
    if(i<i||i>L.length+1) return ERROR;    i值不合理
    if(L.length==MAXSIZE) return ERROR;    当前存储空间已满
    for(int j=L.length-1;j>i-1;j++)
        L.elem[j+1]=L.elem[j];               插入位置及之后的元素后移
    L.elem[i-1]=e;                           将新元素e放入第i个位置
    L.length++;                              表长+1
    return OK;
}

数据元素a(下标i-1)和a(下标i)之间的逻辑关系发生了变化。在线性表的顺序存储结构中,由于逻辑上相邻的数据元素在物理位置上也是相邻的,因此,除非i=n+1,否则必须移动元素才能反映这个逻辑关系的变化。
一般情况下,在第i(1<i n)个位置插入一个元素时,需从最后一个元素即第n个元素开始,依次向后移动一个位置,直至第个元素(共n-i+1个元素)。

上述算法没有处理表的动态扩充,因此当表长已经达到预设的最大空间时,则不能再插入元素。

6删除:

Status ListDelete(SqList &L,int i)
{在顺序表L中删除第i个位置的元素,1 ≤ i ≤ L.Length
    if(i<1||i>i.L.length) return ERROR;    i值不合理
    for(int j=i;j<=L.length-1;j++)
        L.elem[j-1]=L.elem[j];               被删除元素之后的元素前移
    L.length--;                              表长-1
    return OK;
}

数据元素a(下标i-1)、a(下标i)和a(下标i+1)之间的逻辑关系发生了变化,为了在存储结构上反映这个变化,同样需要移动元素。

一般情况下,删除第1(1≤i≤n)个元素时需将第i+1个至第n个元素(共n-i个元素)向前移动一个位置(i=n时无须移动)。

顺序表可以随机存取表中任一元素,其存储位置可用一个简单直观的公式来表示。
然而,从另一方面来看,这个特点也造成了这种存储结构的缺点:在做插入或删除操作时,需动大量元素。另外由于数组有长度相对固定的静态特性,当表中数据元素个数较多且变化较时,操作过程相对复杂,必然导致存储空间的浪费。

所有这些问题,都可以通过线性表的另一种表示方法——链式存储结构来解决。

线性表的链式表示和实现:

单链表的定义和表示:

线性表链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。

因此,为了表示每个数据元素a(下标i)与其直接后继数据元素a(下标i+1)之间的逻辑关系,对数据元素a(i)来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(直接后继的存储位置)。

这两部分信息组成数据元素a(下标i)的存储映像,称为结点(node)。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域

指针域中存储的信息称作指针或链。n个结点[a(下标i)(1≤i≤n)的存储映像]链接成一个链表,即为线性表:(a(下标1),a(下标2),...,a(下标n))的链式存储结构。又由于此链表的每个结点中只包含一个指针域,故又称线性链表或单链表


根据链表结点所含指针个数、指针指向和指针连接方式,可将链表分为单链表、循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等。其中单链表、循环链表和双向链表多用于实现线性表的链式存储结构,其他形式多用于实现树和图等非线性结构。


本节先讨论单链表,整个链表的存取必须从头指针开始进行,头指针指示链表中第一个结点(第一个数据元素的存储映像,也称首元结点)的存储位置。同时,由于最后一个数据元素没有直接后继,则单链表中最后一个结点的指针为空(NULL)。

用单链表表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的。

换句话说,指针为数据元素之间的逻辑关系的映像,则逻辑上相邻的两个数据元素其存储的物理位置不要求紧邻,由此,这种存储结构为非顺序映像或链式映像。


通常将链表画成用箭头相链接的结点的序列,结点之间的箭头表示链域中的指针。

图2.7所示的单链表可画成如图2.8所示的形式,这是因为在使用链表时,关心的只是它所表示的线性表中数据元素之间的逻辑顺序,而不是每个数据元素在存储器中的实际位置。

--------单链表的存储结构--------
typedef struct LNode
{
    ElemType data;         结点的数据域
    struct LNode *next;    结点的指针域
}LNode,*LinkList;         LinkList为指向结构体LNode的指针类型

(1)这里定义的是单链中每个结点的存储结构,它包括两部分:存储结点的数据域data,其类型用通用类型标识符ElemType表示;存储后继结点位置的指针域next,其类型为指向结点的指针类型LNode*。
(2)为了提高程序的可读性,在此对同一结构体指针类型起了两个名称,LinkList与LNode,两者本质上是等价的。

        通常习惯上用LinkList定义单链表,强调定义的是某个单链表的头指针;用LNode*定义指向单链表中任意结点的指针变量。                 

        例如,若定义LinkList L,则L为单链表的头指针,若定义LNode *p,则p为指向单链表中某个结点的指针,用*p代表该结点。当然也可以使用定义LinkList p,这种定义形式完全等价于LNode *p。
(3)单链表是由表头指针唯一确定的,因此单链表可以用头指针的名字来命名。若头指针名是L,则简称该链表为表L。
(4)注意区分指针变量和结点变量两个不同的概念,若定义LinkList p或LNode *p.则p为指向某结点的指针变量,表示该结点的地址;而*p为对应的结点变量,表示该结点的名称。

------

头结点:

一般情况下,为了方便处理,在单链表的第一个结点之前附设一个结点,称之为头结点

 链表增加头结点的作用如下。
(1)便于首元结点的处理:
        增加了头结点后,首元结点的地址保存在头结点(其“前驱”结点)的指针域中,则对链表的第一个数据元素的操作与对其他数据元素的操作相同,无须进行特殊处理。
(2)便于空表和非空表的统一处理:
        当链表不设头结点时,假设L为单链表的头指针,它应该指向首元结点,则当单链表为长度n为0的空表时,L指针为空(判定空表的条件可记为:L==NULL)。
       增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。如图2.10(a)所示的非空单链表,头指针指向头结点。若为空表,则头结点的指针域为空(判定空表的条件可记为:L->next==NULL),如图2.10(b)所示。

        在顺序表中,由于逻辑上相邻的两个元素在物理位置上紧邻,则每个元素的存储位置都可从线性表的起始位置计算得到。而在单链表中,各个元素的存储位置都是随意的。
        然而,每个元素的存储位置都包含在其直接前驱结点的信息之中。假设p是指向单链表中第i个数据元素(结点a,即数据域为a的结点)的指针,则p->next是指向第i+1个数据元素(结点a+)的指针。换句话说,若p->data=a,则p->next->data=am。
        由此,单链表是非随机存取的存储结构,要取得第i个数据元素必须从头指针出发顺链进行寻找,也称为顺序存取的存储结构。因此,其基本操作的实现不同于顺序表。

 typedef结构体指针:
typedef struct LNode
{    // Singly linked list Node
     ElemType  data;
     struct LNode   *next; // Pointer to next node
}LNode,*LinkList;    

strcut LNode *next; ,这里仿佛是在边定义节点类型,又在里面定义指针域,其实这样是可以的,记住:在C语言中,只有这里可以这样用,其他地方都不可以,而且只能是指针,不能是结构体变量,否则会陷入无限递归。

LinkList为指向struct  LNode地址的指针,next为指向动态开辟存储空间的起始地址的指针

LinkList 就是指向结构体LNode的指针;next就是指向结点的指针

单链表基本操作的实现:

1字长:
Status ListLength(LinkList L)
{
    p=L->next;int i=0;
    while(p)
    {
        p=p->next;
        j++;
    }
    return j;
}

2初始化:
Status InitList(LinkList &L)
{构造一个空的单链表L
    L=new LNode;    生成新结点作为头结点          
    L->next=NULL;    头结点的指针域置空
    return OK;
}

3取值:
Status GetElem(LinkList L,int i,ElemType &e)
{在带头结点的单链表L中根据序号i获取该元素的值,用e返回L中第i个数据元素的值
    p=L->next;j=1;         初始化,p指向首元结点,计数器j初值赋为1
    while(p&&j<i)          顺链域向后查找,知道p为空或p指向第i个元素
    {
        p=p->next;         p指向下一个结点
        j++;              计数器j相应+1     
    }
    if(!p||j>i)return ERROR;    i值不合理i>n或i<0
    e=p->data;                    取第i个结点的数据域
    return OK;  
}

和顺序表不同,链表中逻辑相邻的结点并没有存储在物理相邻的单元中,这样,根据给定
的结点位置序号i,在链表中获取该结点的值不能像顺序表那样随机访问,而只能从链表的首元结点出发,顺着链域next逐个结点向下访问。

4查找:
LNode *LocateElem(LinkList L,ElemType e)
{在带头结点的单链表L中查找指向数据域值为e的元素的指针
    p=p->next;
    while(p||p->data!=e)
    {
        p=p->next;
    }
    return p;
}

链表中按值查找的过程和顺序表类似,从链表的首元结点出发,依次将结点值和给定值e进行比较,返回查找结果。

算法的平均时间复杂度为O(n)。

按序号查找:

查找指向第i个元素的指针:

按值查找:

查找指向第一个值为x 的指针:

5插入:
Status ListInsert(LinkList &L,int i,ElemType e)
{在带头结点的单链表L中第i个位置插入数据域值为e的新结点
    p=L;int j=0;
    while(p||j<i-1)
    {    
        p=p->next;j++;     查找第i-1个结点,p指向该结点 
    }
    if(!p||j>i-1)return ERROR;    i>n+1或i<1
    s=new LNode;           生成新结点
    s->data=e              将结点*s的数据域置为e
    s->next=p->next;       将结点*s的指针域指向结点a(下标i)
    p->next=s->next;       将结点*p的指针域指向结点*s
    return OK;
}

假设要在单链表的两个数据元素a和b之间插入一个数据元素x,已知p为其单链表存储结构
中指向结点a的指针,如图2.11(a)所示。

为插入数据元素x,首先要生成一个数据域为x的结点,然后将之插入单链表中。根据插入操作的逻辑定义,还需要修改结点x中的指针域应指向结点b;修改结点a中的指针域,令其指向结点x,从而实现3个元素a、b和x之间逻辑关系的变化。

插入后的单链表如图2.11(b)所示。假设s为指向结点x的指针,则上述指针修改用语句描述即为:

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

和顺序表一样,如果表中有n个结点,则插入操作中合理的插入位置有n+1个,即1<i<n+l。当i=n+l时,新结点则插在链表尾部。

插入算法时间复杂度为O(n)。 

6删除:
Status ListDelete(LinkList &L,int i)
{在带头结点的单链表L中,删除第i个位置的结点
    p=L;int j=0;
    while((p->next) &&(j<i-1))    查找第i-1个结点,p指向该结点
    {
        p=p->next;j++;
    }
    if(!(p->next)||(j>i-1)) return ERROR;    当i>n或i<1时,删除位置不合理
    q=p->next;                         临时保存被删除结点的地址以备释放
    p->next=q->next;                   改变删除结点前驱结点的指针域
    delete q;                         释放删除结点的空间
    return OK;
}

要删除单链表中指定位置的元素,同插入元素一样,首先应该找到该位置的前驱结点。如图2.13所示,在单链表中删除元素b时,应该首先找到其前驱结点a。

为了在单链表中实现元素a、b和c之间逻辑关系的变化,仅需修改结点a中的指针域即可。假设p为指向结点a的指针,则修改指针的语句为:

p->next=p->next->next;

但在删除结点b加时,除了修改结点a的指针域外,还要释放结点b所占的空间,所以在修改指针前,应该引入另一指针q,临时保存结点b的地址以备释放。 

删除算法时间复杂度为O(n)。 

插入和删除循环条件区别:

删除算法中的循环条件(p-next&&j<i-1)和插入算法中的循环条件(p&&(j<i-1))是有所区别的。

因为插入操作中合法的插入位置有n+1个,而删除操作中合法的删除位置只有n个,如果使用与插入操作相同的循环条件,则会出现引用空指针的情况,使删除操作失败。

创建单链表:

算法2.6的初始化操作是创建一个只有一个头结点的空链表,而上面链表的其他算法都是假定链表已经存在多个结点。那么,如何建立一个包括若干个结点的链表呢?

链表和顺序表不同,它是一种动态结构。整个可用存储空间可为多个链表共同享用,每个链表占用的空间不需预先分配划定,而是由系统按需即时生成。

因此,建立线性表的链式存储结构的过程就是一个动态生成链表的过程。即从空表的初始状态起,依次建立各元素结点,并逐个插入链表。


根据结点插入位置的不同,链表的创建方法可分为前插法和后插法。

术语补充

​ 首结点第一个有效结点
​ 尾结点最后一个有效结点
​ 头结点第一个有效结点之前的那个节点
​ 头指针指向头结点的指针变量
​ 尾指针指向尾结点的指针变量

(1)前插法:


前插法是通过将新结点逐个插入链表的头部(头结点之后)来创建链表,每次申请一个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后。

void CreatList_H(LinkList &L,int n)
{逆位序输入n个元素的值,建立带头结点的单链表L
    L=new LNode;    
    L->next=NULL;    先建立一个带头结点的空链表
    for(i=0;i<n;i++)
    {
        p=new LNode;    生成新结点*p
        cin>>p->data;   输入元素值赋给新结点*p的数据域
        p->next=L->next;
        L->next=p;      将新结点*p插入到头结点之后
    }
}

图2.15所示为线性表(a,b,c,d,e)前插法的创建过程,因为每次插入在链表的头部,所以应该逆位序输入数据,依次输入e、d、c、b、a,输入顺序和线性表中的逻辑顺序是相反的。

时间复杂度为O(n)。 

(2)后插法:

后插法是通过将新结点逐个插入链表的尾部来创建链表。同前插法一样,每次申请一个新结点,读入相应的数据元素值。

不同的是,为了使新结点能够插入表尾,需要增加一个尾指针r指向链表的尾结点。

void CreatList_R(LinkList &L,int n)
{正位序输入n个元素的值,建立带头结点的单链表L
    L=new LNode;
    L->next=NULL;    先建立一个空的单链表L
    r=L;             尾指针r指向头结点
    for(i=0;i<n;i++)
    {
        p=new LNode;    生成新结点*p
        cin>>p->data;   输入元素值赋给新结点*p的数据域
        p->next=r->next;
        r=p;            r指向新的尾结点*p
    }
}

图2.16所示为线性表(a,b,c,d,e)后插法的创建过程,读入数据的顺序和线性表中的逻辑顺序是相同的。

时间复杂度为O(n)。 

循环链表:

循环链表(Circular Linked List)是另一种形式的链式存储结构。其特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。

由此,从表中任一结点出发均可找到表中其他结点,图2.17所示为单链的循环链表。类似地,还可以有多重链的循环链表。

 
循环单链表的操作和单链表基本一致,差别仅在于:当链表遍历时,判别当前指针p是否指向表尾结点的终止条件不同。

在单链表中,

判别条件为p!=NULL或p>next!=NULL,

而循环单链表的判别条件为p!=L或p>next!=L。

在某些情况下,若在循环链表中设立尾指针而不设头指针[见图2.18(a)],可使一些操作简化。

例如,将两个线性表合并成一个表时,仅需将第一个表的尾指针指向第二个表的第一个结点,第二个表的尾指针指向第一个表的头结点,然后释放第二个表的头结点。

当线性表以图2.18(a)的循环链表作存储结构时,这个操作仅需改变两个指针值即可,主要语句段如下:

p=B->next->next;
B->next=A-next;
A->next=p;

上述操作的时间复杂度为O(1),合并后的表如图2.18(b)所示。

双向链表:

以上讨论的链式存储结构的结点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针向后寻查其他结点。若要寻查结点的直接前驱,则必须从表头指针出发。

换句话说,在单链表中,查找直接后继的执行时间为O(1),而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可利用双向链表(Double Linked List)。
顾名思义,在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱,结点结构如图2.19(a)所示,在C语言中可描述如下:

和单循环链表类似,双向链表也可以有循环表,如图2.19(c)所示,链表中存有两个环,
图2.19(b)所示为只有一个表头结点的空的双向循环链表。
 
在双向链表中,若d为指向表中某一结点的指针(d为DuLinkList型变量),则显然有:

d->next->prior=d->prior>next=d

这个表示方式恰当地反映了这种结构的特性。

------双向链表的存储结构------
typedef struct DuLNode
{
    ElemType data;    数据域
    struct DuLNode *prior;    指向直接前驱
    struct DuLNode *next;     指向直接后继
}DuLNode,*DuLinkList;

在双向链表中,有些操作(如ListLength、GetElem和LocateElem等)仅需涉及一个方向的指针,则它们的算法描述和线性链表相同;

但在插入、删除时有很大的不同,在双向链表中进行插入、删除时需同时修改两个方向上的指针:

图2.20和图2.21分别显示了插入和删除结点时指针修改的情况。在插入结点时需要修改4个指针,在删除结点时需要修改两个指针。它们的实现分别如算法2.13和算法2.14所示,两者的时间复杂度均为O(n)。

 双向链表的插入:

Status ListInsert_DuL(DuLinkList &L,int i,ElemType e)
{在带头结点的双向链表L中第i个位置之前插入新元素e
    if(!(p=GetElem_DuL(L,i)))    在L中确定第i个元素的位置指针p
    return ERROR;                     p为NULL时,第i个元素不存在
    s=new DuLNode;                    生成新结点*s
    s->data=e;                        将结点*s数据域置为e
    s->prior=p->prior;                将节点*s插入L中,对应图2.20一
    p->prior->next=s;                 对应图2.20二
    s->next=p;                        对应图2.20三
    p-prior=s;                        对应图2.20四
  
}

双向链表的删除:

Status ListDelete_DuL(DuLinkList &L,int i)
{删除带头结点的双向链表L中的第i个元素
    if(!(p=GetElem_DuL(L,i)))  在L中确定第i个元素的位置指针p
        return ERROR;                p为NUL时,第i个元素不存在
    p->prior->next=p->next;          修改被删除结点的前驱结点的后继指针,对应图2.21一
    p-next->prior=p->prior;          修改被删除结点的后继结点的前驱指针,对应图2.21二
    delete p;                        释放被删除结点的空间
    return ok;
}

广义表:

多重链表:

多重链表的定义:

多重链表图: 

Tag、Head、Term结点: 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值