《2020王道》| 数据结构 | 学习笔记 | 第二章 | 线性表

线性表


线性表的定义和基本操作

线性表的定义

线性表是具有相同数据类型的 n ( n ≥ 0 ) n(n \ge 0) n(n0)个数据元素的有限序列,其中 n n n为表长,当 n n n为表长,当
n = 0 n=0 n=0时,该线性表为空表。

线性表的一般表示为 L = ( a 1 , a 2 , . . . , a i , a i + 1 , . . . , a n ) L=(a_{1},a_{2},...,a_{i},a_{i+1},...,a_{n}) L=a1,a2,...,ai,ai+1,...,an)。式中, a 1 a_{1} a1是唯一的“第一个”数元素,又称表头元素; a n a_{n} an是唯一的“最后一个”元素,又称表尾元素。

除第一个元素外,每个元素有且仅有一个直接前驱。除最后一个元素外,每个元素有且仅有一个直接后续。
线性表的特点:

  • 表中元素的个数是有限的;
  • 表中元素具有逻辑上的顺序性,在序列中个元素排序有其先后次序;
  • 表中元素都是数据元素,每个元素都是单个元素;
  • 表中元素的数据类型相同,每个元素占用相同大小的存储空间;
  • 表中元素具有抽象性。

线性表的基本操作

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

线性表的顺序定义

顺序表的定义

线性表的顺序存储又称为顺序表,是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使逻辑上相邻的两个元素在物理位置上也相邻。

数组下标 顺序表 内存地址
0 a 1 a_{1} a1 LOC{A}
1 a 2 a_{2} a2 LOC{A}+sizeof(ElemType
i-1 a i a_{i} ai LOC{A}+(i-1) ∗ * sizeof(ElemType}
n-1 a n − 1 a_{n-1} an1 LOC{A}+(n-1) ∗ * sizeof(ElemType} 就、吗,吗,

线性表的顺序存储类型描述为

//静态分配
#define MaxSize 50		//定义线性表的最大长度
typedef struct{
	ElemType data{MaxSize};	//顺序表的元素
	int Length;		//顺序表的当前长度
}SqList;			//顺序表的类型定义

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

顺序表的主要特点是随机访问,即通过首地址和元素序号就可在时间 O ( 1 ) O(1) O(1)内找到指定的元素。顺序表的存储密度高,每个结点只存储数据元素。顺序表逻辑上相邻的元素物理上也相邻。

顺序表上的基本操作实现

插入操作

在顺序表 L L L的第 i i i个位置插入新元素 e e e ( 1 ≤ i ≤ L , . L e n g t h + 1 ) (1 \le i \le L,.Length+1) (1iL,.Length+1) 。若 i i i的输入不合法,则返回 f a l s e false false,表示插入失败;若为合法输入则将顺序表的第 i i i个元素及其之后的所有元素右移一个位置,获得一个空位及插入新元素 e e e,同时顺序表长度加1,插入成功,返回 t r u e true true

bool LisInsert(SqList &L, int i, ElemType e){
	if(i < 1 || i > L.Length+1)		//判断i的范围是否有效
		return false;
	if(L.Length >= MaxSize)			//当表的存储空间已满时,无法插入
		return false;
	for(int j = L.Length; j >=  i; j--)	//将第i个元素及其后的元素全部后移一位
		L.data[j] = L.data[j-1];
	l.data[i-1] = e;			//插入新元素e
	L.Length++;				//表长加1
	return true;
} 

最好情况:在表尾插入(即 i = n + 1 i=n+1 i=n+1),元素后移语句不执行,时间复杂度为 O ( 1 ) O(1) O(1)
最坏情况:在表头插入(即 i = 1 i=1 i=1),元素后移语句将执行 n n n次,时间复杂度为 O ( n ) O(n) O(n)
平均情况:假设 p i ( p i = 1 n + 1 ) p_{i}(p_{i}=\frac{1}{n+1}) pi(pi=n+11)是在第 i i i个位置上插入一个新元素的概率,则在长度为 n n n的线性表中插入新结点时,所需移动元素的平均次数为
∑ i = 1 n + 1 p i ( n − i + 1 ) = ∑ i = 1 n + 1 1 n + 1 ( n − I + 1 ) \sum_{i=1}^{n+1}p_{i}(n-i+1)=\sum_{i=1}^{n+1}\frac{1}{n+1}(n-I+1) i=1n+1pi(ni+1)=i=1n+1n+11(nI+1)
= 1 n + 1 ∑ i = 1 n + 1 ( n − i + 1 ) =\frac{1}{n+1}\sum_{i=1}^{n+1}(n-i+1) =n+11i=1n+1(ni+1)
= 1 n + 1 n ( n + 1 ) 2 =\frac{1}{n+1}\frac{n(n+1)}{2} =n+112n(n+1)
= n 2 =\frac{n}{2} =2n
线性表插入算法的平均时间复杂度为 O ( n ) O(n) O(n)

删除操作

删除顺序表 L L L中第 i ( 1 ≤ i ≤ L . L e n g t h ) i(1\le i \le L.Length) i(1iL.Length)个位置的元素,若成功则返回 t r u e true true,并将删除的元素引用变量 e e e返回,否则返回 f a l s e false false

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; i++)		//将第i个位置后的元素前移
		L.data[j-1] = L.data[j];
	L.Length--;				//表长减1
	return true;
}

