线性表(List) 二

线性表的链式存储结构

概念

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

将存储数据元素信息的域称为数据域,把存储直接后继位置的域成为指针域。指针域中存储的信息成为指针或链,这两个部分信息组成数据元素 ai 的存储映像,称为结点(Node)。

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

将链表中第一个结点的存储位置叫做头指针,为了方便对链表进行操作,会在单链表的第一个结点前附设一个结点,称其为头结点

下面描述头指针和头结点的异同:

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

在C语言中,单链表可以用结构指针来描述,如下所示:

typedef struct Node{  //结点
   ElemType data;  //数据域
   struct Node *next;  //指针域
}Node;
type struct Node *LinkList;  /*定义单链表 LinkList*/

单链表

读取链表第i个数据的操作

思路:声明一个指针 p 指向链表第一个结点,初始化 j 从1开始;当j < i 时,遍历链表,让 p 的指针不断向后移动,不断指向下个结点, j 累加1;若到链表末尾 p 为空了,则说明这个结点不存在;否则就查找成功,返回结点 p 的数据。核心思想是“工作指针后移”,时间复杂度为 O(n) 。实现代码如下:

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

单链表第i个数据插入结点操作

思路:声明一指针p指向链表头结点,初始化j从1开始;当 j < i时,遍历链表,让p的指针后移,不断指向下一结点,j累加1;若到链表末尾,则说明第i个结点不存在;否则,查找成功,在系统中生成一个空的s结点;将数据元素e赋值给s->data;单链表的插入标准语句 s->next=p->next; p->next=s; 注意这两句的位置不能更换,否则插入后面的数据会丢失;返回成功。实现代码如下所示:

/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个结点位置之前插入新的数据元素e,L的长度+1*/
Status ListInsert(LinkList *L,int i,ElemType e){
   int j;  //j为计数器
   LinkList p,s;  //声明两个指针,一个用于遍历,一个用于生成新结点
   p=*L;  //头指针
   j=1;
   while(p && j<i){ //循环继续条件,不使用for
      p=p->next;  //让p指向下一结点
      ++j;
   }
   if(!p||j>i) return ERROR; //第i个结点不存在
   /*生成新结点(C标准函数)*/
   s=(LinkList)malloc(size(Node));
   s->data=e;
   s->next=p->next; //插入结点的固定语句
   p->next=s;  //插入结点的固定语句
   return OK;
}

单链表第i个数据删除结点操作

思路:声明一指针p指向链表头结点,初始化j从1开始;当 j < i时,遍历链表,让p的指针后移,不断指向下一结点,j累加1;若到链表末尾,则说明第i个结点不存在;否则,查找成功,将要删除的结点p->next赋值给q;单链表的插入标准语句 p->next=q->next; 将q结点中的数据赋值给e,作为返回;释放q结点,返回成功。实现代码如下所示:

/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L中的第i个结点,并用e返回其值,L的长度-1*/
Status ListDelete(LinkList *L,int i,ElemType *e){
   int j;  //j为计数器
   LinkList p,q;  //声明两个指针,一个用于遍历,一个用于指向删除结点
   p=*L;  //头指针
   j=1;
   while(p && j<i){ //循环继续条件,不使用for,找到第i-1个结点
      p=p->next;  //让p指向下一结点
      ++j;
   }
   if(!p||j>i) return ERROR; //第i个结点不存在
   q=p->next;  //q指向要删除结点
   p->next=q->next;  //删除第i个结点
   *e=q->data;  //保存要删除结点的数据
   free(q);  //释放内存,回收该结点
   return OK;
}

单链表的整表创建操作

思路:声明一指针p和计数器变量i;初始化一个空链表L;让L的头结点的指针指向NULL,即建立一个带有头结点的单链表;循环(生成一个新结点赋值给p,随机生成一数字赋值给p的数据域p->data,将p插入到头结点和前一新结点之间)。这种方法为头插法,实现代码如下所示:

