数据结构-线性结构-基础介绍

前言

本文是看 bilibili · 王道考研 · 数据结构 的视频课程时做的一些记录,如构成侵权,请联系我删除

另外,本文有部分内容是根据我的理解写的,可能有不明确的地方 awa

王道考研的视频地址我就不放了,大家请移步 bilibili 直接搜索 王道考研 就能搜索到

线性结构 - 一对一关系

除了第一个元素之外,每个元素都有唯一前驱;除最后一个元素外,每个元素都有唯一后继

线性表 - Linear List

定义

  • 线性表中的数据元素的 数据类型 相同

  • 有限序列 - 元素之间有次序 + 元素有限

  • 若 n 为表长,n=0时表示线性表为空表

  • 线性表的角标是从1开始的,这个角标表示其位置,叫作位序

    a1 叫作表头元素,an 叫作表尾元素

    线性表:L=(a1,a2,a3,······,ai,······,an)

    程序中数组的下标从0开始

基本操作

  1. 初始化线性表 - InitList(&L)
  2. 销毁线性表 - DestoryList(&L)
  3. 插入元素 - ListInsert(&L,i,e)
  4. 删除元素 - ListDelete(&L,i,&e) - e返回删除的值
  5. 按值查找 - LocateElem(L,e)
  6. 按位查找 - GetElem(L,i)
  • 其他常见函数
  1. 求表长 - Length(L)
  2. 输出线性表元素 - PrintList(L)
  3. 判空 - Empty(L)

为什么要做这个基本操作

  • 方便队友调用
  • 减少同一功能的重复编写,降低出错率

其实可以理解为线性表是元素之间的逻辑关系,表达元素之间的线性结构组成,线性表更加符合使用场景中的描述【位序从a1开始,到an结束】
后续出现的顺序表、链表、栈、队列等都可以看成特殊的线性表,但是顺序表等数据结构更加考虑到程序设计和实现的情况【数组位序从a0开始】

顺序表 - 用顺序存储的方式实现线性表

定义

  • 通过数据元素的大小 + 线性表首元素的地址 == 定位顺序表的元素位置

    数据元素的大小:sizeof(Elem Type) 会返回一个 值=Elem 的大小

  • 特点

    随机访问:O(1)时间找到指定位置的元素

    存储密度高:每个节点只存储数据元素,(不存在指针等占用空间)

    拓展容量不方便

    数据的增删操作不方便

静态分配 - 这个很简单的

这个静态分配更加类似于数组的结构

#include<stdio.h>
#define MaxSize 20	//宏定义最大长度 

typedef int ElemType; //定义数据类型 
typedef struct
{
    //顺序表最大容量
    //程序运行时,内存就会分配MaxSize*sizeof(Elem Type)的空间给程序
	ElemType data[MaxSize];	  
	int length;	//顺序表长度(元素个数)  
}SqList;

//初始化顺序表 
void InitList(SqList &L)
{
	for(int h=0;h<L.length;h++)
		L.data[h]=0;	//此步骤可以省略,详细解释如下
	L.length=0;
}

初始化顺序表:可以省略,不过如果不进行初始化,数组可能获取到之前遗留的脏数据,导致没有进行赋值的数据元素呈现怪异值

脏数据:多数是"没有进行初始化+没有进行赋值"的数据,读取了内存中残留的数据

C语言为数组的初始值设为 0:这个其实和编译器有关,并不是一定的

动态分配 - 解决静态分配顺序表长度不可变的问题

这个动态分配就更加类似于 动态数组 - 用数组头结点指针表示数组

  • 指针指向动态顺序表的第一个元素的地址

  • malloc函数、free函数 - (C++ 可以使用new函数、delete函数)

    malloc函数、free函数包含在头文件 stdlib.h

    malooc每次都向内存申请一片新的连续的地址,返回一个无类型的、指向这片地址的首地址 的指针
    无类型:所以使用malloc时,需要在最后强制转换返回值类型为我定义的数据元素类型的指针

    申请空间的大小由malloc的参数决定

    //此处的InitSize为顺序表的初始长度,malloc也是首次申请内存
    //L.data指向动态顺序表的第一个元素地址
    L.data=(ElemType *)malloc(sizeof(ElemType)*InitSize);
    
//realloc 可以实现动态数组的扩容,不过此处不展示
#include<stdio.h>
#include<stdlib.h> 
#define InitSize 10 // 最开始时顺序表容量 
#define creaselen 10 // 每次扩容的长度 

typedef int ElemType;
typedef struct
{
	int * data;	//顺序表首地址 
	int length;	//顺序表元素个数  
	int MaxSize;//顺序表最大容量 
}SeqList; 

