【大话数据结构】第三章总结——线性表

1、线性表

零个或多个数据元素的有限序列。

若线性表中存在多个元素,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。

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

2、线性表的抽象数据类型

对于一个线性表来说,插入数据和删除数据都是必须的操作。

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

ADT 线性表(List)

Data

线性表的数据对象集合为{a1, a2, ..., an},每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。

Operation

InitList(*L): 初始化操作,建立一个空的线性表L。

扫描二维码关注公众号,回复: 4752611 查看本文章

ListEmpty(L): 若线性表为空,返回true,否则返回false。

ClearList(*L): 将线性表清空。

GetElem(L,i,*e): 将线性表L中的第i个位置元素值返回给e。

LocateElem(L,e): 在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。

ListInsert(*L,i,e): 在线性表L中的第i个位置插入新元素e。

ListDelete(*L,i,*e): 删除线性表L中的第i个位置元素,并用e返回其值。

ListLength(L): 返回线性表L的元素个数。

endADT

3、线性表的顺序存储结构

    1.顺序存储定义

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素

    2.顺序存储方式

    在C语言中,可以使用一维数组来实现顺序存储结构,即把第一个数据元素存到数组下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。

    线性表的顺序存储的结构代码:

#define MAXSIZE 20             /*存储空间初始分配量*/
typedef int ElemType;          /*ElemType类型根据实际情况而定,这里假设为int*/
typedef struct{
    ElemType data[MAXSIZE];    /*数组存储数据元素,最大值为MAXSIZE*/
    int length;                /*线性表当前长度*/
}SqList;

  

    3.数组长度与线性表长度区别

    在任意时刻,线性表的长度应该小于等于数组的长度。

    4.地址计算方法

    假设占用的是c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LOC表示获得存储位置的函数)

LOC(ai + 1) = LOC(ai) + c

    所以对于第i个数据元素ai的存储位置可以由a1推算得出:

LOC(ai) = LOC(ai) + (i-1) * c

    对于计算机来说,线性表位置的存入或者取出数据,时间都是相等的,存取时间性能为O(1)。

我们通常把具有这一特点的存储结构称为随机存储结构

4、顺序存储结构的插入与删除

    1.获得元素操作

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/*Status是函数的类型,其值是函数结果状态代码,如OK等*/
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结束:用e返回L中第i个数据元素的值*/
Status GetElem(SqList L, int i, ElemType *e){
    if(L.length == 0 || i < 1 || i > L.length){
        return ERROR;
    }
    *e = L.data[i-1];
    return OK;
}

   

    2.插入操作

    插入算法的思路:

  • 如果插入位置不合理,抛出异常;
  • 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
  • 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
  • 将要插入元素填入位置i处;
  • 表长加1;
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(SqList *L, int i, ElemType e){
    int k;
    if(L->length == MAXSIZE){/*顺序线性表已经满*/
        return ERROR;
    }
    if(i < 1 || i > L->length + 1){/*当i不在范围内时*/
        return ERROR;
    }
    if(i <= L->length){/*若插入数据位置不在表尾*/
        for(k = L->length - 1; k >= i-1; k--){/*将要插入位置后数据元素向后移动一位*/
            L->data[k+1] = L->data[k];
        }
    }
    L->data[i-1] = e;/*将新元素插入*/
    L->length++;
    return OK;
}

    3.删除操作

    删除算法的思路:

  • 如果删除位置不合理,则抛出异常;
  • 取出删除元素;
  • 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
  • 表长减1;
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(SqList *L, int i, ElemType *e){
    int k;
    if(L->length == 0){/*线性表为空*/
        return ERROR;
    }
    if(i < 1 || i > L->length){/*删除位置不正确*/
        return ERROR;
    }
    *e = L->data[i-1];
    if(i < L->length){/*如果删除不是最后位置*/
        for(k = i; k < L->length; k++){/*将删除位置后继元素前移*/
            L->data[k-1] = L->data[k];
        }
        L->length--;
        return OK;
    }
}

    线性表的顺序存储结构,

    在存、读数据时,时间复杂度都是O(1),

    在插入、删除时,时间复杂度都是O(n)。

    4.线性表顺序存储结构的优缺点

    优点:

  • 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
  • 可以快速地存取表中任一位置地元素

    缺点:

  • 插入和删除操作需要移动大量元素
  • 当线性表长度变化较大时,难以确定存储空间的容量
  • 造成存储空间的“碎片”

5、线性表的链式存储结构

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

    1.数据域

存储数据元素信息的域

    2.指针域