最好情况:删除表尾元素(即 i = n i=n i=n),不需要移动元素,时间复杂度为 O ( 1 ) O(1) O(1)
最坏情况:删除表头元素(即 i = 1 i=1 i=1),需要移动除第一个元素外的所有元素,时间复杂度为 O ( n ) O(n) O(n)
平均情况:假设 p i ( p i = 1 n ) p_{i}(p_{i}=\frac{1}{n}) pi(pi=n1)是删除第 i i i个位置上的元素的概率,则在长度为 n n n的线性表中删除结点时,所需移动元素的平均次数为
∑ i = 1 n + 1 p i ( n − i ) = ∑ i = 1 n 1 n ( n − i ) \sum_{i=1}^{n+1}p_{i}(n-i)=\sum_{i=1}^{n}\frac{1}{n}(n-i) i=1n+1pi(ni)=i=1nn1(ni)
= 1 n ∑ i = 1 n + 1 ( n − i ) =\frac{1}{n}\sum_{i=1}^{n+1}(n-i) =n1i=1n+1(ni)
= 1 n n ( n − 1 ) 2 =\frac{1}{n}\frac{n(n-1)}{2} =n12n(n1)
= n − 1 2 =\frac{n-1}{2} =2n1
线性表的删除算法的平均时间复杂度为 O ( n ) O(n) O(n)

按值查找(顺序查找)

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

int LocateElem(SqList L, ElemType e){
	int i;
	for(i = 0; i < L.Length; i++))
		if(L.data[i] = e)
			return i+1;
}

最好情况:查找的元素就在表头,仅需比较一次,时间复杂度为 O ( 1 ) O(1) O(1)
最坏情况:查找的元素在表尾(或者不存在)时,需要比较 n n n次,时间复杂度为 O ( n ) O(n) O(n)
平均情况:假设 p i ( p i = 1 n ) p_{i}(p_{i}=\frac{1}{n}) pi(pi=n1)是查找的元素在第 i i i个位置上的元素的概率,则在长度为 n n n的线性表中查找值为 e e e的元素所需比较的平均次数为
∑ i = 1 n + 1 p i ∗ i = ∑ i = 1 n 1 n ∗ i \sum_{i=1}^{n+1}p_{i}*i=\sum_{i=1}^{n}\frac{1}{n}*i i=1n+1pii=i=1nn1i
= 1 n n ( n + 1 ) 2 =\frac{1}{n}\frac{n(n+1)}{2} =n12n(n+1)
= n + 1 2 =\frac{n+1}{2} =2n+1
线性表按值查找算法的平均时间复杂度为 O ( n ) O(n) O(n)


线性表的链式表示

单链表的定义

线性表的链式存储又称单链表,是指通过一组任意的存储单元来存储线性表中的数据元素。
单链表中结点类型描述:

《2020王道》| 数据结构 | 学习笔记 | 第二章 | 线性表_第1张图片
typedef struct LNode{		//定义单链表结点类型
	ElemType data;		//数据域
	struct LNode *next;	//指针域
}LNode, *LinkLIst;

通常用头指针来标识一个单链表,头指针为 N U L L NULL NULL时表示一个空表。
在单链表第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,也可以交流表长等相关信息。头结点的指针域指向线性表的第一个元素结点。

头结点和头指针的区别: 无论是否有头结点,头指针始终致那些个链表的第一个结点,而头结点是带头结点的链表中的第一个结点,头结点内通常不存储数据信息。

  • 由于链表开始结点的位置被存放在头结点的指针域内,所以在链表的第一个位置上的操作和在链表的其他位置上的操作一致;
  • 无论链表是否为空,其头指针都指向头结点的非空指针(空表中的头结点的指针为空)。

单链表上基本操作的实现

采用头插法建立单链表

在空表上生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后。头插法建立单链表的算法如下:

《2020王道》| 数据结构 | 学习笔记 | 第二章 | 线性表_第2张图片
LinkList List_HeadInsert(LinkList &L){
	LNode *s;					
	int x;						
	L = (Link:ist)malloc(sizeof(LNode));		// 创建头结点
	L->next = NULL;					//创建空链表
	scanf("%d", &x);				//输入结点的值
	while(x != 9999){				//判定值的范围
		s = (LNode*)malloc(sizeof(LNode));	//创建新结点
		s->data = x;				//将值赋给新结点
		s->next = L->next;			//将新结点插入到单链表中
		L->next = s;				//L为头指针
		scanf("%d", &x);			//输出该值
	}
	return x;
}

采用头插法建立单链表时,读入数据的顺序与生成链表中元素的顺序是相反的。每一个新结点插入的时间复杂度为 O ( 1 ) O(1) O(1),若设单链表长度为 n n n,则建立单链表的时间复杂度为 O ( n ) O(n) O(n)

采用尾插法建立单链表

将新结点插入到当前链表的表尾,并增加一个表尾指针 r r r,使其始终指向当前链表的尾结点。尾插法算法如下:

《2020王道》| 数据结构 | 学习笔记 | 第二章 | 线性表_第3张图片
LinkList List_TailInsert(LinkList &L){
	int x;
	L = (LinkList)malloc(sizeof(LNode));		//创建头结点
	LNode *s;					
	LNode *r = L;					//创建表尾指针r
	scanf("%d", &x);				//输入结点的值
	while(x != 9999){
		s = (LNode*)malloc(sizeof(LNode));	//创建新结点
		s->data = x;				//给新结点赋值
		x->next = s;				//将新结点插入表尾
		r = s;					//r指向新的表尾结点
		scanf("%d", &x);
	}
	r->next = NULL;					//尾结点指针r置空
	return L;
}

尾插法的时间复杂度为 O ( n ) O(n) O(n)

按序号查找结点值

在单链表中从第一个结点出发,顺指针 n e x t next next域逐个往下搜索,知道找到第 i i i个结点为止,否则返回最后一个结点的指针域 N U L L NULL NULL。按序号查找的算法如下:

LNode *GetElem(LinkList L, int i){
	int j = 1;		//计数标记
	LNode *P = L->next;	//链表头结点指针赋给p指针
	if(i == 0)			
		return L;	//若i=0,则返回头结点
	if(i < 1)
		return NULL;	//若i无效,则返回NULL
	while(p && j < i){   	//从第1个结点开始查找,直到第i个结点
		p = p->next;	//p指针指向下一个结点
		j++		//计数加1
	}
	return p;		//返回第i个结点的指针,若i大于表长,则p=NULL,直接返回p
}

按序号查找的时间复杂度为 O ( n ) O(n) O(n)

按值查找表结点

从单链表的第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的值等于给定值 e e e,则返回该结点的指针,拖这个单链表中都没有查找到该值,则返回 N U L L NULL NULL
按值查找的算法如下:

LNode *LocateElem(LinkList L, ElemType e){
	LNode *p = L->next;			
	while(p != NULL && p->data != e)	//从第i个结点开始查找data域为e的结点
		p = p->next;			//p指针指向下一个结点
	return p;				//找到该结点后返回该结点的指针,否则返回NULL
}

插入结点操作

插入结点操作将值为 x x x的新结点插入到单链表的第 i i i个位置上,先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第 i − 1 i-1 i1个结点,然后在其后插入新结点。

算法首先调用按序号查找算法 G e t E l e m ( L , i − 1 ) GetElem(L, i-1) GetElem(L,i1),查找第 i − 1 i-1 i1个结点。假设返回的第 i − 1 i-1 i1个结点为 ∗ p *p p,然后令新结点 ∗ s *s s的指针指向 ∗ p *p p的后继结点,再令结点 ∗ p *p p的指针域指向新插入的结点 ∗ s *s s

《2020王道》| 数据结构 | 学习笔记 | 第二章 | 线性表_第4张图片

实现插入结点的代码如下:

p = GetElem(L, i-1);	//查找插入位置的前驱结点
s->next = p->next;	//现将新结点的后继指针连接到插入位置的后继结点
p->next = s;		//再将前驱结点的后继指针链接到新结点

算法中的后两个语句顺序不能颠倒,否则将失去指向插入位置的后继结点的指针。
本算法中主要的时间开销是擦很重啊插入位置,时间复杂度为 O ( n ) O(n) O(n),而插入结点的时间复杂度为 O ( 1 ) O(1) O(1)

删除结点操作

将单链表的第 i i i个结点删除,要先检查删除位置的合法性,然后查找表中第$i-1个结点,即被删除结点的前驱结点,再将其删除。

假设结点 ∗ q *q q为被删除结点, ∗ p *p p为删除结点的前驱结点,为实现这一操作后的逻辑关系的变化,需要修改 ∗ p *p p的指针域,即将 ∗ p *p p的指针域 n e x t next next指向 ∗ q *q q的下一个结点。

《2020王道》| 数据结构 | 学习笔记 | 第二章 | 线性表_第5张图片

实现删除的代码如下:

p = GetElem(L, i-1);		//查找被删除结点的前驱结点
q = p->next;			//令q指向被删除结点
p->next = q->next;		//将被删除结点的后继结点链接到被删除结点的前驱结点
free(q);			//释放结点的存储空间

删除算法的时间复杂度为 O ( n ) O(n) O(n)

求表长的操作

计算单链表中数据结点(不含头结点)的个数,从第一个结点开始顺序访问表中的每一个结点,并设置一个计数变量,每访问一个变量后,计数变量加1,知道访问到空结点位置。算法的时间复杂度为 O ( n ) O(n) O(n)

双链表

双链表结点中有两个指针 p r i o r ) prior) prior) n e x t next next, 分别指向其前驱结点和后继结点。

双链表中结点类型的描述:

typedef struct DNode{
	ElemType data;			//定义双链表结点类型
	struct DNode *prior, *next;	//数据域
}DNode, *DLinkList;			//前驱和后继指针

双链表仅在单链表的结点中增加了一个指向其前驱的 p r i o r prior prior指针,因此在双链表中指向按值查找和按序号查找的操作与在单链表中相同。但是双链表增加了 p r i o r prior prior指针,其关键是保证在修改链表的过程中不会断链,并且可以很方便的找到其前驱结点。因此,插入、删除结点的算法时间复杂度均为 O ( 1 ) O(1) O(1)

插入操作

在双链表中 p p p所指的结点之后插入结点 ∗ s *s s的算法代码如下:

《2020王道》| 数据结构 | 学习笔记 | 第二章 | 线性表_第6张图片
s->next = p->next;	//首先将插入位的后继结点链接到插入结点的后继指针
p-next->prior = s;	//再将插入位后继结点的前驱指针指向插入结点
s->prior = p;		//然后将插入结点的前驱指针指向插入位的前驱结点
p->next = s;		//最后将插入位前驱结点的后继指针指向插入结点

上述语句的顺序不是唯一的,但也不是任意的。其中的第一、二句的顺序不能颠倒,否则将失去指向 ∗ p *p p的后继结点的指针。