//初始化顺序表 
void InitList(SeqList &L)
{
//	这边注意一个地方:sizeof(ElemType),而不是SeqList 
	L.data=(ElemType *)malloc(InitSize*(sizeof(ElemType)));
	L.length=0;
	L.MaxSize=InitSize;
}
//给顺序表扩容 
//基本思路:先用p保存数据,然后给L一个新&&大的空间,然后将p的数据复制到L 
void IncreaseSize(SeqList &L)
{
	ElemType * p; // p为ElemType的指针类型变量
	p=L.data;
	L.data=(ElemType *)malloc((L.MaxSize+creaselen)*sizeof(ElemType));
	for(int h=0;h<L.length;h++)
//	用法有点像数组,可以去看看动态数组-指针数组 
		L.data[h]=p[h];
	L.MaxSize=L.MaxSize+creaselen;
	free(p);
} 

基本操作及代码 + 时间复杂度分析

//假设i位置在1~L.MaxSize之间
//在线性表中的第i位置插入数据e
void ListInsert(SeqList &L,int i,ElemType e)
{ 
	i--; //位序和数组序转换一下【位序 == 数组下标 + 1】
	if(L.length+1>L.MaxSize)
		IncreaseSize(L);
	for(int h=L.length;h>=i;h--)
		L.data[h]=L.data[h-1];
	L.data[i]=e;
	L.length++;
}
/*时间复杂度分析
	1. 最好情况:插入到表尾,T(n)=O(1)
	2. 最坏情况:插入到表头,T(n)=O(n)
	3. 平均情况:假设新元素插入到任何位置的概率(p)相同
	T(n)=np+(n-1)p+(n-2)p+···+2p+p,取极限为n/2,T(n)=O(n)
*/
//----------------------------------------------------------
//删除表中第i位置的元素,并用e返回删除值 
void ListDelete(SeqList &L,int i,ElemType &e)
{
	//位序和数组序转换
	i--;
	e=L.data[i];
	for(int h=i;h<L.length-1;h++)
		L.data[h]=L.data[h+1]; 
	L.data[L.length-1]=0; //初始化
	L.length--; 
}
/*时间复杂度分析
	同'在线性表中的第i位置插入数据e',不同点在于删除操作在局部上看,需要操作的次数比插入操作少一次,但是在讨论时间复杂度的整体来看,差别不大
	1. 最好情况:T(n)=O(1)
	2. 最坏情况:T(n)=n-1=O(n)
	3. 平均情况:T(n)=(n-1)/2=O(n)
*/
//----------------------------------------------------------
//按值查找  ==  返回值所在的第一个位置 ,如果没有,返回0 
int LocateElem(SeqList L,ElemType e)
{
	for(int h=0;h<L.length;h++)
		if(L.data[h]==e)
			return (h+1);
	return -1; 
} 
/*时间复杂度分析
	同'在线性表中的第i位置插入数据e'的时间复杂度
*/
//----------------------------------------------------------
//按位查找 ,i表示第i位置 
ElemType GetElem(SeqList L,int i)
{
	i--;
	return L.data[i]; 
}
/*时间复杂度分析
	T(n)=1
*/
//----------------------------------------------------------
//输出顺序表元素 
void PrintList(SeqList L)
{
	for(int h=0;h<L.length;h++)
		printf("%d ",L.data[h]);
	printf("\n");
}

链表 - 链式结构实现线性表

单链表 - 每个节点需要存储下一节点的地址

不要求大片连续的空间,改变容量方便 + 但是不能随机存储,要耗费一定空间存放指针 我觉得应该还有增删操作不便【但是比起顺序表来说那还是方便多了】

  • 不带头结点
  • 带头结点 - 更方便 —— 所以一般采用带头结点的方式

单链表的定义

LNode * 和 LinkList【图片来自王道考研视频】

// 不带头结点 
#define ElemType int // 其实这个预定义的 ElemType 也可以用 typedef 实现
typedef struct LNode //这个LNode是为了在下面定义指针时使用 即 LNode* next
{
    ElemType data;
    LNode * next; // 这边可以不加 struct是因为我用的 C++ 
}LNode,* LinkList;

/*
    // 以上代码等价于
    struct LNode
    {
    	ElemType data;
    	LNode * next; // 这边可以不加 struct是因为我用的 C++ 
    };
    typedef LNode LNode;
    typedef LNode * LinkList;
    */ 

// 初始化
bool InitList(LinkList &L)
{
    L=NULL; // 防止脏数据 
    return true;	
} 
// 带头结点 
#define ElemType int
typedef struct LNode
{
    ElemType data;
    LNode * next; // 这边可以不加 struct是因为我用的 C++ 
}LNode,* LinkList;

