【数据结构】——栈和队列的数组vs链表,含实操源码(中缀表达式,括号匹配,循环队列实现)

目录

1.基础回顾

1.栈的定义

1、顺序表和顺序栈的区别

2、栈的常用基本方式

3、栈函数基本操作

4、栈实操题目

(1)括号匹配问题

(2)中缀表达式实现

队列

1、队列的定义

2、队列的常用基本方式

3、队列函数基本操作

队列实操题目

栈和队列的结构对比(数组和链表)

(1)数组与链表:

(2)栈和队列使用的结构


1.基础回顾

回归本质,数组和链表最基础的就是实现他们的增删查改,那么我们就来说下他们各自的实现形式吧。

数组顺序表:确保数据的连续性,在修改的时候需要往前移动数据,若为表尾则不需要。

链表:通过节点链接和断开进行增删,查改直接遍历。

1.栈的定义


栈(Stack):是只允许在一端进行插入或删除的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。

栈顶(Top):线性表允许进行插入删除的那一端。
栈底(Bottom):固定的,不允许进行插入和删除的另一端。
空栈:不含任何元素的空表。

栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构

typedef int Stdatatype;        //typedef栈数据,方便修改

typedef struct stack 
{
	Stdatatype* arr;
	int top;
	int capacity;
}ST;

(1)顺序表和顺序栈的区别

解析: 顺序表可以在任意位置对数据进行操作,我们只需要知道所要数据的下标即可,而顺序栈是利用顺序表构建的栈,依靠栈顶top指针对栈顶元素进行操作,实现后进先出。我们可以设定固定大小的栈,或者动态增长的栈。栈一定要有栈顶指针top。

 base == top 是栈空标志,top 指示真正的栈顶元素之上的下标地址。

栈满时的处理方法:

1、报错,返回操作系统。

2、分配更大的空间,作为栈的存储空间,将原栈的内容移入新栈。

2、栈的常用基本方式(函数实现于下文,&是引用的意思

(1)StackInit(&S):初始化一个空栈S。
(2)StackEmpty(S):判断一个栈是否为空,若栈为空则返回true,否则返回false。
(3)StackPush(&S, x):进栈(栈的插入操作),若栈S未满,则将x加入使之成为新栈顶。
(4)StackPop(&S):栈为非空,出栈(栈的删除操作)。
(5)StackTop(S, &x):读栈顶元素,若栈S非空,则用x返回栈顶元素。
(6)StackDestroy(&S):栈销毁,并释放S占用的存储空间。


 

栈顶有两种表达方式:

 区别:当栈为空的时候栈顶的指向不同,指向0或者-1,同时进栈的方式也不同,栈顶top为指向顶部元素时是top先加一位再赋值,栈顶top指向顶部的下一位时是赋值后top再后移一位。

各有各的优点,按个人习惯为主或具体问题方便优先。

3.栈函数基本操作

(1)栈初始化(动态开辟空间)

//初始化
void StackInit(ST* ps)
{
	assert(ps);//判断是否为空,ps为空无法进行
	Stdatatype* temp = (Stdatatype*)malloc(0 * sizeof(Stdatatype));
	if (temp == NULL)
	{
		printf("Init:fail\n");
		return;
	}
	ps->arr = temp;
	ps->capacity = 0;
	ps->top = 0;//top定义为0的话那么就对应输入值的下一位,即先输入再++,
    //若是-1为初始化,就是先++再输入
}

(2)栈判空

bool StackEmpty(ST* ps)
{
	assert(ps);
	//直接返回
	return ps->top == 0;
}

(3)取栈顶元素

Stdatatype Stacktop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);
	return ps->arr[ps->top-1];//top指向最后一个元素的下一个,故-1
}

(4)入栈(动态增长版本)

void StackPush(ST* ps, Stdatatype x)
{
	assert(ps);

	if (ps->capacity == ps->top)
	{
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		Stdatatype* temp = (Stdatatype*)realloc(ps->arr,newcapacity * sizeof(Stdatatype));
		if (!temp)
		{
			printf("realloc:fail\n");
			exit(-1);//错误就无法进行了,直接退出
		}
		ps->capacity = newcapacity;
		ps->arr = temp;
	}
	
	ps->arr[ps->top] = x;
	ps->top++;

}

(5)出栈

void StackPop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);//等于0就说明没有数据了。不能删了
	ps->top--;
}

(6)栈销毁

