数据结构_线性表

1、线性表的定义
线性表(List):零个或多个数据元素的有限序列。
如果用数学语言来描述 ,可如下;
在这里插入图片描述
所以线性表的个数n(n大于等于0)定义为线性的长度,当n=0时,称为空表。
在非空表中,每一个元素,除了首元素外,都有一个前驱,除尾元素外,都有一个后继。
线性表中 的数据具有相同类型。

2、线性表的抽象数据类型
在这里插入图片描述
对于不同应用,线性表的基本操作是不同的,但可以通过这些基本操作的组合来实现。如下:

/*将所有的在线性表Lb中但不在线性表La中的数据元素插入到La中*/
void union(List* La,List Lb)
{
 int La_len, Lb_len;
 La_len = ListLenght(La);
 Lb_Len = ListLenght(Lb);
 ElemType e;
 for (int i = 0; i < Lb_len; i++)
 {
  GetELem(Lb,i,e);   /*获得Lb中第i个元素并赋值给 e*/
  if (!LocationElem(La, e))      /*判断La中是否存在与元素 e相等的元素*/
   ListInsert(La,La_len++,e); /*在La末尾插入元素e*/
 }
}

3、线性表的顺序存储结构
(1)定义
线性表的顺序存储结构指的是用一组地址连续的存储单元依次存储线性表的数据元素。

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

可以发现线性表的存储结构需要三个属性:
存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置;
线性表的最大存储容量:数组长度MAXSIZE;线性表当前长度。
(2)数据长度与线性表长度的区别
数组的长度是存放线性表的存储空间的长度,存储分配后这个量是一般不变的。
线性表的长度是线性表中数据元素的个数,随着线性表中插入和删除操作的进行,这个量是变化的。
在任意时刻,线性表的长度应该小于等于数组的长度。
(4)地址计算方法
C语言中数组的第一个下标是从0开始的,于是线性表的第i个元素是要存储在数组下标为i-1的位置,即数组元素的序号和存放它的数组下标之间的对应关系如下图:
在这里插入图片描述用数组存储数据表意味着要分别配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于等于当前的线性表长度。
存储器的每个存储单元都有自己的编号,这个编号称为地址。
假设线性表中每个数据元素占用的c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LOC表示获得存储位置的函数):
在这里插入图片描述
从线性表的任意位置存入或者取出数据,对计算机来说时间是相等的,也就是一个长度,因此时间复杂度为O(1)。通常把具有这一特点的存储结构称为随机存储结构。

4、顺序存储结构的插入和删除
(1)获得元素操作

/*获得元素操作*/
#define OK 1;
#define ERROR 0;
#define TRUE 1;
#define FALSE 1;
typedef int Status;            /*用Status代替int,含义更清楚些*/
/*Status是函数的类型,其值是函数结果状态代码,如OK等
初始条件:顺序线性表L已存在,1<=i<=ListLenght(L);
操作结果:用e返回L中第i个数据元素的值。
*/
Status GetElem(Sqlist L,int i,ElemType* e)
{
 if (0 == L.length || 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)
{/*这里e用指针形式是为了避免赋值操作时拷贝耗时*/
 int k;
 if (0 == L->length)              /*线性表为空*/
  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)线性表顺序存储结构的优缺点
优点:无须为表示表中的逻辑关系而增加额外的存储空间;可以快速地存储表中任一元素。
缺点:插入和删除操作需要移动大量元素;当线性表长度变化较大时,难以确定存储空间的容量;造成存储空间的“碎片”。(因为要求连续的、大小为MAXSIZE的存储空间,所以小于该大小的存储空间不能使用,造成内存碎片)。

5、线性表的链式存储结构
(1)线性表链式存储结构定义
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表中的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着这数据元素可以存在内存未被占用的任意位置,如下图:
在这里插入图片描述
在链式结构中,除了要存储数据元素外,还要存储它的后继元素的存储地址。
把存储数据元素的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素ai的存储映像,称为节点(Node)。
n个节点链结成一个链表,即为线性表的链式存储结构。因为此链表的每个节点中只包含一个指针域,所以称为单链表。单链表正是通过每个节点的指针域将线性表中的数据元素按其逻辑次序链接在一起,如下图:
在这里插入图片描述链表中第一个结点的存储位置叫作头指针,最后一个结点指针为“空”。
(2)头结点与头指针的异同
在这里插入图片描述头指针:
头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针;头指针具有标识作用,故常用头指针冠以链表的名字;无论链表是否为空,头指针均不为空;头指针是链表的必要元素。
头结点:头结点是为了操作的方便和统一而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度);有了头结点,对在第一元素结点之前插入或删除第一结点,其操作与其他结点的操作就统一了;头结点不一定是链表必要素。
(3)线性链表存储结构代码描述
若线性链表为空,则头结点的指针域为空 。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
单链表中,在 c语言中可以用结构指针来描述,如下:

/*线性表的单链表存储结构*/
typedef struct Node
{
 ElemType data;            /*数据域*/
 struct Node* next;        /*指针域*/
}Node;/*Node相当于struct Node*/
typedef struct Node* LinkList;/*定义LinkList,LinkList相当于struct Node* */

假设p是指向线性表第i个元素的指针:
在这里插入图片描述
6、单链表的读取
获得链表第i个数据的算法思路:
声明一个结点p指向链表第一个结点,初始化j从1开始;
当j小于i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
若到链表末尾p为空,则说明第i个元素不存在;
否则查找成功,返回结点p的数据。
实现代码如下:

/*单链表的获取操作:读取第i个数据元素*/
Status GetElem(LinkList L,int i,ElemType* e)
{
 int j ;        /*计数器j*/
 LinkList p;    /*声明一个结点*/
 j = 1;         /*从1开始计数而非0*/
 p = L->next;  /*让p指向链表L的第一个结点,而非头结点*/
 while (p&&j < i)/*因为不知道要循环多少次,所以不用for*/
 {
  p = p->next;/*让p指向下一个结点*/
  j++;        /*计数器加1*/
 }/*当j==i时,退出,此时p指向i-1的下一个结点,即第i个结点*/
 if (!p || j > i)
  return ERROR;
 *e = p->data;  /*取第i个元素的数据*/
 return OK;
}

从头开始查找,其时间复杂度为O(n)。
该算法的核心思想是‘工作指针后移"。

7、单链表的插入与删除
(1)单链表的插入
假设存储元素e的结点为s,要实现p、p->next和s之间的逻辑关系的变化,只需将结点s插入结点p和结点p->next之间即可。插入代码为

s->next = p->next; p->next = s;

如下图:
在这里插入图片描述
对于单链表的表头和表尾的特殊情况,操作也相同,如下图:
在这里插入图片描述
单链表第 i个数据元素插入结点的算法思路如下:
声明一结点p指向链表第一个结点,初始化j为1;
当j<i时就遍历链表,让p的指针向后移动,不断指向下一结点,j加1;
若到链表末尾p为空,则说明第i个数据不存在;
否则查找成功,在系统中生成一个空结点s;
将数据元素e赋值给s->data;
单链表的插入标准语句s->next = p->next; p->next = s;
返回成功。
实现代码算法如下:

/*单链表的插入操作*/
/*初始条件:单链表L已存在,1<=i<=LinkList(L)*/
/*操作结果:在L中第i个位置之前(如果*L表示头结点,否则是之后)插入新的数据元素e,L的长度加1*/
Status ListInsert(LinkList* L,int i,ElemType e)
{
 int j;
 LinkList p,s;
 j = 1;
 p = *L;            /*头结点,而非第一个结点?*/
 while (p&&j < i)
 {/*遍历*/
  p = p->next;    /*指针后移*/
  j++;
 }
 if (!p || j > i)   /*第i个元素不存在*/
  return ERROR;
 s =(LinkList) malloc(sizeof(Node)); /*生成新结点(C语言标准函数)*/
 s->data = e;       /*给结点s数据域赋值*/
    /*插入操作*/
 s->next = p->next;
 p->next = s;
 return OK;
}

(2)单链表的删除
设存储元素ai的结点为q,要实现从单链表中删除结点q的操作,其实就是将结点q的前继结点的指针绕过,指向它的后继结点即可,如下图:
在这里插入图片描述
删除标准语句为q=p->next;p->next=q->next;
单链表第i个数据元素删除结点的算法思路如下:
声明一结点p指向链表的第一结点,初始化j从1开始;
当j<i时就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
若到链表末尾p为空,则说明第i个元素不存在;
否则查找成功,将欲删除的结点p->next赋值给q;
单链表的删除标准语句q=p->next;p->next=q->next;
将q结点中的数据赋值给e,作为返回;
释放结点q;
返回成功。
实现代码算法如下:

/*单链表的删除操作*/
/*初始条件:单链表L已存在,1<=i<=LinkList(L)*/
/*操作结果:删除L中第i个数据元素,并用e返回其值,L的长度减1*/
Status ListInsert(LinkList* L, int i, ElemType* e)
{
 int j;
 LinkList p, q;
 j = 1;
 p = *L;             /*头结点,而非第一个结点?*/
 while (p->next&&j < i)
 {/*遍历*/
  p = p->next;    /*指针后移*/
  j++;
 }
 if (!p || j > i)   /*第i个元素不存在*/
  return ERROR;
 q = p->next;
 p->next = q->next; /*将q的后继赋值给p的后继*/
 *e = q->data;      /*将q结点中的数据给e*/
 free(q);           /*让系统回收此结点,释放内存*/
 return OK;
}

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

