《数据结构与算法》| 清华大学 | 第三章 | 栈和队列

栈是受限的序列,只能在栈顶插入和删除,栈底是盲端。栈属于序列的特例,所以可以基于顺序表和链表派生。
在这里插入图片描述

1. 顺序栈

空间管理

#define MaxSize 50				// 定义栈中元素的最大个数
typedef struct {
    
    
	ElemType data[MaxSize];		// 存放栈中元素
	int top;					// 栈顶元素
} SqStack; 						

静态操作

bool Top(SqStack S, ElemType &e)
{
    
    
	if (S.top==-1)	return false;	// 栈空,报错
	e=S.data[S.top];
	return true;
}

动态操作

bool Push(SqStack &S, ElemType e)
{
    
    
	if (S.top==MaxSize-1)	return false;	// 栈满,报错
	S.data[++S.top]=e;
	return true;
}
bool Pop(SqStack &S, ElemType &e)
{
    
    
	if (S.top==-1)	return false;
	e=S.data[S.top--];
	return true;
}

2. 链栈

空间管理

#include "LinkList.h"

静态操作

bool Top(LinkStack S, ElemType &e) 	// 取顶=获取首元素 
{
    
     
	if (S->next==NULL)	return false;	// 栈空,报错
	e=S->next->data;
	return true;
} 	

动态操作

void Push(LinkStack &S, ElemType e) 	// 入栈=插入首元素
{
    
     	Insert(S,1,e);	}
void Pop(LinkStack &S, ElemType &e) 	// 出栈=删除首元素 
{
    
     	Delete(S,1,e);	}

应用

1. 进制转换

问题描述:给定任一10进制非负整数 n n n,将其转换为 λ \lambda λ 进制表示形式?
算法思想: n n n λ \lambda λ 反复取模和整除,每次取模结果保存栈中,最后输出栈内所有元素,即自低到高 λ \lambda λ 进制

void convert(stack<char> &S, int n, int base) {
    
    \
	char digit[] = "0123456789ABCDEF";	// 新进制符号 
	while (n > 0) {
    
    
		S.push( digit[ n % base] );		// 余数入栈 
		n /= base;	
	}
} // 新进制下由高到低的各位数,自顶而下保存在栈中 

2. 括号匹配

问题描述:如何判断表达式的括号是否匹配?

在这里插入图片描述

减而治之:消除一对紧邻的括号,不影响全局的匹配判断。

在这里插入图片描述

