《王道》数据结构之线性表(二)


概要


一、线性表的概念和性质

即线性表的逻辑结构和定义在逻辑结构上的数据运算

1.1 线性表的概念

1.1.1 线性表的定义

线性表是具有相同数据类型的n(n≥0)个数据元素的有限 序列(当n = 0时线性表是一个空表。一般表示为L = (a1, a2, … , ai, ai+1, … , an))

术语解释
位序从1开始到表长n
表头元素和表尾元素位序为1和位序为n
直接前驱和直接后继除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继

1.1.2 线性表的特点

  • 元素个数有限表中元素的个数有限
  • 每个元素占有相同大小的存储空间
  • 元素有逻辑上的顺序性(有先后次序)

1.2 线性表的性质

  • 晚点写

二、线性表的存储结构

2.1 顺序表

2.1.1 顺序表的定义及特点

1. 定义
顺序存储的方式实现线性表,即用一组地址连续的存储单元依次存储数据元素,元素之间的关系由存储单元的邻接关系来体现

2. 特点

  • 随机访问(通过首地址和元素序号可在O(1)内找到指定元素)
  • 存储密度高(每个节点只存储数据元素)
  • 插入、删除需移动大量元素
  • 拓展容量不方便(全部复制到新存储空间时间开销大)

2.1.2 顺序表的两种实现方式

  • 静态数组实现(定长顺序存储):栈区,数组长度不能改变
  • 动态数组实现(堆分配存储):堆区,数组长度可以改变

1. 静态分配实现顺序表
数据空间的大小和空间已经固定不能修改

代码实现:

#define MaxSize 10			//定义最大长度

//静态分配实现顺序表结构体(初始化需对数据元素和长度赋初值)
typedef struct{
	ElemType data[MaxSize]; //用静态的“数组”存放数据元素
	int length;				//顺序表的当前长度
}SqList; 					//顺序表的类型定义(Sq:sequence 顺序,序列)

2. 动态分配实现顺序表
数据空间占满可开辟更大的存储空间替换(注意要new和delete)

代码实现:

#define InitSize 10 	//顺序表的初始长度

//动态分配实现顺序表结构体(初始化需对数据元素和长度赋初值)
typedef struct{
	ElemType *data; 	//动态分配数组的指针
	int MaxSize; 		//顺序表的最大容量
	int length;			//顺序表的当前长度
} SeqList;				//顺序表的类型定义

2.2 链式表

这里先介绍单链表

2.2.1 单链表的定义及特点

1. 定义
链式存储的方式实现线性表,每个结点除了存放数据元素外(数据域),还要存储指向下一个节点的指针(指针域)
各结点间的先后关系用一个指针表示

2. 特点

  • 插入、删除方便
  • 拓展容量方便
  • 不能随机访问(找某个结点时要从头遍历)
  • 存储密度低(每个节点存储数据元素和指针)

2.2.2 单链表的两种实现方式

要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点,分为不带头结点的单链表和带头结点的单链表

  • 不带头结点实现:对第一个数据结点和后续数据结点的处理、对空表和非空表的处理都需要不同的代码逻辑
    如判空:
    不带头结点:L == NULL
    带头结点:L->next == NULL
  • 带头结点实现:方便

代码实现:

//结点结构(带头结点和不带头结点的结点结构)
typedef struct LNode{
	ElemType data; 			//每个结点存放的数据元素
	struct LNode *next;		//指向下一个结点
}LNode, *LinkList;

typedef <数据类型> <别名> --数据类型重命名
typedef <struct LNode> <LNode>(强调是一个结点) =
typedef <struct LNode> <*LinkList>(强调是一个单链表--指向第一个结点的指针)

2.3 顺序表和链式表的比较

  • 顺序表
    优点:可随机存取,存储密度高
    缺点:要求大片连续空间,改变容量不方便
  • 单链表
    优点:不要求大片连续空间,改变容量方便
    缺点:不可随机存取,要耗费一定空间存放指针,找某个结点时要从头遍历

三、线性表的基本操作