/*建立带有表头结点的单链线性表L(头插法:新结点始终在第一位)*/
void CreateListHead(LinkList *L,int n){
   int i;
   LinkList p;
   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;  //随机生成1-100 以内的数字
      p->next=(*L)->next;  //插入在头结点和最新结点之间
      (*L)->next=p;
   }
}
/*建立带有表头结点的单链线性表L(尾插法:新结点始终在最后)*/
void CreateListHead(LinkList *L,int n){
   int i;
   LinkList p,r;
   srand(time(0));  //初始化随机数种子
   *L=(LinkList)malloc(sizeof(Node));  //初始化空的单链表
   r=*L;  //r为指向链表末尾结点的指针
   for(i=0;i<n;i++){
      p=(LinkList)malloc(sizeof(Node));  //生成新结点
      p->data=rand()%100+1;  //随机生成1-100 以内的数字
      r->next=p;  //将末尾的指针指向新结点
      r=p;  //将新结点定义为新的尾部结点
   }
   r->next=NULL;  //表示当前链表结束
}

单链表的整表删除操作

思路:声明一指针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结点
      p=q;  //将q赋值给p,相当于指针后移
   }
   (*L)->next=NULL;  //头结点的指针域为空
   return OK;
}

小结

将单链表结构和顺序存储结构进行对比,如下所示:

存储分配方式 时间性能 空间性能
顺序存储结构用一段连续的存储单元依次存储线性表的数据元素 查找(顺序存储结构 O(1);单链表 O(n)) 顺序存储结构需要预分配存储空间,分大了,浪费,分小了容易发生上溢
单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素 插入和删除(顺序存储结构需要平均移动表长一半的元素,时间复杂度为 O(n) ;单链表在找出某位置的指针后,插入和删除的时间仅为 O(1)) 单链表不需要分配存储空间,只要有可以分配的即可,元素的个数也不受限制

结论:

1.若查找频繁,而插入删除操作很少时,应该采用顺序存储结构;若插入删除频繁,应该采用单链表结构。

2.当线性表中的元素个数变化比较大时或者根本不知道多大时,最好使用单链表结构,这样可以不考虑存储空间大小的问题。

各有优缺点,根据实际情况来综合平衡确定使用的数据结构。

静态链表

用数组来代替指针来描述单链表,数组的元素由两个数据域来组成,data(数据域)和cur(类于指针域,存放后继元素的下标,游标)。用数组描述的链表就叫做静态链表(游标实现法)。

/*线性表的静态链表存储结构*/
#define MAXSIZE 1000  /*大小可以再大些,避免数据溢出*/
typedef struct{
   ElemType data;
   int cur;  /*游标(Cursor),为0时表示无指向*/
}Component,StaticLinkList[MAXSIZE];

数组的第一个和最后一个元素做特殊元素处理,不存储数据。第一个元素,下标为0,指向备用链表的第一个结点下标;最后一个元素,存放第一个有数值的元素的下标,相当于头结点作用。

元素的插入操作

思路:用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放,所以需要自己实现结点申请和节点释放两个函数。将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入操作时,就从备用链表上取得第一个结点作为待插入的新结点。实现代码如下:

/*若备用空间链表非空,则返回分配的结点下标,否则返回0*/
int Malloc_SLL(StaticLinkList space){
   int i=space[0].cur;  //备用链表第一个结点的下标
   if(space[0].cur)
   //因为要拿一个分量使用,所以要取出下一个分量备用。
   //因为要占用一个备用链表结点,所以第一元素的cur值也需要变化
      space[0].cur=space[i].cur;
      return i;
}

/*在L中第i个元素之前插入新的数据元素e*/
Status ListInsert(StaticLinkList L,int i,ElemType e){
   int j,k,l;  //j为计数器
   k=MAX_SIZE-1; //k是最后一个元素的下标
   if(i<1||i>ListLength(L)+1)  return ERROR; //插入位置错误
   j=Malloc_SLL(L); //获得空闲分量的下标
   if(j){ //备用链表存在空闲位置
      L[j].data=e;  //将数据赋值给对应位置元素
      //找到第i个元素之前的位置,相当于不断向后遍历
      for(l=1;l<i-1;l++) k=L[k].data;
     //插入语句,将第i个元素游标赋值给新元素,第i-1个元素的游标指向新元素
      L[j].cur=L[k].cur;
      L[k].cur=j;
      return OK;
   }
   return ERROR;
}

元素的删除操作

思路:用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放,所以需要自己实现结点申请和节点释放两个函数。将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入操作时,就从备用链表上取得第一个结点作为待插入的新结点。实现代码如下:

/*删除在L中第i个元素*/
Status ListDelete(StaticLinkList L,int i){
   int j,k;  //j为计数器
   k=MAX_SIZE-1; //k是最后一个元素的下标
   if(i<1||i>ListLength(L))  return ERROR; //删除位置错误
   for(j=1;j<=i-1;j++) k=L[k].cur;  //查找第i-1个元素
   j=L[k].cur; //j是第i个元素的下标
   L[k].cur=L[j].cur;  //删除第i个元素,将第i-1个元素的游标指向第i+1个
   Free_SLL(L,i);  //释放第i个元素(回收到备用链表)
   return OK;
}

/*将下标为k的空闲节点会受到备用链表,相当于将k结点头插入备用链表*/
void Free_SLL(StaticLinkList space,int k){
   space[k].cur=space[0].cur;
   space[0].cur=k;
}
/*初始条件:静态链表L已经存在;操作结果:返回L中数据元素个数*/
int ListLength(StaticLinkList L){
   int j=0;
   int i=MAX_SIZE-1; //i是最后一个元素的下标
   while(i){  //遍历到最后一个元素
      i=L[i].cur;
      j++;
   }
   return j;
}

小结

总结下静态链表的优缺点,如下所示:

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

结论:

静态链表是为了给没有指针的高级语言设计的一种实现单链表能力的方法,可以掌握这样的思考方式。

循环链表

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

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

可以改造循环链表,不用头指针,用指向终端结点的尾指针来表示循环链表,这样查找头结点和终端结点的时间复杂度均为 O(1) ,就很简单了。

eg:将两个循环列表合并成一个循环列表,用尾指针就很简单了:

p=rearA->next; //保存表A的头结点
rearA->next=rearB->next->next; //将A的末尾连接B的第一结点
q=rearB->next;
rearB->next=p; //将表B的末尾和表A的头结点连接
free(q);

双向链表

双向链表(Double Linked List)是在单链表的每个节点中,在设置一个指向其前驱结点的指针域。所以双向链表有两个指针域,一个指向直接后继,一个指向直接前驱。

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

双向链表可以反向遍历查找,但是在插入和删除操作时,需要更改两个指针变量。(以空间换时间)

插入操作(顺序很重要)–结点s插入到p,q之间

s->prior=p;  //将p赋值给s的前驱
s->next=p->next;  //将p->next赋值给s的后继
p->next->prior=s;  //将s赋值给p的后继的前驱
p->next=s;  //将p的后继赋值为s,最后,因为前面两步使用了p->next

删除操作–删除结点p

p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
时间: 2024-10-20 01:59:08

线性表(List) 二的相关文章

数据结构——线性表

提示:以下内容不适合零基础人员,仅供笔者复习之用. 一.线性结构的基本特征: 1.集合中必存在唯一的一个"第一元素": 2.集合中必存在唯一的一个 "最后元素": 3.除最后元素在外,均有 唯一的后继: 4.除第一元素之外,均有 唯一的前驱. 如:java中的List接口,就是线性表.ArrayList就是顺序线性表,LinkedList就是链表线性表. 二.线性表的基本操作: 1.InitList(*L): 初始化操作,建立一个空的线性表L. 2.ListEmpt

数据结构(二)线性表——链表

通常情况下,链接可分为单链表.双向链表和循环链表三种常用类型. 一.单链表基本操作的实现 使用链式存储结构来实现的线性表称为链表.首元结点.头结点.头指针.空指针. 1.单链表的类型定义 typedef struct LNode//结点类型 { LElemType data;//数据域 struct LNode * next;//指针域 } LNode, * LinkList; 2.初始化操作InitLinkList(&L) Status InitLinkList(LinkList &L)

二 线性表

一. 线性表的几种形式: 1.线性表是最常用且最简单的一种数据结构. 线性表中元素的个数n定义为线程表的长度,n= 0时称为空表. 2. 线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素. 这种顺序存储结构的线性表为顺序表. 线性表的特点: 优点是:可以随机存取的存储结构 缺点是:插入和删除时间复杂度高,主要耗费在移动元素上. 时间复杂度O(n).      数组 3.线性表的链式表示和实现  [字典,集合] 链式表是一组任意的存储单元存储线性表的数据元素(这组存储单元可以

03.线性表(二)链式存储结构.单链表1

链式存储结构.单链表1 1.基本概念 为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置) (1)数据域:存储线性表数据元素数据信息的域称为数据域: (2)指针域:把存储直接后继位置(下一个数据元素的地址)的域称为指针域,指针域中存储的信息为指针或链: (3)结点(Node):由数据域和指针域两部分信息组成数据元素ai的存储映像,称为结点. (4)头指针:把链表中第一个结点的存储

