数据结构 第二章 线性表(2)

目录

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

2.5.1单链表的定义和表示

2.5.2 单链表基本操作的实现

1.初始化

2.取值

3.查找

4.插入

5.删除

6.创建单链表

2.5.3循环链表

2.5.4双向链表


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

2.5.1单链表的定义和表示

        线性链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素a_{i}与其直接后继数据元素a_{i+1}之间的逻辑关系,对数据元素a_{i}来说,除了存储器本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。这两部分信息组成数据元素a_{i}的存储映像,称为结点(node)。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个结点链结成一个链表,即为线性表的链式存储结构。又由于此链表的每个结点中只包含一个指针域,故又称为线性链表或单链表

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

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

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

        通常将链表画成用箭头相链接的结点的序列,结点之间的箭头表示链域中的指针。上图所示的单链表可画成下图所示的形式,这是因为在使用链表时,关心的只是它所表示的线性表中数据之间的逻辑顺序,而不是每个数据元素在存储器中的实际位置。

        由上述可见,单链表可由头指针唯一确定,在C语言中可用“结构指针”来描述:

//单链表的存储结构
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)注意区分指针变量和结点变量两个不同的概念,若定义Link List p或LNode *p,则p为指向某节点的指针变量,表示该结点的地址;而*p为对应的结点变量,表示该结点的名称

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

         下面对首元结点、头结点、头指针三个容易混淆的概念加以说明。

        1)首元结点是指链表中存储第一个数据元素a_{1}的结点。如上图所示的结点“ZHAO”。

        2)头结点是在首元结点之前附设的一个结点,其指针域指向首元结点。头结点的数据域可以不存储任何信息,也可存储与数据元素类型相同的其他附加信息。例如,当数据元素为整数型时,头结点的数据域中可存放该线性表的长度。

        3)头指针是指是指向链表中第一个结点的指针。若链表设有头结点,则头指针所指结点为线性表的头结点;若链表不设头结点,则头指针所指结点为该线性表的首元结点。

        链表增加头结点的作用如下。

        1)便于首元结点的处理

        增加了头结点后,首元结点的地址保存在头结点(即其”前驱“结点)的指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需进行特殊处理。

        2)便于空表和非空表的统一处理

        当链表不设头结点时,假设L为单链表的头指针,它应该指向首元结点,则当单链表为长度n为0的空表时,L指针为空(判定空表的条件可记为:L==NULL)。

        增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。如下图(a)所示的非空单链表,头指针指向头结点。若为空表,则头结点的指针域为空(判定空表的条件可记为:L->next==NULL),如下图(b)所示。

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

2.5.2 单链表基本操作的实现

1.初始化

        单链表的初始化操作就是构造一个如上图(b)所示的空表。

算法2.6 单链表的初始化

【算法步骤】

        1)生成新结点作为头结点,用头指针L指向头结点。

        2)头结点的指针域置空。

【算法描述】

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

2.取值

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

算法2.7 单链表的取值

【算法步骤】

        1)用指针p指向首元结点,用j做计数器初值赋为1。

        2)从首元结点开始依次顺着链域next向下访问,只要指向当前结点的指针p不为空(NULL),并且没有到达序号为i的结点,则循环执行以下操作:

                \bullet p指向下一结点。

                \bullet  计数器j相应加1。

        3)退出循环时,如果指针p为空,或者计数器j大于i,说明指定的序号i值不合法(i>=n或i<=0),取值失败返回ERROR;否则取值成功,此时j=i,p所指的结点就是要找的第i个结点,用参数e保存当前结点的数据域,返回OK。

【算法描述】

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;                  //计数器相应增加1
    }
    if(!p||j>i)
        return ERROR;         //i值不合法,i>n或i<=0
    e=p->data;                //取第i个结点的数据域
    return OK;
}

【算法分析】

        该算法的基本操作是比较j和i并后移指针p,while循环体中的语句频度与位置i有关。若1\leqslant i\leqslant n,则频度为i-1,一定能取值成功;若i>n,则频度为n,取值失败。因此算法2.7的最坏时间复杂度为O(n)。

        假设每个位置上元素的取值概率相等,即p_{i}=\frac{1}{n},则ASL=\frac{1}{n}\sum_{i=1}^{n}(i-1)=\frac{n-1}{2}

