数据结构——线性表(三)

作者:黑衣侠客


一、线性表的定义

线性表,从名字来看,可以发现,是具有像线一样性质的表

线性表:零个或多个数据元素的有限序列。

首先,它是一个序列,也就是说,元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。然而,线性表强调是有限的,指元素个数有限。事实上,在计算机中处理的对象都是有限的,那种无限的数列,只存在于数学的概念中。

用数学语言,我们可以将线性表定义成:

若将线性表记为(a1,…,ai-1,ai,ai+1,ai+2,…,an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,…,n-1时,ai有且仅有一个直接后继,当i=2,3,…,n时,ai有且仅有一个直接前驱。

所以,线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。在非空表中的每个数据元素都有一个确定的位置,如a1是第一个数据元素,an是最后一个数据元素,ai是第i个数据元素,称i为数据元素ai在线性表中的位序。

那么,班级同学的点名册,是不是线性表呢?

是,这和刚才的友谊关系是不同的,因为它是有限序列,也满足类型相同的特点。这个点名册,每个元素除了学生的学号外,还可以有同学的姓名、性别、出生年月日等,这其实就是我们说的数据项。在较为复杂的线性表中,一个数据元素可以由若干个数据项组成。

ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,......,an},每个元素的类型均为DataType。
其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,
除了最后一个元素an外,每一个元素有且只有一个直接后继元素。
数据元素之间的关系是一对一的关系。
OperationInitList(*L);  初始化操作,建立一个空的线性表L0ListEmpty(L);   若线性表为空,返回true,否则返回falseClearList(*L); 将线性表清空GetElem(L,i,*e);将线性表L中的第i个位置元素值返回给eLocateElem(L,e);在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败ListInsert(*L,i,e); 在线性表L中的第i个位置插入新元素eListDelete(*L,i,*e); 删除线性表L中第i个位置元素,并用e返回其值ListLength(L);      返回线性表L的元素个数
endADT

现在,我们来举一个例子:
假设有集合A,和集合B两个集合
我们想要集合A取A和B的并集(意思是,将集合B中,不属于集合A的元素,插入到集合A当中),那么,我们该如何实现?

/*将所有的在线性表Lb中但不在La中的数据元素插入到La中*/
void union(List *La,List Lb)
{int La_len,Lb_len,i;ElemType e;        //声明与La和Lb相同的数据元素eLa_len = ListLength(La); //求线性表的长度Lb_len = ListLength(Lb);for(i=1;i<=Lb_len;i++){GetElem(Lb,i,e);    //取Lb中第i个数据元素赋给eif(!LocateElem(La,e,equal)) //La中不存在和e相同的数据元素ListInsert(La,++La_len,e);   //插入}
}

可见,这些个复杂的操作,其实就是将基本操作组合起来,进而实现。

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

1. 顺序存储

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

如该图所示:

线性表的顺序存储结构,就是在内存中找一个位置,通过占位的形式,把一定内存空间给占了,然后把相同数据类型的数据元素依次存放在这块空地上。==既然,线性表的每个数据元素的类型都相同,所以,可以用C语言(其他语言也可以)的一维数组来实现顺序存储结构,即把第一个数据元素存储到数组下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。

接下来,我们看看线性表的顺序存储结构的代码:

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

我们发现,描述顺序存储结构需要三个属性:

  • 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
  • 线性表的最大存储容量:数组长度MaxSize
  • 线性表的当前长度:length

2. 数据长度与线性表长度区别

这里需要注意的是:数组长度和线性表长度是不一样的。

数组长度:

  • 数组长度是存放线性表的存储空间的长度,存储分配后,这个量一般是不变的。

线性表长度

  • 线性表长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是在变化的。

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

3. 地址计算方法

在线性表中,线性表是从1开始的,但是存入的数组元素是从0开始的,于是,线性表的第i个元素是要存储在数组下标为i-1的位置,即数据元素和存放它的数组下标之间存在对应的关系。

用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此,分配的数组空间要大于等于当前线性表的长度。

其实,内存中的地址,就像图书馆或电影院里的座位一样,都是有编号的。存储器中的每一个存储单元都有自己的编号,这个编号称为地址。

假设,每个数据元素占用c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足如下关系:

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

