括号匹配问题(链表)(栈)

背景

现有如下问题:

现在给出一串字符串,里面的括号要不成双出现,要不就不出现,也就是括号匹配。括号的种类包括’{’、’}’、’(’、’)‘、’[’、’]’。请大家判断字符串是否括号匹配,是则输出"Yes",反之输出"No"。

问题分析

几种可能的方法:

1.设立计数器,如果遇到 ( 或 { 或 [ 对应的计数器就 ++ ,如果遇到 ) 或 } 或 ] 对应的计数器就 --。

可行性分析:如果遇到的是 ({[]}) 型的字符串可以通过,并且空间复杂度O(1),时间复杂度O(N)。但是如果遇到 }])([{ 的情况,明显括号没有匹配,但是该方法显示通过。

#include<stdio.h>
#include<string.h>
int main()
{
	char a[100];
	scanf("%s",a);
	int x=0,y=0,z=0;
	int n=strlen(a);
	int count,count1,count2;
	int ret;
	for(count=0;count<n;count++)
	{
		if(a[0]==')'||a[0]=='}'||a[0]==']') 
		{
			x=1;
			break;
		}
		if(a[count]=='(')
			x++;
		if(a[count]=='{')
			y++;
		if(a[count]=='[')
			z++;
		if(a[count]==')')
			x--;
		if(a[count]=='}')
			y--;
		if(a[count]==']')
			z--;
	}
	if(x==0&&y==0&&z==0)
	printf("Yes");
	else
	printf("No");
	return 0;
}
2.在方法1的基础上建立标记,初始标记为flag=0,如果遇到 ( 或 { 或 [ ,对应的flag标记为1,如果遇到 ) 或 } 或 ] 并且flag=1时, 进入判断并且对应的flag标记为0。

可行性分析:这样避免了}{ 、)( 、][ 被识别为匹配的情况,但是这也有一个极大的隐患:如果为(( ))的情况时,flag的变化情况为:(初始)0-1-1-0-0,导致最后一个 )甚至无法进入判断。

#include<stdio.h>
#include<string.h>
int main()
{
	char a[100];
	scanf("%s",a);
	int x=0,y=0,z=0;
	int n=strlen(a);
	int count,count1,count2;
	int ret;
	int flag1=0,flag2=0,flag3=0;
	for(count=0;count<n;count++)
	{
		if(a[count]=='(')
		{
			flag1=1;
			x++;
		}
		if(a[count]=='{')
		{
			flag2=1;
			y++;
		}
		if(a[count]=='[')
		{
			flag3=1;
			z++;
		}
		if(a[count]==')'&&flag1==1)
		{
			x--;
			flag1=0;
		}
		if(a[count]=='}'&&flag2==1)
		{
			y--;
			flag2=0;
		}
		if(a[count]==']'&&flag3==1)
		{
			z--;
			flag3=0;
		}
		
	}
	if(x==0&&y==0&&z==0)
	printf("Yes");
	else
	printf("No");
	return 0;
}
3.(忍无可忍)(逼上梁山) 怎么办?有没有什么万全之策?我们可以用栈来分析。

如果想进一步了解栈,可以参考一下我的另一篇博文: https://blog.csdn.net/zjnyly/article/details/104435808

这篇文章是在模拟的层面(函数调用)来模拟程序在计算机内存中类似栈的结构的运行。

栈的运行方式

所谓数据结构,就是数据存储的方式。

最常见的数据结构有数组、链表以及栈等。本文我们就围绕这几个数据结构展开讨论。

其中,栈的运行模式很适合作为我们这道题的解决方案:

栈是如何运行的?记住一个原则:先入后出。比如有一串数据——1 2 3 4 5 6;我们来模拟一下数据在栈中的存储方式:

1 //入栈

1 2

1 2 3

1 2 3 4

1 2 3 4 5

1 2 3 4 5 6 //出栈

1 2 3 4 5

1 2 3 4

1 2 3

1 2

1

至此,这一串数就完成了入栈和出栈的过程。有没有发现,栈的顶部一直都是上一个输入的数据?

这和我们的这道题目有什么关系呢?假设这道题目给出的字符串是

abc(abc){haha}{([])}

首先要明确什么情况入栈,什么情况出栈。这道题我们可以明确为:遇到左括号就入栈,遇到右括号,先将其入栈,如果栈中的前两个数据匹配为() 、{} 、[] 那么我们就让他们出栈。(为什么要让右括号先入栈呢?为什么不能直接判断栈顶元素是否与右括号匹配?说起来都是泪,经过一番摸索,我才明白了栈的真实含义以及如何真正地使用栈这个数据结构,这也是我写这篇文章的原因,后续我可能会讲到)

我们来模拟一下这个字符串入栈和出栈的过程

()

{

{}

{

{(

{( [

{( [ ]

{(

{( )

{

{ }

遍历结束。我们发现如果字符串匹配的话,那么栈结构最终是空的。

如果不匹配,你可以自己模拟一下,肯定至少存在一个元素在栈中。

我们再把讨论方案一和二的干扰样例再栈这个方法中模拟一下,发现都可以完美排除,至此,我们明确了这道题的最佳解法——栈。

建栈

栈这么好用我们当然要学会怎么建栈啦,以后遇到类似情形,我们都可以用栈来解决。

建栈的方法:

数组 || 链表

为什么数组和链表可以建栈呢?

我们发现栈是一个顺序的数据结构,他和数组的存储方式十分相似,数组一个数接着一个数往后存,栈一个数一个数往里进。

链表为什么可以呢?链表也是顺序的线性存储结构,我们甚至可以认为链表这个数据结构的实现就是为了模拟数组,继而推之,模拟栈。

数组空间的分配一定是一段连续的内存空间,如果我们要很大的空间,而内存整体很混乱,只有大量的碎片空间时,我们就可以用链表,用链子将这些碎片空间有序地串在一起,达到高效利用空间并且实现数组功能的目的,链表中的每一个环节都可以想象成一个数组,例如我们这样定义链表结构

struct node
{
	int value[10];
	struct node* next;
};

这就是将一系列数组串起来了

struct node
{
	int value;
	struct node* next;
};

或者像本文一样,就建立每个节点仅存储一个值的单向链表。

栈的调用

我们可以对栈进行什么样的操作呢?

//对栈进行初始化
void InitiateStack(Stack* stack);

//将数据压入栈
void push(Stack* stack, const int val);

//将数据弹出栈
void pop(Stack* stack);

//判断栈是否为空
int empty(const Stack* stack);

//获取栈顶的数据
int top(const Stack* stack);

//获取栈的大小
int size(const Stack* stack);

//按顺序遍历并且打印栈中数据
void print(const Stack* stack);

栈的调用之数组实现

int stack[256];
int size=0;
void InitiateStack(int stack[])
{
	//这一个函数在数组实现中并不需要 
}

//将数据压入栈
void push(int stack[], const int val)
{
	stack[size]=val;
	size++;
}
//将数据弹出栈
void pop(int stack[])
{
	if(size>0)
	{
		stack[size]=0;
		size--;
	}
}
//判断栈是否为空
int empty(const int stack[])
{
	return size==0;
}
//获取栈顶的数据
int top(const int stack[])
{
	return stack[size];
}
//获取栈的大小
int size(const int stack[])
{
	return size+1;
}
//按顺序遍历并且打印栈中数据
void print(const Stack* stack)
{ 
	int cnt=0;
	while(stack[cnt++]!=0)
	{
		printf("%d",stack[cnt++]);
	}
}

数组的实现方法很简单,应该不用赘述,不过以上代码未经过编译,也没有进行实际测验,如有错误多多包涵:)

栈的调用之链表实现

我们先来看下一整套函数,然后再细讲:

typedef struct Node
{
    int val;
    struct Node* next;
} node;

typedef struct 
{
    int size;
    node* top;
} Stack;

void InitiateStack(Stack* stack)
{
	stack->top=NULL;
    stack->size=0;
}
void push(Stack* stack, const int val)
{
	node* newNode=(node*)malloc(sizeof(node));
	newNode->next=stack->top;
	newNode->val=val;
	stack->top=newNode;
	stack->size=stack->size+1;
}
void pop(Stack* stack)
{
	node* temp;
	temp=stack->top;
	stack->top=temp->next;
	stack->size=stack->size-1;
	free(temp);
}
int empty(const Stack* stack)
{
	if(stack->size==0)
	return 1;
	else
	return 0;
}
int top(const Stack* stack)
{
	if(stack->size == 0) 
	{
        return 0;
    }
	return stack->top->val;
}
int size(const Stack* stack)
{
	return stack->size;
}
void print(const Stack* stack)
{
	node* temp=stack->top;
	while(temp->next!=NULL)
	{
		printf("%d->",temp->val);
		temp=temp->next;
	}
	printf("null\n");
}

比数组复杂很多对吗?但是为什么本文主要选择讲述链表建栈呢?原因是当处理更多的数据时,链表显然能够高效地利用一切内存空间,作为程序员,这应该是对于自己写出的代码的一大追求:可靠,高效。

高效地利用内存空间体现在两点:1.当分配内存空间时,我们是链接碎片空间,而非像数组一样的连续空间。 2.当我们将栈顶地数据弹出时,由于我们使用了malloc函数,动态分配的堆中内存可以随时被free函数清理掉,而数组仅仅是将下标回退一位。(附:内存中也有栈、堆等结构,但是还是和本文中地栈以及数据结构堆有所区别的,更类似与我给出的链接中讲述地栈)

好了我们来具体分析一下代码

typedef struct Node
{
    int val;
    struct Node* next;
} node;

typedef struct 
{
    int size;
    node* top;
} Stack;

typedef的作用是将struct Node宏替换为更简洁的node。

这里选择定义了两个结构体,第一个结构体描述的是链表中的节点,其中node . val表示节点存储的值,node . next 代表一个指针,这个指针指向的数据类型是我们定义的node数据结构,用途是实现向下一个节点的连接。

第二个结构体是一个头指针,具体原因是因为我们利用的单链表更新中的头插法,稍后我们再详细讲述该方法。这个结构体包括两个成员,一个是Stack . size,用于表示链表中存储的数据的总数,另一个是Stack . top,这也是一个指针,仔细观察,是一个指向上一个结构体定义的node节点的指针,我们将这个指针指向链表的第一个node节点。

头插法

在这里插入图片描述

这个我不会讲太多,因为目前我们设计的链表更多是为了实现栈的功能,因此其实是个功能不齐全的链表。

什么是头插法?为什么要头插?试想一下,我们如何在内存的茫茫大海中找到我们的这一串链表?我们需要知道链表的头节点在哪,知道了头节点的内存地址,我们就可以随时想调用这个链表就调用。

如果头节点要删除呢?如果头节点前面想插入一个新节点呢?毕竟链表的一大好处就是可以随心所欲地在每一个环节增删数据。这就导致一个问题——我们现在不知道原有的头节点的内存地址了,新的头节点是一个新的地址。链表不像数组,你将原有地址+/-1就可以得到新地址。我们的线索断掉了!要想实时定位这个链表,我们就必须一直返回新的头节点的地址给我们的定位器。

有没有什么好方法?我们可以把第一个节点放在第二个节点,这时我们对该链表定义头节点就是第二个节点,而第一个节点就是一个固定的地址确定器。要插入新的头节点,就是在实际的第一和二节点之间插入。要查询新的头节点,我们只需要用实际的第一节点指向它就好了,而实际第一节点我们是始终知道的。

(注意一点,为了讲述方便,我将“实际的第一节点”定义为节点,而实际不是这样的。这个节点,它实际是个指针,这个指针是一个指向节点的指针,由于它太像节点了,他可以通过自己的pointer->next找到头节点,就跟头节点能够通过自己的next找到下一个节点一样。

在这里插入图片描述

但是第一个节点实际上就只有指针成分,而真正的节点既有指针,又有存储的值。怎么区别呢?看有没有malloc一个节点,如果有的话,我们就把他视为真的节点,如果只是声明一个指针,那么他就是一个伪节点。

具体分析:

更具体地链表实现、内存管理我会在下一个篇章描述,这里就简单展示一下

对栈进行初始化

void InitiateStack(Stack* stack)
{
	stack->top=NULL;
    stack->size=0;
}

stack指针就是我们所说的伪节点,他一开始不指向链表元素,所以置为NULL;

将数据压入栈

void push(Stack* stack, const int val)
{
	node* newNode=(node*)malloc(sizeof(node));
	newNode->next=stack->top;
	newNode->val=val;
	stack->top=newNode;
	stack->size=stack->size+1;
}

由于我们模拟的是栈,真头节点就是栈中压入的数据,所以请看以下示意图

在这里插入图片描述

首先通过原有的指针,获取真第一节点的内存位置

新真第一节点指向旧真第一节点

原有指针改变指向,指向新真第一节点

结构体stack中的成分size++

这就完成了元素的插入

将数据弹出栈

void pop(Stack* stack)
{
	node* temp;
	temp=stack->top;
	stack->top=temp->next;
	stack->size=stack->size-1;
	free(temp);
}

在这里插入图片描述

首先通过指针指向第一真节点,通过第一真节点指向第二真节点

然后删除第一真节点

结构体stack中的成分size–

判断栈是否为空

int empty(const Stack* stack)
{
	if(stack->size==0)
	return 1;
	else
	return 0;
}

如果结构体stack中的成分size为0,说明指针没有指向任何一个链表节点。

获取栈顶元素

int top(const Stack* stack)
{
	if(stack->size == 0) 
	{
        return 0;
    }
	return stack->top->val;
}

如果栈元素为零,说明栈中没有元素

否则,通过指针找到第一真节点,获取值

获取栈的大小

int size(const Stack* stack)
{
	return stack->size;
}

返回结构体stack中的成分size的值

这道题用链表实现栈用来解决问题的具体实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Node
{
    int val;
    struct Node* next;
} node;

typedef struct 
{
    int size;
    node* top;
} Stack;

void InitiateStack(Stack* stack)
{
	stack->top=NULL;
    stack->size=0;
}
void push(Stack* stack, const int val)
{
	node* newNode=(node*)malloc(sizeof(node));
	newNode->next=stack->top;
	newNode->val=val;
	stack->top=newNode;
	stack->size=stack->size+1;
}
void pop(Stack* stack)
{
	node* temp;
	temp=stack->top;
	stack->top=temp->next;
	stack->size=stack->size-1;
	free(temp);
}
int empty(const Stack* stack)
{
	if(stack->size==0)
	return 1;
	else
	return 0;
}
int top(const Stack* stack)
{
	if(stack->size == 0) 
	{
        return 0;
    }
	return stack->top->val;
}
int size(const Stack* stack)
{
	return stack->size;
}
void print(const Stack* stack)
{
	node* temp=stack->top;
	while(temp->next!=NULL)
	{
		printf("%d->",temp->val);
		temp=temp->next;
	}
	printf("null\n");
}
int main() {
    Stack mystack;
    InitiateStack(&mystack);
    char a[100];
	scanf("%s",a);
	for(int i=0;i<strlen(a);i++)
	{
		if(a[i]=='{'||a[i]=='['||a[i]=='(')
		{
			push(&mystack,a[i]);
		}
		if(a[i]=='}'||a[i]==']'||a[i]==')')
		{
			int temp1=top(&mystack);
			push(&mystack,a[i]);
			int temp2=top(&mystack);
			if(!empty(&mystack))
			{
				if(temp1=='('&&temp2==')')
				{
					pop(&mystack);
					pop(&mystack);
				}
				if(temp1=='['&&temp2==']')
				{
					pop(&mystack);
					pop(&mystack);
				}
				if(temp1=='{'&&temp2=='}')
				{
					pop(&mystack);
					pop(&mystack);
				}
			}
			else
			break;
		}
	}
	if(!empty(&mystack))
	printf("No");
	else
	printf("Yes");
    return 0;
}

谢谢观看

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值