由此可见,单链表取值算法的平均时间复杂度为O(n)。

3.查找

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

算法2.8 单链表的按值查找

【算法步骤】

        1)用指针p指向首元结点。

        2)从首元结点开始依次顺着链域next向下查找,只要指向当前结点的指针p不为空,并且p所指结点的数据域不等于给定值e,则循环执行以下操作:p指向下一个结点。

        3)返回p。若查找成功,p此时即为结点的地址值,若查找失败,p的值即为NULL。

【算法描述】

LNode *LocateElem(LinkList L,ElemType e)
{//在带头结点的单链表L中查找值为e的元素
    p=L->next;                //初始化p,p指向首元结点
    while(p&&p->data!=e)      //顺域链向后扫描,直到p为空或p所指结点的数据域等于e
        p=p->next;            //p指向下一个结点
    return p;                 //查找成功返回值为e的结点地址p,查找失败p为NULL
}

【算法分析】

        该算法的执行时间与待查找的值e有关,其平均时间复杂度分析类似于算法2.7,也为O(n)。

4.插入

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

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

算法2.9 单链表的插入

【算法步骤】

        将值为e 的新结点插入到表的第i个结点的位置上,即插入到结点a_{i-1}a_{i}之间。

【算法描述】

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

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

【算法分析】

        单链表的插入操作虽然不需要像顺序表的插入操作那样需要移动元素,但平均时间复杂度仍为O(n)。这是因为,为了在第i个结点之前插入一个新结点,必须首先找到第i-1个结点,其时间复杂度与算法2.7相同,为O(n)。

5.删除

        要删除单链表中指定位置的元素,同插入操作一样,首先应找到该位置的前驱结点。如下图所示

        在单链表中删除元素b时,应该首先找到其前驱结点a。为了在单链表中实现元素a、b和c之间逻辑关系的变化,仅需修改结点a中的指针域即可。假设p为指向结点a的指针,则修改指针的语句为:p->next=p->next->next。但在删除结点b时,除了修改结点a的指针域外,还要释放结点b所占的空间,所以在修改指针前,应该引入另一指针q,临时保存b的地址以备释放。 

算法2.10 单链表的删除

【算法步骤】

        删除单链表的第i个结点a_{i}的具体过程如下图,图中对应的4个步骤说明如下:

        1)查找结点a_{i-1}并由指针p指向该结点。

        2)临时保存待删除结点a_{i}的地址在q中,以备释放。

        3)将结点*p的指针域指向a_{i}的直接后继结点。

        4)释放结点a_{i}

 【算法描述】

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

        说明:删除算法中的循环条件(p->next&&j<i-1)和插入算法中的循环条件(p&&(j<i-1)是有区别的。因此插入操作中合法的插入位置有n+1个,而删除操作中合法的删除位置只有n个,如果使用与插入操作相同的循环条件,则会出现引用空指针的情况,使删除操作失败。

【算法分析】

        类似于插入算法,删除算法时间复杂度亦为O(n)。

总结:单链表就相当于是一条笔直的线,线上串了一定的珍珠,如果要确定某个珍珠的位置,则需要从第一颗珍珠依次从前往后数。相比于顺序存储结构,链式存储结构更加灵活,对每个元素的排序要求不高,相当于添加或删除一个元素整个表都会自动重新排序,不需要人为设置。

6.创建单链表

        算法2.6的初始化操作是创建一个只有一个头结点的空链表,而上面链表的其他算法都是假定链表已存在多个结点。那么,如何建立一个包括若干个结点的链表呢?链表和顺序表不同,它是一种动态结构。整个可用存储空间可为多个链表共同享用,每个链表占用的空间不需预先分配划定,而是由系统按需即时生成。因此,建立线性表的链式存储结构的过程就是一个动态生成链表的过程。即从空表的初始状态起,依次建立各元素结点,并逐个插入链表。

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

        1)前插法

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

算法2.11 前插法创建单链表