对于不同存储结构有着不同的运算实现

3.1 创销赋清、增删改查

创销赋清解释
InitList(&L)初始化:构造一个空的线性表L,分配内存空间
DestroyList(&L)销毁:销毁线性表,并释放线性表L所占用的内存空间
增删改查解释
ListInsert(&L,i,e)插入:在表L中的第i个位置上插入指定元素e
ListDelete(&L,i,&e)删除:删除表L中第i个位置的元素,并用e返回删除元素的值
LocateElem(L,e)按值查找:在表L中查找具有给定关键字值的元素
GetElem(L,i)按位查找:获取表L中第i个位置的元素的值

3.2 其他操作

其他操作解释
Empty(L)判空:若L为空表,则返回true,否则返回false
Length(L)求表长:返回线性表L的长度,即L中数据元素的个数
PrintList(L)输出:按前后顺序输出线性表L的所有元素值。

3.3 用顺序表实现基本运算

顺序表有两种实现方式:静态分配和动态分配

#define MaxSize 10			//静态分配顺序表的最大长度
#define InitSize 10 		//动态分配顺序表的初始长度

//1.静态分配实现顺序表结构体(初始化需对数据元素和长度赋初值)
typedef struct{
	ElemType data[MaxSize]; //用静态的“数组”存放数据元素
	int length;				//顺序表的当前长度
}SqList; 					//顺序表的类型定义(Sq:sequence 顺序,序列)

//2.动态分配实现顺序表结构体(初始化需对数据元素和长度赋初值)
typedef struct{
	ElemType *data; 	//动态分配数组的指针
	int MaxSize; 		//顺序表的最大容量
	int length;			//顺序表的当前长度
} SeqList;				//顺序表的类型定义

3.3.1 创

初始化时动态申请和释放堆区内存空间的方法:

  • C —— malloc、free 函数
  • C++ —— new、delete 关键字
    malloc 函数返回一个指针,需要强制转型为你定义的数据元素类型指针,否则无法知道下一个元素的地址(不同数据类型所占空间不同)。如L.data = (ElemType *)malloc (sizeof(ElemType) * InitSize);

代码实现:

//1.静态分配
void InitList(SqList &L){
	for(int i = 0; i<MaxSize;i++)//当L.length=0时,可不用赋初值
	{
		L.data[i]=0;
	}	
	L.length = 0;
}

//2.动态分配
void InitList(SqList &L){
	L.data = new ElemType[InitSize];	//释放时要delete[] L.data;
	L.MaxSize = InitSize;
	L.length = 0;	
}

//2.1动态分配的顺序表可增加动态数组的长度(最大存储空间)
void IncreaseSize(SqList &L, int len){
	ElemType *p = L.data;				//p作为原数组指针的副本,用于复制原数组中的数据元素
	
	L.data = new ElemType[MaxSize+len]	//L.data
	for(i = 0; i < L.length; i++)
	{
		L.data[i] = p[i];
	}
	L.MaxSize += len;					//L.MaxSize
										//L.length不变
	delete[] p;
}

增加动态数组最大长度时,一定记得释放delete原来的空间!!!

3.3.2 增

时间复杂度:O(n)

代码实现:
考虑健壮性:插入位序i是否有效;当前顺序表空间是否已满
静态分配和动态分配操作相同

//1.静态分配
bool ListInsert(SqList &L, int i, int e){	//注意加引用符号
	//1.健壮性
	if(L.length>=MaxSize)
		return false;
	if(i<1 || i>L.length+1)
		return false;
		
	//2.下标为i-1及其之后的元素依次后移	
	for(int j=L.length-1; j>=i-1; j--)
		L.data[j+1] = L.data[j];
		
	//3.插入
	L.data[i-1] = e;
	
	//4.更改顺序表属性length
	L.length++;
	return true;
}

3.3.3 删

时间复杂度:O(n)

代码实现:
考虑健壮性:插入位序i是否有效
静态分配和动态分配操作相同