通过这个公式,我们可以随时算出线性表中任意位置的地址,不管它是第一个还是最后一个,都是相同的时间。那么,我们对每一个线性表位置的存入或者取出数据,对于计算机而言,都是相等的时间,都是一个常数,因此,用我们算法中学到的时间复杂度的概念来说,它的存取时间性能是O(1)。我们通常把具有这一特性的存储结构称为随机存储结构。

4. 顺序存储结构的插入和删除

4.1 获得元素操作

对于线性表的顺序存储结构来说,如果我们要实现GetElem操作,即将线性表L中的第i个位置元素值返回,其实很简单,只要i的数值在数组下标范围内,就是把数组第i-1下标的值返回即可。

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
//Status是函数的类型,其值是函数结果状态代码,如OK等
//初始条件:顺序线性表L已存在,ListLength(L)≥i≥1
//操作结果:用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;
}

4.2 插入操作

算法思路:

  1. 如果插入位置不合理,抛出异常
  2. 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量
  3. 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置
  4. 将要插入元素填入位置i处
  5. 表长加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;}
}

4.3 删除操作

删除算法的思路:

  • 如果删除位置不合理,抛出异常
  • 取出删除元素
  • 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置
  • 表长减一

实现代码:

//初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
//操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减一
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)。

平均情况:

  • 由于元素插入到第i个位置,或删除第i个元素,需要移动n-i个元素。根据概率原理,每个位置插入或删除元素的可能性是相同的,也就是说位置靠前,移动元素多,位置靠后,移动元素少。最终平均移动次数和最中间的那个元素的移动次数相等,为(n-1)/2。

总结:

线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1),而插入或删除时,时间复杂度都是O(n),这说明,它比较适合元素个数不太变化,而更多是存取数据的应用。

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

优点 缺点
无须为表示表中元素之间的逻辑关系而增加额外的存储空间 插入和删除操作需要移动大量元素
可以快速的存取表中任意位置的元素 当线性表长度变化较大时,难以确定存储空间的容量
造成存储空间的“碎片”

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

1. 线性表链式存储结构定义

在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。

为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素ai的存储映像,称为结点。

n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,…,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。

单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起

我们把链表中第一个结点的存储位置叫做头指针,之后的每一个结点,其实就是上一个的后继指针指向的位置。

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

头指针 头结点
头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可以存放链表的长度)
头指针具有表示作用 ,所以常用头指针冠以链表的名字 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了
无论链表是否为空,头指针均不为空。头指针是链表的必要元素 头结点不一定是链表必须要素

3. 线性表链式存储结构的描述

  • 若线性表为空表,则头结点的指针域为“空”
    在单链表中,我们可以用结构指针来描述:
//线性表的单链表存储结构
typedef struct Node
{ElemTYpe data;struct Node *next;
}Node;
typedef struct Node *LinkList;//定义LinkList

从上述代码,我们知道,结点是由存放数据元素的数据域和存放后继结点地址的指针域组成的。

四、单链表的读取

在线性表的顺序存储结构中,我们应该如何计算元素的存储位置?

  • 获得链表第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;  //声明一结点pp=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;
}

从头开始找,直到第i个元素为止。由于这个算法的时间复杂度取决于i的位置,当i=1时,则不需要遍历,第一个就取出数据了,而当i=n时则遍历n-1次才可以。因此,最坏情况的时间复杂度为O(n)。

五、单链表的插入与删除

1. 单链表的插入

假设存储元素e的结点s,要实现结点p、p->next和s之间逻辑关系的变化,只需要将结点s插入到结点p和p->next之间即可。那么,该如何操作呢?

我们需要利用这句代码:

s->next=p->next;  //将p的后继结点改成s的后继结点
p->next=s;          //将结点s变成p的后继结点

注意:这两句代码是不可以颠倒顺序的

如果颠倒顺序,先将p->next=s;再s->next=p->next;此时,会使得将p->next给覆盖成s的地址。那么s->next=p->next,其实就是s->next=s,这样真正的拥有ai+1数据元素的结点就没有了上级。此时插入失败了,所以无论如何顺序都不能变。

先连后面,再连前面
对于单链表的表头和表尾的特殊情况,操作是相同的:

单链表第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个结点
}
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;

