数据结构复习笔记 第二章 线性表

知识框架

在这里插入图片描述

线性表的定义与基本操作

线性表的定义

线性表是具有相同数据类型的n (n≥0)个数据元素的有限序列

线性表的基本操作

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(I):输出操作。
按前后顺序输出线性表L的所有元素值。
Empty(L):判空操作。
若L为空表,则返回true,否则返回false。
DestroyList(&L):销毁操作。
销毁线性表,并释放线性表L所占用的内存空间。

一、顺序表

1.顺序表的定义

线性表的顺序存储称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。
顺序表的特点是元素的逻辑顺序与其物理顺序相同。
在这里插入图片描述
假定线性表的元素类型为ElemType
注意区别顺序表的位序和数组下标

2.顺序表的静态存储实现

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

3.顺序表的动态存储实现

#define InitSize 100		//表长度的初始定义
typedef struct{
	ElemType *data;				//指示动态分配数组的指针
	int MaxSize,length;		//数组的最大容量和当前个数
} SeqList;					//动态分配数组顺序表的类型定义

C的初始动态分配语句为.
L. data= (ElemType* ) malloc (sizeof (ElemType) *InitSize) ;
C++的初始动态分配语句为
L. data=new ElemType [InitSize] ;

注意:动态分配并不是链式存储,它同样属于顺序存储结构,物理结构没有变化,依然是随机存取方式,只是分配的空间大小可以在运行时决定。

4.顺序表的初始化

void InitList(SeqList &L){
	L. data= (ElemType* ) malloc (sizeof (ElemType) *InitSize) ;
	L.length=0;
	L.MaxSize=InitSize;
}

5.增加动态数组的长度

void IncreaseSize(SeqList &L,int len){
	int *p=L.data;
	L. data= (ElemType* ) malloc (sizeof (ElemType) *InitSize) ;
	for(int i=0;i<L.length;i++){
		L.data[i]=p[i];
	}
	L.MaxSize=L.MaxSize+len;
	free(p);
}

6.顺序表的插入

bool ListInsert (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;					//在位置i处放入e
	L.length++;						//线性表长度加1
	return true;
	}

时间复杂度:
最好情况:在表尾插入(即i=n+1),时间复杂度为O(1)。
最坏情况:在表头插入(即i=1),时间复杂度O(n)。
平均情况:假设pi(pi= 1/(n+1)是在第i个位置上插入一个结点的概率,则在长度为n的线性表中插入一个结点时,所需移动结点的平均次数为
在这里插入图片描述

7.顺序表的删除

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

时间复杂度:
最好情况:删除表尾元素,不需要移动其他元素
i = n,循环 0 次;最好时间复杂度 = O(1)
最坏情况:删除表头元素,需要将后续的 n-1 个元素全都向前移动
i = 1,循环 n-1 次;最坏时间复杂度 = O(n);

平均情况:假设删除任何一个元素的概率相同,即 i = 1,2,3, … , length 的概率都是 p i=1/n
i = 1,循环 n-1 次;
i=2 ,循环 n-2 次;
i=3,循环 n-3 次
……
i =n 时,循环0次
问题规模 n = L.length (表长)
平均循环次数 = (n-1)p + (n-2)p + …… + 1⋅p = (n-1)/2
平均时间复杂度 = O(n)

8.顺序表的按位查找

ElemType GetElem(SqList L, int i){
return L.data[i-1];
}

9.顺序表的按值查找

//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SeqList L,ElemType e){
	for(int i=0;i<L.length;i++)
		if(L.data[i]==e)
			return i+1; //数组下标为i的元素值等于e,返回其位序 i+1
	return 0; //退出循环,说明查找失败
}

最好情况:目标元素在表头,循环1次;
最好时间复杂度 = O(1)
最坏情况:目标元素在表尾,循环 n 次;
最坏时间复杂度 = O(n);
平均情况:假设目标元素出现在任何一个位置的概率相同,都是p=1/n
目标元素在第1位,循环1次;在第2位,循环2次;…… ;在第 n 位,循环 n 次
平均循环次数 = 1⋅ p + 2 ⋅ p + 3 ⋅ p + …… + n ⋅ p =(n+1)/2
平均时间复杂度 = O(n)

二、单链表

结点的数据结构:

struct LNode{			//定义单链表结点类型
	ElemType data;		//每个节点存放一个数据元素
	struct LNode *next;	//指针指向下一个节点
}

为了增强代码的可读性,可以用typedef <数据类型> <别名>简化代码:

typedef struct LNode LNode;
//LNode等价于struct LNode
typedef struct LNode *LinkList;
//LinkList等价于struct LNode *

以上两串代码可以直接写成:

typedef struct LNode{	//定义单链表结点类型
	ElemType data;		//每个节点存放一个数据元素
	struct LNode *next;	//指针指向下一个节点
}LNode,*LinkList;

虽然LNode *L效果上等价于LinkList L,但通常LNode *L强调是一个结点,而LinkList L强调是一个单链表。
单链表分为 带头结点 和 不带头结点 的两种,因为带头结点的方便,通常默认带头结点

1.单链表的建立

头插法

//带头结点
LinkList List_HeadInsert(LinkList &L) { //逆向建立单链表
	LNode *s;//声明新结点,此时s没有分配空间
	int x;//设ElemType为整型
	L=(LinkList)malloc(sizeof(LNode)); //创建头结点
	L->next=NULL;//初始为空链表
	scanf("%d",&x);//输入结点的值
	while(x!=9999) {//输入9999表示结束建表
		s=(LNode*)malloc(sizeof(LNode)); //创建新结点
		s->data=x;
		s->next=L->next;
		L->next=s;//将新结点插入表中,L为头指针
		scanf("%d", &x);
	}
	return L;
}

先将新结点的next指向旧结点(头结点的next,L->next),再将头结点的next指向新结点,然后循环新结点变为旧结点。
该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后,如图所示。
在这里插入图片描述
采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的。每个结点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。

//不带头结点(自己改的,可能有错)
LinkList List_HeadInsert(LinkList &L) { //逆向建立单链表
	LNode *s;//声明新结点,此时s没有分配空间
	int x;//设ElemType为整型
		scanf("%d",&x);
		while(x!=9999) {//输入9999表示结束建表
			s=(LNode*)malloc(sizeof(LNode)); //创建新结点
			s->data=x;//数据域赋值
			s->next=L;//新结点的next指向NULL
			L=s;//将新结点插入表中,L为头指针
			scanf("%d",&x);
		}
	return L;
}

尾插法

//带头结点
LinkList List_TailInsert(LinkList &L) { //正向建立单链表
	int X;//设ElemType为整型
	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=s;
		r=s;//r指向新的表尾结点
		scanf("%d",&x);
	}
	r->next=NULL;//尾结点指针置空
	return L;
}

该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点,如图所示。

在这里插入图片描述

//不带头结点(自己改的,可能会错)
LinkList List_TailInsert(LinkList &L) { //正向建立单链表
	int X;//设ElemType为整型
	LNode *s,*r;//r为表尾指针
	scanf("%d",&x);//输入结点的值
	if(x==9999)
		return L;
	s=(LNode*)malloc(sizeof(LNode));
	L=s;
	r=s;
	scanf("%d",&x);
	while(x!=9999){//输入9999表示结束
		s=(LNode*)malloc(sizeof(LNode));
		s->data=x;
		r->next=s;
		r=s;//r指向新的表尾结点
		scanf("%d",&x);
	}
	r->next=NULL;//尾结点指针置空
	return L;
}

2.单链表的初始化

1)不带头结点的单链表

·初始化
bool InitList(LinkList &L){
	L=NULL;//空表,防止脏数据
	return  true;
}
·判断单链表是否为空
bool Empty(LinkList L){
	return (L==NULL);
}

2)带头结点的单链表

·初始化
bool InitList(LinkList &L){
	L=(LNode *)malloc(sizeof(LNode));
	if(L==NULL)
		return false;
	L->next =NULL;
	return true;
}
·判断单链表是否为空
bool Empty(LinkList L){
	return (L->next == NULL);
}

3.求单链表表长

//求表的长度
int Length(LinkList L){
	int len = 0; //统计表长
	LNode *p = L;
	while (p->next != NULL){
		p = p->next;
		len++;
	}
	return len;
}

4.单链表的查找

按位查找(带头结点)

LNode *GetElem(LinkList L,int i){
	if(i<0)
		return NULL;//若i无效,则返回NULL
	if(i==0)
		return L;//若i等于0,则返回头结点
		int j=0;//计数,初始为1
	LNode *p=L->next;//头结点指针赋给p
	while(p!=NULL&&j<i){//从第1个结点开始找,查找第i个结点
		p=p->next;
		j++;
	}
	return p;//返回第i个结点的指针,若i大于表长则返回NULL
}

平均时间复杂度O(n).

按值查找

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

5.单链表的插入

单链表按位序插入(在第i个位置插插入元素e)

带头结点的单链表(默认)
//即在i-1个元素后插入e
bool ListInsert(LinkList &L, int i, ElemType e){
	if(i<1)
		return false;
	LNode *p; //指针p指向当前扫描到的结点(扫描指针)
	int j=0;//代表当前p指向的是第几个结点
	p=L;//L指向头结点,头结点是第0个结点(不存数据)
	while(p!=NULL && j<i-1){ //循环找到第 i-1 个结点
		p=p->next;
		j++;
	}
	if(p==NULL) //i值不合法
		return false;
	LNode *s=(LNode *)malloc(sizeof(LNode));
	s->data=e;
	s->next=p->next;
	p->next=s;//将结点s连到p之后;
	return true;//插入成功
}

