数据结构第三章——栈与队列

本文详细介绍了栈(包括顺序栈和链栈)的概念、操作以及在括号匹配、表达式求值和递归中的应用;同时探讨了队列(顺序队列、循环队列和链式队列)的基本概念、操作和在层次遍历及计算机系统中的运用,为IT技术学习者提供基础知识梳理。
摘要由CSDN通过智能技术生成



前言

提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、思维导图

二、栈

1.栈的基本概念

(1)栈的定义

栈是只允许在表的一端进行插入和删除操作的线性表,

(2)栈的特点

卡特兰数:n个不同的元素进栈时,出栈元素不同排列的个数为

2.栈的基本操作

这些基本操作可直接在答案中调用

3.栈的存储结构

(1)顺序栈

顺序栈是栈的重点,而链栈基本上与链表保持一致,所以这里重点讨论顺序栈;实际应用中更多的也是顺序栈。

顺序栈的存储结构与顺序表一样,一般也有两种表示方式:静态分配,动态分配;

静态分配

(参考王道版本)

//顺序栈的存储结构——静态分配
#define maxsize 100 
typedef struct{
	int Data[maxsize];//开一个长度为100的数组,用作栈
	int top           //定义top指针; 
}SqStack; 

初学者可能会在这里产生一点疑惑,为什么在这里定义一个int型的变量可以将其称之为指针呢?这里的指针并不是真正意义上的指针,我们知道,真正的指针存放的是一个地址,在这个栈(数组)中其实地址就是0,1,2,3,4,5......0号地址第一个元素,1号地址存第二个元素......,所以用top变量用来存放栈顶元素在数组当中的哪个位置。

此外,用静态分配的方式来表示一个栈时,我们只用了一个栈顶指针,没有用栈底指针,栈底指针是固定在栈底的,所以有没有都一样。我们需要重点关注的时栈顶指针的指向,在这里,栈顶指针一般是两种指向方式,两种指向方式的不同会导致一些具体操作的不同,为方便表示,我用手写的方式呈现:

下面基本操作仅以s.top=-1表示,s.top=0类比,很简单的 

初始化
void InitSqStack(SqStack &S){
	s.top=-1;
}
判断空
bool SqStackEmpty(SqStack &S){
	if(s.top==-1){
		return true;
	}
	else{
		return false;
	}
}
入栈
//入栈
bool Push(Sqstack &S,int x){
	//首先判断空是否满
	if(s.top==maxsize-1){
		return false;
	} 
	s.Data[++S.top]=x;
	/*
	s.Data[++S.top]=x;
	这一行代码就是
	s.top=s.top+1;
	s Data[s.top]=x; 
	*/
	return true; 
} 
出栈
//出栈 
bool Pop(Sqstack &s,int &x){
	//首先需要判断栈是否为空
	if(s.top==-1){
		return false;	
	} 
	x=s.Data[s.top--];
	/*
	x=s.Data[s.top--];
	这行代码等效于
	x=s.Data[s.top];
	s.top=s.top-1; 
	*/ 
	return true
} 
读取栈顶元素
//读栈顶元素
int GetTop(SqStack S,int &x){
	//首先需要判空
	if(s.top==-1){
		return false;
	} 
	x=s.Data[s.top]; 
	//这里将栈顶元素,赋值给变量x,因为这里参数传来的是&x(可以理解为x的地址)
	//在这个函数内部修改x的值(也就是将栈顶元素赋值给x)在函数外,x的值同样被修改
	//所以直接返回bool型即可,此时x的值已经被“返回”了; 
	return true;
} 
动态分配

所谓的顺序栈的动态分配就是利用malloc函数在内存中动态的分配内存空间用来存放数据。下面我说明一下这个所谓的动态分配,首先看malloc函数,malloc函数的作用就是在内存中开辟一段地址连续的空间(更详细的内容请看c语言库函数)然后在这段地址连续的空间内进行操作,当空间存满时,就会动态分配,这时候一般使用realloc函数比如(int *)realloc(sizeof(int )*n),(n一定是大于原来malloc中的长度)此时realloc函数会在内存中寻找一片更大的地址连续的空间,然后将原来的数据复制到这一段更大的空间中。

存储结构
//顺序栈——动态分配
//存储结构
typedef struct{
	int *top;
	int *base;
    int stackszie;
}SqStack;

与静态分配不同,这里定义了栈底指针。也就是动态分配的原因使得这个栈底指针需要定义

