【数据结构之线性表】熬夜暴肝,有亿点详细

前言

我们今天来学习线性表,线性表是数据结构中比较简单的一个数据结构了,但是它的重要性不容忽略,废话不多,直接正文。

初识线性表

线性表的定义

线性表是具有相同类型的n(n>=0)个元素的有限序列,其中n为表长,当n=0时,该表为空表

线性表的特点:

1.表中元素个数有限
2.表中元素具有逻辑上的顺序性,在序列中各个元素排序有其先后次序
3.表中元素都是数据元素,每个元素都是单个元素
4.表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间
5.表中元素具有抽象性,即讨论元素间一对一的逻辑关系,而不考虑元素究竟表示的内容
6.线性表是一种逻辑结构,表示元素之间一对一相邻的关系
7.线性表在数据元素有限集中,除第一个元素无直接前驱,最后一个元素无直接后续以外,每个数据元素有且仅有一个直接前驱元素和一个直接后继元素。

线性表的基本操作

还记得之前说过运算的定义依靠数据的逻辑结构实现,运算的实现针对于数据的存储结构吗?不过不记得你可以去看看数据结构入门篇

线性表是一种逻辑结构,因此具体操作我们是无法实现的,不过我们可以定义这些操作。

我们主要从参数,返回值来说一下基本操作吧

&L指传入一个引用,这个引用指向线性表表头