L为指向头结点的指针,也可以表示这整个单链表。
在第i个位置插入元素e。定义一个扫描指针p,先指向头结点,与L相同;
定义一个 j,用来计数;
然后执行p=p->next直到p指向第i-1个结点;
再定义一个结点s,将元素e的值赋到s的数据域;
最后先将s->next指向第i个结点(s->next=p->next),再将p->next指向结点s。

bool ListInsert(LinkList &L, int i, ElemType e){
	if(i<1)
		return false;
		LNode *p=GetElem(L,i-1);//按位查找(带头结点)
		return InsertNextNode(p,e);//在后面的后插操作
不带头结点的单链表
bool ListInsert(LinkList &L,int i, ElemType e){
	if(i<1)
		return false;
	if(i==1){ //插入第1个结点的操作与其他结点操作不同
		LNode *s = (LNode *)malloc(sizeof(LNode));
		s->data = e;
		S->next=L;
		L=s;//头指针指向新结点
		return true;
	}
	LNode *p; //指针p指向当前扫描到的结点
	int j=1; // 当前p指向的是第几个结点
	p=L;
	//p指向第1个结点(注意: 不是头结点)
	while (p!=NULL && j<i-1) { //循环找到第 i-1个结点
		p=p- >next;
		j++;
	}
	if(p==NULL) //i值不合法
		return false;
	LNode *s = (LNode *)malloc(sizeof(LNode));
	s->data = e;
	s->next=p->next;
	p->next=s;
	return true;//插入成功
}

后插操作:(在p结点之后插入元素e)

bool InsertNextNode(LNode *p, ElemType e){
	if (p==NULL )
		return false;
	LNode *S=(LNode *)malloc(sizeof(LNode)) ;
	if(s==NULL) //考虑内存分配失败的可能性
		return false;
	S->data=e;//用结点s保存数据元素e
	s->next=p->next;
	p->next=s;//将结点s连到p之后
	return true ;
}

与按序插入的后半部分相同。写代码的时候可以考虑写这个函数,在按需插入的函数中调用这个函数。

前插操作:(在p结点之前插入元素e)

bool InsertPriorNode (LNode *p, ElemType e){
	if (p==NULL)
		return false;
	LNode *s = (LNode *)malloc(sizeof(LNode));
	if (s==NULL) //内存分配失败
		return false;
	s->next=p->next;
	p->next=s;//新结点s连到p之后
	s->data=p->data;//将p中元素复制到s中
	p->data=e;//p中元素覆盖为e
	return true;
}

无法找到p结点的前一个结点,就在p结点后插入结点,然后将 p结点的数据域 与 新建结点s的数据域(元素e)交换。

6.单链表的删除

按位序删除(带头结点)

bool ListDelete(LinkList &L, int i, ElemType &e){
	if(i<1) .
		return false;
	LNode *p; //指针p指向当前扫描到的结点
	int j=0; // 当前p指向的是第几个结点
	p=L;//L指向头结点,头结点是第0个结点(不存数据)
	while (p!=NULL && j<i-1) { //循环找到第 i-1 个结点
		p=p->next;
		j++;
	}
	if( p==NULL)//i值不合法
		return false ;
	if(p->next == NULL) //第i-1个结点之后已无其他结点
		return false;
	LNode *q=p->next;//令q指向被删除结点
	e = q->data;//用e返回元素的值
	p->next=q->next;//将*q结点从链中“断开”
	free(q);//释放结点的存储空间
	return true;//删除成功
}

删除指定结点p

bool DeleteNode(LNode *p){
	if (p==NULL)
		return false;
	LNode *q=p->next;//令q指向*p的后继结点
	p->data=p->next->data; //和后继结点交换数据域
	p->next=q->next;//将*q结点从链中“断开”
	free(q);//释放后继结点的存储空间
	return true; 
}

声明一个指针q指向p的后继结点(LNode *q=p->next),然后 后继结点*q的数据域将p的数据域覆盖(p->data=p->next->data),最后将p->next指向q的后继结点(p->next=q->next)。

三、双链表

1.双链表的数据结构

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

2.双链表的初始化(带头结点)

//初始化双链表
bool InitDLinkList(DLinklist &L){
	L=(DNode *) malloc(sizeof(DNode));	//分配一个头结点
	if (L==NULL) 						//内存不足, 分配失败
		return false;
	L->prior = NULL;					//头结点的prior永远指向NULL
	L->next = NULL;						//头结点之后暂时还没有节点
	return true;
}

3.判断双链表是否为空(带头结点)

bool Empty(DLinklist L){
	if (L->next == NULL)
		return true;
	else
		return false;
}

4.双链表的插入(后插)

//在p结点之后插入s结点
bool InsertNextDNode(DNode *p,DNode *s){
	if(p==NULL || s==NULL) //非法参数
		return false;
	s->next=p->next;
	if (p->next != NULL)//如果p结点有后继结点
		p->next->prior=s;
	s->prior=p;
	p->next=s;
	return true;
}

5.双链表的删除(后删)

//删除p结点的后继结点
bool DeleteNextDNode(DNode *p){
	if (p==NULL)  return false ;
	DNode *q = p->next; //找到p的后继结点q
	if (q==NULL)  return false; //p没有后继
	p->next=q->next :
	if (q->next !=NULL)] //q结点不是最后一个结点
		q->next->prior=p;
	free(q);//释放结点空间
	return true;
}
void DestoryList(DLinklist &L){	//循环释放各个数据结点
while (L->next != NULL)
DeleteNextDNode(L);
free(L);						//释放头结点
L=NULL;							//头指针指向NULL
}
 