初始化
//初始化
bool InitStack(SqStack &s){
	s.base=(int *)malloc(sizeof(int)*length);
	if(s.base==NULL){
		return false
	}
	s.top=s.base;
	s.stacksize=length;
	return true;
} 
取栈顶元素
//取栈顶元素
bool  InitStack(SqStack &s int &x){
	if(s.top==s.base){
		return false; 
	}
	x=*(s.top-1);
	return true;
} 
入栈
//入栈 
bool  Push(SqStack &s int x){
	if(s.top-s.base>=s.stacksize){
		s.base=(int *)realloc(s.base,(s.stacksize+length1)*sizeof(int));
		if(s.base==NULL){
			return false;
		}
		s.top=s.base+stacksize;
		s.stacksize+=length1;
	}
	*s.top++=e;
	return true;
} 
出栈
//出栈 
bool  Pop(SqStack &s int &x){
	if(s.top==s.base){
		return false;
	}
	x=*--s.top;
	return true;
} 
(2)链栈

链栈基本上与链表一致,只不过是限定在一段进行操作的链表,所以各种操作基本与链表一致

4.栈的应用

1.括号匹配

所谓括号匹配就是利用栈实现检查括号是否成对出现

规则:依次扫描所有字符,遇到左括号就入栈,遇到右括号就弹出栈顶元素,与当前括号检查是否匹配。

代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#define MaxSize 100 //定义栈中元素最大个数

typedef struct{
	char data[MaxSize];
	int top;
}SqStack;

//初始化栈
void InitStack(SqStack *S){
	S->top = -1;
}
//判断栈是否为空
bool IsEmpty(SqStack S){
	if(S.top == -1){
		return true;
	}
	return false;
}
//新元素入栈
void Push(SqStack *S,char x){
	if(S->top == MaxSize-1){
		printf("栈已满"); //栈已满
		return;
	}
	S->top += 1;
	S->data[S->top] = x;
}
//栈顶元素出栈,用x返回
void Pop(SqStack *S,char *x){
	if(S->top == -1){
		printf("栈已满");
		return;
	}
	*x = S->data[S->top];
	S->top -= 1;
}
//匹配算法
bool bracketCheck(char str[],int length){
	SqStack S;
	InitStack(&S); //初始化栈
	for(int i=0;i<length;i++){
		if(str[i]=='('||str[i]=='{'||str[i]=='['){
			Push(&S,str[i]); //扫描到左括号就入栈
		}else{
			if(IsEmpty(S)){ //扫描到右括号,当前栈为空,即右括号单身情况
				return false; //匹配失败
			}
			char topElem; //用来保存弹出栈的栈顶元素
			Pop(&S,&topElem); //栈顶元素出栈
			if(str[i]==')'&&topElem!='('){
				return false;
			}
			if(str[i]=='}'&&topElem!='{'){
				return false;
			}
			if(str[i]==']'&&topElem!='['){
				return false;
			}
		}
	}
	return IsEmpty(S);
}
int main(){
	char s[MaxSize];
	printf("请输入需要判断的括号序列:\n");
	scanf("%s",s);
	int len = strlen(s);
	if(bracketCheck(s,len)){
		printf("匹配成功!");
	}else{
		printf("匹配失败!");
	}
	return 0;
}
2.表达式求值

这个部分内容不太好用语言讲述明白,建议观看王道的视频,这里就用思维导图的形式完整梳理下这个考点

3.栈与递归

发生递归时,计算机会在内存中开辟一段空间,这一段空间就是栈,栈的作用是保存每一层递归的信息,当满足递归终止条件时,递归调用栈会返回每一层的数据,即栈顶元素出栈,完成递归。

三、队列

1.队列的基本概念

(1)队列的定义

队列也是一种操作受限的线性表,只允许在表的一端进行插入,在表的另一端进行删除。操作特性时先进先出。

2.队列的顺序存储结构

(1)存储结构

队列的顺序存储是分配一块连续的存储单元来存放队列中的元素,设两个"指针",队头指针front,队尾指针rear。

//队列的顺序存储
#define maxsize 100
typedef struct{
	int data[maxsize];
	int front,rear;
}SqQueue; 

初始时Q.front=Q.rear;

进队:队不满时,先入队,队尾指针+1;

出队:队不空时,先出队,队头指针+1;

此时需要思考一个问题,当队空时,判空条件是Q.front==Q.rear;但如果队满时,判满条件会是Q.rear==maxsize吗?显然队列的特性是从一端插入,从另一端删除,我们假设这样一个情景,在向队列插入元素的同时,从另一端删除一些元素,这样rear不停的+1,直到Q.rear==maxsize,而在这个过程中我们在另一端也删除了元素,这时如果以Q.rear==maxsize作为判满条件是不对的,此时队列并不满。所以我们引入了循环队列

(2)循环队列

循环队列的定义

循环队列即将队列想像成为一个环状空间(但实际上该怎么存还怎么存),从逻辑上将队列视为环

初始时:Q.front=Q.rear=0;

队首指针+1:Q.front=(Q.front+1)%maxsize;

队尾指针+1:Q.rear=(Q.rear+1)%maxsize;