//栈销毁
void StackDestroy(ST* ps)
{
	assert(ps);
	free(ps->arr);
	ps->capacity = 0;
	ps->top = 0;
	//free(ps);//一般在内部释放外部的栈,不确定栈是否是malloc的
    //外部创建外部释放
}

4.栈实操题目

(1)括号匹配问题

问题:给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

1.输入:s = "()"        输出:true
2.输入:s = "(]"        输出:false     

思想:利用栈后进先出的特点,依次遍历字符串,遇到左括号进栈,遇到右括号用其与栈顶元素对比,若左右括号匹配,就pop掉栈顶左括号,接着匹配下一对括号。

 代码实现:

bool isValid(char * s){

assert(s);
ST ps = {0};
StackInit(&ps);//初始化栈

while(*s)
{
    if((*s =='(')||(*s=='{')||(*s=='['))
    {
    StackPush(&ps,*s);
    s++;
    }   
    else
    {
        if(StackEmpty(&ps))                  //如果再这为空说明该开始是右括号或者为空
            return false;
        char top = Stacktop(&ps);//*s只包含括号,所以只需要考虑括号的比就行
        StackPop(&ps);
        if((*s==']'&&top!='[')||(*s=='}'&&top!='{')||(*s==')'&&top!='('))
            return false;
        else
        {
            s++;
        }
    }
}
if(StackEmpty(&ps))//此时栈为空则正确,不为空可能s只含有左括号
    return  true;
return false;
}

(2)中缀表达式实现