// 初始化
bool InitList(LinkList &L)
{
    L=(LNode *)malloc(sizeof(LNode)); // 分配一个头结点
    if(L==NULL) // 没分配到空间
        return false;
    else
    {
        L->next=NULL; // L为指针用 -> 
        return true;
    }
} 

单链表的基本操作

带头结点
// 按位序插入 - 在 i 号位置上插入元素 e 
bool ListInsert(LinkList &L,int i,ElemType e)
{
	// 判断 i 合法性 ···
	if(i<1) return false; // 默认头节点为 0号节点
	LNode* q=L; // 遍历链表 
	while(q!=NULL&&i>1)  // 目的就是找到第 i-1 的结点【位序】
	{
		i--;
		q=q->next;
	} 
	if(q==NULL) return false; 
    // i 值不合法 - i所在位置没有前驱节点
	// 如 往链表[1,2]中插入6号位置节点 
	// 使用上述判断能够成功的原因是,我们需要前驱节点做链接,遍历到插入位置的前一个位置就停止
	LNode* p=(LNode*)malloc(sizeof(LNode));
	if(p==NULL) return false; // 没有申请到空间 
	LNode* temp=q->next;
	q->next=p;
	p->next=temp;
	p->data=e;
	return true;
} 
//上述代码的插入方式平均时间复杂度为O(n) -- 很好理解的吧

// 输出函数
void ListOut(LinkList L)
{
	printf("\n链表输出:\n");
	LNode* q=L;
	q=q->next; //隐藏头节点 
	while(q!=NULL)
	{
		printf("%d ",q->data);
		q=q->next;
	}
} 
// 向指针形式的结点 p 之后插入元素 e ===== 时间复杂度O(1)
bool InsertNextNode(LNode* p,ElemType e)
{
	//【需要判断 p 的存在噢】 
	if(p==NULL) return false; 
	LNode* q=(LNode*)malloc(sizeof(LNode));
	if(q==NULL) return false;
	q->data=e;
	LNode* temp;
	temp=p->next;
	p->next=q;
	q->next=temp;
	return true;
}  
// 向指针形式的结点 p 之前插入元素e ===== 时间复杂度O(n)
bool InsertPriorNode(LNode* p,ElemType e)
{
	if(p==NULL) return false;
	LNode* q=(LNode*)malloc(sizeof(LNode));
	if(q==NULL) return false;
	q->data=e;
	LNode* prior=L; // 这里的 L 不声明是因为我在最前面声明了全局变量【大聪明】
	while(prior->next!=p) prior=prior->next;
	prior->next=q;
	q->next=p;
	return true;
} 
// 向指针形式的结点 p 之前插入元素e-交换数据法 【这可真是个小机灵鬼啊】 ===== 时间复杂度O(1)
bool InsertPriorNodeSwap(LNode* p,ElemType e)
{
	if(p==NULL) return false;
	LNode* q=(LNode*)malloc(sizeof(LNode));
	if(q==NULL) return false;
	// 先使用后插法,将新结点插到 p 后面,然后,交换 p 和新结点的数据
	LNode* temp=p->next;
	p->next=q;
	q->next=temp;
	q->data=p->data;
	p->data=e;
	return true;
} 

当链表 L 作为全局变量在程序开头声明时的情况,L 本身参与各个函数运算,因此,函数会对 L 本身做出改变的时候,可以不用 &

向指针形式的结点 p 之前插入元素e-交换数据法
这个方法有个很明显的 BUG:无法处理【向头结点之前插入元素】的操作

// 删除位序 i 的结点,并返回结点 i 的值
bool ListDelete(int i,ElemType &e)
{
	if(i<1) return false; 
	LNode* p=L;
	while(p!=NULL&&i>1) // 定位到 i-1 结点,记为 p 
	{
		i--;
		p=p->next;
	} 
	if(p==NULL) return false; // i-1 超出范围 
	LNode* Nodei=p->next; // i 结点 
	e=Nodei->data;
	p->next=Nodei->next;
	free(Nodei);
	return true;
} 
// 删除指针形式的结点 p,并返回 p 结点的值
bool DeleteNode(LinkList &L,LNode* p,ElemType &e)
{
	if(p==NULL) return false; // p 指针判空 
	LNode* ReadNode=L; // ReadNode 为 p 结点的前一个结点 
	while(ReadNode->next!=p) ReadNode=ReadNode->next;
	ReadNode->next=p->next;
	e=p->data;
	free(p);
	return true; 
} 

