首页 > 其他分享 >【数据结构】详细剖析线性表

【数据结构】详细剖析线性表

时间:2023-12-31 21:33:07浏览次数:46  
标签:链表 结点 return 线性表 元素 剖析 false 数据结构 指针

顺序表与链表的比较

【数据结构】详细剖析线性表_线性表

导言

大家好,很高兴又和大家见面啦!!!

经过这段时间的学习与分享,想必大家跟我一样都已经对线性表的相关内容比较熟悉了。为了更好的巩固线性表相关的知识点,下面我们一起将线性表这个章节的内容梳理一遍吧。

一、线性表

线性表的相关概念

线性表时具有相同数据类型【数据结构】详细剖析线性表_数据结构_02个数据元素的有限序列,其中【数据结构】详细剖析线性表_线性表_03为表长,当【数据结构】详细剖析线性表_链表_04时线性表是一个空表。

如果我们以【数据结构】详细剖析线性表_C语言_05作为线性表的各个元素时,元素【数据结构】详细剖析线性表_顺序表_06为线性表唯一的第一个元素,称为表头元素;元素【数据结构】详细剖析线性表_链表_07为线性表唯一的最后一个元素,称为表尾元素


线性表的逻辑特性是:

  • 除了表头元素外,每个元素有且仅有一个直接前驱
  • 除了表尾元素外,每个元素有且仅有一个直接后继

线性表的特点是:

  • 表中元素的个数是有限的
  • 表中元素具有逻辑上的顺序性,表中元素有其先后次序
  • 表中元素都是数据元素,每个元素都是单个元素
  • 表中元素的数据类型都相同,这意味着每个元素所占空间大小相同
  • 表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素的内容

线性表的基本操作:

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

复习完了线性表的相关知识点,下面我们来看一下线性表的两种存储结构;

二、线性表的存储结构

线性表的各元素在内存中存储时满足在逻辑上相邻,也在物理位置上相邻,这样的存储结构称为顺序存储,又称为顺序表。通常用高级程序设计语言中的数组来表示顺序存储结构

线性表中的各个元素在内存中存储时满足在逻辑上相邻,在物理位置上不一定相邻,元素与元素之间通过“链”建立起来的逻辑关系,这样的存储结构称为链式存储,又称为链表。


对于顺序表和链表它们又有哪些异同点呢?

三、顺序表和链表的相同点

顺序表和链表的相同点都是建立在它们的本质上面的,下面我们就来看看它们具有哪些相同点:

  • 顺序表和链表的元素个数都是有限的;
  • 顺序表和链表的元素类型都是相同的;
  • 顺序表和链表的元素都满足在逻辑上相邻;
  • 顺序表和链表的元素都是数据元素,且每个元素都是单一元素;
  • 顺序表和链表的元素都满足只有一个唯一的表头元素和一个唯一的表尾元素;
  • 顺序表和链表的逻辑特性都是:
  • 除了表头元素外,每个元素都有且仅有一个直接前驱;
  • 除了表尾元素外,每个元素都有且仅有一个直接后继;
  • 循序表和链表都能进行创建、销毁、增删改查等基本操作;
  • 顺序表和链表在金泰

因此,我们可以得到结论:

  • 顺序表和链表的本质都是线性表,只不过线性表强调的数据元素在内存上的逻辑结构,而顺序表与链表强调的是数据元素在内存上的存储结构

介绍完了顺序表与链表的相同点,接下来我们继续来看一看它们的不同点;

四、顺序表与链表之间的差异

  1. 存取方式不同
  • 顺序表是顺序存取结构,同时也是随机存取结构;
  • 链表是链式存取结构,同时也是非随机存取结构;

因此,我们想要访问顺序表的各个元素时,只需要知道元素的下标就可以直接查找到,此时的时间复杂度是O(1);而链表想要访问第i个元素时,需要从表头开始进行遍历,此时的时间复杂度是O(n);

  1. 物理结构不同
  • 顺序表的各个元素在物理位置上也是相邻的;
  • 链表的各个元素在物理位置上不一定相邻;

因此顺序表在存储时可以做到密集存储,但是需要在内存中消耗一块连续的存储空间;而链表在存储时是离散存储的,但是需要消耗额外的空间来存放指向下一个结点的指针;

  1. 创建方式不同
  • 顺序表在创建时需要明确最大表长、当前表长、数据存放的空间;
  • 链表在创建时需要明确数据域和指针域;
  • 顺序表在初始化时可以只初始化当前表长;
  • 链表在初始化时需要对头结点进行初始化,链表的表长需要通过额外的计数器来确定;

