数据结构学习 --2 线性表

数据结构学习 --1 绪论
数据结构学习 --2 线性表
数据结构学习 --3 栈,队列和数组
数据结构学习 --4 串
数据结构学习 --5 树和二叉树
数据结构学习 --6 图
数据结构学习 --7 查找
数据结构学习 --8 排序

本人学习记录使用 希望对大家帮助 不当之处希望大家帮忙纠正

数据结构学习 --2 线性表



前言

在这里插入图片描述


提示:以下是本篇文章正文内容,下面案例可供参考

2.1 线性表的定义和基本操作

2.1.1 线性表的定义

线性表是具有相同数据类型的 n(n>=0)个数据元素的有限序列,其中n为表长,当n = 0 时线性表是一个空表,若用 L命名线性表,则其一般表示为
L=(a1,a2,……,an)
式中,a1是唯一的“第一个”数据元素,又称表头元素;an是唯一的“最后一个”数据元素,又称表尾元素。除第一个元素外,每个元素有且仅有一个直接前驱,除最后一个元素外,每个元素有且仅有一个直接后继(“直接前驱”和“前驱”、“直接后继”和“后继”通常被视为同义词)。以上就是线性表的逻辑特性,这种线性有序的逻辑结构正是线性表名字的由来。
线性表的特征如下
表中元素的个数有限
表中元素具有逻辑上的顺序性,表中元素有其先后次序。
表中元素都是数据元素,每个元素都是单个元素。
表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间
表中元素具有抽象性,即仅探讨元素间的逻辑关系,而不考虑元素究竟表示什么内容。
线性表是一种逻辑结构,表示元素之间一对一的相邻关系,顺序表和链表直指存储结构,两者属于不同层面的概念,因此不要将其混淆。

2.1.2 线性表的基本操作

一个数据结构的基本操作是指其最核心、最基本的操作。其他较复杂的操作可以通过调用其基本操作来实现,线性表的主要操作如下。

InitList(& L) #初始化表。构造一个空的线性表。
Length(L) #求表长。返回线性表L的长度,即L中数据元素的个数。
LocateElem(L,e) #按值查找操作。在表L中查找具有给定关键字的元素。
GetElem(L,i) #按位查找操作。获取表中L中第i个位置的元素的值。
ListInsert(&L,i,e) #插入操作。在表L中第i个位置插入制定元素e。
ListDelete(&L,i,&e)#删除操作。删除表第L中第i个位置的元素,并用e返回删除元素的值。
PrintList(L) #输出操作。按前后顺序输出线性表L的所有元素值。
Empty(L) #判空操作。若L为空表,则返回true ,否则返回false。
DestroyList(&L) #销毁操作。销毁线性表,并释放线性表L所占用的内存空间。

2.2 线性表的顺序表示

2.2.1 顺序表的定义

线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。顺序表的特点是表中的元素的逻辑顺序于其物理顺序相同。
通常高级程序设计语言中的数组来描述线性表的顺序存储结构。
线性表中元素的位序是从1开始的,而数组中元素的下标是从0开始。

#假定线性表的元素类型为ElemType,则线性表的顺序存储类型描述为
#define MaxSise 50 //定义线性表的最大长度
typedef struct {
	ElemType data[MaxSize]; //顺序表的元素
	int length; //顺序表的当前长度
}SqList;    //顺序表的类型定义

一维数组可以是静态分配的,也可以是动态分配的。在静态分配时,由于数组的大小和空间事先已经固定,一旦空间占满,在加入新的数据就会产生溢出,进而导致程序崩溃。
而在动态分配时,存储数组的空间是在程序执行过程中通或动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,用来替换原来的存储空间,从而达到扩充存储数据空间的目的,而不需要为线性表一次性地划分所有空间。

#define InitSise 100 //表长度的初始定义
typedef struct {
	ElemType *data; //指示动态分配数组的指针
	int MaxSize,length; //数组的最大容量和当前个数
}SeqList;    //动态分配数组顺序表的类型定义
//c的初始动态分配语句为
L.data = (ElemType *)malloc(sizeof(ElemType)*InitSise);
//c++的初始动态分配语句为
L.data = new ElemType[InitSise];

顺序表最主要的特点是随机访问,即通过首地址和元素序号可在时间内哦O(l)内找到指定的元素。 顺序表的存储密度高,每个结点只存储数据元素
顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素

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