6.双链表的遍历

后向遍历:
while (p!=NULL){ //对结点p做相应处理,如打印 p = p->next; }
前向遍历:
while (p!=NULL){ //对结点p做相应处理 p = p->prior; }
前向遍历(跳过头结点):
while (p-> prior != NULL){ //对结点p做相应处理 p = p->prior; }
双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。时间复杂度 O(n)。

四、循环链表

1.循环单链表

typedef struct LNode{			//定义单链表结点类型
	ElemType data;				//每个节点存放一一个数据元素
	struct LNode *next;			//指针指向下一个节点
}LNode, *LinkList;				

//初始化一个循环单链表
bool InitList(LinkList &L) {
	L = (LNode *) malloc(sizeof(LNode)); //分配一个头结点
	if (L==NULL)						//内存不足,分配失败
		return false;
	L->next = L;//头结点next指向头结点
	return true;
}

//判断循环单链表是否为空
bool Empty(LinkList L){
	if (L->next == L)
		return true;
	else
		return faLse;
}

//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L,LNode *p){
	if (p->next==L)
		return true;
	else
		return false;
}

2.循环双链表

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

//初始化空的循环双链表
bool InitDLinkList(DLinklist &L){
	L=(DNode *)malloc(sizeof(DNode));	//分配一个头结点
	if (L==NULL)						//内存不足,分配失败
		return false;
	L->prior=L;							//头结点的prior指向头结点
	L->next=L;							//头结点的next 指向头结点
	return true;
}

//判断循环双链表是否为空
bool Empty(DLinklist L){
	if (L->next == L)
		return true;
	else
		return false;
}

//判断结点p是否为循环单链表的表尾结点
booL isTail(DLinklist L, DNode *p){
	if (p->next==L)
		return true;
	else
		return false;
}

五、静态链表

#define Maxsize 10		//静态链表的最大长度
struct Node{			//静态链表结构类型的定义
	ElemType data;			//存储数据元素
	int next;				//下一个元素的数组下标
}SLinkList[MaxSize];

void testSLinkList(){
	SLinkList a;//等价于struct Node a[MaxSize];
	//a是一个静态链表
	//......后续代码
}

等价于:

#define Maxsize 10		//静态链表的最大长度
struct Node{			//静态链表结构类型的定义
	ElemType data;			//存储数据元素
	int next;				//下一个元素的数组下标
};
typedef struct Node SLinkList[MaxSize];
//可用 SLinkList 定义“一个长度为 MaxSize 的Node型数组”

void testSLinkList(){
	struct Node a[MaxSize];//等价于SLinkList a;
	//a是一个Node型数组
	//......后续代码
}

静态链表:用数组的方式实现的链表
优点:增、删 操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变
适用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)

顺序表与链表的比较

1.存取(读写)方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。例如在第i个位置
上执行存或取的操作,顺序表仅需-次访问,而链表则需从表头开始依次访问i次。
2.逻辑结构与物理结构
采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻
辑上相邻的元素,物理存储位置则不一定相邻, 对应的逻辑关系是通过指针链接来表示的。
3.查找、插入和删除操作
对于按值查找,顺序表无序时,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查
找,此时的时间复杂度为O(logmn)。
对于按序号查找,顺序表支持随机访问,时间复杂度仅为0(1),而链表的平均时间复杂度为
O(n)。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需
修改相关结点的指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大。
4.空间分配
顺序存储在静态存储分配情形下,- - -旦存储空间装满就不能扩充,若再加入新元素,则会出
现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量
闲置;预先分配过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量元
索,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。链式存
储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。

在实际中应该怎样选取存储结构呢?

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


复习参考资料为王道考研书

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值