本篇涉及【参数为指针形式的结点】时,不考虑结点不在单链表之内的情况

删除指针形式的结点 p,并返回 p 结点的值
这个也有交换【结点 p | 结点 p 之后的结点】的值的做法,但是,明显,有无法完成删除尾节点操作的BUG

// 查找 - 按位查找 查找位序为 i 的结点并返回结点指针 
// 这个返回 NULL 是我自己发挥的,在主程序运行会出问题的 
LNode* GetElem(LinkList L,int i)
{
	// 考虑头结点作为 0 位序结点,不能被查找 
	if(i<1) return NULL;
	LNode* ansnode=L;
	while(ansnode!=NULL&&i>0)
	{
		ansnode=ansnode->next;
		i--;
	}
	if(ansnode==NULL) return NULL;
	return ansnode;
} 
int elem_pos; // 服务于主函数的输出,作为查找函数并不需要 
// 查找 - 按值查找 查找到值为 e 的结点,并返回该结点指针
LNode* LocateElem(LinkList L,ElemType e)
{
	elem_pos=1; // 服务于主函数的输出,作为查找函数并不需要 
	LNode* p=L->next; // 不从L开始是因为本篇的头结点在逻辑上是没有data的
	while(p!=NULL&&p->data!=e)
	{
		p=p->next;
		elem_pos++; // 服务于主函数的输出,作为查找函数并不需要 
	}
	if(p==NULL)
	{
		elem_pos=-1; // 服务于主函数的输出,作为查找函数并不需要 
		return NULL;
	}
	return p;	
} 
// 求表长
int Length(LinkList L)
{
	int len=-1;
	LNode* p=L;
	while(p!=NULL)
	{
		p=p->next;
		len++;
	}
	return len;
} 
// 尾插法构建单链表 
LinkList List_TailInsert(LinkList &L)
{
	int n; // 每次向单链表中插入的元素值 
	L= (LinkList)malloc(sizeof(LNode)); // 声明头结点,使用 LNode* 作为 malloc 返回类型也行
	L->data=0; // 我自己需要的补充赋值 
	LNode* tail=L; // 用作标记尾节点 
	LNode* temp; // 用作声明每次新增的结点 
	while(scanf("%d",&n)&&n!=-1) // 当输入的值不为 -1 时,将该值插入链表 
	{
		temp=(LNode*)malloc(sizeof(LNode));
		temp->data=n;
		tail->next=temp;
		tail=temp;
	} 
	tail->next=NULL; // 尾节点的 next 赋值为 NULL 
	return L;
} 
// 头插法构建单链表 
LinkList List_HeadInsert(LinkList &L)
{
	int n;
	L=(LinkList)malloc(sizeof(LNode));
	L->data=0; // 补充赋值 
	L->next=NULL;  // 赋值为 NULL 
	// 1. 之后 temp->next=L->next; 操作需要 next | 2. 防止默认的 next 指针指向内存的奇怪位置 
	LNode* temp; // 新加入的结点 
	while(scanf("%d",&n)&&n!=-1)
	{
		temp=(LNode*)malloc(sizeof(LNode));
		temp->data=n;
		temp->next=L->next;
		L->next=temp;
	}
	return L; 
}

头插法的重要使用场景:链表的逆置

不带头节点
// 按位序插入 - 在位置 i 的地方插入 e
bool ListInsert(LinkList &L,int i,ElemType e) 
{
	if(i<=0) return false; // 判断 i 合法性
	LNode* p=(LNode*)malloc(sizeof(LNode)); // 申请一个结点空间放要插入的东西 
	if(p==NULL) return false; // 没有申请到新结点的空间
	p->data=e; 
	p->next=NULL;
	if(i==1)
	{
//		LNode* temp=L;
//		L=p;
//		L->next=temp; 下面这种写法很明显优于我的awa 
		p->next=L;
		L=p; 
		return true;
	}
	LNode* q=L; // 记录 i-1 的结点
	/*
	注意:为啥这里是 i>2 ,对比"带头结点的",目的都是找到 i-1 位序的元素指针
	1. 对于带头结点的: i-1 位序 ==> 头结点0 结点1 ··· 结点i-1 结点i
	2. 对于不带头结点的:i-1 位序 ==> 头结点1 结点2 ··· 结点i-1 结点i
	因此,不带头结点的单链表,遍历到 i-1 需要比带头结点的单链表少移动一次 
	则【如上:1.到结点 1 移动 1 次,2.到结点 1 移动 0 次】 
	*/
	while(q!=NULL&&i>2)
	{
		i--;
		q=q->next;
	}
	LNode* temp=q->next;
	q->next=p;
	p->next=temp;
	return true;
}