这里讨论顺序表的插入,删除和按值查找的算法,其他基本操作的算法都比较简单。
(1)插入操作
在顺序表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) //判断i的范围是否有效
		return false;
	if(L.length >= MaxSise ) //当前存储空间已满,不能插入
		return false;
	for(int j=L.length;j>=i;j--)
		L.data[j] = L.data[j-1];
	L.data[i-1] =e; //在位置i处放入e
	L.length++; //线性表长度加1
	return true;
}

最好情况 :在表尾插入即(i = n+1),元素后移语句将不执行,时间复杂度为O(l)。
最坏情况:在表头插入(i=1),元素后移语句将执行n次,时间复杂度为O(n) 。
平均情况 问为 n/2
因此,顺序表的插入算法的平均时间复杂度为O(n)。

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

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

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

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

2.3 线性表的链式表示

2.3.1 单链表的定义

线性表的链式存储又称单链表,他是值通过一组任意的存储单元来存储线性表中的数据元素,为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。
单链表中结点类型描述 如下:

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

头结点和头指针的区分:不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。
引入结点后,可以带来两个优点:

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

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

  1. 采用头插法建立单链表
    改方法从一个空表开始,生成新结点,并将读取到的数据放到新节点的数据域中,然后将新节点插入到当前链表的表头,即头结点之后
LinkList List_HeadInsert(LinkList &L){//逆向建立单链表
	LNode *s; int x;
	L =(LinkList)malloc(sizeof(LNode )); //创建头结点
	L->next =null; //初始为空链表
	scanf("%d",&x); //输入结点的值
	while(x!=9999){ //输入 9999 表示结束
		s=(LinkList)malloc(sizeof(LNode ));//创建新结点
		s->data=x;
		s->next=L->next;
		L->next=s;	//将新节点插入表中,L为头指针
		scanf("%d",&x);
	}
	return L;
}
  1. 采用尾插法建立单链表
    头插法建立单链表的算法虽然简单,但生成的链表中结点的次序和输入数据的顺序不一致,若希望两者次序一致,则可采用尾插法。该方法将新节点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点
LinkList List_TailInsert(LinkList &L){//逆向建立单链表
    int x; 
   L =(LinkList)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=L->s;
   	r=s;       //r指向新的表尾结点
   	scanf("%d",&x);
   }
   r->next=NULL; //尾结点指针置空
   return L;
}
  1. 按序列号查找结点
    在单链表中从第一个结点出发,顺指针 next 域逐个往下搜索,直到找到第i个结点为止,否则放回最后一个结点指针域 NULL。
LNode *GetElem(LinkList L,int i){
	if(i<1) return NULL; //若i无效,则返回NULL
	int j = 1; //计数,初始为1
	LNode *p = L->next; //第1个结点指针赋值给p
	while(p != NULL && j<i){ //从第1个结点开始找,查找第i个结点
		p = p->next;
		j++;
	} 
	return p; //返回第i个结点的指针,若i大于表长,则返回null
}

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

LNode *LocateElem(LinkList L,ElemType e){
	LNode *p = L->next;
	while(p!=NULL && P->data != e) p = p->next; //从第i个结点开始查找data域为e的结点
	return p;
}
  1. 插入结点操作
    插入结点操作将值为x的新结点插入到单链表的第i个位置上。先检查插入位置的合法性,然后找到代找到插入位置的前驱结点,即第i-1 个结点,再在其后插入新的结点。
    代码片段如下
 p = GetElem(L,i-1); //查找插入位置的前驱结点
 s->next = p->next;
 p->next=s;

扩展 对于某一节点进行前插操作
前插操作是指在某结点的前面插入一个新结点,后插操作的定义刚好与之相反。在单链表插入算法中,通常采用后插操作

//将*s 结点插入到*p 之前的主要代码片段如下:
s->next = p->next; //修改指针域,不能颠倒
p->next = s;
temp = p->data; //交换数据域部分
p->data = s->data;
s->data = temp;
  1. 删除结点操作
    删除结点操作是将单链表的第i个结点删除。先检查删除位置的合法性,后查表中第i-1个结点,即 被删除结点的前驱结点,在将其删除
    假设结点p 为找到的被删结点的前驱结点,为实现这一操作后的逻辑关系的变化,仅需修改 p的指针域,即将p的指针域next 指向q的下一个结点。
p = GetElem(L,i-1); //查找删除位置的前驱结点
q = p->next;  //令q指向被删除结点
p->next = q->next; //将 *q 结点从链中“断开”
free(q);  //释放结点的存储空间