【算法步骤】

        1)创建一个只有头结点的空链表。

        2)根据待创建链表包括的元素个数n,循环n次执行以下操作:

                \bullet 生成一个新结点*p;

                \bullet 输入元素值赋给新结点*p的数据域;

                \bullet 将新结点*p插入到头结点之后。

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

 

 【算法描述】

void CreateList_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.11的时间复杂度O(n)。

        2)后插法

        后插法是通过将新结点逐个插入到链表的尾部来创建链表。同前插法一样,每次申请一个新结点,读入相应的数据元素值。不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针r指向链表的尾结点。

算法2.12 后插法创建单链表

【算法步骤】

        1)创建一个只有头结点的空链表。

        2)尾指针r初始化,指向头结点。

        3)根据创建链表包括的元素个数n,循环n次执行以下操作:

                \bullet 生成一个新结点*p;

                \bullet 输入元素值赋给新结点*p的数据域;

                \bullet 将新结点*p插入到尾结点*r之后;

                \bullet 尾指针r指向新的尾结点*p;

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

 【算法表述】

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

 算法2.12 的时间复杂度亦为O(n)。

总结:前插法和后插法的创建单链表对于栈和队列的存储结构来说具有重要意义。

2.5.3循环链表

        循环链表(Circular Linked List)是另一种形式的链式存储结构。其特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到表中其他结点,下图所示为单链的循环链表。类似地,还可以有多重链的循环链表。

  

         循环单链表的操作和单链表基本一致,差别仅在于:当链表遍历时,判别当前指针p是否指向表尾结点的终止条件不同。在单链表中,判别条件p!=NULL或p->next!=NULL,而循环单链表的判别条件为p!=L或p->next!=L。

        在某些情况下,若在循环链表中设立尾指针而不设头指针(见下图(a)),可使一些操作简化。例如,将两个线性表合并成一个表时,仅需将第一个表的尾指针指向第二个表的第一个结点,第二个表的尾指针指向第一个表的头结点,然后释放第二个表的头结点。当线性表以下图(a)的循环链表作存储结构时,这个操作仅需改便两个指针值即可,主要语句段如下:

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

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

总结:循环链表就相当于是把前面那一串珍珠首尾相连,但仍然只能单向地从前向后查找。对于循环链表,只是在单链表的基础上增加了一个可循环的功能,个人认为其实际作用不太大。

2.5.4双向链表

        以上讨论的链式存储结构的结点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针向后寻查其他结点。若要寻查结点的直接前驱,则必须从表头指针出发。换句话说,在单链表中,查找直接后继结点的执行时间为O(1) ,而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可利用双向链表(Double Linked List)。

        顾名思义,在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱,结点结构如下图(a)所示,在C语言中可描述如下:

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

        和单链的循环表类似,双向链表也可以有循环链表,,如下图(c)所示,链表中存有两个环,下图(b)所示为只有一个表头结点的空表。

         在双向链表中,若d为指向表中某一结点的指针(即d为DuLinkList变量),则显然有

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

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

        在双向链表中,有些操作(如Listlength、GetElem和LocateElem等)仅需涉及一个方向的指针,则它们的算法描述和线性链表的操作相同,但在插入、删除时有很大的不同,在双向链表中需同时修改两个方向上的指针,如下图(a)、(b)分别显示了插入和删除结点时指针修改的情况。在插入结点时需要修改四个指针,在删除结点时需要修改两个指针。它们的实现分别如算法2.13和算法2.14所示,两者的时间复杂度均为O(n)。

 算法2.13 双向链表的插入

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中
    p->prior->next=s;                            
    s->next=p;
    p->prior=s;
    return OK;
}

算法2.14 双向链表的删除

【算法描述】

Status ListDelete_DuL(DuLinkList &L,int i)
{//删除带头结点的双向链表L中的第i个元素
    if(!(p=GetElem_DuL(L,i)))            //在L中确定第i个元素的位置指针p
        return ERROR;                    //p为NULL时,第i个元素不存在
    p->prior->next=p->next;              
    p->next->prior=p->prior;
    delete p;                            //释放被删除结点的空间
    return OK;
}

总结:双向链表的好处就是既可以从前向后查找,也可以从后向前查找。它的操作性比循环链表相对来说要大一些,也是为了克服单链表的单一性。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值