InitList(&L):
初始化表。构造一个空的线性表。
DestroyList(&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。
Length(L):
求表长。返回线性表L的长度,即L中数据元素的个数。

以上就是线性表的对基本操作的定义,对这些操作的实现我们得依靠存储结构来说了。

线性表根据存储结构的不同又分为顺序表与链表,接下来我们就逐一说一下他们两个吧!

顺序表

顺序表就是线性表的顺序表示,它的主要特点是逻辑结构与存储结构相同都是连续的。

顺序表的定义

一组地址连续存放的存储单元依次存放线性表的元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻

这样一说会感觉这个和数组十分相似啊。的确十分相似,但是却有所不同,详见下文。

顺序表与数组

相同之处:数组与线性表相同都是一组地址连续存放的存储单元依次存放元素

不同之处:数组下标从0开始,而顺序表下标从1开始。数组有最大存储空间是静态的,而顺序表可以是动态的。

定义的实现

顺序表定义的实现有两种分别是静态分配和动态分配。它们的区别是使用静态分配时顺序表的容量是被规定好的,无法增加的。使用动态分配时顺序表的容量可以变大。

实现数组静态分配:

# define MaxSize 100;
typedef struct{
	ElemType date[MaxSize];//ElemType是某种元素类型
	int length ;//顺序表长度
}sqList;//顺序表名字

我们使用结构体实现顺序表的定义,这里使用了某种类型的数组存放某种数据,数组容量为常量MaxSize。

实现数组动态分配:

# define MaxSize 100;
typedef struct{
	ElemType *date;
	int length;
}sqList;

动态分配语句:

C: L.data = (Elemtype*)malloc(sizeof(ElemType)*InitSize);
C++:L.data = new ElemType[InitSize]

顺序表的基本操作

顺序表的基本操作有插入,删除,查找

顺序表的插入

这个是给顺序表L在指定位置i处,插入元素e

bool ListInsert (SqList &L,int i,ElemType e){
//除去不合法的i,保证不能空下一个位置
//length+1是因为有可能最后一个位置插入
	if(i<1||i>L.length+1){
		return false;
	}
	if(i>=MaxSize){
		return false;
	}
	//每个元素向后移动给i-1下标位置空出来
	for(int j=L.length;j>=i;j--){
		L.date[j]=L.date[j-1];
	}
	L.date[i-1]=e;
	return true;
}

顺序表的删除

传入要删除的位置和一个元素的引用来保存要删除元素的值

bool ListDelete(SqList &L,int i,ElemType &e){
	if(i<1||i>L.length){
		return false;
	}
	//保存要删除的元素
	e=L.date[i-1];
	//后面的元素向前逐个移动
	for(int j=i;j<L.length;j++){
		L.date[j-1]=L.date[j];
	}
	L.length--;
	return true;
}

顺序表的查找

按值查找元素位置(位置从1开始)

int ListDelete(Sqlist L,ElemType e){
	int i;
	for(i=0;i<L.length;i++){
		if(L.date[i]==e)
			return i+1;
	}
	return 0;
}

如果是按位置查找直接L.date[i-1]即可得位置为i处的值是多少。

链表

链表我们最常见的就是单链表,除此之外还有一些特殊链表,我们重点还是说说单链表。

单链表的定义

单链表是线性表的链式存储,通过一组任意的存储单元来存储线性表中的数据元素,通过指针来实现逻辑关系

定义的实现

单链表的每一块都分为数据域指针域,指针域用来存放逻辑上下一块内存的地址

typedef struct LNode{
	ElemType date;
	struct LNode *next;
}LNode,*LinkList;

链表又有两种,一种是有头结点的,一种是没有头结点的。我们为了提高效率一般使用带头结点的单链表,后面操作讲解也都是带头结点的链表。

头结点放在链表的最前面,结构与每一块结点相同,不过数据域中为空或者存放链表长度等重要元素,头结点的指针域指向链表的第一个元素。

使用头结点能帮助我们处理头部时方法与其他位置处理方法相同,操作时不需要判断是否为首元素了,大大提高了代码效率。

单链表的基本操作

单链表基本操作有:创建单链表,查找,插入和删除。

创建单链表

创建单链表我们分为头插法创建和尾插法创建,区别就是生成的链表顺序与插入时顺序是否相同。显然头插法创建单链表是顺序相同。
头插法
头插法就是每次插入元素时在链表的头结点后插入,最后生成的链表顺序与插入时顺序相反

LinkList List_HeadInsert (LinkList &L){
	LNode *s;
	int x;
	L=(LinkList)malloc(sizeof(LNode));
	L->next=NULL;
	scanf("%d",&x);
	while(x!=99999){
		s=(LNode*)malloc(sizeof(LNode));
		s->date=x;
		s->next=L->next;
		L->next=s;
		scanf("%d",&x);
	}
	return L;
}

尾插法
尾插法就是每次插入元素时在链表的末尾插入元素,最后生成的链表顺序与插入时顺序相同

LinkList List_HeadInsert (LinkList &L){
	LNode *s,*r;
	int x;
	L=(LinkList)malloc(sizeof(LNode));
	L->next=NULL;
	*r=L;
	scanf("%d",&x);
	while(x!=99999){
		s=(LNode*)malloc(sizeof(LNode));
		s->date=x;
		r->next=s;
		r=s;
		scanf("%d",&x);
	}
	r->next=NULL;
	return L;
}

单链表的查找

查找又根据需求分为按序查找和按值查找。

按序查找

如果查找时需要查找某个位置元素的结点,那么就是按序查找。传入的参数为一个链表和需要查找的位置。

LNode *GetElem(LinkList L,int i){
	int j=1;
	LNode *p=L->next;
	if(i==0)
		return L;
	if(i<1)
		return NULL;
	while(p&&j<i){
		p=p->next;
		j++;
	}
	return p;
}

按值查找

如果需要在链表中查找某个值e的结点,则:

LNode *LocateElem(LinkList L,ElemType e){
	LNode *p=L->next;
	while(p!=NULL&&p->date!=e){
		p=p->next;
	}
	return p;
}

单链表的插入

插入有两种,第一种是插入到指定位置,第二种是默认在链表头部插入
插入到指定位置

要求插入到i位置处时,需要先找到i-1处的结点,然后插入

p=GetElem(L,i-1);
s->next=p->next;
p->next=s;

默认插入头部

在链表头部插入元素时:

s->next=head;
head=s;

单链表的删除

删除i位置的结点:

p=GetElem(L,i-1);
q=p->next;
p->next=q->next;
free(q);

如果需要删除指定结点*q那么只需要:

q=p->next;
p->date=p->next->date;
p->next=q->next;
free(q);

特殊链表

特殊链表有:双链表,循环链表和静态链表。

双链表

双链表顾名思义就是比单链表高级了一点,一个双链表的结点在单链表的基础上又增加了一个指针,这个指针指向它的前一个结点
结点情况如下:

指针域 数据域 指针域
prior date next

定义的实现:

typedef struct DNode{
	ElemType date;
	struct DNode *prior,*next;
}DNode,*DLinklist;

结点发生改变了。对链表的一些操作也会发生改变
插入操作:

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

删除操作:

p->next=q->next;
q->next->prior=p;
free(q);

循环链表

之前的链表都是条状的,循环链表是头尾相连了,就是将链表的尾部与头部连接起来。有因为条状链表有单链表与双链表之分,所以循环链表也有两种分别是:循环单链表循环双链表

对于循环单链表我们一般设置尾指针这样操作效率会更高。

对于循环双链表我们头结点的prior结点指向最后一个节点,尾结点的next指针指向头结点,以形成循环双链表。

之前的链表我们判断是否为空表都是看最后一个结点的next结点是否为空。显然,循环链表不可以这样判断,我们判断方式如下:

循环单链表判断空链表条件:

if(L->next==L)

循环双链表判断空链表条件:

if(L->next==L&&L->prior==L)

静态链表

静态链表这个名字听起来高大上,其实也是非常简单的,它和数组是非常相似的。先来看它的结构组成:

#define MaxSize 50
typedef struct DNode{
	ElemType date;
	int next;
}SLinkList[MaxSize];

它其实就是一个数组只不过不连续而已,结点分为两部分。date域存放数据,而next域存放下一个存放数据的数组的下标

使用数组下标索引实现了逻辑结构。

线性表的常用操作

到此为止我们已经学过了线性表按照存储结构不同的两种分类,分别是顺序表与链表,以及他们的一些基本操作。但是在我们以后在做题时候,不会直接来考插入,删除等操作,它们都是贯穿在很多的大的操作中去的,我愿把他们称为非基本操作。。

下面我们来讲一下对于线性表的一些常考的操作分别是:最值,逆置,归并

最值

最值顾名思义就是求出线性表中的最大以及最小值,我们分别用顺序表与链表实现。

顺序表实现

int min = L[0];
int max = L[0];
for(int i = 0; i < n; i++){
	if(min > L[i])
		min = L[i];
	if(max < L[i])
		max = L[i];
}

链表实现:

int min = p -> next ->data;
int max = p -> next ->data;
p = p -> next;
while(p != NULL){
	if(min > p ->data)
		min = p ->data;
	if(max < p ->data)
		max = p ->data;
	p = p ->next;
}

逆置

逆置就是把线性表中元素顺序调反

顺寻表实现

int i = 0;
int j = n-1;
while(i < j){
	temp = L[i];
	L[i] = L[j];
	L[j] = temp;
	i++;
	j--;
}

链表实现

//r为尾结点
while(p ->next != r){
	temp = p -> next;
	p -> next = temp -> next;
	temp -> next = r -> next;
	r -> next = temp;
}

归并

归并就是把两个线性表变为一个,这里其实又有两种了,一种是无序归并,还有一种是有序的归并。无序归并十分简单,这里就不说了,我们主要讲的就是有序归并

有序归并的栗子:
L1=(1,8,6)
L2=(5,6,9)
归并后为:L3=(1,5,6,6,8,9)

顺序表实现

int i = 0, j = 0;
int k = 0;
for( ; i < L1_Size && j < L2_Size; k++){
	if(L1[i] < L2[j])
		L[k] = L1[i++];
	else
		L[k] = L2[j++];
}
while(i < L1_Size)
	L[k++] = L1[i++];
while(j < L2_Size)
	L[k++] = L2[j++];

链表实现归并

while(p->next!=NULL && q->next!=NULL){
	if(p->next->data < q->next->data){
		r->next = p->next;
		p->next= p->next->next;
		r = r->next;
	}
	else{
		r->next = q->next;
		q->next= q->next->next;
		r = r->next;
	}
}
if(p->next != NULL) r -> next = p ->next;
if(q->next != NULL) r -> next = q ->next;
free(p); 
free(q)

顺序表与链表的比较及选择

到这里我们的顺序表与链表就全部学习完了,那我们在什么时候应该选择顺序表,什么时候又该选择链表呢?我们对顺序表与链表做一个比较来看看吧!!

逻辑结构和物理结构

顺序表:

逻辑相邻物理上也相邻,通过相邻表示逻辑关系

单链表:

逻辑相邻物理上不一定相邻,通过指针表示逻辑关系

基本操作的时间复杂度
插入和删除:

单链表为O(1)(节点指针已知);O(n)(节点指针未知),但操作时只需修改指针

顺序表为O(n)且需要大量移动元素

查找:

按值查找中单链表和顺序表(无序)都为O(n)

按序查找中单链表为O(n),顺序表为O(1)

内存空间
顺序存储:

无论静态分配还是非静态都需要预先分配合适的内存空间 静态分配时预分配空间太大回造成浪费,太小会造成溢出

动态分配时虽不会溢出但是扩充需要大量移动元素,操作效率低

链式存储:

在需要时分配结点空间即可,高效方便,但指针要使用额外空间

如何选择合适的线性表呢?

根据上面的比较做了以下选择总结:

问题情况 顺序表(较稳定) 单链表(较动态)
规模难以估计 *
存储密度大 *
按序号访问 *
插入与删除 *
基于数组 *
基于指针 *

结言

本来打算两天就总结一篇数据结构的,但是我大意了,写起来属实很费劲,很累,甚至一度向放弃。

But我是不会放弃的,下一篇栈与队列,敬请关注!

有问题请指正,感谢支持!

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>