问题:用栈实现中缀表达式的识别运算 (中缀表达式:运算符在数据中间,1+2*3-6这种。

输入:1+2*3-6        输出:1

输入:9+4*(2+3)        输出:33

思想:创建一个数据栈OPND,一个符号栈OPTR,设OPTR栈头默认为  ’‘#“ ,字符串尾也添加 ’‘#“  ,用以表示结束(当两个#相碰撞时),接着遍历字符串为数据时,push数据进入OPND栈中,当遍历字符串为符号时,与符号栈OPTR栈顶元素比较,优先级比栈顶大就进栈。优先级比栈顶小就拿出数据栈中前两个数据与符号栈OPTR栈顶的符号进行运算。(下面有优先级对比解释)运算结果压栈到数据栈中。然后继续遍历字符串直到尾部#。

总结符号优先级(四则运算)

\Theta1:符号栈OPTR栈顶的运算符 

\Theta2:表达式中当前的cur指向的运算符

\Theta1 > \Theta2时,说明这时候栈顶优先级符号比较大,我们要先算数据。

\Theta1 > \Theta2时,说明栈顶符号比较小,可以之后算,先算乘除后算加减。而且 \Theta1是左括号( 时,他的优先级最小,先把后面的压进来先, \Theta1是右括号时要优先算括号中的数据,故先要用之前的符号也就是符号栈OPTR栈顶符号与数据栈中前两个运算。

 \Theta1 = \Theta2时,说明遇到了一对()或者一对#,一对括号就说明括号中间的运算结束了,把这两括号去掉即可,即pop掉符号栈OPTR栈顶的(,与cur指向下一位。一对#即可结束。

 具体实现图解:

 实现代码:

char infix()//两个# 代表结束
{
	ST OPTR,OPND;
	StackInit(&OPTR);
	StackInit(&OPND);

	char a, b,theta;
	StackPush(&OPTR, '#');
	char c = getchar();
	while (c != '#' || StackTop(&OPTR) != '#')//两个同时为# 才是结束,这两个挨在一起
	{
		if (isdigit(c))
		{
			StackPush(&OPND, c);
			c = getchar();
		}
		else if (!in(c))
		{
			printf("出现非法字符\n");
			exit(-1);
		}
		else
		{
			switch (judge(StackTop(&OPTR),c))
			{
			case '>':
				//OPTR栈顶符号大,说明前面的数要先算
				a = StackPop(&OPND);
				b = StackPop(&OPND);
				theta = StackPop(&OPTR);
				StackPush(&OPND, calculate(b, theta, a));
				break;									//这时候c还没有用上,不用再取
			case '=':
				StackPop(&OPTR);
				c = getchar();
				break;
			case '<':
				//后者大,就把符号位压进来
				StackPush(&OPTR, c);
				c = getchar();
				break;
			}
	
		}
	}//while
	return StackTop(&OPND);
}

判断是否非法符号函数:

bool in(char c) 
{
	char a[] = { '+','-','*','/','(',')','#' ,'\0'};
	char* pa = a;
	while (*pa != '\0')
	{
		if (c == *pa)
			return true;
		else
			pa++;
	}
	return false;
}

比较符号优先级函数:

char judge(char a, char b)
{
	if ((a == '#' && b == '#') || a == '(' && b == ')')
		return '=';
	else if (a == '#' || a == '(' || b == '(' ||
		((a == '+' || a == '-') && (b == '*' || b == '/')))				//a是开括号就都要压栈
		return '<';
	else
		return '>';
}

运算函数:

char calculate(char a, char theta, char b)
{
	char temp = 0;
	switch (theta)
	{
	case '+':
		temp = (a - '0') + (b - '0') + 48;
		break;
	case '*':
		temp = (a - '0') * (b - '0') + 48;
		break;
	case '/':
		temp = (a - '0') / (b - '0') + 48;
		break;
	case '-':
		temp = (a - '0') - (b - '0') + 48;
		break;
	}
	return temp;
}


队列

1.队列的定义

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。


队头(Front):允许删除的一端,又称队首。
队尾(Rear):允许插入的一端。
空队列:不包含任何元素的空表。

typedef int Qdatatype;     //typedef队列数据元素,方便之后更改

typedef struct QNode        //链式结构,先定义节点
{
	Qdatatype val;
	struct QNode* next;
}QNode;

typedef struct Queue        //定义队列结构,首尾指针
{
	QNode* head;    //fornt,前
	QNode* tail;    //rear,后
}Queue;

2、队列的常用基本方式(函数实现于下文,&是引用的意思

QueueInit(&Queue):初始化队列,构造一个空队列Queue。
QueueEmpty(&Queue):判队列空,若队列Queue为空返回true,否则返回false。
QueuePush(&Queue,& x):入队,若队列Queue未满,将x加入,使之成为新的队尾。
QueuePop(&Queue,& x):出队,若队列Queue非空,删除队头元素,并用x返回。
QueueFront(&Queue):读队头元素,若队列Queue非空,则将队头元素赋值给x。QueueDestroy(&Queue):销毁所创造的队列

图解:

 队列可以用数组或者链表实现:但是都需要有头指针 front 和尾指针 rear 。依次进入,从头fornt开始出数据,链式队列就不需要考虑如上图的空间问题,还得导一下回到空余的空间(这便是下文的循环队列)。故队列优先考虑链式结构,栈优先考虑顺序结构,之后有二者的比较。

3.队列函数基本操作

(1)队列初始化

void QueueInit(Queue* ps)
{
	ps->head = NULL;
	ps->tail = ps->head;
}

(2)队列判空

bool QueueEmpty(Queue* ps)
{
	assert(ps);
	return ps->head == NULL;//head为空就是true
}

(3)入队列

void QueuePush(Queue* ps, Qdatatype x)
{
	assert(ps);//判断的是这个结构体是否为空
	QNode* newNode = (QNode*)malloc(sizeof(QNode));
	if (!newNode)
	{
		printf("push:fail\n");
		return;
	}
	newNode->val = x;
	newNode->next = NULL;
	if (!ps->head)//head为空就直接赋值
	{
		ps->head = newNode;
		ps->tail = ps->head;
	}
	else			//不为空就链接
	{
		ps->tail->next = newNode;
		ps->tail = ps->tail->next;
	}
}

(4)出队列

void QueuePop(Queue* ps)
{
	assert(ps);
	assert(ps->head && ps->tail);//不能无元素

	QNode* next = ps->head->next;
	Qdatatype temp = ps->head->val;
	free(ps->head);
	ps->head = next;
	if (!ps->head)//如果head为空,那么就是到结尾了,tail也要置空
		ps->tail = NULL;
}

(5)取队列首元素

Qdatatype QueueFront(Queue* ps)
{
	assert(ps);
	assert(ps->head);
	return ps->head->val;
}

(6)队列销毁

void QueueDestroy(Queue* ps)
{
	assert(ps);
	QNode* cur = ps->head;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
	ps->head = ps->tail = NULL;
}

队列实操题目

力扣622:设计循环队列

 思想:循环利用之前出队列的空间,需要考虑的是如何表示空队列和满队列,若是整个队列放慢,我们就无法分别到底是队列空还是满。如图:

建立多一个空间,如N个,那么最多存储N-1个数据,那么当front(head)指针等于rear(tial)指针时为空,rear(tail)指针的下一位是front(head)指针时候为满队列

 

 代码实现:(数组法)

//数组
typedef struct {
    int* arr;        //存放数据的数组
    int head;
    int tail;
    int k;//记录数组总长度,但是实际大小要比k大1
} MyCircularQueue;


//循环队列初始化
MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* ps = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    if (ps == NULL)
        return NULL;

    ps->arr = (int*)malloc((k + 1) * sizeof(int));
    ps->head = ps->tail = 0;
    ps->k = k;
    return ps;
}

//循环队列判满
bool myCircularQueueIsFull(MyCircularQueue* obj) {
    if (obj->tail == obj->k)
        return obj->head == 0;
    else
        return obj->head == obj->tail + 1;
}

//循环队列判空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->head == obj->tail;
}


//push元素
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    //入队
    if (myCircularQueueIsFull(obj))
        return false;           //队列满了就无法入队了

    //没有满直接添加
    obj->arr[obj->tail] = value;
    if (obj->tail != obj->k)
    {
        obj->tail++;
    }
    else
    {
        obj->tail = 0;
    }
    return true;
}