因此顺序表在创建后表的大小是固定的,虽然可以通过malloccallocrealloc来申请新的空间达到修改表长的目的,但是在申请完新的空间后还伴随着大量的复制操作,此时的时间复杂度是O(n);而链表在创建时链表的大小是可以随时进行修改的,只需要在插入新结点时修改对应结点的指针域就行,此时的时间复杂度是O(1);

  1. 查找方式不同
  • 顺序表在进行按位查找时,可以直接通过位序查找到对应的元素;
  • 链表在进行按位查找时,需要通过从头结点往后进行遍历才能找到对应的元素;
  • 顺序表在进行按值查找时,可以通过不同的查找方式进行快速查找;
  • 链表在进行按值查找时,需要从头结点开始往后进行顺序查找;

因此顺序表在进行按位查找时的时间复杂度是O(1),在进行按值查找时的最坏时间复杂度为O(n);而链表在进行按位查找与按值查找的时间复杂度都是O(n);

  1. 插入与删除方式不同
  • 顺序表在进行插入与删除时需要移动大量的元素;
  • 链表在进行插入与删除时只需要修改对应的指针域;

因此顺序表的插入与删除操作的时间复杂度为O(n);而链表的插入与删除的时间复杂度为O(1);

  1. 空间分配不同
  • 顺序表在进行动态分配时,需要申请一块连续的空间,且空间大小不能修改;
  • 链表在进行动态分配时,需要申请对应的结点的空间,空间大小可以随时修改;
  • 顺序表在修改表长时,需要移动大量的元素;
  • 链表在修改表长时,只需要修改对应结点的指针域;

因此,顺序表在修改表长时对应的时间复杂度为O(n);而链表在修改表长时对应的时间复杂度为O(1);


在了解了顺序表与链表的异同点后,我们又应该如何进行选择呢?

五、存储结构的选择

我们在实际运用中要选择对应的存储结构,就需要结合具体的情况进行分析。从前面的介绍中我们可以看到,顺序表的优势是在查找元素的上面,而链表的优势是在增加、删除上面。因此我们在选择时需要考虑以下几点:

  1. 存储的考虑

在不确定线性表的长度与存储规模时,宜采用链表;

在确定线性表的表长,且需要密集存储时,宜采用顺序表;

  1. 运算的考虑

在表长固定的情况下需要经常对表中元素进行按序号访问时,宜采用顺序表;

在表长需要进行增加、删除的操作时,宜采用链表;

  1. 环境的考虑

在不支持指针的计算机语言中,虽然可以通过静态链表来实现链表,但是顺序表会更加简单一点;

在支持指针的计算机语言中,就需要从存储与运算这两个方面的角度去考虑了。


总之,两种存储结构各有长短,选择哪一种还是得由实际情况来决定。通常较稳定的线性表选择顺从存储;需要平方进行插入、删除操作的线性表选择链式存储。要想更加深刻的理解顺序存储与链式存储之间的优缺点,还是需要熟练的掌握它们才行。

六、静态顺序表的基本操作

在前面我们只介绍了动态顺序表的基本操作,今天我将补上静态顺序表基本操作,其对应的的基本格式如下所示:

//顺序表的静态创建
#define MaxSize 10//定义最大表长
typedef struct Sqlist {
	ElemType data[MaxSize];//存储数据元素的数组
	int length;//当前表长
}Sqlist;//重命名后的静态顺序表类型名
//顺序表的初始化
bool InitList(Sqlist* L){
	if (!L)
		return false;//指针L为空指针时返回false
	L->length = 0;//初始化当前表长
	return true;
}
//顺序表的按位查找
ElemType GetElem(Sqlist L, int i){
	if (i<1 || i>L.length)//位序小于1,或者位序大于当前表长时表示位序不合理
		return -1;//位序不合理,返回-1
	return L.data[i - 1];//位序合理,返回下标为i-1的元素的值
}
//顺序表的按值查找
int LocateElem(Sqlist L, ElemType e) {
	for (int i = 0; i < L.length; i++)
	{
		if (L.data[i] == e)
			return i + 1;//找到对应的值返回位序i+1
	}
	return -1;//没有对应的值返回-1
}
//顺序表的插入
bool ListInsert(Sqlist* L, int i, ElemType e) {
	if (i<1 || i>L->length + 1)//位序小于1,或者位序大于当前表长+1时表示位序不合理
		return false;//位序不合理返回false
	if (L->length >= MaxSize)
		return false;//当前线性表已存满,返回false
	for (int j = L->length; j >= i; j--) {
		L->data[j] = L->data[j - 1];//元素往后移动
	}
	L->data[i - 1] = e;//将元素e插入位序i的位置
	L->length++;//当前表长+1
	return true;//插入成功返回true
}
//顺序表的删除
bool ListDelete(Sqlist* L, int i, ElemType e) {
	if (i<1 || i>L->length)//位序小于1,或者位序大于当前表长时位序不合理
		return false;//位序不合理返回false
	e = L->data[i - 1];
	for (int j = i - 1; j < L->length; j++) {
		L->data[j] = L->data[j + 1];//元素往前移
	}
	L->length--;//当前表长-1
	return true;//删除成功返回true
}