存储直接后继位置的域,指针域中存储的信息称做指针或链

    3.结点(Node)

由数据域指针域组成数据元素ai的存储映像

    4.链式存储结构

n个结点(ai的存储映像)链结成一个链表,即为线性表(a1, a2, ..., an)的链式存储结构

    5.单链表

因为此链表的每个结点中只包含一个指针域,所以叫做单链表

    6.头指针

链表中第一个结点的存储位置

    7.头结点

为了更加方便地对链表进行操作,会在单链表地第一个结点前附设一个结点,称为头结点

头结点地数据域可以不存储任何信息,也可以存储线性表的长度等附加信息

头结点的指针域存储指向第一个结点的指针,如下图所示:

    8.头指针与头结点的异同

    头指针

  • 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
  • 头指针具有标识作用,所以常用头指针冠以链表的名字
  • 无论链表是否为空,头指针均不为空。头指针是链表的必要元素

    头结点

  • 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可以存放链表的长度)
  • 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
  • 头结点不一定是链表必须要素

    9.线性表链式存储结构代码描述

    若线性表为空表,则头结点的指针域为“空”,如下图所示

    若带有头结点的单链表,则如下图所示

    代码描述

/*线性表的单链表存储结构*/
typedef struct Node{
    ElemType data;
    struct Node *next;
}Node;
typedef struct Node *LinkList;    /*定义LinkList*/

 

6、单链表的读取

获取链表第i个数据的算法思路:

  1. 声明一个指针p指向链表第一个结点,初始化j从1开始
  2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
  3. 若到链表末尾p为空,则说明第i个结点不存在
  4. 否则查找成功,返回结点p的数据

实现代码算法如下:

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L, int i, ElemType *e){
    int j;
    LinkList p;    /*声明一指针p*/
    p = L->next;   /*让p指向链表L的第一个结点*/
    j = 1;         /*j为计数器*/
    while(p && j < i){/*p不为空且计数器j还没有等于i时,循环继续*/
        p = p->next;/*让p指向下一个结点*/
        ++j;
    }
    if(!p || j > i){
        return ERROR;/*第i个结点不存在*
    }
    *e = p->data;    /*取第i个结点的数据*/
    return OK;
}

最坏情况的时间复杂度为O(n),其主要核心思想是"工作指针后移"

7、单链表的插入

单链表第i个数据插入结点的算法思路:

  1. 声明一指针p指向链表头结点,初始化j从1开始;
  2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
  3. 若到链表末尾p为空,则说明第i个结点不存在;
  4. 否则查找成功,在系统中生成一个空结点s;
  5. 将数据元素e赋值给s->data;
  6. 单链表的插入标准语句s->next = p->next; p->next = s;
  7. 返回成功

实现代码算法如下:

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:在L中第i个结点位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(LinkList *L, int i, ElemType e){
    int j;
    LinkList p,s;
    p = *L;
    j = 1;
    while(p && j < i){/*寻找第i-1个结点*/
        p = p->next;
        ++j;
    }
    if(!p || j > i){
        return ERROR;/*第i个结点不存在*/
    }
    s = (LinkList)malloc(sizeof(Node));/*生成新结点(C标准函数)*/
    s->data = e;
    s->next = p->next;/*将p的后继结点赋值给s的后继*/
    p->next = s;/*将s赋值给p的后继*/
    return OK;
}

8、单链表的删除

单链表第i个数据删除结点的算法思路:

  1. 声明一指针p指向链表头指针,初始化j从1开始;
  2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
  3. 若到链表末尾p为空,则说明第i个结点不存在;
  4. 否则查找成功,将欲删除的结点p->next赋值给q(存储ai元素的结点);
  5. 单链表的删除标准语句p->next = q->next;
  6. 将q结点中的数据赋值给e,作为返回;
  7. 释放q结点;
  8. 返回成功

实现代码算法如下:

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:删除L的第i个结点,并用e返回其值,L的长度减1*/
Status ListDelete(LinkList *L, int i, ElemType *e){
    int j;
    LinkList p,q;
    p = *L;
    j = 1;
    while(p && j < i){/*遍历寻找第i-1个结点*/
        p = p->next;
        ++j;
    }
    if(!(p->next) || j > i){
        return ERROR;/*第i个结点不存在*/
    }
    q = p->next;
    p->next = q->next;/*将q的后继赋值给p的后继*/
    *e = q->data;/*将q结点中的数据给e*/
    free(q);/*让系统回收此结点,释放内存*/
    return OK;
}

显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。

9、单链表的整表创建

单链表整表创建的算法思路:

  1. 声明一指针p和计数器变量i;
  2. 初始化一空链表L;
  3. 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
  4. 循环:
  •        生成一个新结点赋值给p;
  •        随机生成一数字赋值给p的数据域p->data;
  •        将p插入到头结点与前一新结点之间

头插法实现代码算法如下:

/*随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList *L, int n){
    LinkList p;
    int i;
    srand(time(0));/*初始化随机数种子*/
    *L = (LinkList)malloc(sizeof(Node));
    (*L)->next = NULL;/*先建立一个带头结点的单链表*/
    for(i = 0; i < n; i++){
         p = (LinkList)malloc(sizeof(Node));/*生成新结点*/
        p->data = rand() % 100 + 1;/*随机生成100以内的数字*/
        p->next = (*L)->next;
        (*L)->next = p;/*插入到表头*/
    }
}