//  输出函数 
void ListOut(LinkList L)
{
	LNode* q=L;
	printf("单链表为:\n");
	while(q!=NULL)
	{
		printf("%d ",q->data);
		q=q->next;
	}
	printf("\n");
} 
// 求表长
int Length(LinkList L)
{
	int len=0;
	LNode* p=L;
	while(p!=NULL)
	{
		p=p->next;
		len++;
	}
	return len;
} 
// 尾插法构建单链表
LinkList List_TailInsert(LinkList &L)
{
	int n;
	L=(LinkList)malloc(sizeof(LNode));
	L->data=NULL; // 提前赋值,防止出现直接输入 -1【即建立空链表】时出现 data 为存在的任意值
    // 因为尾插法的核心思想是:在一个记录尾结点的 tailnode 后不断插入新结点
    // 当为不带头结点的链表时,第一个尾结点为逻辑上不存在的链表头结点
	LNode* tailnode=L; // 尾结点 
	LNode* tempnode; // 新结点
	bool flag=true; // 计数,记录当前被插入的结点是否应该是头结点
    // 不能直接使用 else 中的语句,要考虑到第一个结点一定要变成头结点 L 的问题
	while(scanf("%d",&n)&&n!=-1)
	{
		if(flag)
		{
			tailnode->data=n;
			flag=false;
		}
		else
		{
			tempnode=(LNode*)malloc(sizeof(LNode));
			tempnode->data=n;
			tailnode->next=tempnode;
			tailnode=tempnode;
		}
	} 
	tailnode->next=NULL; // 尾结点 next 赋空值
	return L;
}
// 头插法构建单链表 
LinkList List_HeadInsert(LinkList &L)
{
	int n;
	L=(LinkList)malloc(sizeof(LNode));
    // 初始化 L,防止建立空链表时出错
	L->data=NULL;
	L->next=NULL;
	LNode* temp;
	bool flag=true;
	while(scanf("%d",&n)&&n!=-1)
	{
		if(flag)
		{
			L->data=n;
			flag=false;
		}
		else
		{
			temp=(LNode*)malloc(sizeof(LNode));
			temp->data=n;
			temp->next=L;
			L=temp;
		}
	}
	return L;
}

头插法的思想是:
新结点的 next 是原链表的头结点,然后原链表头结点换成现在的新结点
而不是 新结点成为头结点,然后继承原来头结点的后继

双链表

基础理解:双链表就是设计一个数据结构 DNode ,这个结构中含有数据项
数据
前驱节点指针
后继节点指针

双链表是一个多了前驱结点指针的单链表,指针双向,因此称为双链表

双链表的定义

typedef struct DNode
{
	ElemType data;
	DNode* prior; // 前驱结点
	DNode* next; // 后继结点 
} * DLinkList, DNode;

双链表的基本操作【伪代码】

// 初始化
// 注意, 带头结点的双链表 L->next=NULL; 注意区分双链表和循环链表
// 双链表和单链表原则上是一致的,只是比单链表多加一个指向前驱结点的指针 
bool InitDLinkList(DLinkList &L)
{
	L=(DNode*)malloc(sizeof(DNode));
	if(L==NULL) return false;
	L->next=NULL;
	L->prior=NULL; // 此处的头结点的前驱结点其实确定 == NULL 
	return true;	
} 

// 判空
bool isEmpty(DLinkList L)
{
 /*
 	if(L->next==NULL) ==> isEmpty
 */ 
}

// 在 p 结点之后插入一个结点 s 
// 由于,s 结点是会修改其前驱后继结点的,因此需要取地址符 
// 需要着重注意的是:p 结点有可能是尾结点 
bool InsertNextDNode(DLinkList &L,DNode* p,DNode* s)
{
 /*
 	if(p->next==NULL)
 		p->next=s;
 		s->prior=p;
 		s->next=NULL;
	else
		DNode* oldnext=p->next;
		oldnext->prior=s;
		s->next=oldnext;
		p->next=s;
		s->prior=p;
	// =========================================== //
	DNode* oldnext=p->next;
	p->next=s;
	s->next=oldnext;
	s->prior=p;
	if(oldnext!=NULL)
		oldnext->prior=s; // 王道考研的视频写为 p->next->prior=s; 但是我觉得我写得好些 
 */
}

// 删除 结点 p 的后继结点
// 注意:p 结点 的后继结点是尾结点的情况 
bool DeleteNextDNode(DLinkList &L)
{
 /*
 	DNode* nextNode=p->next;
 	if(nextNode==NULL) return false; // 排除 p 没有后继结点的情况
	p->next=nextNode->next;
	if(nextNode->next!=NULL)
		nextNode->next->prior=p; // 依然不建议使用这种方式 
	free(nextNode);	
	return true;
 */
} 