七、无头结点单链表的基本操作

前面的介绍中,我只介绍了有头结点的单链表与双链表的基本操作,这里给大家补上无头结点的单链表的基本操作,其对应的格式如下所示:

//无头结点单链表的基本操作
//单链表类型的声明
typedef struct LNode {
	ElemType data;//数据域
	struct LNode* next;//指针域
}LNode, * LinkList;//重命名后的结点类型与单链表类型名
//单链表的初始化
bool InistList(LinkList* L) {
	if (!L)
		return false;//L为空指针时返回false
	L= NULL;//头指针初始化为空指针
	return true;//初始化完返回true
}
//单链表的创建——头插法
LinkList List_HeadInsert(LinkList* L){
	if (!L)
		return NULL;//当L为空指针时返回NULL
	LNode* s = NULL;//指向表头结点的指针
	ElemType x = 0;//存放数据元素的变量
	……;//获取数据元素
	while (x != EOF)//为创建过程设置一个终点
	{
		//头插操作
		if (!(*L))//单链表为空表时
		{
			*L = (LNode*)calloc(1, sizeof(LNode));//创建表头结点
			assert(*L);//创建失败报错
			(*L)->data = x;//将数据元素放入数据域中
			(*L)->next = NULL;//表头结点指针域指向空指针
		}
		else {
			s = (LNode*)calloc(1, sizeof(LNode));//创建新结点
			assert(s);//创建失败报错
			s->data = x;//数据元素存放进数据域中
			s->next = (*L)->next;//新结点指针域指向表头结点
			(*L)->next = s;//头指针指向新结点
		}
		……;//获取新的数据元素
	}
	return *L;//返回创建好的单链表
}
//单链表的创建——尾插法
LinkList List_TailInsert(LinkList* L) {
	if (!L)
		return NULL;//当L为空指针时返回NULL
	LNode* r = NULL;//指向表尾结点的指针
	LNode* s = NULL;//指向新结点的指针
	ElemType x = 0;//存放数据元素的变量
	……;//获取数据元素
	while (x != EOF)//为创建过程设置一个终点
	{
		//尾插操作
		if (!(*L))//单链表为空表时
		{
			*L = (LNode*)calloc(1, sizeof(LNode));//创建表头结点
			assert(*L);//创建失败报错
			(*L)->data = x;//将数据元素放入数据域中
			(*L)->next = NULL;//表头结点指针域指向空指针
			r = (*L);//表尾指针指向表头结点,此时的表头结点也是表尾结点
		}
		else {
			s = (LNode*)calloc(1, sizeof(LNode));//创建新结点
			assert(s);//创建失败报错
			s->data = x;//数据元素存放进数据域中
			s->next = r->next;//新结点指针域指向表尾结点
			r->next = s;//表尾结点的指针域指向新结点
			r = s;//表尾指针指向新结点
		}
		……;//获取新的数据元素
	}
	return *L;//返回创建好的单链表
}
//单链表的按位查找
LNode* GetElem(LinkList L, int i) {
	if (!L)
		return NULL;//单链表为空表时返回NULL
	if (i < 1)
		return NULL;//位序不合理时返回NULL
	int j = 1;//单链表结点的位序
	LNode* s = L->next;//指向查找结点的指针
	while (j < i && s)//当位序相等时,结束查找;当s为空指针时,结束查找
	{
		s = s->next;//向后遍历
	}
	return s;//查找结束,返回当前结点
}
//单链表的按值查找
LNode* LocateElem(LinkList L, ElemType e) {
	if (!L)
		return NULL;//当表为空表时返回NULL
	LNode* s = L->next;//指向查找结点的指针
	while (s->data == e && s)//当元素相等时,结束查找,当s为空指针时,结束查找
	{
		s = s->next;//向后遍历
	}
	return s;//查找结束,返回当前结点
}
//单链表的前插操作——指定结点
bool InsertPriorNode(LNode* p, ElemType e) {
	if (!p)
		return false;//p为空指针,返回false
	LNode* s = (LNode*)calloc(1, sizeof(LNode));//创建新结点
	assert(s);//创建失败,报错
	s->data = p->data;//p结点的数据元素放入新结点的数据域中
	p->data = e;//插入的数据元素放入p结点的数据域中
	s->next = p->next;//新结点的指针域指向p结点的后继结点
	p->next = s;//p结点的指针域指向新结点,完成插入操作
	return true;//插入成功,返回true
}
//单链表的前插操作——指定位序
bool InsertPriorNode(LinkList* L, int i, ElemType e) {
	if (!L)
		return false;//L为空指针时返回false
	if (!(*L))
		return false;//单链表为空表时返回false
	if (i < 1)
		return false;//位序不合理时,返回false
	LNode* p = GetElem(*L, i - 1);//指向前驱结点的指针
	LNode* s = (LNode*)calloc(1, sizeof(LNode));//创建新结点
	assert(s);//创建失败,报错
	s->data = e;//数据元素放入新结点的数据域中
	s->next = p->next;//新结点的指针域指向p结点的后继结点
	p->next = s;//p结点的指针域指向新结点,完成插入操作
	return true;//插入成功,返回true
}
//单链表的后插操作——指定结点
bool InsertNextNode(LNode* p, ElemType e) {
	if (!p)
		return false;//p为空指针,返回false
	LNode* s = (LNode*)calloc(1, sizeof(LNode));//创建新结点
	assert(s);//创建失败,报错
	s->data = e;//数据元素放入新结点的数据域中
	s->next = p->next;//新结点的指针域指向p结点的后继结点
	p->next = s;//p结点的指针域指向新结点,完成插入操作
	return true;//插入成功,返回true
}
//单链表的后插操作——指点位序
bool InsertNextNode(LinkList* L, int i, ElemType e) {
	if (!L)
		return false;//L为空指针时返回false
	if (!(*L))
		return false;//单链表为空表时返回false
	if (i < 1)
		return false;//位序不合理时,返回false
	LNode* p = GetElem(*L, i);//指向位序i结点的指针
	if (!p)
		return false;//结点p为空指针时,返回false
	LNode* s = (LNode*)calloc(1, sizeof(LNode));//创建新结点
	assert(s);//创建失败,报错
	s->data = e;//数据元素放入新结点的数据域中
	s->next = p->next;//新结点的指针域指向p结点的后继结点
	p->next = s;//p结点的指针域指向新结点,完成插入操作
	return true;//插入成功,返回true
}
//单链表的删除操作——指定位序
bool ListDelete(LinkList* L, int i, ElemType e) {
	if (!L)
		return false;//L为空指针时返回false
	if (!(*L))
		return false;//单链表为空表时返回false
	if (i < 1)
		return false;//位序不合理时返回false
	LNode* p = GetElem(*L, i - 1);//位序i的前驱结点
	if (!p)
		return false;//结点p为空指针时,返回false
	LNode* q = p->next;//需要删除的结点
	if (!q)
		return false;//结点q为空指针时,返回false
	e = q->data;//需要删除的元素存放在变量e中
	p->next = q->next;//前驱结点的指针域指向删除结点的后继结点
	free(q);//释放被删除的结点空间
	return true;//删除完成,返回true
}
//单链表的删除操作——指定结点
bool ListDelete(LinkList* L,LNode* p, ElemType e) {
	if (!L)
		return false;//L为空指针时返回false
	if (!(*L))
		return false;//单链表为空表时返回false
	if (!p)
		return false;//p为空指针时,返回false
	LNode* q = (*L)->next;//指向前驱结点的指针
	while (q->next != p)//判断q是否为p的前驱结点
	{
		q = q->next;//向后遍历
	}
	q->next = p->next;//前驱结点指向删除结点的后继结点
	free(p);//释放删除结点的空间
	return true;//删除完成,返回true
}