尾插法实现代码算法如下:

/*随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void CreateListHead(LinkList *L, int n){
    LinkList p,r;
    int i;
    srand(time(0));/*初始化随机数种子*/
    *L = (LinkList)malloc(sizeof(Node));
    r = *L;/*r为指向尾部的结点*/
    for(i = 0; i < n; i++){
         p = (Node *)malloc(sizeof(Node));/*生成新结点*/
        p->data = rand() % 100 + 1;/*随机生成100以内的数字*/
        r->next = p;/*将表尾终端结点的指针指向新结点*/
        r = p;/*将当前的新结点定义为表尾终端结点*/
    }
    r->next = NULL;/*表示当前链表结束*/
}

10、单链表的整表删除

单链表整表删除的算法思路:

  1. 声明一结点p和q;
  2. 将第一个结点赋值给p;
  3. 循环:
  •        将下一结点赋值给q;
  •        释放p;
  •        将q赋值给p;

实现代码算法如下:

/*初始条件:顺序线性表L已存在,操作结果:将L重置为空表*/
Status ClearList(LinkList *L){
    LinkList p,q;
    p = (*L)->next;/*p指向第一个结点*/
    while(p){
        q = p->next;
        free(p);
        p = q;
    }
    (*L)->next = NULL;/*头结点指针域为空*/
    return OK;
}

11、单链表结构与顺序存储结构优缺点

存储分配方式:

  • 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
  • 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素

时间性能:

  • 查找
  • 顺序存储结构O(1)
  • 单链表O(n)
  • 插入和删除
  • 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
  • 单链表在找出某位置的指针后,插入和删除时间仅为O(1)

空间性能:

  • 顺序存储结构需要预分配存储空间,分大了,浪费,分小了,易发生上溢
  • 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制

结论:

  • 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
  • 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。

12、静态链表

用数组描述的链表叫做静态链表

首先让数组的元素由data和cur(游标)两个数据域组成,其中data数据域用来存放数据元素,cur数据域相当于单链表中的next指针,存放该元素的后继在数组中的下标。

为了方便插入数据,通常把数组建立得更大一些,以便有一些空闲空间可以便于插入时不至于溢出。

/*线性表的静态链表存储结构*/
#define MAX_SIZE 1000    /*假设链表的最大长度是1000*/
typedef struct{
    ElemType data;
    int cur;    /*游标(Cursor),为0时表示无指向*/
}Component,StaticLinkList[MAXSIZE];

另外会对数组第一个和最后一个元素作为特殊元素处理,不存数据。

通常把未被使用的数组元素称为备用链表

数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;

数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为0^2

初始化静态链表实现代码算法如下:

/*将一维数组space中各分量链成一备用链表*/
/*space[0].cur为头指针,"0"表示空指针*/
Status InitList(StaticLinkList space){
    int i;
    for(i = 0; i < MAX_SIZE - 1; i++){
        space[i].cur = i + 1;
    }
    space[MAX_SIZE - 1].cur = 0;/*目前静态链表为空,最后一个元素的cur为0*/
    return OK;
}

在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放问题,所以我们需要自己实现malloc()和free()函数,才可以做插入和删除的操作。

为了辨明数组中哪些分量未被使用,解决的方法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。

类似malloc函数的代码实现如下:

/*若备用空间链表非空,则返回分配的结点下标,否则返回0*/
int Malloc_SLL(StaticLinkList space){
    int i = space[0].cur;/*当前数组第一个元素的cur存的值*/
                         /*就是要返回的第一个备用空闲的下标*/
    if(space[0].cur){
        space[0].cur = space[i].cur;/*由于要拿出一个分量来使用*/
                                    /*所以就得把它的下一个分量用来做备用*/
    }
    return i;
}