// 删除一个双链表
bool DestoryList(DLinkList &L)
{
 /*
 	//遍历链表,删除所有结点,最后 L 指向 NULL
 	DNode* readNode=L;
 	while(readNode!=NULL)
 		DNode* temp=readNode;
 		readNode=readNode->next;
 		free(temp);
 	L=NULL;
 	// =============================================================== //
 	// 王道考研的思路是:删除头结点后的每个结点,调用 DeleteNextNode 函数 ,最后释放 L 
 */
} 

// 双链表的前向遍历 | 双链表的后向遍历 ==> p 结点向前或向后遍历
// 注意前向遍历到头结点的时候的处理 
void ReadForeward(DLinkList L,DNode* p)
{
 // 前向遍历的循环结束条件是 readNode->prior==NULL 
} 
void ReadBackward(DLinkList L,DNode* p)
{
	
}

// 按位查找 | 按值查找 
/*
	其实按位查找和按值查找的操作和单链表几乎一样,甚至可以一样,时间复杂度 O(n) 
*/
DNode FindbyPos(int i)
{
	
}
DNode FindbyValue(ElemType e)
{
	
} 

循环链表

循环链表是一个首位相连的单链表或者双链表

虽然循环链表与普通的单链表、双链表没太大的差距,但是循环链表有其特殊的应用场景:
对于需要频繁操作表头、表尾的链表,可以使用循环链表

|| 利用 尾结点 -> next == 头结点的特性,可以将头尾操作控制在时间复杂度 O(1) ,即每次操作都针对尾结点
|| 需要注意 == 链表指针指向尾结点,这样才能每次直接通过链表指针找到尾结点
|| 需要注意 == 当链表尾结点发生变化时,也要同步链表指针的变化

对于循环链表的定义与操作和单链表、双链表相似,只是需要特殊注意头结点和尾结点的处理

循环单链表

首尾相接的单链表:尾结点 -> next = 头结点

链表初始状态为 L -> next = L;

循环双链表

首尾相接的双链表:尾结点 ->next = 头结点 && 头结点 -> prior = 尾结点

链表初始状态为 L -> next = L; 此时也有 L -> prior = L;
但是 判断循环双链表是否为空 可以只需要一个条件 L -> next = L;

静态链表

  • 由代码可知:这个静态链表物理结构上其实是一个数组 StsticNode array[]
    因此,这个链表在内存中分配到的是一整片位置
    同时具有数组的性质:只要知道数组的0下标指针,那么其余下标的元素的地址可计算,数组的元素平均分配一整片空间,每个元素分配到的大小为 sizeof(StaticNode)
  • 代码的逻辑关系是:带头结点的单链表。
    数据项 data 对应结点的值,next 对应本结点的后继结点的数组下标
  • 实现逻辑比较灵活,如:StaticLinkList[0]为头结点,
    StaticLinkList[0].next=2; 表示头结点的下一个结点(后继结点)在数组下标为2的地方,
    StaticLinkList[2],这个结点的下一个结点可能是数组任意一个位置
    尾结点的next标记为-1
  • 空链表可以表示为 StaticLinkList[0].next=-1;
  • 在定义静态链表的时候,规定 next = -2; 为了之后取用数据结点时,判断结点是否还没被使用
#include<stdio.h>
#include<stdlib.h>
#define Maxsize 100
#define ElemType int
// 以上 define 语句可替换成 typedef int ElemType; [注意需要 ; ] 

typedef struct
{
	ElemType data;
	int next=-2;
} StaticLinkList[Maxsize];
 /*
 	上面这种写法确实很少见,毕竟一般我们很少这样定义,我们的一般写法是 
	struct StaticNode
	{
		ElemType data;
		int next=-2;
	};
	typedef StaticNode StaticLinkList[Maxsize];
	其中需要注意:
	1. struct本身的使用法就带有分号 struct Name{ };
	2. typedef本身的使用法也带有分号 typedef struct newname;
	3. typedef StaticNode StaticLinkList[Maxsize]; 
	   代码特殊说明:按照 C 语言的使用方式,StaticNode 前面需要加上 struct
	   声明 StaticLinkList 为大小为 Maxsize 的 StaticNode 类型的数组 [注意声明数组的写法] 
 */
// 由以上代码可知:StaticLinkList == StaticNode array[Maxsize];
// 以上写法是为了代码的可读性,StaticLinkList声明的数据结构为一个静态链表,array声明的是数组 

