数据结构之栈和队列

栈和队列

栈和队列的基本概念

1. 栈的基本概念

1.1 栈的定义

  栈是一种只能在一端进行插入或者删除操作的线性表。其中允许进行插入或删除操作的一端称为栈顶(TOP)。栈顶由一个叫作栈顶指针来表示,栈的另外一端称之为栈底,栈底是固定不变的。栈的插入和删除操作一般可以称之为入栈出栈。

1.2 栈的特点

  主要特点为先进先出(FILO)。

1.3 栈的存储结构

  可以用顺序表和链表来存储栈,分为顺序栈和链栈。

1.4 栈的数学性质

  当n个元素以某种顺序进栈时,并且可以在任意时刻出栈,所获得的排列组合数目恰好满足函数Catalan():
Catalan()

2. 队列的基本概念

2.1 队列的定义

  队列简称队,只允许一端插入另一端删除的线性表。可插入的一端称之为队尾(rear),可删除的一端称之为队头(front)。插入元素称为进队,删除元素称为出队。

2.2 队列的特点

  队列的特点为先进先出(FIFO)。

2.3 队列的存储结构

  可用顺序表和链表来存储队列,分为顺序队和链队。

栈和队列的存储结构与算法

下面主要介绍上述提到的顺序栈、顺序队、链栈和链队。

顺序栈

  顺序栈主要有两个特殊状态和两个操作,分别为栈空、栈满和进栈、出栈。

#define MaxSize	1024

/*
 *	the sequence queue
 **/ 
typedef struct
{
	int data[MaxSize];
	int front;
	int rear;
}SeqQueue;

/*
 *	the sequence queue's operator
 **/
 
/*
 *	initQueue()
 *	initializate the sequence queue
 **/
void initQueue(SeqQueue &qu)
{
	qu.front = qu.rear = 0;
}
/*
 *	isEmpty()
 *	emptiness(judge the sequence queue)
 **/
int isEmpty(SeqQueue qu)
{
	if(qu.front == qu.rear) return 1;
	else return 0;
}
/*
 *	push()
 *	Enter the sequence queue
 **/
int push(SeqQueue &qu,int x)
{
	if((qu.rear+1) & MaxSize == qu.front) return 0;
	qu.rear = (qu.rear+1) % MaxSize;
	qu.data[qu.rear] = x;
	return 1;
}
/*
 *	pop()
 *	out of the sequence queue
 **/
int pop(SeqQueue &qu,int x)
{
	if(qu.front == qu.rear) return 0;
	qu.front = (qu.front+1) % MaxSize;
	x = qu.data[qu.front];
	return 1;
}; 

  这里其实是按照标准操作来写的顺序栈,完全可以简化为我们常用的数组加个top指向栈顶就可以达到一样的作用。

int SeqStack[MaxSize]; int top = -1; // 初始化 顺序栈
SeqStack[++top] = x; // 进栈操作
x = SeqStack[top--]; // 出栈操作 !注意! 这里不能换成--top哈,道理很简单,自己领会。

链栈

  链栈对应顺序栈来讲,同样有两个状态和两个操作。

#define MaxSize	1024
/*
 *	the link stack
 **/
typedef struct LStackNode
{
	int data;  // data store the node's data field (default type int)
	struct LStackNode *next;  // point to successor node
}LStackNode; 

/*
 1. the link stack's operator
 **/
 
/*
 2. initStack()
 3. initializate the Stack
 **/
void initStack(LStackNode* &lst)
{
	lst = (LStackNode*)malloc(sizeof(LStackNode));
	lst->next = NULL;
}
/*
 4. isEmpty()
 5. emptiness(judge the Stack)
 **/
int isEmpty(LStackNode *lst)
{
	if(lst->next == NULL) return 1;
	else return 0;
}
/*
 6. push()
 7. Enter the stack
 **/
int push(LStackNode *lst,int x)
{
	LStackNode *p;
	p = (LStackNode*)malloc(sizeof(LStackNode));
	p->next = NULL;
	/* the insert of rear  */
	p->data = x;
	p->next = lst->next;
	lst->next = p;
}
/*
 8. pop()
 9. out of the stack
 **/
int pop(LStackNode *lst,int &x)
{
	LStackNode *p;
	if(lst->next == NULL) return 0;
	/* the delete of link list  */
	p = lst->next;
	x = p->data;
	lst->next = p->next;
	free(p);
	return 1;
}

  这里涉及到一些链表的头插法,可以复习下前面有讲过的单链表的操作,结合代码比较容易理解,注解中有说明哪里用到了头插法。