这段代码中,我们用到了C语言的malloc标准函数,它的作用是生成一个新的结点,其类型与Node是一样的,其实质就是在内存中找了一小块空地,准备存放e数据s结点。

2. 单链表的删除

设存储元素ai的结点为q,要实现将结点q删除单链表的操作,其实就是将它的前继结点的指针绕过,指向它的后继结点即可。我们要做的就是p->next=p->next->next,用q来取代p->next

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

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

  1. 声明一结点p指向链表第一个结点,初始化j从1开始;
  2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
  3. 若到链表末尾p为空,则说明第i个元素不存在;
  4. 否则查找成功,将欲删除的结点p->next赋值给q;
  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->next&&j<i)    //遍历寻找第i个元素{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结点中的数据给efree(q);            //让系统回收此结点,释放内存return OK;
}

这段算法中,我们用到了另一个C语言的标准函数free,它的作用就是让系统回收一个Node结点,释放内存。

我们发现,单链表的插入和删除算法,都是有两部分组成:

  1. 第一部分是遍历查找第i个元素
  2. 第二部分是插入和删除元素

总结:

纵观整个算法:他们的时间复杂度都是O(n)。如果在我们不知道第i个元素的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。但如果,我们希望从第i个位置,插入10个元素,对于顺序存储结构意味着,每一次插入都需要移动n-i个元素,每次都是O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单的通过赋值移动指针而已,时间复杂度都是O(1)。显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。

六、单链表的整表创建

顺序存储结构,其实就是一个数组的初始化,即声明一个类型和大小的数组并赋值的过程。而单链表和顺序存储结构就不一样,它不像顺序存储结构这么集中,它可以很分散,是一种动态结构。对于每个链表来说,它所占用的空间大小和位置是不需要预先分配划分的,可以根据系统的情况和实际的需求即时生成。

实际上,创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始状态起,依次建立个元素结点,并逐个插入链表。

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

  1. 声明一结点p和计数器变量1
  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 CreateListTail(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;                          //表示当前链表结束}

注意L与r的关系,L是指整个单链表,而r是指向尾结点的变量,r会随着循环不断地变化结点,而L则是随着循环增长为一个多结点的链表。

从代码来看,我们不难理解尾插法的含义。

七、单链表的整表删除

当我们不想使用这个单链表时,我们需要把它销毁,其实就是在内存总将它释放掉,以便于留出空间给其他程序或软件使用。

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

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

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

简单地对单链表结构和顺序存储结构做对比:

存储分配方式 时间性能 空间性能
顺序存储结构用一段连续的存储单元依次存储线性表的数据元素 查找:顺序存储结构为O(1),单链表为O(n) 顺序存储结构需要预分配存储空间,分大了,浪费,分小了易发生上溢
单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素 插入和删除:顺序存储结构需要平均移动表长一半的元素,时间为O(n) 单链表在线性表的某个指针后,插入和删除的时间仅为O(1)
总结

若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。比如说,游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以,应该考虑用顺序存储结构。而游戏中的玩家的武器或者装备列表随着,玩家在游戏的过程中,可能会随时增加或删除,此时,顺序存储结构显然不适合,这时,单链表就表现出它独有的优点了。

九、静态链表

C语言具有指针能力,使得它可以非常容易的操作内存中的地址和数据,这相比于其他高级语言更加的灵便,后来的面向对象语言,如Java、C#等,虽然不使用指针,但是因为启用了对象引用机制,从某种角度也就间接实现了指针的某些作用。但是对于Basic、Fortran等早期的编程高级语言,由于没有指针,链表结构按照我们之前说的方法,就没有办法实现了。
因此,有人想到用数组来代替指针,描述单链表

首先,我们让数组的元素都是由两个数据域组成data和cur。也就是说,数组的每一个下标都对应一个data和一个cur。数据域data,用来存放数据元素,而指针域cur,用来存放单链表的next指针,存放该元素的后继在数组中的下标。我们将用数组描述的链表叫做静态链表,也叫做游标实现法。

一般来说,为了我们方便插入数据,我们通常会把数组建立的大一些,以便于有一些空闲的空间可以便于插入时,不至于溢出。

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

另外,我们对数组第一个和最后一个元素作为特殊元素处理,不存数据。我们通常把未被使用的数组元素称为备用链表。而数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标,而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为0²。


此时的图片相当于初始化的数组状态,见下面代码:

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

1. 静态链表的插入操作

静态链表中我们要解决的是:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。

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

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

//若备用空间链表非空,则返回分配的结点下标,否则返回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 ListInsert(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_SSL(L);    //获得空闲分量的下标if(j){L[j].data = e;        //将数据赋值给此分量的datafor(l=1;l<=i-1;l++)  //找到第i个元素之前的位置k=L[k].cur;L[j].cur = L[k].cur;     //把第i个元素之前的cur赋值给新元素的curL[k].cur = j;              //把新元素的下标赋值给第i个元素之前元素的curreturn OK;}return ERROR;
}