8、单链表的整体创建
顺序存储结构的创建,其实就是一个数组的初始化,即声明一个类型 和大小的数组并赋值的过程。
对于每个单链表来说,它所占用的空间大小和位置是不需要预先分配和划定,可以根据系统的情况和实际需求即时生成。
创建单链表的过程就是一个动态生成单链表的过程。即从空表的初始状态起,依次建立各元素结点,并 逐个插入链表。
单链表整表创建的算法思路:
声明一结点p和计数器变量i;
初始化一空链表L;
让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
循环:
生成一新结点赋值给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;         /*随机生成10以内的数字*/
  p->next = (*L)->next;               /*头结点的后继结点称为新结点的后继结点*/
  (*L)->next = p;                     /*在头结点后插入新结点,即插入到表头*/
 }
}

在这里插入图片描述
尾插法 :把每次新结点都插在终端结点的后面。
尾插法的实现代码算法如下:

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

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

9、单链表的整表删除
单链表整表删除的算法思路如下:
声明一结点p和q;
将第一个结点值赋值给p;
循环:
将下一结点值赋值给q;
释放p;
将q赋值给p。
实现代码算法如下:

/*单链表的整表删除*/
/*初始条件:单链表L已存在,操作结果:将L重置为空表*/
Status ClearList(LinkList* L)
{
 LinkList p, q;
 p = (*L)->next;       /*将p指向第一个结点*/
 while (p)             /*尾结点的指针域为空,说明不存在下一结点,已完成删除*/
 {
  q = p->next;      /*将下一结点值赋值给q.在单链表中,下一结点的位置只有上一结点的指针域知道*/
  free(p);          /*删除该结点*/
  p = q;            /*将下一结点赋值给p*/
 }
 (*L)->next = NULL;    /*将头结点指针置为空*/
 return OK;          
}

注意:这里q存在的意义

10、单链表结构和顺序存储结构的优缺点
从存储分配方式、时间性能和空间性能三方面比较这两种存储结构:
在这里插入图片描述
通过上面的对比,可以得到一些经验性的结论:
1)当线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构;若需要频繁插入和删除时,宜采用单链表结构。
2)当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不用考虑存储空间大小的问题。而如果事先知道线性表的长度,比如一年有12个月,一星期有7天,这种用顺序存储结构效率会高很多。

个人在代码实现方面上 的体会:
不论是单链表结构还是顺序存储结构,要想对结构中的指定数据元素进行操作(包括读取、插入和删除等),都需要知道该元素的在内存中的位置。只不过在顺序存储结构中,数据连续存储,所以可以像操作数据一样采用下标的方式访问数据元素;而在单链表存储结构中,数据非连续存储,本结点的位置只有上一结点知道,所以在未获得本结点之前,需要先获得上一结点,如此往前递推,只有先获得头结点(*L)或第一结点((*L)->next)。

11、静态链表
C语言具有的指针能力,使得它可以非常容易地操作内存中地地址和数据。而其他没有指针能力的高级语言,可以用数组来代替指针,具体做法如下:
首先让数组的元素都是由两个数据域组成,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[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为0*/
 return OK;
}

假设我们已经将数据存入静态链表,比如分别存放着甲、乙、丁、戊、己、庚,则它将处于下图所示这种状态:在这里插入图片描述
(1)静态链表插入操作
静态链表中要解决的问题是:如何用静态模拟动态链表的结构的存储空间的分配,需要时申请,无用时释放。
在动态链表中,结点的申请和释放分别借用free()和malloc()两个函数来实现。在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放问题,所以我们需要自己实现这两个函数,才可以实现插入和删除操作。
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。
静态链表中malloc实现代码如下:

/*若备用空间链表非空,则返回分配的结点下标,否则返回0*/
int Malloc_SLL(StaticLinkList space)
{
 int i = space[0].cur;   /*数组第一个结点的游标存放的是备用存储空间中第一个结点的下标*/
 if (space[0].cur)       /*备用空间链表非空*/
  space[0].cur = space[i].cur;  /*用于拿出一个分量来使用了,所以就拿它的下一个分量用作备用*/
 return i;
}