//1.静态分配
bool ListDelete(SqList &L, int i, int &e){	//注意加引用符号
	//1.健壮性
	if(i<1 || i>L.length)
			return false;
	//2.提取删除元素	
	e = L.data[i-1];

	//3.下标为i-1及其之后的元素依次前移	
	for(int j=i-1;j<L.length-1;j++)
			L.data[j] = L.data[j+1];
			
	//4.更改顺序表属性length
	L.length--;
	return true;
}

3.3.4 查

分为按位查找操作和按值查找操作
1. 按位查找操作

时间复杂度:O(1)

代码实现:
考虑健壮性:插入位序i是否有效
静态分配和动态分配操作相同

//1.静态分配
ElemType GetElem(SqList L, int i){
	//健壮性
	if(i<1 || i>L.length)
		return false;
	return L.data[i-1];
}

2. 按值查找操作

时间复杂度:O(n),遍历数组

代码实现:
静态分配和动态分配操作相同

//1.静态分配
int LocateElem(SqList L, ElemType e){
	for(int i = 0;i<L.length;i++)
	{
		if(L.data[i]==e)	//若L.data[i]是结构体变量需要依次对比各个分量来判断两个结构体是否相等
			return i+1;
	}		
	return 0;	//查找失败返回0
}

3.4 用单链表实现基本运算

单链表有两种实现方式:不带头结点的和带头结点的实现
结点结构是一样的

//结点结构
typedef struct LNode{
	ElemType data; //每个节点存放的数据元素
	struct LNode *next;//指向下一个节点
}LNode, *LinkList

.单链表的建立:
分为头插法和尾插法
尾插法:

3.4.1 创

代码实现:
健壮性:判断分配的指针是否为空(内存分配失败)

//1. 不带头结点的初始化
bool InitList(LinkList &L){
	L = NULL; 		//空表,无结点,防止脏数据
	return true;
}

//2. 带头结点的初始化
bool InitList(LinkList &L){
	//1.分配头结点
	L = new LNode;

	//2.健壮性
	if (L = NULL)//内存不足,分配失败
		return false;

	//3.更改头结点属性next指针(头结点一般不存放数据)
	L->next = NULL 		//头结点之后暂时没有结点
	
	return true;
}

一般先声明一个指向单链表的指针LinkList L; 再初始化InitList(L);

3.4.2 增

  • 按位序插入
    (按位序插入可看成找到第i-1个元素+指定(第i-1个)结点的后插)
  • 指定结点的后插
  • 指定结点的前插
    (指定结点的前插可看成指定结点的后插+交换两结点中的数据)

1. 按位序插入
找到第i-1个结点,将新结点插入其后(头结点可看作是第0个结点)
在这里插入图片描述
时间复杂度:O(n)

代码实现:
考虑健壮性:插入位序i是否大于等于1,是否存在第i-1个元素

//1.带头结点的插入
bool ListInsert(LinkList &L, int i, ElemType e){
	//1.健壮性1
	if(i<1)
		return false;
	
	//2.找到第i-1个元素
	LNode *p = L;//p为当前扫描到的结点,初始化为第0个头结点
	int j = 0; //j指示当前是第几个结点
	
		//(每次循环结束,p指向第j个结点(使退出循环时p指向第i-1个结点))
	while(p!=NULL && j<i-1){ 
			p = p->next;
			j++;			
	}
		//健壮性2:第i-1个结点及其之前结点出现空,则插入不了
	if(p==NULL)			
		return false;	
			
	//3.创建新的结点s,并更新结点s的属性(可以加一次判断内存分配是否失败)
	LNode *s = new LNode;
	s->data = e;
	s->next = p->next;
	
	//4.插入到第i-1个元素之后,更新结点p的属性
	p->next = s;
	
	return true;
}