结语

到这里咱们今天的内容就全部介绍完了,线性表的相关知识点也全部介绍完了,希望这些内容能帮助大家更好的学习线性表。 接下来我们将开始进入数据结构——栈、队列和数组的学习,大家记得关注哦!最后,感谢各位的翻阅,咱们下一篇再见!!!

标签:链表,结点,return,线性表,元素,剖析,false,数据结构,指针
From: https://blog.51cto.com/u_16231477/9050444

相关文章

  • 2023 408数据结构总结
    持续更新完善中。一、线性表顺序存储的有序表非空双向链表时间复杂度二、栈、队列和数组稀疏矩阵3三元组:(行、列、值)表示矩阵非0元素三、树与二叉树二叉树二叉树的遍历5先序遍历NLR(根左右)中序遍历LNR后序遍历LRN==【题目】==树与二叉树的应用4哈弗曼编码的加......
  • redis设置database 不生效剖析
    (设置database不生效剖析)前言  事情是这样的今天在拉取了同事的代码做redis缓存设置的时候,发现即使已经设置了database,但是存数据的时候还是用的默认0数据库。这引起了我的好奇,遂开始琢磨是什么情况造成的这种现象。配置上述仅为测试代码问题,为了便于维护可以这么写......
  • 【数据结构】链式家族的成员——循环链表与静态链表
    循环链表与静态链表导言大家好!很高兴又和大家见面啦!!!经过前面的介绍,相信大家对链式家族的成员——单链表与双链表的相关内容都已经熟练掌握了。前面我们重点介绍了通过C语言来实现单链表与双链表的一些基本操作,希望大家私下能够多多练习一下,帮助自己去吸收消化这些内容。在今天的篇......
  • 数据结构应用之桶排序
    问:有10G的订单数据,希望订单金额(假设都是正整数)进行排序,但我们内存有限,只有几百MB,如何进行排序?答:因内存有限,需要排序的数据量巨大,所以,此时需要外部排序,外部排序采用的是一种分治思想,外部排序最常用的是多路归并排序,即将大数据切成多份一次可以载入内存的小数据,对小数据进行内......
  • 【数据结构】C语言实现双链表的基本操作
    双链表导言大家好,很高兴又和大家见面啦!!!经过前面几个篇章的内容分享,相信大家对顺序表和单链表的基本操作都已经熟练掌握了。今天咱们将继续分享线性表的链式存储的第二种形式——双链表。在今天的内容中,咱们将介绍双链表的创建以及一些基本操作,接下来跟我一起来看看吧!一、单链表与双......
  • 数据结构实验代码分享 - 4
    迷宫与栈问题(图的应用)【问题描述】以一个m*n的长方阵表示迷宫,0和1分别表示迷宫中的通路和障碍。设计一个程序,对任意设定的迷宫,求出一条从入口到出口的通路,或得出没有通路的结论。输入:行列迷宫,0表示无障碍,1表示有障碍输出:一条Path或“NOPATH” 注:参考了《数据结......
  • 数据结构实验代码分享 - 3
    哈夫曼编码/译码系统(树应用)[问题描述]任意给定一个仅由26个大写英文字母组成的字符序列,根据哈夫曼编码算法,求得每个字符的哈夫曼编码。要求:1)输入一个由26个英文字母组成的字符串,请给出经过哈夫曼编码后的编码序列及其编码程度。(编码)2)采用上一问题的哈夫曼编码,给定一串编......
  • 数据结构之<散列表>的介绍
    简介散列表也叫做哈希表,是根据键值对(key,value)进行存储的一种数据结构。散列表利用哈希函数将给定的键映射到一个特定的位置(通常是数组索引),这个位置通常被称为哈希值或哈希地址。这里可以举个微信好友列表的例子说明,存放好友首字母的表对应的就是散列表。1.哈希函数哈希函数是......
  • 数据结构&&集合总结
    总结数据结构数据结构:保存数据的一种方式常见的数据结构通过数组来保存,基于数组的数据结构(动态数组,长度可变的数组)基于数组的结构的优缺点​ 1.通过下标查询元素,效率高​ 2.通过下标修改元素,效率高​ **查改快**​ 在需要扩容的时候:添加慢,删除慢,插入元素慢......
  • 【数据结构】C语言实现单链表的基本操作
    单链表基本操作的实现导言大家好,很高兴又和大家见面啦!!!在上一篇中,我们详细介绍了单链表的两种创建方式——头插法与尾插法,相信大家现在对这两种方式都已经掌握了。今天咱们将继续介绍单链表的基本操作——查找、插入与删除。在开始今天的内容之前,我们先通过尾插法创建一个单链表,如......