删除操作

删除双链表中结点 ∗ p *p p的后继结点 ∗ q *q q的算法代码如下:

《2020王道》| 数据结构 | 学习笔记 | 第二章 | 线性表_第7张图片
p->next = q->next;	//被删除结点的前驱结点的后继指针指向被删除结点的后继结点
q->next->prior = p;	//被删除结点的后继结点的前驱指针指向被删除结点的前驱结点
free(q);		//释放被删除结点的存储空间

循环链表

循环单链表

循环单链表和单链表的区别在于循环单链表的最后一个结点的指针不是 N U L L NULL NULL,而是指向表头结点,从而使整个单链表形成一个环。

在循环单链表中,表尾结点 ∗ r *r r n e x t next next域指向 L L L,故表中没有指针域为 N U L L NULL NULL的结点,所以循环单链表的判空条件不是头结点的指针是否为空,而是判断头结点的指针是否指向自己。

循环双链表

《2020王道》| 数据结构 | 学习笔记 | 第二章 | 线性表_第8张图片

在循环双链表 L L L中,头结点的 p r i o r prior prior指针要指向表尾结点,即设某结点 ∗ p *p p为表尾结点,则有

p->next = NULL;

当循环双链表为空表时,其头结点的 p r i o r prior prior n e x t next next指针域都等于 L L L

静态链表

静态链表是借助数组来描述线性表的链式存储结构,结点同样有数据域 d a t a data data和指针域 n e x t next next。静态链表的指针是指数组结点的相对地址(即数组下标),又称为游标。

《2020王道》| 数据结构 | 学习笔记 | 第二章 | 线性表_第9张图片

静态链表结构类型描述:

#define MaxSize 50;		//定义静态链表的最大长度

typedef struct{			//静态链表结构类型定义
	ElemType data;		//数据域
	int next;		//指针域(数组下标)
}SLinkList[MaxSize];

静态链表以 n e x t = = − 1 next == -1 next==1作为链表结束的标志。

顺序表和链表的比较

存取方式

顺序表可以顺序存取,也可以随机存取,而链表只能顺序存取;

逻辑结构和物理结构

顺序表中,逻辑上相邻的元素其对应的物理存储位置也相邻。而链表中,逻辑上相邻的元素其物理存储位置则不一定相邻,其对应的逻辑关系是通过指针域来表示。

查找、插入和删除操作

对于按值查找,顺序表无序时,两种存储方式的时间复杂度均为 O ( n ) O(n) O(n);而当顺序表有序时,采用折半查找法的时间复杂度均为 O ( log ⁡ 2 n ) O(\log_2n) O(log2n)
对于按序号查找,顺序表支持随机访问,时间复杂度为 O ( 1 ) O(1) O(1),而链表的平均时间复杂度为 O ( n ) O(n) O(n)

顺序表的插入、删除操作平均需要移动半个表长的元素,而链表只需要修改相关结点的指针域即可。但由于链表的每个节点都有指针域,在存储空间密度上要比顺序表小。

空间分配

顺序存储在静态存储分配下,当存储看尽啊装满则不能扩展,若再加入新的元素则就会发生内存溢出的情况,因此需要预先分配足够大的存储空间。但是如果预先分配过大,可能会导致顺序表空间的闲置,分配过小又会导致内存溢出。而动态存储分配的存储空间虽然可以扩充,但需要移动大量元素完成操作,会导致算法运行效率降低,并且如果内存中没有合适的连续存储空间,则会导致动态分配失败。

链式存储的结点空间只在需要时进行申请分配,并且只要内存有空间就可以分配。

  • 基于存储的考虑:
    难以估计线性表的长度好存储规模时,首先考虑采用链式存储方式。
  • 基于运算的考虑:
    若经常需要进行按序号查找访问元素,首先考虑顺序存储;
    若需要进行删除、插入操作较频繁,首先考虑链式存储。
  • 基于环境的考虑:
    顺序表较容易实现,一般通过数组类型定义即可实现;
    链表的操作基于指针,较为复杂。

你可能感兴趣的:(GCT学习笔记,数据结构与算法)