//pop元素
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    //出队列
    if (myCircularQueueIsEmpty(obj))
        return false;

    //不是空就出数据
    if (obj->head != obj->k)
    {
        obj->head++;
    }
    else
        obj->head = 0;
    return true;

}

//返回队列首元素
int myCircularQueueFront(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj))
        return -1;
    return obj->arr[obj->head];
}


//返回队列尾元素
int myCircularQueueRear(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj))
        return -1;
    if (obj->tail == 0)
    {
        return obj->arr[obj->k];
    }
    else
        return obj->arr[obj->tail - 1];
}

//销毁队列
void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->arr);
    free(obj);
    obj == NULL;
}

 数组循环队列指针一旦到了顶部就需要循环到起始0位置,这样方可达到循环的目的。


栈和队列的结构对比(数组和链表)

数组:在内存上给出了连续的空间.

链表:内存地址上可以是不连续的,是通过上一个节点来找到下一个节点的。

链表和数组的本质差异    
     
  1  、 在访问方式上 :数组可以随机访问其中的元素 ,链表则必须是顺序访问,不能随机访问    
     
  2 、 空间的使用上  :链表可以随意扩大数组则不能  
 


(1)数组与链表:

1.数组内的数据可随机访问.但链表不具备随机访问性..数组在内存里是连续的空间.可以通过下标随机访问

2.数组高速缓存命中率更高,这里涉及计组内存问题,不展开讲。这是由于数组是连续的空间。链表是一块一块内存中的空间建联的,访问起来速度相对于没有数组那么快。

3.内存空间创建问题,数组是一次性开辟的空间,是有限定的(除了动态开辟,但是空间利用也没有链表好),链表是创建一个利用一个,链表节点会附加上一块或两块下一个节点的信息.但是数组在建立时就固定了.所以也有可能会因为建立的数组过大或不足引起内存上的问题.。

4.插入,删除,更改数据问题,数组在尾部插入数据时比较快的,这一点链表也一样,但是在头部插入数据链表较快,因为数组需要一个个移动。更改数据时候,数组可以随机直接访问,它更改速度比较快,而链表需要一个个从头开始遍历才能找到要更改的数据,删除分任意位置删除和尾删头删,数组在除了尾部的删除都是比较麻烦,因为要保持不缺空间,要一个个往回填。链表删除比较简单,但是需要获得目标节点的前一个节点才行。

(2)栈和队列使用的结构

栈和队列都可以使用顺序表(数组)和链表,但是具体哪种比较方便还需要仔细对比,但是并不是说另外一种不行,只是相对来说。

栈:栈一般使用顺序表,首先栈是后进先出,数组肯定是从前到后插入数据的,因为不确定具体数据量,所以是前面进后面出,而以数组的优势来说就是方便尾删尾插,而链式栈的尾删需要更前一位的节点才可以删,这样就需要遍历才能找到,或者用多一个指针一直指向尾节点的前一个节点,但这样就操作麻烦了一点点,相对没有顺序栈那么优。

队列:队列一般是使用链表,队列是先进先出,对于数组来说就需要尾插头删,那么每次删除都需要往前面移动元素来保持顺序结构。而链式结构不用,他也是尾插头删,尾插和数组一样方便时间复杂度为O(1),但是头删没有数组麻烦,链表只需要移动一下头节点即可。故链式队列相对较好。


结束尾言:码字不易,若是你对本文感觉不差,望三连鼓励😀
 

  • 29
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 35
    评论
评论 35
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值