算法思想:顺序扫描表达式,栈记录已经扫描的部分,遇到 “(" 则进栈,遇到 “)” 则出栈,反复迭代消除紧邻括号。

bool paren(const char exp[], int lo, int high) {
    
    
	stack<char> S;	// 栈记录左括号
	for (int i = lo; i < hi; i++) {
    
    
		if ( ( exp[i] == '(' ) ) S.push( exp[i] );	// 遇左括号,则进栈左括号 
		else if ( !S.empty() ) S.pop();				// 遇右括号且栈非空,则出栈左括号 
		else return false; 							// 遇右括号且栈空,则不匹配 
	} 
	return S.empty(); 	// 匹配,则栈空 
} 

3. 栈混洗

栈混洗(stack permutation):栈 A = a 1 , a 2 , . . . a n , B = S = ∅ A = { a_1, a_2, ... a_n },B=S=∅ A=a1,a2,...anB=S=,只允许栈 A A A 顶元素弹出入栈 S S S,栈 S S S 顶元素弹出入栈 B B B,最终栈 A A A 元素全转入栈 B B B,该过程称为栈混洗。

在这里插入图片描述

问题描述:判断栈混洗的数量
算法思想:每种栈混洗中第一个元素的位置是确定,该元素左右两侧元素的栈混洗排列是相互独立的,所以第一个元素在某个位置 k k k的栈混洗数量是 S P ( n − k ) ∗ S P ( k − 1 ) SP(n-k)*SP(k-1) SP(nk)SP(k1),而第一个元素的位置数量是 1 1 1 ~ n n n, 所以可以得到递推式 S P ( n ) = ∑ k = 1 n S P ( n − k ) ∗ S P ( k − 1 ) = ( 2 n ) ! ( n + 1 ) ! ⋅ n ! SP(n) = \sum_{k=1}^{n} SP(n-k)*SP(k-1)=\frac{(2n)!}{(n+1)!·n!} SP(n)=k=1nSP(nk)SP(k1)=(n+1)!n!(2n)!

在这里插入图片描述

问题描述:甄别全排列中的非栈混洗
算法思想: [ p 1 , p 2 , . . . , p n ] [p_1, p_2, ..., p_n] [p1,p2,...,pn] [ 1 , 2 , 3 , . . . , n ] [1, 2, 3, ..., n] [1,2,3,...,n] 的栈混洗,当且仅当任意 i < j i<j i<j 不含模式 [ . . . , j + 1 , . . . , i , . . . , j , . . . ] [..., j+1, ..., i, ..., j, ...] [...,j+1,...,i,...,j,...]

在这里插入图片描述

4. 中缀表达式

问题描述:给定语法正确的算术表达式,计算与之对应的数值?

在这里插入图片描述

  • 策略一:括号匹配

减而治之:如果算术表达式允许添加括号,则按照需要的运算顺序添加括号,从而将运算符优先级转换为括号匹配的优先级计算表达式。

在这里插入图片描述

算法思想:设置一个操作数栈和符号栈,操作数栈存储左括号和数字,符号栈存储运算符号。

  1. 从左到右扫描算术表达式,遇到数字入操作数栈,遇到算术符号和左括号入符号栈
  2. 遇到右括号则操作数栈顶元素、次栈顶元素,符号栈栈顶元素和次栈顶元素出栈。
  3. 操作数的次栈顶元素作为操作数,栈顶元素作为被操作数,使用符号栈出栈的符号计算并将计算结果入操作栈,如此循环直至扫描完毕。
float calculate(float a, char op, float b) {
    
    	// 计算结果 
	switch(op) {
    
    					
				case '+': return a+b;
				case '-': return a-b; 
				case '*': return a*b; 
				case '/': return a/b; 
	}
}

float evaluate(const char S[], int lo, int hi) {
    
    
	stack<float> opnd; 	stack<char> optr;	// 操作数栈、符号栈 
	float result = 0;						// 运算结果 
	
	for (int i = lo; i < hi; i++) {
    
    			
		if ( S[i] == ' ' ) continue;		// 忽略空格 
		
		if ( isdigit(S[i]) ) 				// 操作数入栈
			opnd.push(S[i] - '0');			
		else if ( S[i] != ')') 				// 运算符和左括号入栈 
			optr.push(S[i]);				
		else if ( S[i] == ')') {
    
    			// 遇右括号,运算符和左括号出栈,操作数 a 和被操作数 b 出栈 
			char op = optr.top();   optr.pop(); optr.pop();	  
			float b = opnd.top();   opnd.pop();
			float a = opnd.top();   opnd.pop();
			result = calculate(a, op, b);
			opnd.push(result);				// 结果入栈 
		}
	}
	return result;
}
  • 策略二:后缀表达式

减而治之:如果算术表达式不允许添加括号,则优先级高的局部执行,并被代以其数值,运算符渐少,直至得到最终结果。

在这里插入图片描述

中缀转后缀:中缀表达式转换为后缀表达式,可以实现保证优先级高的局部执行。设置优先级表 M 用于判断运算符优先级,栈 op 保存运算符,后缀表达式靠前的运算符优先级高

  1. 从左到右扫描算术表达式,遇到数字即输出,遇到左括号即入栈
  2. 遇到右括号,意味着括号内运算优先级高,则一直输出栈内运算符并出栈,直至遇到左括号并把左括号出栈
  3. 遇到运算符,栈空则入栈;栈不空且运算符优先级小于等于栈顶元素,则输出栈顶元素并出栈,然后将运算符入栈
  4. 算术表达式扫描完毕,但栈非空则一直输出栈内元素并出栈
map<char,int> M = {
    
    
	{
    
    '+',1}, {
    
    '-',1}, {
    
    '*',2}, {
    
    '/',2}
};

bool isOperator(char ch) {
    
    
	if (ch == '+' || ch == '-' || ch == '*' || ch == '/')
		return true;
	return false;
}

void convert(const char exp[], int lo, int hi) {
    
    
	stack<char> op;
	
	for (int i = lo; i < hi; i++) {
    
    				
		if(isdigit(exp[i])) {
    
     		// 如果是数字
        	cout << exp[i];
      	} else if(exp[i] == '(') {
    
    	// (:左括号
        	op.push(exp[i]);
    	} else if(exp[i] == ')') {
    
    	// 如果是右括号,一直推出栈中操作符,直到遇到左括号(
        	while(op.top() != '(') {
    
    
          		cout << op.top();
          		op.pop();
        	}
        	op.pop(); // 把左括号(推出栈
      	} else if(isOperator(exp[i])) {
    
     // 操作符
        	if(op.empty()) {
    
    // 如果栈空,直接压入栈
          		op.push(exp[i]);
        	}
        	else {
    
    
        	  	// 比较栈op顶的操作符与ch的优先级
          		// 如果ch的优先级高,则直接压入栈
          		// 否则,推出栈中的操作符,直到操作符小于ch的优先级,或者遇到(,或者栈已空
          		while(!op.empty()) {
    
    
            		if(M[exp[i]] > M[op.top()]) break;	// 优先级高于 
              		cout << op.top();	op.pop();
          		} // while结束
          		op.push(exp[i]); // 防止不断的推出操作符,最后空栈了;或者ch优先级高了
        	}
      } 
	}
		
	while (!op.empty()) {
    
    
		cout << op.top();	op.pop();
	}
}

后缀表达式求值:后缀表达式按照优先级排列运算符,仍需要设置栈将局部执行的结果保存在原来位置。

  1. 从左到右扫描算术表达式,遇到操作数则入栈
  2. 遇到运算符则弹出栈顶元素和次栈顶元素,使用运算符计算并将结果入栈,最后栈顶元素是最终运算结果
float calculate(float a, char op, float b) {
    
    	// 计算结果 
	switch(op) {
    
    					
				case '+': return a+b;
				case '-': return a-b; 
				case '*': return a*b; 
				case '/': return a/b; 
	}
}

float evaluate(const char exp[], int lo, int hi) {
    
    
	stack<float> opnd;	
	
	for (int i = lo; i < hi; i++) {
    
    				
		if(isdigit(exp[i]))  // 如果是数字
        	opnd.push(exp[i] - '0');	
      	else {
    
     				// 操作符 
			float b = opnd.top();   opnd.pop();
			float a = opnd.top();   opnd.pop();
			float result = calculate(a, exp[i], b);
			opnd.push(result);				// 结果入栈
      } 
	}
	return opnd.top();		// 返回栈顶元素 
}

队列

队列和栈一样也是受限的序列,只能在队尾插入和队头删除。队列属于序列的特例,所以可以基于向量和列表派生。
在这里插入图片描述

1. 循环队列

空间管理

#define MaxSize 50				// 定义队列中元素的最大个数
typedef struct {
    
    
	ElemType data[MaxSize];		// 存放队列元素
	int front, rear;			// 队头指针和队尾指针
};

动态操作

bool EnQueue(SqQueue &Q, ElemType x)
{
    
    
	if ((Q.rear+1)%MaxSize==Q.front)	return false;	// 队满
	Q.data[Q.rear]=x;
	Q.rear=(Q.rear+1)%MaxSize;							// 队尾指针加1取模
	return true;
}
bool DeQueue(SqQueue &Q, ElemType &x) 
{
    
    
	if (Q.rear==Q.front)	return false;				// 队空,报错
	x=Q.data[Q.front];
	Q.front=(Q.front+1)%MaxSize;						// 队头指针加1取模
	return true;
}

2. 链队列

空间管理

#include "LinkList.h"
typedef struct {
    
    				// 链式队列
	Position front, rear;		// 队列的队头和队尾指针
}LinkQueue;

动态操作

void EnQueue(LinkQueue &Q, ElemType x)
{
    
    
	Position s =(Position)malloc(sizeof( struct Node ));
	s->data=x;	s->next=NULL;	// 创建新结点,插入到链尾
	Q.rear->next=s;	Q.rear=x;
}
bool DeQueue(LinkQueue &Q, ElemType &x)
{
    
    
	if (Q.front==Q.rear)	return false;	// 空队
	Position=Q.front->next;	x=p->data;
	Q.front->next=p->next;
	
	if (Q.rear==p)	Q.rear=Q.front;			// 队列只有一个结点,删除后变空
	free(p);
	return true;
}

应用

1. 资源循环分配

问题描述:一群客户共享同一资源时,如何兼顾公平和效率?(如多进程共享CPU)

在这里插入图片描述

算法思想:客户按照到达先后排成队列,队头客户出队,接受服务,再次入队,依次进行直至服务关闭。

RoundRobin {
    
     // 循环分配器
	Queuue Q( clients );	// 客户队列
	while ( !ServiceClosed() ) {
    
    
		e = Q.dequeue();	// 队头客户出队
		serve( e );			// 接受服务
		Q.enqueue( e );		// 再次入队
	}
}

2. 双栈当队

问题描述:如何使用两个栈模拟一个队列?

在这里插入图片描述

算法思想:因为栈只能在栈顶进行操作,而队列在队尾插入队头删除,所以需要将栈 S 1 S1 S1 和栈 S 2 S2 S2 的栈顶分别作为队尾和队头。入队就是 S 1 S1 S1 入栈;出队时需要将栈 S 1 S1 S1 的所有元素出栈并入栈 S 2 S2 S2,再让栈 S 2 S2 S2 栈顶元素出栈;两栈均空则队空。

template <typename T>
class Queue : public Stack<T> {
    
    		// 双栈模拟队列
	public:
		void enqueue( T const & e) {
    
     S1.push(e); }		// 入队
		T dequeue() {
    
     									// 出队
			while ( !S1.empty() ) {
    
    
				S2.push( S1.top() );	S1.pop();
			}
			S2.pop();
		}		
		T &front() {
    
     return S2.top(); }					// 队首
	
	private:
		Stack<T> S1, S2;	
};

猜你喜欢

转载自blog.csdn.net/K_Xin/article/details/107323914
今日推荐