王道408考研数据结构-线性表-第二章

2.1 线性表的定义

2.1.1 线性表的定义

        线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n=0 时线性表是一个空表。若用L命名线性表,则其一般表示为

L=(a1,a2,…,ai,ai+1,…,an)

式中,a1是唯一的“第一个”数据元素,又称表头元素;an是唯一的“最后一个”数据元素,又 称表尾元素。除第一个元素外,每个元素有且仅有一个直接前驱。除最后一个元素外,每个元素有且仅有一个直接后继(“直接前驱”和“前驱”、“直接后继”和“后继”通常被视为同义词)。以上就是线性表的逻辑特性,这种线性有序的逻辑结构正是线性表名字的由来。

由此,我们得出线性表的特点如下:

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

2.2 线性表的顺序表示

2.2.1 顺序表的定义

        线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元 素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。第1个元素存储在顺序表的起始位置, 第i个元素的存储位置后面紧接着存储的是第i+1个元素,称i为元素a?在顺序表中的位序。因此,顺序表的特点是表中元素的逻辑顺序与其存储的物理顺序相同。

        假设顺序表L存储的起始位置为LOC(A),sizeof(ElemType)是每个数据元素所占用存 储空间的大小,则表L所对应的顺序存储如图2.1所示。

假定线性表的元素类型为 ElemType,则静态分配的顺序表存储结构描述为:

#define MaxSize 50
typedef struct{
    ElemType data[MaxSize];
    int length
}SqList;

 动态分配的顺序表存储结构描述为:

#define InitSize 100
typedef struct{
    ElemType *data;
    int MaxSize,length;
}SeqList;

顺序表的主要优点:

  • ①可进行随机访问,即可通过首地址和元素序号可以在O(1)时间内找到 指定的元素;

  • ②存储密度高,每个结点只存储数据元素。

顺序表的缺点也很明显:

  • ①元素的插入 和删除需要移动大量的元素,插入操作平均需要移动n/2个元素,删除操作平均需要移动(n-1)/2 个元素;

  • ②顺序存储分配需要一段连续的存储空间,不够灵活。

 2.2.2 顺序表上基本操作的实现

1.顺序表的初始化

静态分配初始化:

//SqList L;
void Initlist(SqList &L){
    L.length=0; //顺序表初始长度为0
}

动态分配的初始化:

void InitList(Seqlist &L){
    L.data=(ElemType *)malloc(MaxSize*sizeof(ElemType));//分配存储空间
    L.length=0; //顺序表初始长度为0
    L.MaxSize=InitSize; //初始存储容量
}
2.插入操作

        在顺序表L的第i(1<=i<=L.length+1)个位置插入新元素e。若i的输入不合法,则 返回 false,表示插入失败;否则,将第i个元素及其后的所有元素依次往后移动一个位置,腾 出一个空位置插入新元素e,顺序表长度增加1,插入成功,返回true。

bool ListInsert(Sqlist &L,int i,ElemType e){
    if(i<1||i>L.length+1) //判断 i的范围是否有效
        return false;
    //当前存储空间已满,不能插入 
    if(L.length>=MaxSize)
        return false
    //将第1个元素及之后的元素后移 
    for(int j=L.length;j>=i;j--)
        L.data[j]=L.data[j-1];
    L,data[i-1]=e; //在位置i处放入e
    //线性表长度加1 
    L.length++;
    return true;
}

顺序表插入算法的平均时间复杂度为O(n)。

3.删除操作

        删除顺序表L中第i(1<=i<=L.length)个位置的元素,用引用变量e返回。若i的输 入不合法,则返回 false;否则,将被删元素赋给引用变量e,并将第i+1个元素及其后的所有 元素依次往前移动一个位置,返回true。

bool ListDelete(SqList &L,int i,ElemType &e){
    //判断i的范围是否有效 
    if(i<1||i>L.length)
        return false;
    //将被删除的元素赋值给e 
    e=L.data[i-1];
    for(int j=i;j<L.length;j++)
        L.data[j-1]=L.data[j];
    L.length--;
    return true;
}

顺序表删除算法的平均时间复杂度为O(n)。

4.按值查找(顺序查找)

        在顺序表L中查找第一个元素值等于e的元素,并返回其位序。

int LocateElem(SqList L,ElemType e){
    int i;
    for(i=0;i<L.length;i++)
        if(L.data[i]==e)
            return i+1; //下标为i的元素值等于e,返回其位序i+1
    return 0; //退出循环,说明查找失败
}

顺序表按值查找算法的平均时间复杂度为O(n)。

2.3 线性表的链式表示

2.3.1 单链表的定义

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

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

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

        由于单链表的元素离散地分布在存储空间中,因此是非随机存取的存储结构,即 不能直接找到表中某个特定结点。查找特定结点时,需要从表头开始遍历,依次查找。

        通常用头指针L(或 head 等)来标识一个单链表,指出链表的起始地址,头指针为 NULL 时表示一个空表。此外,为了操作上的方便,在单链表第一个数据结点之前附加一个结点,称 为头结点。

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

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

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

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

1.单链表的初始化

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