2. 静态链表的删除操作

如果我们想要删除第一个结点的元素,那么就需要用到释放结点函数free()

//删除在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_SSL(L,j);return OK;
}
//将下标为k的空闲结点回收到备用链表
void Free_SSL(StaticLinkList space, int k)
{space[k].cur = space[0].cur;  //把第一个元素cur值赋值给要删除的分量curspace[0].cur = k;              //把要删除的分量下标赋值给第一个元素的cur
}

当然,静态链表也有其他相应的操作实现:

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

3. 静态链表的优缺点

优点 缺点
在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点 没有解决连续存储分配带来的表长难以确定的问题
失去了顺序存储结构随机存取的特性
总结:

静态链表其实是为了给没有指针的高级语言设计的一种实现单链表能力的方法。

十、循环链表

对于单链表,由于每个结点只存储了向后的指针,到了尾标志就停止了向后链的操作,这样,当中某一结点就无法找到它的前驱结点了,就像我们刚才说的,不能会到从前。

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

循环链表解决了一个很麻烦的问题,如何从当中一个结点出发,访问到链表的全部结点

为了使空链表与费空链表处理一致,我们通常设了一个头结点,而并不是说:循环链表一定要头结点

其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->next不等于头结点,则循环未结束。

在单链表中,我们有了头结点时,我们可以用O(1)的时间来访问第一个结点,但对于要访问到最后一个结点,却需要O(n)时间,因为,我们需要将单链表全部扫描一遍。但是,有没有可能用O(1)的时间由链表指针访问到最后一个结点呢?结果时一定的,不过,我们需要改造一下循环链表,不用头指针,而是用指向终端结点的尾指针来表示循环链表,此时,查找开始结点和终端结点都很方便了。

从上图,我们可以看出:终端结点用尾指针rear指示,则查找终端结点是O(1),而开始结点,其实就是rear->next->next,其时间复杂度同样为O(1)。

如果我们想要将两个链表合并成一个表时,有了尾指针就非常简单了。例如:下面的两个循环链表,它们的尾指针分别是rearA和rearB。

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

十一、 双向链表

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

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

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

既然,单链表可以有循环链表,那么,双向链表当然也可以是循环链表。

双向链表既然是比单链表多了可以反向遍历查找等数据结构,那么也需要付出一些代价:在插入和删除时,需要更改两个指针变量。

插入操作其实并不复杂,但是一定注意顺序,千万不能写反了


代码部分:

s->prior = p;    // 把p赋值给s的前驱
s->next = p->next;   //把p->next赋值给s的后继
p->next->prior = s;  //把s赋值给p->next的前驱
p->next = s;        //把s赋值给p的后继
删除某个结点的操作:


删除p结点:

p->prior->next = p->next;      //把p->next赋值给p->prior的后继
p->next->prior = p->prior;        //把p->prior赋值给p->next的前驱
free(p);                //释放结点p