因为已经为数组分配内存,所以这里的Malloc_SLL只需指定需要的是哪个位置的内存,以及相应的操作(如空闲的第一个结点位置发生变化等)。
以上图3-12-2为例,Malloc_SLL返回结果为 7,则空闲空间的第一个结点变为8。
继续以图3-12-2为例,实现在乙和丁之间插入丙,如下图。静态链表中的插入操作并不像顺序线性表一样,将丙插入丁的位置,丁及丁以后的数据元素均向后移动一位,而是将丙放在所有数据元素的最后,再将乙的游标等于丙的下标,丙的游标等于丁的下标。如此,实现了在数组中,不移动元素便插入了元素:
在这里插入图片描述
插入操作实现代码如下:

/*静态链表的插入操作*/
Status ListInsert(StaticLinkList L,int i,ElemType e)
{
 int j, k, n;/*j为空闲空间的第一个元素下标*/
             /*k为元素下标;n为计数器*/
 k = MAXSIZE - 1; /*注意:k首先为最后一个元素的下标*/
 /*i的合理性检查*/
 if (i<1 || i>ListLength(L) + 1)
  return ERROR;
 j = Malloc_SLL(L);/*为插入的元素分配空间并返回空闲空间的第一个元素的下标*/
 if (j)/*链表非空*/
 {
  L[j].data= e;   /*将e赋值给该结点的数据量*/
  for (n = 1; n <= i - 1; n++)
  {/*从第一个元素开始,遍历第i个元素之前的元素,以获得第i-1个元素的下标*/
   k = L[k].cur;
  }
  /*插入操作*/
  L[j].cur = L[k].cur;  /*把第i-1个元素之前的游标赋值给新的第i个元素的游标*/
  L[k].cur = j;         /*把新的第i个元素的下标赋值给第i-1个元素的游标*/
  return OK;
 }
 return ERROR;
}

个人体会:要对操作某个数据元素,需要知道该元素在内存中的位置。在静态链表中,通过元素在数组中的下标获得元素在内存中的位置,但因为静态链表中的元素并非连续存储,所以第i个元素的数组下标并非i-1,即不能直接通过第几个元素获得该元素的下标。考虑到静链表的第i个元素的下标可以通过第i-1个元素的游标获得,故需要从第一个元素开始向后遍历,直至获得第i-1个元素的下标,接着通过下标访问的方式即可获得第i个元素的下标。而第一个元素的下标由数组中最后一个位置的游标获得。
(2)静态链表中的 删除操作
在静态链表中实现释放结点的函数free()操作:

/*在静态链表中实现free()操作,类似于内存回收*/
void Free_SLL(StaticLinkList L,int k)
{
 L[k].cur = L[0].cur;        /*删除结点的游标为原来空闲空间的第一个结点的游标*/
                             /*意味着原来空闲空间的第一个结点发生了变化*/
 L[0].cur = k;               /*删除结点的变成空闲空间第一个结点,
        即头结点的游标等于删除结点的下标*/
}

在静态链表中实现山粗 元素操作:

/*删除静态链表L中的第i个元素*/
Status ListDelete(StaticLinkList L, int i)
{
 int j, n;   /*n计数器*/
 /*i的合理性检查*/
 if (i < 1 || i>ListLength(L))
  return ERROR;
 n = MAXSIZE - 1;
 for (n=1;n<i-1;n++)
 {/*遍历以获得第i-1个元素的下标*/
  n = L[n].cur;
 }
 j = L[n].cur;   /*第i个元素的下标*/
 L[n].cur = L[j].cur;/*第i-1个元素的下标等于原来的第i个元素的下标*/
 Free_SLL(L, i);
 return OK;
}

和线性表一样,通过下标获取元素。

(3)静态链表的优缺点
在这里插入图片描述
其中,表长难以确定是指需要提前分配 最大存储空间,即上述中的MAXSIZE。
总的来说,静态链表其实是为了给没有指针的高级语言设计的一种实现单链表能力的方法。这种思考方法非常巧妙,可以在需要的时候借用。
在代码实现上的个人体会:将游标当作单链表中的next指针,元素之间的关系决定具体操作(本元素的游标保存下一元素的下标;逻辑上相邻的元素在物理位置上不一定相邻),特殊位置的游标决定操作起点(头结点的游标表示空闲空间的第一个结点的下标;数组最后一个结点的游标表示第一个有数值的结点的下标)。

12、循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成了一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list)。
循环链表解决了如何从当中一个结点出发,访问到链表的全部结点。
为了使空链表与非空链表处理 一致,通常设一个头结点,当然,这并不意味着,循环链表一定要有头结点。循环链表带有头结点的空链表如下图:
在这里插入图片描述
从上图 可以看出,循环链表为空时,头结点的next指针指向头结点。