bool InitList(LinkList &L){ //带头结点的单链表的初始化
    L=(LNode*)malloc(sizeof (LNode));//创建头结点
    L->next=NULL;//头结点之后暂时还没有元素结点
    return true;
}
2.求表长操作

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

int Length(LinkList L){
    int len=0; //计数变量,初始为0
    LNode *p=L;    
    while(p->next!=NULL){
        p=p->next;
        len++; //每访问一个结点,计数加1
    }
    return len;
}

        求表长操作的时间复杂度为 O(n)。另需注意的是,因为单链表的长度是不包括头结点的,因此不带头结点和带头结点的单链表在求表长操作上会略有不同。

3.按序号查找结点

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

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

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

4.按值查找表结点

        从单链表的第一个结点开始,从前往后依次比较表中各结点的数据域,若某结点的 data 域等于给定值e,则返回该结点的指针;若整个单链表中没有这样的结点,则返回NULL。

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

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

5.插入结点操作

        插入结点操作将值为x的新结点插入到单链表的第i个位置。先检查插入位置的合法性,然后找到待插入位置的前驱,即第i-1个结点,再在其后插入。其操作过程如图2.5所示。

        首先查找第i-1个结点,假设第i-1个结点为*p,然后令新结点*s的指针域指向*p 的后继,再令结点*p 的指针域指向新插入的结点*s。

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

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

6.删除结点操作

        删除结点操作是将单链表的第i个结点删除。先检查删除位置的合法性,然后查找表中第i-1个结点,即被删结点的前驱,再删除第i个结点。其操作过程如图2.6所示。

        假设结点*p 为找到的被删结点的前驱,为实现这一操作后的逻辑关系的变化,仅需修改*p的指针域,将*p的指针域 next 指向*q的下一结点,然后释放*q的存储空间。

bool ListDelete(LinkList &L,int i,ElemType &e)(
    LNode *p=L;//指针p指向当前扫描到的结点 
    int j=0; //记录当前结点的位序,头结点是第0个结点
    while(p!=NULL&&j<i-1)(
        P=P->next;
        j++;
    }
    if(p==NULL||p->next==NULL) //i 值不合法
        return false;
    LNode *q=P->next; //令 q指向被删除结点
    e=q->data;//用e返回元素的值 
    p->next=q->next;//将*q结点从链中“断开” 
    free(q);//释放结点的存储空间 
    return true;
}

        同插入算法一样,该算法的主要时间也耗费在查找操作上,时间复杂度为O(n)。当链表不带头结点时,需要判断被删结点是否为首结点,若是,则要做特殊处理,将头指针L指向新的首结点。当链表带头结点时,删除首结点和删除其他结点的操作是相同的。

7.采用头插法建立单链表

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

LinkList List_HeadInsert(LinkList &L)(//逆向建立单链表
    //设元素类型为整型 
    LNode *s; int x;
    L=(LNode*)malloc(sizeof(LNode)); //创建头结点
    L->next=NULL; //初始为空链表
    //输入结点的值 
    scanf("%d",&x);
    while(x!=9999)( //输入9999表示结束
        s=(LNode*)malloc(sizeof(LNode));//创建新结点
        s->data=x;
        s->next=L->next;
        T->next=s //将新结点插入表中,L为头指针
        scanf("%d",&x);
    }
    return L;
}

        采用头插法建立单链表时,读入数据的顺序与生成的链表中元素的顺序是相反的,可用来实现链表的逆置。每个结点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。

8.采用尾插法建立单链表

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

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;
}

 因为附设了一个指向表尾结点的指针,所以时间复杂度和头插法的相同。

2.3.3双链表

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

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

typedef struct DNode{//定义双链表结点类型
    ElemType data;//数据域
    struct DNode *prior,*next;//前驱和后继指针
}DNode,*DLinklist;

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

1.双链表的插入操作

在双链表中 p所指的结点之后插入结点*s,其指针的变化过程如图2.10所示。

插入操作的代码片段如下:

① s->next=p->next; //将结点*s插入到结点*p之后
② p->next->prior=s;
③ s->prior=p;
④ p->next=s

        上述代码的语句顺序不是唯一的,但也不是任意的,①步必须在④步之前,否则*p 的后继结点的指针就会丢掉,导致插入失败。为了加深理解,读者可以在纸上画出示意图。若问题改成要求在结点*p 之前插入结点*s,请读者思考具体的操作步骤。

2.双链表的刷除操作

删除双链表中结点*p的后继结点*q,其指针的变化过程如图2.11 所示。

删除操作的代码片段如下:

p->next=q->next;  //图2.11中步骤① 
q->next->prior=p; //图2.11 中步骤②
free(q); //释放结点空间

2.3.4 循环链表

1.循环单链表

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

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

2.循环双链表

        由循环单链表的定义不难推出循环双链表。不同的是,在循环双链表中,头结点的 prior指针还要指向表尾结点,如图2.13 所示。当某结点*p为尾结点时,p->next==L;当循环双链表为空表时,其头结点的 prior域和 next 域都等于L。

2.3.5 静态链表

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

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

#define MaxSize 50 //静态链表的最大长度
typedef struct{ //静态链表结构类型的定义
    ElemType data; //存储数据元素 
    int next; //下一个元素的数组下标
}SLinkList[MaxSize];

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值