数据结构——线性表(三)相关推荐

  1. 数据结构-线性表之用队列实现栈用栈实现队列

    文章目录 **********用队列实现栈 一:思路 二:实现 (1)结构体定义 (2)初始化和销毁 (3)进"栈" (4)出"栈" 三:代码 ******** ...

  2. 数据结构线性表基本操作

    数据结构线性表基本操作 基本内容 线性表的顺序表示和实现 线性表的顺序存储表示 顺序表中基本操作的实现 1.顺序表的初始化 2.取值 3.查找 4.插入 5.删除 线性表的链式表示和实现 单链表的定义 ...

  3. 数据结构-线性表之带头结点的双向循环链表

    文章目录 前言 实现 (1)结构定义 (2)基本函数 (3)操作实现 测试 代码 前言 链表的类型有很多种(根据带头或不带头,循环或非循环等等),但是我们重点研究的只有两种,一种结构非常简单是无头单向 ...

  4. 数据结构摧毁线性表用c语言,[简述]数据结构-线性表(c语言实现)

    [简述]数据结构-线性表(c语言实现)second60 20180422 1. 线性表的定义 线性表是具有相同特性的数据元素的一个有限序列. 2. 线性表抽象数据类型描述 ADT  List { 数据 ...

  5. 王道数据结构线性表:有读者认为直接去掉p结点会造成断链?

    王道数据结构线性表:有读者认为直接去掉p结点会造成断链? 我用图解的方式来说明一下,文字有点多,看起来比较眼疼,但是内容不多,希望能对你有帮助. 书上的代码 解释 (ps:对上面解释的一点补充↓)

  6. 数据结构-线性表(严书代码实现)

    数据结构-线性表的顺序表示代码 //头文件内容实现 #ifndef SEQLIST_H_INCLUDED #define SEQLIST_H_INCLUDED #include<string.h ...

  7. 数据结构-线性表-思维导图+小结

    数据结构-线性表思维导图+小结 1 数据结构-第二章-线性表-思维导图 2 数据结构-第二章-线性表-习题小结 2.1 概念性习题小结 2.2 操作性习题小结 1 数据结构-第二章-线性表-思维导图 ...

  8. c语言构造一个空线性表l,数据结构线性表顺序结构的定义与实现C语言-Go语言中文社区...

    大家好,今天给大家总结了一下数据结构里面的线性表的顺序结构,顺序表表示的是用一组地址连续的存储单元依次存储线性表的数据元素,所以顺序结构的实现一般采用数组的方式来实现,存储空间也采用动态分配的方式.在 ...

  9. 数据结构——线性表(2)

    上接 数据结构--线性表(1) 上文中介绍了线性表的顺序存储结构和单链表的介绍与代码实现,接下来,继续介绍线性表的链式存储 循环链表   在座的各位都很年轻,不会觉得日月如梭.可上了点年纪的人,比如我 ...

最新文章

  1. java自定义配置文件_自定义配置文件如何配置
  2. Golang的指针类型
  3. 汽车之家基于 Flink + Iceberg 的湖仓一体架构实践
  4. 高并发系统数据库设计
  5. java 默认参数_Java 方法的参数可以有默认值吗?
  6. 【Windows】Windows10-Telnet的使用
  7. SqlServer动态表查询
  8. jQuery-Selectors(选择器)的使用(一、基本篇)
  9. synchronized 异常_Java:synchronized的深度解析
  10. 免费pdf转换成txt转换器
  11. TMDB 5000电影数据集
  12. Troubleshooting: WAITED TOO LONG FOR A ROW CACHE ENQUEUE LOCK!
  13. datanucleus+spring 的JDO操作 select save update delete
  14. 使用win7自带的备份还原、创建系统镜像
  15. 计算机专业答辩评分表,计算机本科专业毕业论文评分标准.doc
  16. #智能制造#第一章 智能制造,缘何而起?
  17. linux ps2键盘不能用,解决usb鼠标与ps2键盘合用时开机键盘失效
  18. DIV+CSS布局心得
  19. JVM调优总结--压力测试
  20. 2021星巴克月饼全新上市;万豪旗下源宿品牌入驻中国东南区;现代汽车承诺到2045年实现碳中和 | 美通社头条...

热门文章

  1. nextdate函数白盒测试问题 软件测试_nextdate白盒测试用例
  2. 【CS224n】(lecture9)Transformer的变体
  3. 铁道部新客票系统设计(一)
  4. Excel如何提取含有关键词的所有数据
  5. Microsoft .NET Compact Framework 开发常见问题解答 - 专注.NET技术及其相关应用开发! - 博客园...
  6. uploadlabs--文件上传靶场-第一关
  7. iPhone 部分机型尺寸
  8. 一篇文章理解JS中同步任务和异步任务以及宏任务与微任务的原理和执行机制
  9. eclipse 代码折叠插件-folding
  10. 很多小银行给的存款利息比大银行高出很多,这些银行安全吗?