《数据结构》第二章 线性数据结构

线性数据结构(简称线性结构)是包含n个相同性质数据元素的有限序列。它的基本特征是,在数据元素的非空有限集中,

(1)存在唯一的“最后的元素”。

(2)存在唯一的“第一个元素”。

(3)除最后的元素之外,其它数据元素均匀位移的“直接后继”。

(4)出第一个元素之外,其它数据元素均有唯一的“直接前驱”。

在线性结构中,数据元素的前后关系是“一对一”的,即线性关系。

一个元素的前面的第一个元素叫做这个元素的“直接前驱”,后面的第一个元素叫做这个元素的“直接后继”。

2.1典型线性数据结构

线性结构是最简单且最常用的一类数据结构,典型的有栈、队列和线性表,其中

(1)如果只允许在序列末端进行操作,这种线性结构称为

(2)如果只允许在序列两端进行操作,这种线性结构称为队列

(3)如果允许在序列任意位置进行操作,这种线性结构称为线性表

2.1.1 线性结构的逻辑描述

1.栈

对于栈来说,最后一个元素所在端具有特殊意义,称为栈顶,相对应地,第一个元素所在端称为栈底。不含任何元素的栈称为空栈

栈的插入操作是将新元素压进栈顶,称为入栈,或压栈

栈的删除操作是删除栈顶元素,称为出栈,或弹栈

栈的操作是按后进先出(或者先进后出)的原则进行的,因此,栈又称为后进先出LIFO)的线性结构,或称先进后出FILO)的线性结构。

栈的常用操作包括入栈、出栈和取栈顶元素等。

b2397af236c54a52b2b666847c48ff7d.jpeg

2.队列

队列也是一种操作位置受限的线性结构。通常将队列的插入称为入队,队列的删除操作称为出队,允许插入的一端称为队尾,允许删除的一端称为队头

不含任何元素的队称为空队列。队列也称为先进先出FIFO)的线性结构。

队列的常用操作包括入队、出队和取队头元素等。

ced83c25b85748cf9d7f139177230112.jpeg

3.线性表

线性表是最一般的线性结构,与栈和队列相比,线性表的操作位置不受限制。

线性表中所含数据元素的个数称为线性表的长度,长度为0的线性表称为空表

按元素值非递减(也称为升序)或非递增(降序)排列的线性表称为有序线性表,简称有序表

线性表中数据元素的类型可以视具体情况而定,它是一个数、一个字符,也可以是更复杂的信息。

线性表操作十分灵活,其常用操作包括任意位置插入和删除,以及查询和修改任何位置的元素等。

2.1.2线性结构的存储表示

线性结构的存储表示主要有两种:顺序存储和链式存储。

顺序存储特点是借助数据元素在存储空间中的相对位置来表示元素之间的逻辑关系。最简单的一种方式是用相邻位置表示逻辑关系,即逻辑相邻的元素在物理上也相邻。

假设线性结构L为

(a1,a2,a3,......,an)

其中L的元素存在一组逻辑关系为

<a1,a2><a2,a3>,<a3,a4>,......,<an-1,an>

L的元素存储在一段地址连续的空间,第一个元素a1的存储位置为起始地址,称为基址31af00414eea40e4b16f39841c9ab18e.jpeg

与顺序存储相对应的是链式存储,链式存储的特点是借助指示数据存储地址的指针表示元素之间的逻辑关系。同样的假设线性结构L为

(a1,a2,a3,......,an)

其中L的元素存在一组逻辑关系为

<a1,a2><a2,a3>,<a3,a4>,......,<an-1,an>

链表中的结点除了存储数据元素本身的值外,还存储一个指向其直接后继结点的指针信息。

a0bf6b76c0474bcdbc7f898e45f40373.jpeg

又由于栈、队列和线性表具有不同的操作特点,在实际组织链表时,会采用不同的方式:

  1. 由于栈的操作只允许在栈顶进行,故将其链表的头指针作为栈顶指针,指向栈顶元素结点;
  2. 由于队列的操作只允许在队头和队尾两端进行,故将其链表的头指针作为对头指针,指向队头元素结点,并增设队尾指针指向队尾元素结点;
  3. 由于线性表的操作位置不受限制,且操作种类丰富,故其链表组织方式可有多种变化。

2.2顺序栈

2.2.1栈的顺序表示和实现

采用顺序存储结构的栈称为顺序栈,即需要用一篇地址连续的空间来存储栈的元素,很容易想到用一个一维数组实现,并指定栈顶位于序列末端。

每当新元素入栈或者栈顶元素出栈时,数组中的所有元素都不需要移动,只需改变栈顶位置即可。

在C语言的一维数组中,大小是在定义时就被确定,无法改变,在用完之后无法添加新元素。

因此,可以使用动态分配的一块连续空间来存储栈的元素,并在栈满时,通过重新分配更大空间进行扩容。

顺序栈的类型定义如下:

typedef struct{
	ElemType *elem;		//存储空间的基址
	int top;				//栈顶元素怒的下一个位置,简称栈顶位标
	int size;				//当前分配的存储容量
	int increment;			//扩容时,增加的存储容量
}SqStack;					//顺序栈

栈的常用操作包括入栈、出栈和取栈顶元素等,除此之外,站的操作还有栈的初始化、销毁以及判空等,因此,在顺序栈中,可将栈的操作定义成如下接口:

1.初始化操作

该操作将顺序栈S置于初始容量为size的空栈。

算法:顺序栈的初始化        时间复杂度:O(1)