2、蛤蟆的数据结构笔记之二线性表

2.蛤蟆的数据结构笔记之二线性表 到了笔记二了,每个笔记开头都应该弄个语句激励一下自己和小伙伴. "人生中最重要的不是位置,而是前进的方向" 这次咱们学习表,没错是表.什么表?额,汉字真是博大精深,没错,只是个表.不要想歪了. 欢迎转载,转载请标明出处: 1.  定义 线性表(亦作顺序表)是最基本.最简单.也是最常用的一种数据结构.线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的.线性表的逻辑结构简单,便于实现和操作.因此,线性表

数据结构和算法 (二)数据结构基础、线性表、栈和队列、数组和字符串

Java面试宝典之数据结构基础 —— 线性表篇 一.数据结构概念 用我的理解,数据结构包含数据和结构,通俗一点就是将数据按照一定的结构组合起来,不同的组合方式会有不同的效率,使用不同的场景,如此而已.比 如我们最常用的数组,就是一种数据结构,有独特的承载数据的方式,按顺序排列,其特点就是你可以根据下标快速查找元素,但是因为在数组中插入和删除元素会 有其它元素较大幅度的便宜,所以会带来较多的消耗,所以因为这种特点,使得数组适合:查询比较频繁,增.删比较少的情况,这就是数据结构的概念.数据结构 包括

数据结构算法C语言实现(二)---2.3线性表的链式表示和实现之单链表

一.简述 [暂无] 二.头文件 1 #ifndef _2_3_part1_H_ 2 #define _2_3_part1_H_ 3 //2_3_part1.h 4 /** 5 author:zhaoyu 6 email:[email protected] 7 date:2016-6-4 8 note:realize my textbook <<数据结构(C语言版)>> 9 */ 10 //----线性表的单链表存储结构---- 11 /** 12 My Code 13 to mak

实验二:线性表的实验【物联网1132-11】

<数据结构>实验二:     线性表实验 实验目的 [巩固线性表的数据结构,学会线性表的应用.] 1.回顾线性表的逻辑结构,线性表的物理存储结构和常见操作. 2.学习运用线性表的知识来解决实际问题. 3.进一步巩固程序调试方法. 4.进一步巩固模板程序设计. 实验内容1: [顺序表]实现"建立一个N个学生成绩的顺序表,对表进行插入.删除.查找等操作,分别输出结果."代码如下: [单链表]实现"建立一个N个学生成绩的顺序表,对表进行插入.删除.查找等操作,分别输出结

数据结构(二)——线性表简介

数据结构(二)--线性表简介 一.线性表简介 1.线性表简介 线性表是具有相同类型的n个数据元素的有限序列A0,A1,A2,...,An-1.Ai是表项,n是表的长度. 2.线性表的表现形式 线性表的表现形式:A.零个或多个数据元素组成的集合B.数据元素在位置上是有序排列的C.数据元素的个数是有限的D.数据元素的类型必须相同 3.线性表的性质 线性表的性质:A.A0为线性表的第一个元素,只有一个后继B.An-1为线性表的最后一个元素,只有一个前驱C.除A0与An-1外的其它元素既有前驱又有后继D

数据结构学习系列之线性表(二)

前言 线性表链式存储结构的实现,通过这种方式实现的线性表,简称为链表,这是这篇文章的主题.与顺序存储相对应的是链式存储.链式存储逻辑结构相邻,物理结构可能相邻也有可能不相邻.链式结构的优点有:1.存储空间不限制(操作系统可支持的存储空间范围内):2.插入删除操作不需要移动元素等等.当然链式结构也有缺点,比如每个节点需要维护指向下一个节点的指针:比如需要查找某个节点时,需要从头节点开始查找,时间复杂度O(n)等等.总之,顺序存储以及链式存储各有优缺点,需要根据需求具体情况,选择合适的存储方式.没有