删除结点 *p

q = p->next;  //令q指向*p 的后继结点
p->data = q->next->data;      //用后继结点的数据覆盖
p->next = q->next; //将 *q 结点从链中“断开”
free(q);  //释放结点的存储空间
  1. 求表长操作
    求表长操作就是计算单链表中数据结点的(不含表头)个数
    不带头结点和带头结点在求表长上会略有不同。对不带头结点的单链表,当表为空时,要单独处理。

2.3.3 双链表

		单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问某个结点的前驱结点(插入、删除操作时),只能从头开始遍历,访问后继结点的时间复杂度为 O(I),访问前驱结点的时间复杂度为 O(n)。

为了克服单链表的上述缺点,引入了双链表,双链表结点中有两个指针 prior 和next,分别指向其前驱结点和后继结点
1. 双链表的插入操作
在双链表中p 所指的结点之后插入结点 *s
代码片段如下

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

2.双链表的删除操作
删除双链表中的结点 p的后继结点q,删除操作的代码片段如下

p->next = q->next;   
q->next->prior = p;
free(q);   //释放结点空间

2.3.4 循环链表

  1. 循环单链表

循环单链表和单链表的区别在于,表中追后一个结点的指针不是null ,而是改为指向头结点,从而整个链表形成一个环

循环单链表的插入、删除算法与单链表的几乎一样,所不同的是若操作是在表尾进行,则扩行的操作不同,以让单链表继续保持循环的性质。当然,正是因为循环单链表是一个“环”,因此在任何一个位置上的插入和删除操作都是等价的,无须判断是否是表尾。
在单链表中只能从表头结点开始往后顺序遍历整个链表,而循环单链表可以从表中的任意个结点开始遍历整个链表。有时对循环单链表不设头指针而仅设尾指针,以使得操作效率更高。其原因是,若设的是头指针,对在表尾插入元素需要O(n)的时间复杂度,而若设的是尾指针r->next 即为头指针,对在表头或表尾插入元素都只需要 0(1)的时间复杂度。

2.循环双链表

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

2.3.5 静态链表

静态链表借助数组来描述线性表的链式存储结构,结点也有数据域 data 和指针域next与前面所讲的链表中的指针不同的是,这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。
在这里插入图片描述
静态链表以next==-1作为其结束的标志。静态链表的插入、删除操作与动态链表的相同,只需要修改指针,而不需要移动元素。总体来说,静态链表没有单链表使用起来方便,但在一些不支持指针的高级语言(如 Basic)中,这是一种非常巧妙的设计方法。

2.3.6 顺序表和链表的比较

  1. 存取(读写)方式
    顺序表可以顺序存取,也可以随机存取
    链表只能从表头顺序存取元素
  2. 逻辑结构与物理结构
    采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。
    采取链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的。
  3. 查找 插入和删除操作

对于按值查找,顺序表无序式,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为 O(log2n)

对于按序号查找,
顺序表支持随机访问,时间复杂度仅为O(1)
而链表的平均时间复杂度为O(n)。
顺序表的插入、删除操作,平均需要移动半个表长的元素。
链表的插入、删除操作,只需修改相关结点的指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大。

  1. 空间分配

顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置,预先分配过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量元系,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。

总结
1,甚于存储的考虑
难以估计线性表的长度或存储规模时,不宜采用颇序表:链表不用事先估计存储规模,但链表的存储密度较低,显然链式存储结构的存储密度是小于1的。
2,基于运算的考虑 在顺序表中按序号访问 ;的时间复杂度为 0(1),而表中号的时间复杂度为 O(n)
因此若经常做的运算是按序号访问数据元素,则显然顺序表优于链表。
在顺序表中进行插入、删除操作时,平均移动表中一半的元素,当数据元素的信息量较大且表较长时,这一点是不应忽视的;
在链表中进行插入、删除操作时,虽然也要找插入位置,但操作主要是比较操作, 链表优于顺序表。
3,基于环境的考虑
顺序表容易实现,任何高级语言中都有数组类型;
链表的操作是基于指针的,相对来讲,前者实现较为简单,这也是用户考虑的一个因素。
总之,两种存储结构各有长短,选择哪一种由实际问题的主要因素决定。通常较稳定的线性表选择顺序存储,
而频繁进行插入、删除操作的线性表(即动态性较强)宜选择链式存储

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小*-^-*九

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值