总的来说,静态链表是一个不怎么好用同时又很灵活的单链表。
普通的单链表的结点根据系统分配储存在内存的未知空间,静态链表的结点位置由结构代码决定

顺序表 | 链表 【比较】

复习一下之前的顺序表,顺序表类似于数组
静态实现方式:是标记了元素个数的数组
动态实现方式:是标记了大小、元素个数的动态数组(指针数组)

对于动态数组,理解是:

ElemType* array;
array=(ElemType*)malloc(Maxsize*sizeof(ElemType));
// 以上代码的意思:声明一个指针 array 作为动态数组的头结点
// 以头结点为基础,申请一片连续空间。空间中包含 Maxsize 个 ElemType 元素的大小

顺序表和链表在逻辑关系上:线性表
顺序表和链表在物理关系上:略

顺序表的销毁:需要修改数据项 length=0;
对于顺序表的静态实现方式,设置 length = 0 之后,系统会自动回收空间
对于顺序表的动态实现方式,设置 length = 0 之后,还需要 free(L.data); 回收数据项 data 所代表的动态数组

需要额外注意,从代码逻辑层面分析,顺序表和链表 增、删 操作的时间复杂度为 O(n)
但是
考虑到实际物理分析,顺序表对应的增删操作还要附带其余结点的移动时间,因此,对于数据量大或者数据量不确定的应用场景,选择链表更加适用 == 链表应对增删操作用的实际时间更短

栈【后进先出LIFO】

栈:是只能对一端进行增删操作的线性表。

n个互异的元素出栈的顺序情况数量【图片来自王道考研视频】

如:对 a b c d e 的合法出栈的情况:b c e d a【a in | b in | b out | c in | c out | d in | e in | e out | d out | a out】

顺序栈

理解上来说:顺序栈就是使用数组实现的栈,栈的大小不能变化

实现思想:
数据项 ElemType data[Maxsize] 储存栈中的数据
数据项 top 标记栈顶所对应的游标

顺序栈的定义

struct SqStack
{
	ElemType data[Maxsize];
	int top=-1; // 空栈,如果不提前设置为 -1,则需要 InitSqStack 函数修改 top 进行初始化 
}; // 当然这种定义结构体的方式是我喜欢用的,数据结构更喜欢连带 typedef 使用 

当然,标记栈顶的 top 可以是别的位置
如,top = 0 表示空栈的设计,即 top 标记栈顶的下一个游标(数组下标)
此时,需要注意操作写法:满栈对应 top == Maxsize,Push、Pop操作注意操作顺序

顺序栈对应的基本操作

// 判空 [空栈返回 true]
bool isEmpty(SqStack S)
{
	if(S.top==-1) return true;
	return false;
} 

// 进栈 Push
bool Push(SqStack &S,ElemType e)
{
	if(S.top+1==Maxsize) return false; // 满栈
	S.top++;
	S.data[S.top]=e; // 核心代码 S.data[++S.top]=e; 
	return true;	
} 

// 出栈 Pop [删除栈顶 + 栈顶元素由 e 接收]
bool Pop(SqStack &S,ElemType &e)
{
	if(S.top == -1) return false;
	e=S.data[S.top];
	top--;	// 核心代码 e=S.data[S.top--]; 
	return true;
} 

// 读栈顶 
bool GetTop(SqStack S,ElemType &e)
{
	if(S.top == -1) return false;
	e=S.data[S.top];
	return true;
} 

共享栈

两个栈共用同一片空间,两个栈分别从空间的两端向中间进栈,其中 top0 是栈0的栈顶标记,top1是栈1的栈顶标记
top0 = -1; top1 = Maxsize; 为共享栈的初始状态
top0 + 1 == top1; 表示满栈

链栈

使用链表实现的栈

typedef struct StackNode
{
  ElemType data;
  StackNode* next;
} * LStack;

说明:
链栈的实现思路是:头结点 | 头结点的下一结点 作为栈顶,对 头结点 | 头结点的下一结点 进行操作

带头结点

// 初始化 | 空栈
L->next == NULL;
// 入栈 Push
StackNode* temp = (StackNode*)malloc(sizeof(StackNode));
temp.data = e;
temp.next = L->next;
L->next = temp;
// 出栈 Pop
StackNode* temp = L->next;
e = temp.data; // 返回栈顶元素
L->next = temp->next;
//free(temp);

注意!!!
出栈和获取栈顶元素需要判定是否栈空

不带头结点