判空和判满条件

三种方式处理(之所以会有不同的处理方式,其目的都是为了解决循环队列判空和判满条件可能会冲突的问题,也就是判空和判满条件是一样的)

第一种:牺牲一个存储单元

队空条件:Q.front==Q.rear;

队满条件:(Q.rear+1)%maxsize==Q.front;

   

上图即为牺牲一个存储单元来区分队空和队满,这里需要格外说明一点:这里队列中两个指针的指向,即front(队头指针)指向队头元素,rear(队尾指针)指向的是队尾元素的下一位置。

所以此时判空条件为Q.front==Q.rear;

判满条件为:(Q.rear+1)%maxsize==Q.front;

第二种:增加size数据成员

size用来表示队列中的元素个数,入队一个元素size+1,出队一个元素size-1;

此时判空:Q.size==0;

判满条件:Q.size==maxsize;

第三种:增加tag数据成员,

用tag=0,表示出队成功,tag=1表示入队成功

此时,判空条件Q.front==Q.rear&&Q.tag==0;

判满条件:Q.front==Q.rear&&Q.tag==1;

基本操作

1.初始化

void InitQueue(SqQueue &Q){
	Q.front=Q.rear=0;
} 

2.判断队列是否为空

bool IsEmpty(SqQueue Q){
	if(Q.rear==Q.front){
		return true;
	}
	else{
		return false;
	}
} 

3.入队

bool EnQueue(SqQueue &Q,int x){
	//入队首先需要判断队是否已满,这里采用牺牲一个存储单元的方式
	if((Q.rear+1)%maxsize==Q.front){
		return false;
	} 
	Q.Data[Q.rear]=x;
	Q.rear=(Q.rear+1)%maxsize; 
	return true;
}

4.出队

bool DeQueue(SqQueue &Q,int &x){
	//首先需要判断队列是否为空 
	if(Q.front==Q.rear){
		return false;
	} 
	x=Q.Data[Q.front];
	Q.front=(Q.front+1)%maxsize;
	return true;
}

在入队操作中,rear指向的是当前元素的下一位置,学习一定是要学懂的,如果在这里假设rear指针指向的是当前元素,那么该如何操作呢

1.入队操作改变,尾指针先+1再入队

2.初始化时,front指针和rear指针指向的位置改变,front指针仍然指向0号元素,rear指针指向n-1的位置,如图:

3.判空判满条件发生变化(Q.rear+1)%maxszie==Q.front,判满条件Q.front==Q.rear,

3.队列的链式存储结构

(1)存储结构

队列的链式存储就是一个带有头指针和尾指针的单链表,头指针指向头结点,尾指针指向尾结点

//队列的链式存储
typedef struct Linknode{
	int data;
	struct Linknode *next;
}Linknode;
typedef struct{
	Linknode *front,*rear;
}*LinkQueue;

一般来说,链队列都是带头结点的,不带头结点的链队列在操作上是比较麻烦的,所以下边的基本操作都以带头结点进行

(2)基本操作

初始化
//链队列的初始化
void InitQueue(LinkQueue &Q){
	Q.front=Q.rear=(Linknode *)malloc(sizeof(linknode));
	//生成一个头结点,队头指针和队尾指针均指向头结点;
	Q.front->next=NULL; 
	//将头结点的指针域置为空 
} 
判队空
//判断链队列是否为空
bool IsEmpty(LinkQueue Q){
	if(Q.front==Q.rear){
		return true;
	}
	else{
		return false;
	}
}
入队
//入队
bool EnQueue(LinkQueue &Q,int x){
	Linknode *s=(Linknode *)malloc(sizeof(Linknode));
	s->data=x;
	s->next=NULL;
	Q.rear->next=s;
	Q.rear=s; 
} 
出队
//出队
bool DeQueue(LinkQueue &Q,int &x){
	if(Q.front==Q.rear){
		return false;
	}
	Linknode *p=Q.front->next;
	x=p->data;
	Q.front->next=p->next;
	if(Q.rear==p){      //当队列中除头结点外只有一个结点时,作特殊处理,删除后,尾指针指向头指针(头结点) 
		Q.rear=Q.front;
	} 
	free(p);
	return true;
} 

4.双端队列

所谓双端队列就是队列的两端都可以进行入队和出队的操作。这部分内容多数以选择题的形式考察输出序列。

5.队列的的应用

队列的应用有两个方面:

(1)队列在层次遍历中的应用

队列在层次遍历中的应用主要应用于树

(2)队列在计算机系统中的应用

这部分内容主要应用在操作系统和CPU内,其实很简单的,在操作系统中会有一个缓冲区,这个缓冲区就是用队列实现的,主要解决双方速度不匹配的问题。


总结

提示:这里对文章进行总结:

例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值