Status InitStack_Sq(SqStack &S,int size,int inc){      //初始化空顺序栈S

    S.elem=(ElemType*)malloc(size*sizeof(ElemType));//分配存储空间

    if(S.elem==NULL||inc<=0||size<=0)

    {

        return ERROR;//不成功的情况

    }

    S.top=0;//置S为空栈

    S.size=size;//初始容量值

    S.increment=inc;//初始增量值

    return OK;

2.入栈操作

该操作将元素e压入顺序栈S。首先检查存储空间,若已满(S.top>=S.size),则增加存储容量。然后将新元素放入栈顶位标S.top指示的位置,并将S.top加1

算法:顺序栈的入栈          时间复杂度:O(1)

Status Push_Sq(SqStack &S,ElemType e){//元素e压入栈S

    ElemType*newbase;

    if(S.top>=S.size){//若栈顶位标已到达所分配的容量,则栈满,扩容

    newbase=(ElemType*)realloc(S.elem,(S.size+S.increment)*sizeof(ElemType));

        if(NULL==newbase)

        {

           return OVERFLOW;

        }

        S.elem=newbase;

        S.size=S.increment;

    }

    S.elem[S.top++]=e;//e入栈,并将S.top加1

    return OK;

}

算法:十进制转换为八进制

void Converstion(int N){

    SqStack S;

    ElemType e;

    InitStack_Sq(S,10,5);//栈S的初始容量为10

    while(N!=0){

        Push_Sq(S,N%8);//将N除以8的余数入栈

        N/=8;//N取值为其除以8的商

    }

    while(FLASE==StackEmpty_Sq(S)){//依次输出栈的余数

        Pop_Sq(S,e);

        printf("%d",e);

    }

}

算法:括号匹配

Status Matching(char*exp,int n){//检查exp中长度为n的括号序列是否匹配

    int i=0;

    ElemType e;

    SqStack S;

    InitStack_Sq(S,n,5)

    while(i<n)

    {

        switch(exp[i]){

           case '(':

           case "[":

               Push_Sq(S,exp[i]);

               i++;

               break;

           case ')':

           case ']':

               if(TURE==StackEmpty_Sq(S))

               {

                   return ERROR;

               }

               else{

                   GetTop_Sq(S,e);

                   if((exp[i]==')'&&e=='()')||(exp[i]==']'&&e=='[')){

                       Pop_Sq(S,e);

                       i++;

                   }else return ERROR;

               }

               break;

        }

     }

     if(TURE==StackEmpty_Sq(S))

        return Ok;

    else

        return ERROR;

}

2.3 循环队列

2.3.1 队列的顺序表示

采用顺序存储结构的队列,需要按队列可能的最大长度分配存储空间,其定义类型如下:

typedef struct

{

    ElemType *elem;     //存储空间的基址

    int front;         //对头位标

    int rear;          //队尾位标

    int maxSize;       //存储容量

}SqQueue;

队列经常在队头和队尾分别进行出队和入队操作,需要设置两个位标分别指示队头元素的位置和队尾的下一位置,分别称为队头位标队尾位标。初始化队列Q时,令Q.front=Q.rear=0;出队时,对头位标Q.front增1;入队时,队尾位标Q.rear增1。2ddcc8851c7c40e39772d581515157da.jpeg

顺序存储的队列为空时Q.front==Q.rear。为满时Q.rear==Q.maxSize.

由于出队在队头,入队在队尾,当Q.rear等于Q.maxSize时,若Q.front不等于0,则队列中仍有空闲单元,队列并不是真满。

这时若再有入队操作,会造成假溢出。

解决假溢出的常用方法:

  1. 出队时,将所有元素向前移动,但这会导致算法效率变低。
  2. 将队列看成首位相连的顺序队列,称为循环队列

09c6d2e13c4f4649b0b437c4834ffc9c.jpeg

循环队列的基本操作接口说明:

Status InitQueue_Sq(SqQueue &Q,int size);  //构造一个空队列Q,最大队列长度为size

Status DestroyQueue_Sq(SqQueue &Q);           //销毁队列Q,Q不再存在

void ClearQueue_Sq(SqQueue &Q);               //将Q置为空队列

Status QueueEmpty_Sq(SqQueue Q);          //若队列Q为空队列则返回TRUE,返回否则FALSE

int QueueLength_Sq(SqQueue Q);            //返回队列Q中元素个数,即队列的长度

Status GetHead_Sq(SqQueue Q,ElemType &e);  //若队列不空,即用e返回Q的队头元素,并返回OK;否则返回ERROR

Status EnQueue_Sq(SqQueue &Q,ElemType e);  //在当前队列的尾元素之后插入元素e为新的队尾元素

Status DeQueue_Sq(SqQueue &Q,ElemType &e); //若队列不空则删除当前队列Q中的头元素,用e返回其值,并返回OK;否则返回ERROR

2.3.2循环队列的实现

在循环队列中,如果Q.rear(或Q.front)为Q.maxSize-1,当入队(或出队)时,令Q.rear(或Q.front)回到0,即回到队时对Q.rear的操作为:

if(Q.rear==Q.maxSize-1)

{

    Q.rear=0;

}

else

{

    Q.rear++;

}

可用求余运算(%)化简为

Q.rear=(Q.rear+1)%Q.maxSize;

称为对Q.rear循环加1.出队时对Q.front的操作类似。

948e003a3c9b43fd85a2878ee022bb9e.jpeg

此时,两种情况下都是Q.rear==Q.front,所以无法判断循环队列是空还是满。

解决方法:

  1. 设一标志域标识队列的空或满。
  2. 设一个长度域记录队列中元素的个数。
  3. 少用一个元素空间,一旦Q.front==(Q.rear+1)%Q.maxSize则认为队满。
  4. 对于第三种判断循环队列为空的条件是Q.rear==Q.front,判断循环队列满的条件是Q.front==(Q.rear+1)%Q.maxSize

1.初始化操作

该操作按指定容量size分配存储空间,将队列的最大长度Q.maxSize置为size,并把队头和队尾标的值置于0

算法:循环队列的初始化

Status InitQueue_Sq(SqQueue &Q,int size){//构造一个空队列

    Q.elem=(ElemType *)malloc(size*sizeof(ElemType));//分配存储空间

    if(NULL==Q.elem)//判断是否构造成功

    {

        return OVERFLOW;

    }

    else

    {

        Q.maxSize=size;

        Q.front==Q.rear;    //把Q置为空队列

    }

    return OK;

}

2.出队操作

该操作只能在队头位置出队,首先判断队列是否为空,如果为空则报错,否职将队头元素取出,并将对头位标Q.front循环加1.

算法:循环队列出队

Status DeQueue_Sq(SqQueue &Q,ElemType &e)

{

    //若队列不空,则删除队列Q中的头元素,用e返回其值

    if(Q.front==Q.rear)

    {

        return ERROR;//队空报错

    }

    else

    {

        e=Q.elem[Q.front];

        Q.front=(Q.front+1)%Q.maxSize;  //Q.front循环加1

    }

    return OK;

}

3.入队操作

只能在队尾位置入队列,首先需要需要判断队列是否为满,如果满则报错,否则插入元素到队尾,并将队尾位标Q.rear循环加1.

算法:循环队列入队

Status EnQueue_Sq(SqQueue &Q,ElemType e)

{

    //若队列不满,则在队列Q中的尾元素之后插入元素e为新队列尾元素

    if((Q.rear+1)%Q.maxSize==Q.front)

    {

        return ERROR;//队满报错

    }

    else

    {

        Q.elem[Q.rear]=e;

        Q.rear=(Q.rear+1)%Q.maxSize;    //Q.front循环加1

    }

    return OK;

}

三个算法的时间复杂度O(1)。

2.3.3 应用举例

例:车厢编组

假设队列元素是char类型,‘H’表示硬座,‘S’表示软卧,队列A表示一组硬座和卧铺混编的车厢。要求把队列A中的硬座车厢移动到队列B,队列A中的卧铺移动到队列C,同时保留原有硬座之间和软卧车厢之间的先后顺序不变。

算法:车厢编组

void Arrange(SqQueue A,SqQueue &B,SqQueue &C)

{

    char e;

    InitQueue_Sq(B,A,maxSize);     //空的硬座队列B

    InitQueue_Sq(C,A,maxSize);     //空的软卧队列C

    while(FALSE==QueueEmpty_Sq(A))  //当A不为空的时候,依次处理队列A的每个车厢

    {

        DeQueue_Sq(A,e);           //取队头元素

        if(e=='H')

        {

           EnQueue_Sq(B,e);       //加入硬座队列B

        }

        else

        {

           EnQueue_Sq(C,e);       //加入软卧队列C

        }

    }

}

2.4 顺序表

2.4.1 线性表的顺序表示与实现

      采用顺序存储结构表示的线性表称为顺序表。用一组地址连续的存储单元依次存放线性表的数据元素,即以存储位置相邻表示位序相继的两个元素之间的前驱和后继关系。如果在顺序表中插入或删除元素,则需要移动操作位置之后的所以元素。因此,为避免移动元素,在顺序表的接口定义中只考虑在表尾插入和删除元素,如此实现的顺序表也可称为栈表。顺序表的类型定义如下:

typedef struct

{

        ElemType *elem;     //存储空间的基址

        int length;        //当前长度

        int size;          //存储容量

        int increment;     //扩容的增量

}SqList;               //顺序表

顺序表的接口定义如下:

Status InitList_Sq(SqList &L,int size,int inc);//初始化顺序表

Status DestroyList_Sq(SqList &L);//销毁顺序表L

Status ClearList_Sq(SqList &L);//将顺序表L清空

Status ListEmpty_Sq(SqList L);//若顺序表L为空表,则返回TRUE,否则返回FALSE

int ListLength_Sq(SqList L);//返回顺序表L中的元素个数

Status GetElem_Sq(SqList L,int i,ElemType &e); //用e返回顺序表L的第i个元素的值

int Search_Sq(SqList L,ElemType e);//在顺序表L中顺序查找元素e,成功时返回该元素在表中第一次出现的位置,否则返回-1

Status ListTraverse_Sq(SqList L,Status(*vist)(ElemType e));//遍历顺序表L,以此对每个元素调用函数visit()

Status PutElem_Sq(SqList &L,ElemType &e);//将顺序表L中第i个元素赋值为e

Status Append_Sq(SqList &L,ElemType e);//在顺序表尾添加元素e

Status DeleteLast_Sq(SqList &L,int i,ElemType e);//删除顺序表L的尾元素,并用参数e返回其值

1.删除表尾元素操作

该操作删除顺序表L的表尾元素。若L不空,则用参数e返回表尾元素,并将表长L.length-1。

bbdd235166b749fe9422437ab01d0242.jpeg

算法:在顺序表的表尾删除元素

Status InitList_Sq(SqList &L,int size,int inc);//初始化顺序表

Status DestroyList_Sq(SqList &L);//销毁顺序表L

Status ClearList_Sq(SqList &L);//将顺序表L清空

Status ListEmpty_Sq(SqList L);//若顺序表L为空表,则返回TRUE,否则返回FALSE

int ListLength_Sq(SqList L);//返回顺序表L中的元素个数

Status GetElem_Sq(SqList L,int i,ElemType &e); //用e返回顺序表L的第i个元素的值

int Search_Sq(SqList L,ElemType e);//在顺序表L中顺序查找元素e,成功时返回该元素在表中第一次出现的位置,否则返回-1

Status ListTraverse_Sq(SqList L,Status(*vist)(ElemType e));//遍历顺序表L,以此对每个元素调用函数visit()

Status PutElem_Sq(SqList &L,ElemType &e);//将顺序表L中第i个元素赋值为e

Status Append_Sq(SqList &L,ElemType e);//在顺序表尾添加元素e

Status DeleteLast_Sq(SqList &L,int i,ElemType e);//删除顺序表L的尾元素,并用参数e返回其值

时间复杂度:O(n)

2.顺序查找

该操作在顺序表L中查找元素e。从第一个元素开始,依次与e比较,若相等,则查找成功,返回该元素的位序;若不存在与e相等的元素,则查找失败,返回-1。

int Search_Sq(SqList L,ElemType e){

        //顺序表L中顺序查找数据元素e,成功是返回该元素在表中第一次出现的位置,否则返回-1

        int i=0;

        while(i<L.length&&L.elem[i]!=e)

        {

            i++;//顺序查找e

        }

        if(i<L.length)

        {

            return i;//返回e的位序

        }

        else

        {

            return -1;

        }

}

为确定元素在表中的位置,需要将表中的元素依次与给定值比较。查找成功时比较的次数的期望值被称为查找成功的平均查找长度,简称ASLsucc。对于含有n个元素的顺序表,查找成功的平均查找长度可表示为

ASLsucc=eq?%5Csum_%7Bi%3D0%7D%5E%7Bn-1%7Dpici

其中pi为查找第i元素的概率,且∑pi=1;ci为查找第i元素时需要与给定值进行比较的元素的个数。显然ci根据查找算法的不同而不同。

在等概率的情况下,上面的算法查找成功的平均长度为

ASLsucc=eq?%5Csum_%7Bi%3D0%7D%5E%7Bn-1%7Dpici=eq?%5Csum_%7Bi%3D0%7D%5E%7Bn-1%7D1/n(i+1)=1/n(1+2+3+······+n)=(1+n)/2

因此,在顺序表中查找元素平均需要比较表中的一半元素,则顺序查找的时间复杂度O(n)。若元素不存在,需要与表中所有元素比较,所以查找不成功的事件复杂度为O(n)。

例2-4有序顺序表的归并

用顺序表实现的有序称为有序顺序表。有序顺序表的的归并是将两个有序的顺序表合并为一个有序顺序表。

算法思路如下:

(1)从第一个顺序表和第二个顺序表开始,依次比较其值大小,将较小的元素值添加进第三个顺序表中,并取该有序顺序表的下一项,知道两个顺序表处理完最后一个元素。

(2)将尚未处理的两个顺序表的剩余部分添加到第三个顺序表中。

算法:有序顺序表的归并:

void MergeList_Sq(SqList La,SqList Lb,SqList &Lc)

{

    //已知有序顺序表La和Lb中的数据元素按值非递减排列

    //归并La和Lb得到新的有序顺序表Lc,Lc的数据元素也按值非递减排列

    int i=0,j=0,size,increment=10;

    ElemType ai,bj;

    size=La.length+Lb.length;

    InitList_Sq(Lc,size,increment);//创建空表Lc

    while(i<La.length&&j<Lb.length) //表La和Lb均非空

    {

        GetElem_Sq(La,i,ai);//取La的第i元素到ai

        GetElem_Sq(Lb,i,bj);//取Lb的第i元素到bj

        if(ai<=bj)         //ai较小或相等时取ai

        {

            Append_Sq(Lc,ai);//将ai添加到Lc中

            ++i;//指示La下一项

        }

        else//bj较小

        {

            Append_Sq(Lc,bj);

            j++;

        }

    }

    while(i<La.length) //当La非空且Lb已空

    {

        GetElem_Sq(La,i++,ai);

        Append_Sq(Lc,ai);

    }

    while(j<Lb.length) //表Lb非空且表La已空

    {

        GetElem_Sq(Lb,j++,bj);

        Append_Sq(Lc,bj);

    }

}

2.4.2 一元稀疏多项式

一元多项式Pn(x)可按指数递增的形式表示为

Pn(x)=p0+p1x+p2x2+…+pnxn

其中n+1个系数pi可用线性表表示

P=(p0,p1,p2,…,pn)

每一项中x的指数隐含为系数pi的位序i。在一个一元多项式中,若系数pi不为0的项相对于多项式的最高次数而言少得多,则称该一元多项式为稀疏多项式。如:

Pn(x)=1+7x10000+4x30000

若采用顺序存储分配,则需要30001个元素空间,但其中只有三个非零元素,应避免这种对存储空间的浪费。一元多项式可采用压缩存储,只存非零系数以及对应的指数。a5fc6c44778d4f9b846c72e72931f14f.jpeg

一元稀疏多项式可表示为

Pn(x)=p0xe0+p1xe1+p2xe2+…+pmxem

其中,共有m+1项的系数非零(系数非零的项数也称为一元稀疏多项式的长度),pi是指指数为ei­的项的非零系数,且满足

0≤e0<e1<e2<…<em=n

所哟一,一元稀疏多项式可用一个长度为m+1的顺序表表示为

P=((p0,e0),(p1,e1),(p2,e2),…(pm,em))

显然,该顺序表是按指数有序的。

一元稀疏多项式的类型定义如下:

typedef struct{

    float coft;        //系数

    int expn;          //指数

}ElemType,Term;        //项

typedef struct{

    Term *elem;        //存储空间基址

    int length;        //长度(项数)

}Poly;                 //一元稀疏多项式

一元稀疏多项式的接口如下:

Status CreatPoly(Poly &P,Term e[],int n);//由数组e的前n项构建一元稀疏多项式P

Status DestroyPoly(Poly &P);//销毁一元稀疏多项式P

Status PrintPoly(Poly P);//打印输出一元稀疏多项式

int PolyLength(Poly P);//返回一元稀疏多项式的长度

void AddPoly(Poly pa,Poly pb,Poly &pc);//两个一元稀疏多项式做加法,即pc=pa+pb

void SubPoly(Poly pa,Poly pb,Poly &pc);//两个一元稀疏多项式做减法,即pc=pa-pb

void MulPoly(Poly pa,Poly pb,Poly &pc);//两个一元稀疏多项式做乘法,即pc=pa×pb

void DivPoly(Poly pa,Poly pb,Poly &pc);//两个一元稀疏多项式做减法,即pc=pa/pb

1.构建一元稀疏多项式操作

该操作由数组e的前n项构建一元稀疏多项式P。

算法:构建一元稀疏多项式

Status CreatPoly(Poly &P,Term e[],int n){

    int i;

    P.elem=(Term*)malloc(sizeof(Term)*n);//开辟空间

    if(NULL==P.elem)

    {

        return OVERFLOW;//判断是否为空

    }

    for(i=0;i<n;i++)

    {

        P.elem[i]=e[i];//顺序表存储

    }

    P.length=n;//改变长度

    return OK;

}

2.加法操作

两个一元稀疏多项式pa和pb相加,结果为一元稀疏多项式pc。根据一元稀疏多项式对指数有序的特点,算法思路为:

  1. 从pa和pb的第一项起,依次比较指数大小,直到pa或pb处理完完成最后一项;
    • 若不等则将指数较小项添加到pc,并取所在多项式的下一项。
    • 若相等则指数不变,系数相加,若结果非零,则添加到pc。pa和pb都取下一项。
  2. 将尚未处理的pa和pb的剩余部分依次添加到pc

pc的初始容量可为pa和pb的长度之和。若pa和pb含有指数相同的项,则pc实际长度会小于初始容量,可为其重新分配空间。

如:两个一元稀疏多项式2x+7x4-2x6和-7x4+3x6+9x7+2x9相加。先取pa和pb的长度之和7作为pc的初始容量。相加结果为2x+x6+9x7+2x9。最后根据实际长度重新分配pc的空间。

4229887853774387bf0af2de3dd22ebc.jpeg

算法:一元稀疏多项式的加法

Status AddPoly(Poly pa,Poly pb,Poly &pc){//求pc=pa+pb

    int i=0,j=0,k=0;

    float c;

    pc.elem=(Term*)malloc((pa.length+pb.length)*sizeof(Term));

    if(NULL==pc.elem)

    {

        return OVERFLOW;

    }

    while(i<pa.length&&i<pb.length){//两个多项式均为处理结束

        if(pa.elem[i].expn<pb.elem[j].expn){//pa的项的指数较小,添加到pc

           pc.elem[k++]=pa.elem[i++];

        }

        else if(pa.elem[i].expn>pb.elem[j].expn)//pb项的指数较小,添加到pc

        {

           pc.elem[k++]=pb.elem[j++];

        }

        else{   //指数相等

           c.pa.elem[i].cofe+pb.elem[j].cofe;//系数相加

           if(c!=0){//系数和不为零,构建和项,并添加到pc

               pc.elem[k].expn=pa.elem[i].expn;

               pc.elem[k].cofe=c;

               k++;

           }

           i++;

           j++;//pa和pb均取下一项

        }

    }

    if(i==pa.length)//pa已经处理完,将pb剩余部分添加到pc

    {

        while(j<pb.length){

           pc.elem[k++]=pb.elem[j++];

        }

    }

    if(j==pb.length)//pb已经处理完,将pa剩余部分添加到pc

    {

        while(i<pa.length){

           pc.elem[k++]=pa.elem[i++];

        }

    }

    if(k<pa.length+pb+length)

    {

        pc.elem=(Term*)realloc(pc.elem,k*sizeof(Term))

        if(NULL==pc.elem)

        {

           return OVERFLOW;

        }

    }

    pc.length=k;

    return OK;

}

一元系数多项式的基本操作,除了初始化一元稀疏多项式和两个一元稀疏多项式的加法外,一元稀疏多项式的其他操作,如减法、乘法、除法、销毁、复制等。

2.4.3 稀疏矩阵

矩阵是很多科学与工程计算问题中经常使用的数学对象。在数值分析中常常出现一些阶数较高的矩阵,且矩阵中有很多值为0的元素(称为零元)。为节省存储空间并且加快处理速度,需要对这类矩阵进行压缩存储,压缩存储的原则是,不存储零元。

零元的数目远远多于非零元数目,并且非零元的分布没有规律的矩阵称为稀疏矩阵。假设在m×n的矩阵中,有t个元素不为0。令 δ=t/(m×n),称 δ为矩阵的稀疏因子。稀疏程度是相对的,通常认为 δ≤0.05,矩阵为稀疏矩阵。

压缩存储稀疏矩阵的原则是只存储稀疏矩阵的非零元。除了存储非零元的值外,还必须记下它所在行和列的位置。因此,稀疏矩阵的非零元可用三元组来表示。3378bb2046d9403ea9a4c2f21423b0af.png

1. 三元组顺序表

采用顺序表作为稀疏矩阵的呀缩存储结构,称该顺序表为三元组顺序表,简称三元组表,其类型定义如下:

typedef struct{

    int i,j;//非零元的行和列

    ElemType e;//非零元的值

}Triple;//三元组

typedef struct{

    Triple *data;//非零元三元组,0号单元未用

    int mu,nu,tu;//矩阵的行数、列数和非零元个数

}TSMatrix;//三元顺序表

其中,data域中表示非零元的三元组是以行序为主序顺序排列的。这样处理更有利于矩阵的常见运算。

矩阵运算种类有很多,下面为矩阵运算的接口:

Status CreateSMatrix(TSMatrix &M);//创建稀疏矩阵M

Status DestroySMatrix(TSMatrix &M);//销毁稀疏矩阵M

void PrintSMatrix(TSMatrix M);//打印输出稀疏矩阵M

Status CopySMatrix(TSMatrix M,TSMatrix &T);//由稀疏矩阵M复制到T

Status AddSMatrix(TSMatrix M,TSMatrix N,TSMatrix &Q);//求稀疏矩阵的和Q=M+N

Status SubSMatrix(TSMatrix M,TSMatrix N,TSMatrix &Q);//求稀疏矩阵的差M-N

Status MultSMatrix(TSMatrix M,TSMatrix N,TSMatrix &Q);//求稀疏矩阵的乘积Q=M×N

Status TransposeSMatrix(TSMatrix M,TSMatrix &T);//求稀疏矩阵M的转置矩阵T

转置操作:

m×n的矩阵A的转置B是一个n×m的矩阵,假设a和b时TSMatrix型的变量,分别表示矩阵A和B。

ba37e48c951440479c2e63df48f88411.png

将矩阵A转置为B,就是将A的三元组表a.data置换为矩阵B的三元组表b.data,如果只是简单的交换a.data中的i和j的内容,那么得到的c.data将是一个按列优先顺序存储的稀疏矩阵B,要得到按行优先顺序存储的b.data,就必须重新排列三元组c.data的顺序。

为避免重新排列,考虑矩阵A的列是B的行,因此,按a.data的列序转置,所得到的转置矩阵B的三元组表b.data必定是按行优先存放的。按这种方法设计的算法,其基本思路是:

对A中的每一列col(1≤col≤n),通过从头至尾扫描三元表a.data,找到所有列号等于col的三元组,将它们的行号和列号互换后依次存放入b.data中,即可得到B的按行优先的压缩存储表示。

算法:按列转置

Status TransposeSMatrix(TSMatrix M,TSMatrix &T){//求稀疏矩阵M的转置矩阵T

    //采用三元组表存储表示,求稀疏矩阵M的转置矩阵T

    int i,p,q;

    T.mu=M.nu;//M的列变成T的行

    T.nu=M.mu;//M的行变成T的列

    T.tu=M.tu;//M和T的非零元个数相等

    if(T.tu!=0)

    {

        T.data=(Triple*)malloc((T.tu+1)*sizeof(Triple));//分配空间

        if(NULL==T.data)//开辟空间失败

        {

            return OVERFLOW;

        }

        q=1;

        for(j=1;j<=M.nu;++j)//按列优先转置

        {

            for(p=1;p<=M.tu;++p)//转置矩阵元素

            {

                if(M.data[p].j==j)

                {

                    T.data[q].i=M.data[p].j;

                    T.data[q].j=M.data[p].i;

                    T.data[q].e=M.data[p].e;

                    q++;

                }

            }

        }

        return Ok;

    }

}

算法的时间复杂度为O(tu×nu),即正比于矩阵的列数和非零元个数的乘积,一般传统矩阵的转置算法如下:

for(i=1;i<=mu;++i)

{

    for(j=1;j<=nu;++j)

    {

        A[j][i]=B[i][j];

    }

}

其时间复杂度位O(mu×nu)。它正比于行数和列数的乘积。如果非零元的个数tu和行数mu同数量级,则稀疏矩阵的按列转置与一般传统转置的算法复杂度相同。

第二种转置算法:按矩阵A的a.data中的三元组的次序进行转置,并将转置后的三元组直接置入B中的恰当位置。如果能确定矩阵A中的每一列(即B的每一行)的第一个非零元在b.data的位置(称为该列的起始位置),那么在对a.data中的三元组依次做转置时,便可直接放到b.data中正确的位置。

为了确定矩阵A中的第j列在矩阵B中的起始位置,需要先求得矩阵A中的第j列之前所有非零元的个数。矩阵A中第j列的起始位置等于第j-1列的起始位置加上第j-1的非零元的个数。

为此,需要设置两个一维数组num和cpos。num[j]用来统计矩阵A的a.data中第j列非零元素的个数。

cpos[j]表示矩阵A的a.data中第j列的第一个非零元在b.data中的起始位置。显然有

cpos[1]=1;

cpos[j]=cpos[j-1]+num[j-1];

1c3c39a32738498cb2f193333c77c814.jpeg

算法:快速转置

Status FastTransposeSMatrix(TSMatrix M,TSMatrix &T){

    //采用三元组表存储表示,求稀疏矩阵M的转置矩阵T

    int j,q,k,p;

    int *num,*cpos;

    T.mu=M.nu;

    T.nu=M.mu;

    T.tu=M.tu;

    if(T.tu!=0)

    {

        T.data=(Triple*)malloc((T.tu+1)*sizeof(Triple));//开辟T.data的空间

        num=(int*)malloc((M.nu+1)*sizeof(int));//开辟num的空间

        cpos=(int*)malloc((M.nu+1)*sizeof(int));//开辟cpos的空间

        if(NULL==T.data||NULL==num||NULL==cpos)//判断是否开辟成功 

        {

           return OVERFLOW;

        }

        for(j=1;j<=M.nu;++j)

        {

           num[j]=0;

        }

        for(k=1;k<=M.tu;++k)

        {

           ++num[M.data[k].j];//累加每个列的元素个数

        }

        cpos[1]=1;

        for(j=2;j<=M.nu;++j)

        {

           cpos[j]=cpos[j-1]+num[j-1];//确定第j列第一个元素的起始位置

        }

        for(p=1;p<=M.tu;p++)//转置矩阵元素

        {

           j=M.data[p].j;

           q=cpos[j];

           T.data[q].i=M.data[p].j;

           T.data[q].j=M.data[p].i;

           T.data[q].e=M.data[p].e;

           ++cpos[j];//更新为第j列下一非零元的位置

        }

    }

    free(num);

    free(cpos);

    return OK;

}

这个算法比按列转置多用了两个辅助数组。从时间上看,算法中有4个并列的单循环,循环次数分别为nu和tu,因而总的时间复杂度为O(nu+tu)。在A的非零元个数tu和mu×nu等数量级时,起事件复杂度为O(mu×mu),和经典算法的时间复杂度相同。

    三元组表又称为有序的双下标法,它的特点是非零元在表中按行序有序存储,因此便于进行顺序处理的矩阵运算。然而,若需按行号存取某一行的非零元,则需从头开始查找。

2.带行位置信息的三元组表

为了便于随机存取任意一行的非零元,需要知道每一行的第一个非零元在第三元组表中的位置。为此,可仿照快速转置矩阵算法中创建列其实位置数组的方法,创建行起始位置数组rpos并固定在稀疏矩阵的存储结构。带行位置信息的三元组表类型描述如下:

typedef struct{

    Triple *data;//非零元三元组表,data[0]未用

    int *rpos;//指示各行的其实位置

    int mu,nu,tu;//矩阵的行数、列数和非零元个数

}RLSMatrix;//带行位置信息的顺序表类型

有了指示各行的起始位置rpos域,给定一组下标,求稀疏矩阵的元素值就不必扫描整个三元组表。

算法:求稀疏矩阵的元素值

ElemType value(RLSMatrix M,int r,intc){//给定一组下标,求元素值

    int p;

    p=M.rpos[r];//取出行r的起始位置

    while(M.data[p].i==r&&M.data[p].j<c)

    {

        p++;

    }

    if(M.data[p].i==r&&M.data[p].j==c)

    {

        return M.data[p].e;

    }

    else

    {

        return 0;

    }

}

三元组的另一种变形是将其中的行下标域去掉,得到二元组顺序表,另设行起始位置数组。f498d92bb675471f96f18d325e9c5306.jpeg

2.5 链栈和栈队列

2.5.1链栈

采用链式存储结构的栈称为链栈,设栈S=(a1,a2,a3,…,an),其中a1为栈底元素,an为栈顶元素,栈顶元素指向an。当栈为空时,栈顶指针为NULL。

a3f149f3be4d47da98d80f4de30a7a70.jpeg

链栈是由栈顶指针唯一确定的,其定义类型定义如下:

typedef struct LSNode{

    ElemType data;//数据域

    struct LSNode *next;//指针域

}LSNode,*LStack;//结点和链栈类型

链栈的接口定义如下:

void InitStack_LS(LStack &S);//初始化链栈S

void DestroyStack_LS(LStack &S);//销毁链栈S

Status StackEmpty_LS(LStack S);;//判断空栈,若为空则返回TRUE,否则返回FAlSE

Status Push_LS(LStack &S,ElemType e);//元素e压入栈S

Status Pop_LS(LStack &S,ElemType &e);//栈S的栈顶元素出栈,并用e返回

Status GetTop_LS(LStack S,ElemType &e);//取栈S的栈顶元素,并用e返回

1.入栈操作

该操作为入栈元素e生成一个新结点,并插入在栈顶。设有链栈S=(Z,Q,D,L,H),将元素A压入栈S。

算法:链栈的入栈

Status Push_LS(LStack &S,ElemType e){//元素e压入栈S

    LSNode *t;

    t=(LSNode*)malloc(sizeof(LSNode));//为元素e分配节点空间

    if(NULL==t)

    {

        return OVERFLOE;//分配失败,返回

    }

    t->data=e;

    t->next=S;

    S=t;//在栈顶位置插入新结点

    return OK;

}

2.出栈操作

若链栈S为空,则报错返回;否则,删除栈顶结点,并用参数e返回其元素值。链栈S=(Z,Q,D,L,H),栈顶元素H出栈。

cce8b29d0188445597876b70a8273192.jpeg

算法:链栈的出栈

Status Pop_LS(LStack &S,ElemType &e){//栈S的栈顶元素出栈,并用e返回

    LSNode *t;

    if(S==NULL)//链栈为空

    {

        return ERROR;

    }

    t=S;//t指向栈顶元素的结点

    e=S->data;//用e返回栈顶元素值

    S=S->next;//删除栈顶元素结点

    free(t);//释放结点

    return OK;

}

2.5.2 链队列

采用链式存储结构的队列为链队列,由于队列的操作是在队头和队尾两端进行的,故需设置头尾指针指示链表的头尾结点。

65bbdb430f40482b95d15c5071a3a3d4.jpeg

链队列的类型定义如下:

typedef struct LQNode{

    ElemType data;

    struct LQNode *next;

}LQNode,*QueuePtr;//结点及其指针类型

typedef struct{

    QueuePtr front;//队头指针

    QueuePtr rear;//队尾指针

}LQueue;//链队列

链队列的接口定义:

void InitQueue_LQ(LQueue &Q);//构造一个空队列

void DestroyQueue_LQ(LQueue &Q);//销毁队列Q

Status QueueEmpty_LQ(LQueue Q);//若队列Q为空,则返回TRUE,否则返回FALSE

int QueueLength_LQ(LQueue Q);//返回队列Q中元素个数,即队列的长度

Status GetHead_LQ(LQueue Q,ElemType &e);//若队列不空,则用e返回Q的队头元素,并返回 OK;否则返回ERROR

Status EnQueue_LQ(LQueue &Q,ElemType e);//在队列Q的队尾插入元素e

Status DeQueue_LQ(LQueue &Q,ElemType &e);//若队列Q非空,则删除队头元素,用e返回其值,并返回OK;否则返回ERRER

1.入队操作

该操作在链队列Q的队尾插入一个新的结点,并令队尾指针指向该结点。d6455d5c0916468385d8143d17d10dc2.jpeg

算法:链队列的入队

Status EnQueue_LQ(LQueue &Q,ElemType e){//在队列Q的队尾插入元素e

    LQNode *p;

    p=(LQNode*)malloc(sizeof(LQNode));//开辟空间

    if(p==NULL)

    {

        return OVERFLOW;//开辟失败

    }

    p->data=e;

    p->next=NULL;

    if(NULL==Q.front)

    {

        Q.front=p;//开始队列为空

    }

    else

    {

        Q.rear->next=p;//e插入非空队列

        Q.rear=p;//队尾指针指向新的队尾

        return OK;

    }

}

2.出队操作

该操作删除链队列Q的队头元素结点,并用e返回其值。若Q变为空,则置队尾指针为空。

2f200971bd6c402e9d4f834eec390329.jpeg

算法:链队列的出队

Status DeQueue_LQ(LQueue &Q,ElemType &e){//若队列Q非空,则删除队头元素,并用e返回其值,并返回OK;否则返回ERROR

    LQNode *p;

    if(NULL==Q.front)//队头指向空指针,空队列

    {

        return ERROR;

    }

    p=Q.front;//指向队头结点

    e=p->data;//保存对头结点数据

    Q.front=p->next;

    if(Q.rear==p)

    {

        Q.rear=NULL;//队列只有一个结点,删除后对列为空

    }

    free(p);//释放删除队头结点

    return OK;

}

两个算法的时间复杂度都为O(n)。

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

顺序表以存储位置相邻表示位序相继的两个元素之间的前驱和后继关系。如果在顺序表中进行插入和删除操作,需要移动操作位置之后的所有元素,因此在顺序表的接口定义中只考虑了在表尾插入和删除元素。这种增删位置受限的存储结构显然难以满足更为广泛的需求,因此需要一种能快速在任意位置插入和删除元素的链式存储结构——链表。常见的链表主要有单链表、双向链表、循环链表和双向循环链表等

2.6.1 单链表

单链表的一个结点对应线性表中的一个元素,每个结点包括一个数据域和一个指针域,数据域存放元素的值,指针域存放直接后继结点的地址。其类型定义如下:

typedef struct LNode{

    ElemType data;//数据域

    struct LNode *next;//指针域,指向下一个结点

}LNode,*LinkList;

指向单链表的第一个结点的指针称为头指针,它唯一确定该单链表,单链表中存放的数据元素个数称为单链表的表长,单链表表长为n,n=0时称为空表。存放单链表中第一个数据元素a1的结点称为首元结点,存放最后一个数据元素an的结点称为尾元结点。

单链表有不带头结点和带头结点两中形式。头结点指的是在单链表的首元结点之前附设的一个结点,头结点的数据域可以不存储任何信息,也可以存储一些附加信息,如线性表的长度等,头结点的指针的指针域指向首元结点。当当单链表为空表时,若不带头结点,其头指针L为NULL;若带头结点,其头结点的指针域为NULL

9d837bbce8dd49d0ac4693e8d68142b8.jpeg

若单链表不带头结点,则在其首元结点之前插入新的结点或删除其首元结点需要进行特殊处理,与在其他位置进行插入和删除时的代码不同。因此,为了统一插入和删除的实现,单链表通常带头结点。

单链表接口定义如下:

Status InitList_L(LinkList &L);//构造一个空的单链表L

Status DestroyList_L(LinkList &L);//销毁单链表L

Status ClearList_L(LinkList &L);//将单链表L重置为空表

Status ListEmpty_L(LinkList L);//单链表L为空表时返回TRUE,否则返回FAlSE

int ListLength_L(LinkList L);//求单链表的长度

LNode * Search_L(LinkList L,ElemType e);//查找。返回单链表L中第一个数据域值为e的结点地址,若不存在返回NULL

LNode * NextElem_L(LNode *p);//返回p结点的直接后继结点的指针

LNode * MakeNode_L(ElemType e);//构造元素e的结点,返回指向该点的指针

Status DeleteAfter_L(LNode *p,ElemType &e);//删除p结点的直接后继结点,用e返回结点值,若p空或指向尾元结点则操作失败

Status InsertAfter_L(LNode *p,LNode *q);//在p结点之后插入q结点

void ListTraverse_L(LinkList L,Status (*vist)(ElemType e);//遍历单链表。

该操作构造一个只含头结点的空单链表L,算法时间复杂度为O(1)。

1.初始化操作

算法:单链表的初始化

Status InitList_L(LinkList &L){//构造一个空的单链表L

    if(NULL==(L=(LNode*)malloc(sizeof(LNode))))

    {

        return OVERFLOW;

    }

    L->next=NULL;//头结点的next域为空

    return OK;

}

该操作在单链表L中查找第一个值为e的元素并返回指向该结点的指针p。

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

b5e43a8e8c5642a5bcbe9d403e1c9950.jpeg

2.查找元素操作

算法:单链表查找元素

LNode * Search_L(LinkList L,ElemType e){//查找。返回单链表L中第一个数据域值为e的结点地址,若不存在返回NULL

    LNode* p;

    if(NULL==L)

    {

        return NULL;

    }

    p=L->next;

    while(p!=NULL&&p->data!=e)

    {

        p=p->next;//查找值为e的结点

    }

    return p;//若e存在,则p->data为e,否则p为NULL

}

3.求直接后继操作

该操作返回p结点的直接后继结点的指针,若p结点为尾结点则返回NULL。

算法的时间复杂度为O(1)。

算法:单链表求后继

LNode * NextElem_L(LNode *p){//返回p结点的直接后继结点的指针

    if(NULL==p)

    {

        return NULL;

    }

    return p->next;

}

4.构造结点操作

该操作构造数据域为e的单链表结点,并返回指向该结点的指针,若构造失败则返回NULL。算法的时间复杂度为O(1)。

算法:单链表构造结点

LNode*MakeNode_L(ElemType e){//构造数据域为e的单链表结点

    LNode*p;

    p=(LNode*)malloc(sizeof(LNode));;//开辟空间

    if(p!=NULL)

    {

        p->data=e;

        p->next=NULL

        return p;

    }

    else

    {

        return NULL;

    }

}

5.插入直接后继结点操作

该操作在p结点之后插入q结点,如果p结点为头结点,则q结点成为首元结点,需要修改两个指针域:①令q结点指针域指向p结点的后继。②修改p结点的指针域指向q结点。算法的时间复杂度为O(1)。

5315aeb9bf764a9a8d98b63bec4de275.jpeg

算法:单链表的插入

Status InsertAfter_L(LNode *p,LNode *q){//在p结点之后插入q结点

    if(NULL==p||NULL==q)//参数不合理

    {

        return ERROR;

    }

    q->next=p->next;

    p->next=q;

    return OK;

}

6.删除直接后继结点的操作

该操作删除并释放p结点的直接后继,并用参数e返回被删除结点数据域的值。删除后继操作需要修改p结点的指针域,令其指向被删结点的后继结点。算法的时间复杂度为O(1)。若删除的是首元结点,可令p的实参为头指针L。

e1bdf78dc87a45d8bc6c9ce564ff47cf.jpeg

算法:单链表的删除

Status DeleteAfter_L(LNode *p,ElemType &e){//删除p结点的直接后继结点并用参数e返回被删结点的值

    LNode *q;

    if(NULL==q||NULL==p)

    {

        return ERROR;//删除位置不合理

    }

    q=p->next;//q指向被删结点

    p->next=q->next;//删除q结点

    e=q->data;

    free(p);//释放q结点

    return OK;

}

7.建立单链表

单链表是一种动态存储结构,其所占有的空间不需要预先分配,而是根据需要分配。建立单链表的过程是从空表开始,依次分配并插入每个节点。算法的时间复杂度为O(n)。

算法:建立单链表

Status CreatList_List(LinkList &L,int n,ElemType *A){

    //建立一个长度为n的单链表L,当数组A[]提供n个元素值

    LNode *p,*q;

    int i;

    if(OVERFLOW==InitList_L(L))

    {

        return OVERFLOW;

    }

    p=L;

    for(i=0;i<n;i++)//依次在表尾插入n个结点

    {

        q=MakeNode_L(A[i]);

        InsertAfter_L(p,q);

        p=q;//令p指向当前的尾结点

    }

    return OK;

}

8.单链表的逆置

单链表的逆置是在不分配新储存空间的情况下,将单链表(a1,a2,…,an)逆置为(an,…,a2,a1)实现思路如下:

  1. 将头结点与首元结点断开,作为逆置后单链表的头结点。
  2. 依次将a1,a2,…,an结点插入到头结点之后的位置,最后插入的结点在最前面。

c71a83f10170437aa60f58af48f9d524.jpeg

算法:单链表逆置

void InverseList(LinkList L){//对带头结点的单链表逆置

    LNode *p,*q;

    if(L->next==NULL||NULL==L->next->next)//单链表没有首元结点或者链表只有一个首元结点

    {

         return ;

    }

    p=L->next;//指针p始终指向待插入的结点

    L->next=NULL;//将头结点和首元结点分开

    while(p!=NULL)//将第一个到最后一个结点依次插入

    {

         q=p->next;//指针q始终指向待插入到头结点的后面

         InsertAfter_L(L,p);//将待插入结点插入到头结点后面

         p=q;//指针p指向下一个待插入结点

    }

}

9.有序单链表的合并

和两个有序顺序表的合并不同,有序单链表合并可不额外分配空间,每个结点都使用原来的空间,只需要修改其指针域即可。设置三个指针pa、pb、pc分别指示3个有序表La、Lb、Lc当前处理的元素,在pa、pb所指示的元素中选择较小的元素插入Lc的表尾,直到La、Lb中有一个表处理完毕,再将有剩余结点的链表直接链接在Lc的表尾,算法的时间复杂度为O(n),空间复杂度为O(1)。

算法:有序单链表的合并

void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc)

{

    //将升序单链表La和Lb归并为新的单链表Lc

    LinkList pa,pb,pc,temp;

    pa=La->next;

    pb=Lb->next;

    Lc=pc=La;//用La的头结点作为Lc的头结点

    while(pa&&pb)//依次处理La和Lb的当前结点

    {

        if(pa->data<=pb->data){//将pa结点插入Lc

           temp=pa->next;

           InsertAfter_L(pc,pa);//将pa插入到Lc的表尾

           pc=pa;//令pc指向pa结点

           pa=temp;//令pa指向La中下一个待处理的结点

        }

        else//将pb结点插入Lc

        {

           temp=pb->next;

           InsertAfter_L(pc,pb);

           pc=pb;

           pb=temp;

        }

        pc->next=pa?pa:pb;//插入La或Lb的剩余段

        free(Lb);//释放Lb的头结点

     }

}

2.6.2 双向链表

在单链表中,每个结点只有一个指向其直接后继的指针域。从某个结点出发沿指针域只能访问其后继结点,若要访问其前驱结点,则需从首元结点开始遍历。因此,在单链表中查找p结点的直接后继操作的时间复杂度为O(1),而查找p结点的直接前驱操作的时间复杂度则为O(n)。

如果需要经常访问结点的前驱,则可在结点中增加指向直接前驱的指针域。这样的链表称为双向链表,可在其接口定义中插入直接前驱和删除当前结点的操作,时间复杂度均为O(1)。空的双向链表只包含头结点,其两个指针域值都为NULL。

79e139d85ce84a8fbd403308d1a58275.jpeg

双向链表的定义如下:

typedef struct DuLNode{

    ElemType data;

    struct DuLNode *prior,*next;//分别指向直接前驱和直接后继

}DuLNode,*DuLinkList;//双向链表

双向链表的接口定义如下:

Status InitList_DuL(DuLinkList &L);//初始化双向链表

Status DestroyList_DuL(DuLinkList &L);//销毁双向链表

Status ClearList_DuL(DuLinkList L);//双向链表置空

Status ListEmpty_DuL(DuLinkList L);//双向链表判空

int ListLength_DuL(DuLinkList L);//求双向链表的长度

DuLNode *Search_DuL(DuLinkList L,ElemType e);//查找元素,返回双向链表L的第一个值为e的结点指针,若没有找到则返回NULL

DuLNode *PriorElem_DuL(DuLNode *p);//返回结点p的直接前驱结点指针,若p为首元结点或L为空表则返回NULL

DuLNode *NextElem_DuL(DuLNode *p);//返回结点p的直接后继结点指针,若p为尾元结点或L为空表则返回NULL

DuLNode *MakeNode_DuL(ElemType e);//分配一个数据域为e的结点,返回指向该结点的指针

Status InsertBefore_DuL(DuLNode *p,DuLNode *q);//在p结点之间插入q结点

Status InsertAfter_DuL(DuLNode *p,DuLNode *q);//在p结点之后插入q结点

Status Delete_DuL(DuLNode *p,ElemType &e);//删除指针p所指向的结点,并用参数e返回p结点的元素值

void ListTraverse_DuL(DuLinkList L,Status(*visit)(ElemType e));//遍历双向链表L

在双向链表的接口中,ListLength_DuL、Search_DuL和NextElem_DuL等操作仅涉及后继指针,它们实现和单链表相同。PriorElem_DuL操作仅涉及前驱指针。插入和删除操作须同时修改两方向的指针。在单链表接口的基础上增加了在结点之间插入InsertBefore_DuL和删除当前结点Delete_DuL的操作。双向链表的插入和删除操作的时间复杂度都是O(1)。

1.插入前驱结点操作

该操作实现在p结点之前插入q结点,在p结点之前插入q结点的InsertAfter_DuL操作也可通过移动p指针转化为在p结点之前插入问题。时间复杂度为O(1)。

41c2302e074341b7a5429926300b7fb5.jpeg

算法:双向链表的插入前驱操作

Status InsertBefore_DuL(DuLNode *p,DuLNode *q){//在p结点之间插入q结点

    if(NULL==p||NULL==q||NULL==p->prior)

    {

        return ERROR;//p指向头结点时不能在之前插入

    }

    q->prior=p->prior;

    q->next=p;

    q->prior->next=q;

    p->prior=q;

    return OK;

}

2.双向链表的删除

该操作删除并释放p结点,并用参数e返回p结点的数据域值。如果p结点为头结点,由于不能进行删除头结点的操作,则返回EEROR。算法时间复杂度为O(1)。

27e36382f25f44068023efbdf8217c12.jpeg

算法:双向链表的删除

Status Delete_DuL(DuLNode *p,ElemType &e){//删除指针p所指向的结点,并用参数e返回p结点的元素值

    if(NULL==p||NULL==p->prior)//删除位置不合理

    {

        return ERROR;

    }

    if(p->next!=NULL)

    {

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

    }

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

    e=p->data;

    free(p);

    return OK

}

2.6.3循环链表

1.单循环链表

单循环链表的特点是尾元结点的指针域指向头结点,从而使链表首尾相连构成一个环,从任意结点出发沿后继指针遍历整个链表。

272e3bd9a5d046e2a061a4296e943f9f.jpeg

单循环链表的类型定义如下:

typedef LinkList CirLinkList;

单循环链表类型及接口类型定义与单链表相似。但单循环链表的初始化操作与删除后继结点的操作的实现与单链表有所不同。

typedef LinkList CirLinkList。

算法:单循环链表初始化

Status InitList_CL(CirLinkList &L){//初始化空的单循环链表L

    if(NULL==(L(LNode*)malloc(sizeof(LNode))))//分配头结点的空间

    {

        return OVERFLOW;

    }

    L->next=L;//头结点的next域指向自身

    return OK;

}
  1. 删除直接后继结点操作。该操作删除p结点的直接后继结点。如果p结点不是尾元结点,该操作与单链表相同。如果是尾元结点,则应跳过头结点,删除尾元结点。算法的时间复杂度为O(1)。
Status InitList_CL(CirLinkList &L){//初始化空的单循环链表L

    if(NULL==(L(LNode*)malloc(sizeof(LNode))))//分配头结点的空间

    {

        return OVERFLOW;

    }

    L->next=L;//头结点的next域指向自身

    return OK;

}

算法:单循环链表的删除后继操作

Status DeleteAfter_CL(CirLinkList L,LNode *p,ElemType &e)

{

    //删除单循环链表L中p结点的直接后继结点并用参数e返回被删结点的值

    LNode*q;

    if(L==L->next)//空表无法删除结点

    {

        return ERROR;

    }

    if(p->next==L)//若p为尾元结点,令p指向头结点

    {

        p=L;

    }

    q=p->next;

    p->next=q->next;

    e=q->data;

    free(q);//释放被删结点

    return OK;

}
  1. 分拆单循环链表。

设存在一个包含字母的单循环链表LO,将其分拆成只包含大写字母的单循环链表LC和只包含小写字母的单循环链表LL.

算法:单循环链表的拆分

void Split(CirLinkList &LO,CirLinkList &LC,CirLinkList &LL)//将字母的单循环链表LO分拆成分别只有大写字母和小写字母的两个单链表

{

    char ch;

    CirLinkList po,pc,pl;

    po=LO->next;

    LC=LO;//大写字母链表LC借用原链表的头结点

    InitList_CL(LL);//初始化小写字母链表LL

    pc=LC;

    pl=LL;

    while(po!=LO){//分拆链表LO

        ch=po->data;

        if(ch>='A'&&ch<='Z')

        {

           pc->next=po;

           pc=po;

        }

        else{

           pl->next=po;

           pl=po;

        }

        po=po->next;

    }

    pc->next=LC;pl->next=LL;//构建成循环链表

}

2.双向循环链表

在双向链表中,若令尾元结点的next指针域指向头结点,头结点的prior指针域指向尾结点,则构成双向循环链表。一个空的双向循环链表只包含一个头结点,头结点的next和prior域均指向头结点本身。从双向循环链表的任意一个结点出发,都可以向前或向后遍历整个链表。36ce471e371944ecb4864943fde52347.jpeg

双向循环链表的定义如下:

typedef DuLinkList DuCirLinkList;

双向循环链表的类型和接口定义均与双向链表相似,不同之处在于双向循环链表形成了首位相连的两个方向相反的环,因此一些接口的实现与双向链表略有不同,主要是在空表和尾结点的判断上。

2.7线性表两种存储结构的比较

在顺序表中,由于逻辑上相邻的两个元素在物理位置上也相邻,所以可由顺序表的起始位置计算第i个元素的存储位置,存取任一元素的时间复杂度都为O(1),与存取位置无关。顺序表的缺点是不适合在任意位置插入、删除元素因为需要移动元素,平均时间复杂度为O(n)。因此,在顺序表的接口中只给出了在表尾的插入操作Append_Sq和删除操作DeleteLast_Sq,时间复杂度均为O(1)。

在链表中,逻辑上相邻的元素由指针域相连,其存储地址不一定相邻,需要取第i个元素必须从头指针出发沿着指针域查找,因此平均时间复杂度为O(n)。链表的优点有:

  1. 在链表的任意位置插入或删除元素只需修改相应指针,不需要移动元素。
  2. 按需动态分配,不需要按最大需求预先分配一块连续的空间。

9dc33c2579b7485aae3a49e0b74b9762.jpeg

00e72e54a8b94450b43e2b38ad5594e0.jpeg

  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

提刀立码,调参炼丹

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

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

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

打赏作者

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

抵扣说明:

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

余额充值