// 初始化 | 空栈
L == NULL;
// 入栈 Push
StackNode* temp = (StackNode*)malloc(sizeof(StackNode));
temp.data = e;
temp->next = L;
L = temp;
// 出栈 Pop
StackNode* temp = L;
e = temp.data;
L = temp->next;
//free(temp);

队列【先进先出FIFO】

顺序队列

普通顺序队列

这就不用多说了 数组 + 下标表示队首队尾

  1. 队尾标记为最后插入元素的位置
  2. 队尾标记为最后插入元素的下一个位置

循环队列

在普通队列的基础上,进行以下改进【为了利用空间awa】

  1. 将队列看成环状,队首之前的数组空间重复利用
  2. 每次入队时进行操作
    (rear + 1)%Maxsize == front; 满队
    如果队列不满 rear++; rear = rear%Maxsize
  3. 每次出队时进行操作
    判空
    front = (front+1)%Maxsize;

根据队尾下标的位置,可以有两种初始化选择
front = 0; rear = 0;
front = 0; rea = Maxsize - 1;

特殊设计队列

​ 以上循环队列还是需要一个空数组元素,在判断为满队列时 需要用到一个空数组位置来区分。
​ 因此

  1. 当 rear 指向队尾的下一个元素,rear + 1 == front; 为队满,此时,rear所在位置不能进行入队,如果入队,rear将和front指向同一位置,与空队判定产生冲突
  2. 当 rear 指向队尾元素,rear在初始化时,rear = Maxsize - 1; ,因此array[Maxsize-1]不能是满队列的队尾

​ 解决办法

  1. 设置 size 变量,记录队列元素个数
  2. 设置 tag 变量,记录上一次操作类型。
    产生插入操作 ==> tag = 1 ==> 只可能导致满队列
    产生删除操作 ==> tag = 0 ==> 只可能导致空队列

链队列

typedef struct LNode
{
    ElemType data;
    LNode* next;
} LNode;
typedef struct Queue
{
    LNode* front;
    LNode* rear;
}Queue;

带头结点

这个比较简单
但是注意 :出队之后为空队列的情况【需要改变 front rear】

不带头结点

注意:入队第一个元素有略微的不同,但是也很简单
但是注意 :出队之后为空队列的情况【需要改变 front rear】

双端队列

​ 这是一个可容许队列从两端进行操作的队列

队列是双端队列的一种

栈是双端队列的一种

输入受限的双端队列【一端输入,两端输出】

输出受限的双端队列【一端输出,两端输入】

​ 一般考察:以下哪些次序不能由双端队列的出队操作得到 等

栈 | 队列 - 具体使用场景

括号匹配

// 编程要求:输入一段括号字符串,判断字符串是否为合格括号对  
#include<stdio.h>
#define Maxsize 100
#define ElemType char

struct SqStack
{
	ElemType data[Maxsize];
	int top=0;
};

// 压栈 
void Push(SqStack &S,ElemType temp)
{
	if(S.top == Maxsize)
	{
		printf("栈满\n");
		return ;
	}
	S.data[S.top] = temp;
	S.top++;	
} 

// 判空 
bool isEmpty(SqStack S)
{
	if(S.top == 0) return true;
	else return false;
}  

// 判定括号是否匹配 
bool isMarch(SqStack S,ElemType temp)
{
	// 如果是空栈,返回不匹配  
	if(isEmpty(S)) return false;
	// 获取栈顶元素 
	char topc = S.data[S.top-1]; 
	if(temp == ')' && topc == '(') return true;
	else if(temp == ']' && topc == '[') return true;
	else if(temp == '}' && topc == '{') return true;
	else return false;
} 

// 弹栈
void Pop(SqStack &S)
{
	// 如果空栈,则形成错误提示 
	// 但其实按照代码逻辑,这一步发生在 isMarch 函数之后,在判断匹配时,就已经进行判空,这一步应该可以省略 
	if(isEmpty(S))
	{
		printf("空栈\n");
		return ;
	}	
	S.top--; 
} 

int main()
{
	SqStack S;
	char tempc;
	while(scanf("%c",&tempc))
	{
		if(tempc == '\n') break;
		if(tempc == '(' || tempc == '[' || tempc == '{')
			Push(S,tempc);
		if(tempc == ')' || tempc == ']' || tempc == '}')
		{
			if(isMarch(S,tempc)) Pop(S);
			else
			{
				printf("括号-括号不匹配\n");
				break;
			}
		}
	}
	if(isEmpty(S)) printf("括号匹配\n");
	else printf("非空栈-括号不匹配\n");
	return 0;
} 

表达式求值

posted @ 2024-04-19 14:54  木槐muhuai  阅读(31)  评论(0编辑  收藏  举报