顺序队列

  普通的顺序队列在经过一系列的进队出队操作之后可能会出现”假溢出“,即队内没有元素也不允许元素进队。
  这里主要说明下循环队列,循环队列即将顺序队整成一环,这样头指针和尾指针就可以一直走下去,不会出现“假溢出”。
具体操作过程如下图所示:
进队/出队操作变化
  ①由空队列进队两个元素,尾指针往后移动两格,front指向开头 0,rear指向 2.
  ②接着在①的基础上进队四个元素,出队三个元素,front指向 3,rear指向 6 .
  ③在②的基础上再进队两个元素,出队四个元素,front指向 7, rear指向 0.
  循环队列也是有四个要素即两个状态和两个操作,队空和队满,进队和出队。

#define MaxSize	1024

/*
 *	the sequence queue
 **/ 
typedef struct
{
	int data[MaxSize];
	int front;
	int rear;
}SeqQueue;

/*
 *	the sequence queue's operator
 **/
 
/*
 *	initQueue()
 *	initializate the sequence queue
 **/
void initQueue(SeqQueue &qu)
{
	qu.front = qu.rear = 0;
}
/*
 *	isEmpty()
 *	emptiness(judge the sequence queue)
 **/
int isEmpty(SeqQueue qu)
{
	if(qu.front == qu.rear) return 1;
	else return 0;
}
/*
 *	push()
 *	Enter the sequence queue
 **/
int push(SeqQueue &qu,int x)
{
	if((qu.rear+1) & MaxSize == qu.front) return 0;
	qu.rear = (qu.rear+1) % MaxSize;
	qu.data[qu.rear] = x;
	return 1;
}
/*
 *	pop()
 *	out of the sequence queue
 **/
int pop(SeqQueue &qu,int x)
{
	if(qu.front == qu.rear) return 0;
	qu.front = (qu.front+1) % MaxSize;
	x = qu.data[qu.front];
	return 1;
}; 

链队

  链队顾名思义利用链式存储的队列,这里以单链表实现为例子,链队即不存在队满上溢(除开内存满)。
  链队也四要素,两个操作和两个特殊状态。
出队入队操作可如下图所示:
入队出队操作示意图

#define MaxSize	1024
/*
 *	the link queue
 **/ 
typedef struct LQNode
{
	int data;  // data store the node's data field (default type int)
	struct LQNode *next;  // point to successor node
}LQNode; 

typedef struct
{
	LQNode *front;
	LQNode *rear;
}LinkQueue;
 
/*
 *	the link queue's operator
 **/
 
/*
 *	initQueue()
 *	initializate the link queue
 **/
void initQueue(LinkQueue &linkqueue)
{
	linkqueue = (LinkQueue*)malloc(sizeof(LinkQueue));
	linkqueue->front = linkqueue->rear = NULL;
}
/*
 *	isEmpty()
 *	emptiness(judge the link queue)
 **/
int isEmpty(LinkQueue linkqueue)
{
	if(linkqueue->rear == NULL || linkqueue->front == NULL) return 1;
	else return 0;
}
/*
 *	enQueue()
 *	Enter the link queue
 **/
void enQueue(LinkQueue *linkqueue,int x)
{
	LQNode *p;
	p =  (LQNode*)malloc(sizeof(LQNode));
	p->data = x;
	if(linkqueue->rear == NULL)
	{
		linkqueue->rear = linkqueue->front = p;
	}
	else
	{
		linkqueue->rear->next = p; // use the insert of rear
		linkqueue->rear = p;
	}
}
/*
 *	deQueue()
 *	out of the link queue
 **/
int deQueue(LinkQueue *linkqueue,int &x)
{
	LQNode *p;
	if(linkqueue->rear == NULL) // the linkqueue is empty ,so cant deQueue()
	{
		return 0;
	}
	else
	{
		p = linkqueue->front;
	}
	if(linkqueue->front == linkqueue->rear)  // if the linkqueue only have one LQNode,then it need be processed specially
	{
		linkqueue->front = linkqueue->rear = NULL;
	}
	else
	{
		linkqueue->front = linkqueue->front->next;
	} 
	x = p->data;
	free(p);
	return 1;
}; 

  利用链表存储队列,需要明白的就是,出队和进队操作,因为这两个操作涉及到对链表的删除插入,理解清楚链表相关操作的话,这部分也比较容易理解。