对于非空的循环链表如下图:
在这里插入图片描述
由循环链表的定义可知,循环链表和单链表的主要差异就在于循环的判定条件上,原来 是判断p->next是否为空,现在是判断p->next是否等于头结点。如果不等于 则循环未结束,否则循环已结束。
在单链表中,有了头结点时,可以用O(1)的时间访问第一个结点,但对于要访问到最后一个结点,却需要O(n)时间,因为需要将单链表全部扫描一遍。用指向终端结点的尾指针来表示循环链表,则用O(1)的时间就可以访问到头结点。如下图:
在这里插入图片描述

/*循环链表的整表创建(尾插法)*/
void CreateCListTail(LinkList* L, int n)
{
 LinkList p, r;                           /*p表示新结点,r表示表尾终端结点*/
 int i;
 srand(time(0));/*初始化随机数种子*/
 *L = (LinkList)malloc(sizeof(Node));    /*生成空链表(生成头结点)*/
 r = *L;
 for (i = 0; i < n; i++)
 {
  p = (LinkList)malloc(sizeof(Node)); /*生成新结点*/
  p->data = rand() % 100 + 1;         /*随机生成10以内的数字*/
  r->next = p;                        /*将表尾终端结点的指针指向新的结点*/
  r = p;                              /*将当前的新结点定义为表尾终端结点*/
 }
 r->next = *L;                           /*尾结点指向头结点*/
}

尾指针:从上图中可以看到,终端结点用尾指针rear指示,则查找终端结点是O(1),而开始结点,其实就是rear->next-next,其时间复杂度也为O(1)。
有了尾指针。可以很方便地将两个循环链表连接成一个循环链表,如下图:
在这里插入图片描述
连接实现代码如下:

/*循环链表中用为指针地方式连接两个循环链表成一个循环链表*/
p = rearA->next;  /*p保存链表A的尾指针,即链表A的头结点*/
rearA->nextp=rearB->next->next;/*链表A的尾指针指向链表B的第一个结点*/
rearB->next = p;/*链表B的指针指向链表A的头结点。
    不能将该操作放在最上面,因为会改变获得链表B的第以结点的操作*/
free(p);       /*释放A的头结点p,结果是只剩一个头结点*/
/*注:容易将第二和第三指令的等号两边写反。
有一个方法可以解决,即A指向B,则A在等号左边*/

关于循环链表的小结:循环链表与单链表相比,尾结点指针不为空,而是指向头结点,从而空链表的判定条件和循环结束判定条件发生变化;为了减少查找尾结点的时间开销,多了尾指针,从而允许多链表连接成一个链表。
这里存在一个问题:链表类型变量表示头指针,如LinkList L中的L或者LinkList* L中的*L,单循环链表中用什么表示尾指针呢?可以用函数的方法遍历获得尾结点,或者用成员变量或全局变量表示 。

13、双向链表
双向链表(double linked list)是在单链表的每个结点中,再设计一个指向其前驱结点的指针域。

/*线性表的双向链表存储结构*/
typedef struct DulNode
{
 ElemType data;
 struct DulNode* prior; /*直接前驱指针*/
 struct DulNode* next;  /*直接后继指针*/
}DulNode,*DulLinkList;     /*DulLinkList解引用才相等于DulNode*/

双向链表的循环带头结点的空链表如图:
在这里插入图片描述
双向链表的循环带头结点的空链表如图:
在这里插入图片描述
由于这是双向链表,那么对于链表中的某一个结点p,它的后继的前驱是它自己,它的前驱的后继也还是它自己,即p->next->prior=p=p->prior->next。
双向链表是单链表中扩展出来的结构,所以它的很多操作是和单链表相同的,比如求长度的ListLength()、查找元素的GetElem()、获得元素 位置的LocationElem()(也是需要遍历到指定位置),这些操作只需要涉及一个方向的指针而已,另一个指针对这些操作没什么帮助。
与单链表相比,双向链表可以反向遍历查找,故其插入和删除操作与单链表不同。
插入操作:假设存储元素e的结点为s,要实现将结点s插入结点p和结点p->next之间,如下图,需要下面几步,
在这里插入图片描述

/*双向链表的插入操作*/
s->prior = p;
s->next = p->next;
p->next - prior = s;
p->next = s;

注意这里的顺序,第四步第二和第三步之后,否则达不到预期结果。
删除操作:如下图:
在这里插入图片描述
需要以下两步:

/*双向链表的删除操作*/
p->prior->next = p - next;
p->next->prior = p - prior;

双向链表用空间换来时间。

14、总结
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值