//2.不带头结点的插入(不存在第0个结点,i=1时插入删除需要更改头指针L)
bool ListInsert(LinkList &L, int i, ElemType e){
	//1.健壮性1
	if(i<1)
		return false;
	
	//2. i=1时特殊处理
	if(i == 1)
	{
		LNode *s = new LNode;
		s->data = e;
		s->next = L->next;
		L = s;				//头指针指向新结点
		return true;
	}

	//3.找到第i-1个元素
	LNode *p = L;//p为当前扫描到的结点,初始化为第1个结点
	int j = 1; //j指示当前是第几个结点
	
		//(每次循环结束,p指向第j个结点(使退出循环时p指向第i-1个结点))
	while(p!=NULL && j<i-1){ 
			p = p->next;
			j++;			
	}
		//健壮性2:判断该结点是否已不指向该堆区数据元素(第i-1个结点及其之前结点出现空,则插入不了)
	if(p==NULL)			
		return false;	

	//4.创建新的结点s,并更新结点s的属性
	LNode *s = new LNode;
	s->data = e;
	s->next = p->next;
	
	//5.插入到第i-1个元素之后,更新结点p的属性
	p->next = s;
	
	return true;

2. 指定结点的后插

在p结点之后插入元素e(按位序插入可看成找到第i-1个元素+指定(第i-1个)结点的后插)

时间复杂度:O(1)

代码实现:
考虑健壮性:结点p!=NULL

//1.带头结点的后插
bool ListInsert(LNode *p,ElemType e){
	//1.健壮性
	if(p == NULL)
		return false;
	
	//2.创建新的结点s,并更新结点s的属性(可以加一次判断s==NULL内存分配是否失败)
	LNode *s = new LNode;
	s->data = e;
	s->next = p->next;
	
	//3.插入到第i-1个元素之后,更新结点p的属性
	p->next = s;
}

3. 指定结点的前插
在p结点之前插入新结点

两种方法:

  • 法一:遍历查找结点p的前驱结点q,再对q后插,时间复杂度为O(n)
  • 法二:将新结点插入到p结点之后,然后互换两个结点的数据,时间复杂度为O(1)(可看成指定结点的后插+交换两结点中的数据)

代码实现:
考虑健壮性:结点p!=NULL

//1.带头结点的前插(法二)
bool InsertPriorNode(LNode *p, ElemType e){
	//1.健壮性
	if(p==NULL)
		return false;

	//2.创建新的结点s,并更新结点s的属性(可以加一次判断s==NULL内存分配是否失败)
	LNode *s = new LNode;
	s->data = e;
	s->next = p->next;

	//3.插入到第i-1个元素之后,更新结点p的属性
	p->next = s;

	//4.互换两个结点的数据
	s->data = p->data;
	p->data = e;

	return true;
}

3.4.3 删

  • 按位序删除(可看成找到第i-1个结点,将第i个结点删除)
  • 指定结点的删除

1. 按位序删除
找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点

时间复杂度:O(n)

代码实现:
考虑健壮性:插入位序i是否大于等于1,是否存在第i-1个元素、是否存在第i个元素

//1.带头结点的按位序删除
bool ListDelete(LinkList &L, int i, ElemType &e){
	//1.健壮性1
	if(i<1)
		return false;
	
	//2.找到第i-1个结点
	LNode *p = L;//p为当前扫描到的结点,初始化为第0个头结点
	int j = 0; //j指示当前是第几个结点
	
		//(每次循环结束,p指向第j个结点(使退出循环时p指向第i-1个结点))
	while(p!=NULL && j<i-1){ 
			p = p->next;
			j++;			
	}
		//健壮性2:第i-1个结点及其之前结点出现空、第i-1个结点之后无其他结点,则删除不了
	if(p==NULL)			
		return false;
	if(p->next==NULL)			
		return false;

	//3.指向被删除结点q(作为副本用于复制给结点p/元素e、然后释放该结点)
	LNode *q = p->next;		

	//4.更改结点p属性,删除的数据e
	p->next = q->next;
	e = q->data;

	//5.释放被删除结点的存储空间
	delete q;

	return true;
}

2. 指定结点的删除
删除指定结点p

  • 法一:遍历查找结点p的前驱结点q,删除p后修改前驱结点的next指针,时间复杂度为O(n)
  • 法二:互换结点p和其后继结点q的数据,然后将后继结点删除,时间复杂度为O(1)(可看成指定结点的后插+交换两结点中的数据)
    法二的局限性:若p结点是最后一个结点,则只能用法一找到结点p的前驱

代码实现:
考虑健壮性:结点p!=NULL,q->next!=NULL

//1.带头结点的指定结点的删除(法二)
bool DeleteNode(LNode *p){	
	//1.健壮性
	if(p==NULL)
		return false;

	//2.指向被删除结点q(后继结点)(作为副本用于复制给结点p、然后释放该结点)
	LNode *q = p->next;			//可增加判断q==NULL,因为p可能是最后一个结点

	//3.更新结点p的属性
	p->data = q->data;
	p->next = q->next;

	//4.释放后继结点的存储空间
	delete q;

	return true;
	
}

3.3.4 查

分为按位查找操作和按值查找操作

1. 按位查找操作
返回第i个元素

时间复杂度:O(n)

代码实现:
考虑健壮性:查找位序i是否大于等于1

//1.带头结点的按位查找
LNode* GetElem(LinkList L, int i){
	//1.健壮性1
	if(i<1)
		return NULL;
	
	//2.找到第i个元素
	LNode *p = L;//p为当前扫描到的结点,初始化为第0个头结点
	int j = 0; //j指示当前是第几个结点
	
		//(每次循环结束,p指向第j个结点(使退出循环时p指向第i个结点))
	while(p!=NULL && j<i){ 
			p = p->next;
			j++;			
	}
			
	return p;

2. 按值查找操作

时间复杂度:O(n),遍历数组

代码实现:

//1.带头结点的按值查找
LNode * LocateElem(LinkList L, ElemType e){
	//1.当前查找的结点(从第一个结点开始查找)
	LNode *p = L->next;

	//2.循环查找
	while(p!=NULL && p->data != e){
		p = p->next;
	}
	
	return p;	//找到则返回该结点指针,否则返回NULL
}

3.3.5 赋

初始化后就是将数据元素存入到单链表中,两种方法:
核心就是对指定结点的后插操作

  • 尾插法(设置一个表尾指针r)
  • 头插法(头插法的重要应用:实现链表的逆置)

1. 尾插法
每次取一个数据元素,插入到表尾(对尾结点的后插操作)

代码实现:

//1.带头结点的尾插法建立单链表
LinkList List_TailInsert(LinkList &L, ElemType x[]){
	int length = x.size();
	LNode *r = L;			//r为表尾指针,初始化为头结点
		
	for(int i = 0; i < length; ++i){
		//创建新的结点s,并更新结点s的属性(可以加一次判断内存分配是否失败)
		LNode *s = new LNode;
		s->data = x[i];
		s->next = NULL;		//也可删去该句,在循环结束后加上r->next = NULL

		//插入到尾结点后面
		r->next = s;

		//更新尾结点r
		r = s;
	}
	
	return L;
}

2. 头插法(可实现链表的逆置)
每次取一个数据元素,插入到表头(对头结点的后插操作)

代码实现:

//1.带头结点的头插法建立单链表
LinkList List_HeadInsert(LinkList &L, ElemType x[]){
	int length = x.size();
	
	for(int i = 0; i < length; ++i){
		//创建新的结点s,并更新结点s的属性(可以加一次判断内存分配是否失败)
		LNode *s = new LNode;
		s->data = x[i];
		s->next = L->next;

		//插入到头结点后面,更新头结点的next指针
		L->next = s;
	}
	return L;
}

3.3.6 其他

1. 判断是否空表操作

bool Empty(LinkList L){
   if (L==NULL)//若带头结点则判断条件改为:if (L->next == NULL)
   		return true;
   else
    	return false;
}

2. 带头结点的求表长度

//1.带头结点的求表长度
int Length(LinkList L){
	int len = 0;
	LNode *p = L->next;
	
	while(p!=NULL){
		p = p->next;
		++len;
	}
	
	return len;	
}

四、其他链式表实现基本运算

4.1 双链表

//双链表结点结构
typedef struct DNode{
	ElemType data; 			//每个结点存放的数据元素
	struct DNode *prior, *next;		//前驱和后继指针
}DNode, *DLinkList;

以下都是基于带头结点的

4.1.1 创

代码实现:
健壮性:判断分配的指针是否为空(内存分配失败)

//带头结点的初始化
bool InitDLinkList(DLinkList &L){
	//1.分配头结点
	L = new DNode;

	//2.健壮性
	if (L == NULL)//内存不足,分配失败
		return false;

	//3.更改头结点属性next指针(头结点一般不存放数据)
	L->prior = NULL;		//头结点的prior永远指向NULL
	L->next = NULL; 		//头结点之后暂时没有结点
	
	return true;
}

4.1.2 增

指定结点的插入:在p结点之后插入s结点
代码实现:
健壮性:p != NULL, s !=NULL, p的后继是否为空

//带头结点的指定结点的插入
bool InsertNextDNode(DNode *p, DNode *s){
	//1.健壮性
	if(p == NULL || s == NULL)
		return false;

	//2.从后往前更改三个结点的属性
	if(p->next != NULL)			//结点p的后继(健壮性:如果有后继结点)
		p->next->prior = s;

	s->next = p->next;			//要插入的结点s
	s->prior = p;				

	p->next = s;				//结点p
	
	return true;
}

4.1.3 删

删除指定结点的后继结点

代码实现:
健壮性:判断分配的指针是否为空(内存分配失败)

//带头结点的删除指定结点的后继结点
bool DeleteNextDNode(DNode *p){
	//1.健壮性
	if(p==NULL)
		return false;

	//2.指向被删除结点q(后继结点)并判断q==NULL,因为p可能是最后一个结
	DNode *q = p->next;			
	if(q==NULL)
		return false;

	//3.从后往前更改两个结点的属性(也可从前往后更新)
	if(q->next != NULL)			//结点q的后继(健壮性:如果有后继结点)
		q->next->prior = p;

	p->next = q->next;			//结点q的前驱(即p结点)

	//4.释放结点q的存储空间
	delete q;
	
	return true;
}

4.1.4 遍历

在这里插入图片描述

4.2 循环链表

  • 循环单链表:表尾结点的next指针指向头结点(结点结构与单链表相同)
    从一个结点出发可以找到其他任何一个结点
    在这里插入图片描述
    单手抱住空虚的自己

  • 循环双链表:表头结点的prior指向表尾结点;表尾结点的next指向头结点(结点结构与双链表相同)
    在这里插入图片描述
    双手抱住空虚的自己

4.2.1 创

循环单链表: L->next = L;
在这里插入图片描述
循环双链表: L->prior = L; L->next = L;
在这里插入图片描述

4.2.2 增

循环双链表:不用判断结点p是否有后继结点

4.2.3 删

循环双链表:不用判断被删除结点q和q的后继结点

4.2.4 判空

循环单链表:if(L->next == L)

循环双链表:if(L->next == L)

4.3 静态链表

分配一整片连续的内存空间,各个结点集中安置,用数组的方式实现的链表
在这里插入图片描述

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

代码实现:

#define MaxSize 10

//静态链表结构类型的定义
typedef struct{
	ElemType data;		//存储数据元素
	int next;			//下一个元素的数组下标
}SLinkList[MaxSize];

//声明时:SLinkList L 相当于定义了一个长度为MaxSize的Node型数组L

4.3.1 创

把头结点a[0]的next设为-1,把其他结点的next设为一个特殊值用来表示结点空闲,如-2

4.3.2 增

插入位序为i的结点:
①从头结点出发找到位序为i-1的结点
②找到一个空的结点,存入数据元素
③修改新结点的next
④修改i-1号结点的next

4.3.3 删

删除某个结点:
①从头结点出发找到前驱结点
②修改前驱结点的游标
③被删除结点next设为-2

4.3.4 查

从头结点出发挨个往后遍历结点

总结

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值