共享栈和双端队列

  这边多提一嘴,共享栈和双端队列,都是基于前面所说的栈和队列的改良版。
但个人感觉不太常用,所以大致了解下就好啦!

共享栈

  顾名思义:共享栈共享什么呢,当然是共享存储空间,即栈底分别位于存储空间的两端,栈顶在存储空间中,当两栈顶相遇时就发生了上溢。

双端队列

  双端队列呢,也很容易理解,即两端都可以进行入队出队操作。这边就不再进行延申啦!感兴趣的同学可以去了解下,它的进队出队操作的实现代码。

常见问题(主要关于栈,经典问题需要掌握)

提问:括号配对问题,利用栈来判断一个表达式中的括号是否合法配对,即不出现多余的括号,也没有配对错误的括号(例如" )("),那么这样一个关于栈的操作的函数怎么去编写呢?
【分析】:
  首先,已知有一个表达式需要我们对其进行判别,那么我们根据括号匹配的规则来,可以知道只要一对括号匹配成功就可以将这对括号去除掉,因为他们已经不影响后面的括号配对。这里呢,就是利用栈的特性,先进后出,把表达式中的左括号"(“一个一个提出来,然后将其对后面的括号进行配对操作,如果匹配那么”(“出栈,不配对就接着找下一个”(",最后栈为空则说明括号配对成功,反之则失败。
  还是直接上代码比较容易理解:

int match(char exp[],int n) // exp 为存储括号的字符数组  n 为括号数
{
	char SeqStack[MaxSize];
	int top = -1;  // 顺序栈的定义相较来说还是比较容易
	for (int i = 0; i < n ; ++i)
	{
		if(exp[i] == '(')	SeqStack[++top] = '('; // 将左括号存入栈中
		if(exp[i] == ')')
		{
			if(top == -1) return 0; // 如果exp中出现 右括号 而栈为空 则说明 匹配失败
			else --top; // 不为空 则说明匹配成功一对括号
		}
	}
	if(top == -1) return 1; // 如果到最后 栈为空 则说明 括号都配对成功
	else return 0; // 否则 匹配失败
}

提问:后缀表达式问题,编写函数求后缀式的值?
【分析】:
  首先,这里先来了解下什么是后缀式。通俗的理解就是运算符位于操作符之后,这也是为了计算机方便处理算术表达式。我们平时常用的表达式时中缀式,当然也有前缀式。
  后缀表达式运算规则:从左向右扫描,遇到数字压栈,遇到操作符,弹出栈顶的两个元素,先弹出的元素在右边,后弹出来的在左边,进行计算后,将结果压栈,再往后扫描,直到扫描结束,输出栈顶元素,即为最终结果。
  前缀表达式运算规则:从右向左扫描,遇到数字压栈,遇到操作符,弹出栈顶的两个元素,先弹出的元素在左边,后弹出来的在右边,进行计算后,将结果压栈,再往前扫描,直到扫描结束,输出栈顶元素,即为最终结果。
  举个例子就容易理解
  例如:中缀式:(a+b+c*d)/e
  转化成前缀式为 /++ab*cde
  转换成后缀式为abcd*++e/
代码:

int Op(int a,char op,int b) // 运算函数 根据运算符来计算a 和 b
{
	if(op == '+') return a+b;
	if(op == '-') return a-b;
	if(op == '*') return a*b;
	if(op == '/') // 涉及到除法操作 就需要分类考虑下除数 为零的情况即可
	{
		if(b == 0)
		{
			cout << "the Divisor cant be zero!\n";
			return 0;
		}
		else return a/b;
	}
}

int cal_suffix_exp(char exp[])
{
	int a,b,c;
	int SeqStack[MaxSize];
	int top = -1;
	char op; 
	
	for(int i = 0; exp[i] != '\0'; ++i)
	{
		if(exp[i] >= '0' && exp[i] <= '9') // 这里默认操作数 都是个位
		{
			SeqStack[++top] = exp[i] - '0'; // 字符型传换成整形的常规操作
		}
		else
		{
			op = exp[i]; // 如果不是操作数 那就是运算符
			b = SeqStack[top--];
			
			a = SeqStack[top--]; // 这里就是前面说到的 从栈内连续提取两个数
			c = Op(a,op,b);
			SeqStack[++top] = c; // 将提取出来的两个数 计算后的结果重新存入栈中
		}
	}
	return SeqStack[top]; // 最后栈中只剩栈顶元素 即为最后的结果
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叫我蘑菇先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值