静态链表插入实现代码算法如下:

/*在L中第i个元素之前插入新的数据元素e*/
Status InitInsert(StaticLinkList L, int i, ElemType e){
    int j, k, l;
    k = MAX_SIZE - 1;/*注意k首先是最后一个元素的下标*/
    if(i < 1 || i > ListLength(L) + 1){
        return ERROR;
    }
    j = Malloc_SLL(L);
    if(j){
        L[j].data = e;/*将数据赋值此分量的data*/
        for(l = 1; l <= i - 1; l++){/*找到第i个元素之前的位置*/
            k = L[k].cur;
        }
        L[j].cur = L[k].cur;/*把第i个元素之前的cur赋值给新元素的cur*/
        L[k].cur = j;/*把新元素的下标赋值给第i个元素之前元素的cur*/
        return OK;
    }
    return ERROR;
}

和前面一样,删除元素时,原来是需要释放结点的函数free(),现在需要自己实现它

类似free函数的代码实现如下:

/*将下标为k的空闲结点回收到备用链表*/
void Free_SLL(StaticLinkList space, int k){
    space[k].cur = space[0].cur;/*把第一个元素cur值赋给要删除的分量cur*/
    space[0].cur = k;/*把要删除的分量下标赋值给第一个元素的cur*/
}

静态链表删除实现代码算法如下:

/*删除在L中第i个数据元素e*/
Status ListDelete(StaticLinkList L, int i){
    int j, k;
    if(i < 1 || i > ListLength(L)){
        return ERROR;
    }
    k = MAX_SIZE - 1;
    for(j = 1; j <= i - 1; j++){
        k = L[k].cur;
    }
    j = L[k].cur;
    L[k].cur = L[j].cur;
    Free_SLL(L, j);
    return OK;
}

静态链表长度获取实现代码算法如下:

/*初始条件:静态链表L已存在。*/
/*操作结果:返回L中数据元素个数*/
int ListLength(StaticLinkList L){
    int j = 0;
    int i = L[MAX_SIZE - 1].cur;
    while(i){
        i = L[i].cur;
        j++;
    }
    return j;
}

静态链表优缺点

优点:

  • 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点

缺点:

  • 没有解决连续存储分配带来的表长难以确定的问题
  • 失去了顺序存储结构随机存取的特性

13、循环链表

将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表

循环链表带有头结点的空链表如下图所示:

非空循环链表如下图所示:

若想用O(1)的时间由链表指针访问到最后一个结点,

我们需要改造一下循环链表,不用头指针,而是用指向终端结点的尾指针来表示链表

若需要将两个循环链表合并成一个表

只需要如下的操作即可

p = rearA->next;/*保存A表的头结点,即①*/
rearA->next = rearB->next->next;/*将本是指向B表的第一个结点(不是头结点)*/
                                /*赋值给rearA->next,即②*/
q = rearB->next;
rearB->next = p;/*将原A表的头结点赋值给rearB->next,即③*/   
free(q);/*释放q*/

14、双向链表

在单链表的每个结点中,再设置一个指向其前驱结点的指针域

(所以双向链表中的结点都有两个指针域,一个指向直接后继,一个指向直接前驱

/*线性表的双向链表存储结构*/
typedef struct DulNode{
    ElemType data;
    struct DulNode *prior;/*直接前驱指针*/
    struct DulNode *next;/*直接后继指针*/
}DulNode, *DuLinkList;

双向链表的循环带头结点的空链表如图所示

非空的循环的带头结点的双向链表如图所示

插入操作

假设存储元素e的结点为s,要实现将结点s插入到结点p和p->next之间

其步骤图如下所示:

代码如下

s->prior = p;/*把p赋值给s的前驱,如图中①*/
s->next = p->next;/*把p->next赋值给s的后继,如图中②*/
p->next->prior = s;/*把s赋值给p->next->prior的前驱,如图中③*/
p->next = s;/*把s赋值给p的后继,如图中④*/

删除操作

其步骤图如下所示:

代码如下

p->prior->next = p->next;/*把p->next赋值给p->prior的后继,如图中①*/
p->next->prior = p->prior;/*把p->prior赋值给p->next的前驱,如图中②*/
free(p);/*释放结点*/

 

总结

不怕苦,吃苦半辈子;怕吃苦,吃苦一辈子。

舒适坏境下很难培养出坚强品格,被安排好的人生,也很难做出伟大事业。

猜你喜欢

转载自blog.csdn.net/weixin_41463193/article/details/85612534