数据结构与算法-栈的设计与应用

0 概述

本文主要涉及栈的设计与栈的应用,栈的后进先出特性使其广泛应用于各种问题解决方案中,这里主要介绍以下5种问题的栈方法解决方案。
1)进制转换;
2)括号匹配;
3)多项式求解;
4)八皇后问题;
5)迷宫寻径问题。

1 栈的设计

栈也是一种线性结构,只是其操作仅限于栈顶,只能进行压栈和出栈操作,所以其可以从列表和向量继承而来,并增添对应的压栈(Push)和出栈(Pop)接口,而对其他操作接口加以限制。栈ADT支持的操作接口如下所示:
在这里插入图片描述

2 栈应用之进制转换

在进制转换中,我们一般从最低位到最高位进行处理,如果按照处理顺序输出转换后的数据,则数据顺序是反的。为了解决这个问题,我们可以先将处理结果逐一压栈,等到数据处理完成后再逐一出栈,则结果顺序就是正确的。进制转换的代码实现如下:

/stack :存储转换结果的栈
//data  :原始数据
//base  :需要转换成的进制
void BaseConvert(MyStack<T>& stack, long data, int base)
{
	static char digit[] = { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' };

	//递归版本
	/*
	//将余数结果压栈
	stack.Push(digit[data % base]);

	//处理下一位数据
	data = data / base;
	if (0==data)
		return;
	BaseConvert(stack, data, base);
	
	*/

	//迭代版本
	do
	{
		//将余数结果压栈
		stack.Push(digit[data % base]);

		//处理下一位数据
		data = data / base;
	} while (data);

}

3 栈应用之括号匹配

在对表达式进行正确性检查时,括号匹配是其中应该检查的一项。如果左右括号不相互匹配,则表达式显然是错误的的。括号匹配的检查思路为:
1)遇到左括号则将其压栈;
2)遇到右括号,则将其与栈顶的括号进行匹配,若能组成正确的括号对,则将栈顶的括号出栈,否则表达式括号不匹配(这是因为若括号匹配,则右括号应该与其最近的一个左括号相对应,否则就是错误的)。
3)对表达式进行遍历后,若所以括号均是匹配的,那么左括号所在的栈应该是空的,若不为空,则括号不匹配。
代码实现如下:

//expresion :进行括号匹配的表达式
bool BracketMatch(string& expression)
{
	MyStack<char> ch_stack;

	//遍历表达式
	for (int i = 0; i < expression.length(); i++)
	{
		switch (expression[i])
		{
		case '(':
		case '[':
		case '{':
		{
			//若为左括号则压栈
			ch_stack.Push(expression[i]);
		}; break;
		case ')':
		{
			//若左括号栈提前为空或者与右括号不匹配,则出错
			if (ch_stack.IsEmpty() || (ch_stack.Pop() != '('))
				return false;
		}; break;
		case ']':
		{
			//若左括号栈提前为空或者与右括号不匹配,则出错
			if (ch_stack.IsEmpty() || (ch_stack.Pop() != '['))
				return false;
		}; break;
		case '}':
		{
			//若左括号栈提前为空或者与右括号不匹配,则出错
			if (ch_stack.IsEmpty() || (ch_stack.Pop() != '{'))
				return false;
		}; break;
		default:; break;
		}
	}


	//变量结束后,若左括号栈为空,则表达式括号匹配,否则,表达式括号不匹配
	return ch_stack.IsEmpty();
}

4 栈应用之表达式求值

当我们对一个表达式进行求值时,显然不能简单地从左向右顺序求值,而是应该根据+、-、*、/ 等运算符的优先级和括号位置进行先后计算。利用栈进行表达式求值的思路为:
1)对运算符进行优先级的排序;
2)遍历表达式,遇到数据则将数据压入数据栈(opnd),遇到运算符(设为optr_now),如果当前运算符optr_now比运算符栈(optr)栈顶运算符(设为optr_top)优先级高,则将optr_now压入optr栈(显然如果当前运算符optr_now优先级比之前的运算符optr_top高,则不应该计算之前的运算符);如果当前运算符optr_now比栈顶运算符optr_top优先级低(显然如果之前运算符optr_top优先级更高,而当前运算符optr_now优先级较低,则可以先计算之前的运算符optr_top),则从optr出栈栈顶运算符optr_top并从opnd出栈optr_top要求的操作数数目,经过optr_top计算后将计算结果压入optn。
3)如果出栈optr_top后,新的栈顶运算符optr_top_top仍然有较高的运算符,则继续步骤2,直至栈顶运算符优先级较低。
4)如果遇到左括号,则其优先级最低,如果遇到右括号,则其只与左括号优先级相等。由于所有运算符优先级均比左括号高,故遇到右括号之前,在括号内的运算符经过2-3步骤已经计算完毕,故遇到右括号只需将左括号从optr出栈即可。

代码实现如下:

//定义运算符表的大小
#define N_OPTR 9
//定有运算符的枚举类型值
typedef enum
{
	//+   -    *    /    ^    !    (    )    \0
	ADD, SUB, MUL, DIV, POW, FAC, L_P, R_P, EOE
}Operator;


//定义运算符的优先级
//出现 \0是为了与表达式结尾的\0 相互对应
const char pri_table[N_OPTR][N_OPTR] =
{
	/*         |-------------- 弼前运算符 --------------| */
	/*          +    -    *    /    ^    !    (    )   \0 */
	/* -- + */ '>', '>', '<', '<', '<', '<', '<', '>', '>',
	/* | - */ '>', '>', '<', '<', '<', '<', '<', '>', '>',
	/* 栈 * */ '>', '>', '>', '>', '<', '<', '<', '>', '>',
	/* 顶 / */ '>', '>', '>', '>', '<', '<', '<', '>', '>',
	/* 运 ^ */ '>', '>', '>', '>', '>', '<', '<', '>', '>',
	/* 算 ! */ '>', '>', '>', '>', '>', '>', ' ', '>', '>',
	/* 符 ( */ '<', '<', '<', '<', '<', '<', '<', '=', ' ',
	/*  | ) */ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
	/* --\0 */ '<', '<', '<', '<', '<', '<', '<', ' ', '='
};

/从表达式中读取数据
float ReadNumber(MyStack<float>& stack, char*& expression)
{
	int len = 0;
	int dot_index = 0;
	MyStack<int> int_stack;

	//将字母压栈并找出小数点的索引
	while ((isdigit(*expression)) || (*expression == '.'))
	{
		//确定小数点的索引
		if (*expression == '.')
		{
			dot_index = len++;
			continue;
		}
		int_stack.Push(*expression - 0x30);
		len++;
		expression++;
	}


	if (dot_index <= 0)
	{
		//如果没有找到小数点
		//则将小数点添加到末尾,表达式长度加1
		dot_index = len;
		len = len + 1;
	}

	float sum = 0;
	int pow_index = 0;
	//将数剧依次出栈计算数据
	while (!int_stack.IsEmpty())
	{
		sum = sum + int_stack.Pop() * pow(10, dot_index - (len - 1) + pow_index++);
	}

	//cout << "Read Number is " << sum << endl;

	stack.Push(sum);

	return sum;
}


//比较优先级
char ComparePri(char optr1, char optr2)
{
	int optr1_index = 0;
	int optr2_index = 0;
	switch (optr1)
	{
	case '+':optr1_index = ADD; break;
	case '-':optr1_index = SUB; break;
	case '*':optr1_index = MUL; break;
	case '/':optr1_index = DIV; break;
	case '^':optr1_index = POW; break;
	case '!':optr1_index = FAC; break;
	case '(':optr1_index = L_P; break;
	case ')':optr1_index = R_P; break;
	case '\0':optr1_index = EOE; break;
	}

	switch (optr2)
	{
	case '+':optr2_index = ADD; break;
	case '-':optr2_index = SUB; break;
	case '*':optr2_index = MUL; break;
	case '/':optr2_index = DIV; break;
	case '^':optr2_index = POW; break;
	case '!':optr2_index = FAC; break;
	case '(':optr2_index = L_P; break;
	case ')':optr2_index = R_P; break;
	case '\0':optr2_index = EOE; break;
	}

	//通过索引找到运算符的优先级
	return pri_table[optr1_index][optr2_index];
}

//一元运算符计算(针对阶乘)
//这里虽然类型为float 但是针对的是整数的阶乘
//小数的阶乘未加考虑
float Calcu(float opnd, char optr)
{
	float result = 1;
	if (optr == '!')
	{
		while (opnd > 0)
		{
			result *= opnd--;
		}
	}

	return result;
}


//二元运算符计算
float Calcu(float opnd1, float opnd2, char optr)
{
	float result = 0;
	switch (optr)
	{
	case '+':result = opnd1 + opnd2; break;
	case '-':result = opnd1 - opnd2; break;
	case '*':result = opnd1 * opnd2; break;
	case '/':result = opnd1 / opnd2; break;
	case '^': {
		result = 1;
		while (opnd2--)
		{
			result = result * opnd1;
		}
	}; break;
	}

	return result;
}


//将数据附加到逆波兰表达式中
void Append(char*& exp, float data)
{
	ostringstream oss;
	oss << data;
	string data_str = oss.str();
	for (int i = 0; i < data_str.length(); i++)
	{
		exp[i] = data_str[i];
	}
	exp = exp + data_str.length();
	exp[0] = ' ';
	exp = exp + 1;
}

//将运算符附加到逆波兰表达式中
void Append(char*& exp, char optr)
{
	exp[0] = optr;
	exp++;
	exp[0] = ' ';
	exp++;
}

//计算表达式并自动求出逆波兰表达式
float Evaluate(char* expression, char* rpn)
{
	MyStack<float> opnd;
	MyStack<char> optr;
	//压栈\0 为了与运算符表达的结束标志\0相对应
	optr.Push('\0');
	while (!optr.IsEmpty())
	{
		if (isdigit(*expression))
		{
			//如果为数据则压栈
			float data = ReadNumber(opnd, expression);

			//计算逆波兰表达式
			Append(rpn, data);
		}
		else
		{
			//比较当前运算符optr_now和栈顶运算符optr_top的优先级
			switch (ComparePri(optr.Top(), *expression))
			{
			case '<': {
				//若栈顶optr_top优先级低,也就是当前运算符optr_now优先级高,那么不计算,
				//将optr_now 压栈
				optr.Push(*expression);
				expression++;
			}; break;
			case '=': {
				//若栈顶optr_top优先级==当前运算符optr_now优先级
				//有两种情况左右括号匹配和遇到\0标志表达式结束
				//将栈顶运算符optr_top出栈即可
				optr.Pop();
				expression++;
			}; break;
			case '>':
			{
				//若栈顶optr_top优先级高,也就是当前运算符optr_now优先级低
				//那么按照运算符的要求出栈对应的运算符数,计算后将结果压入操作数栈opnd
				char temp_op = optr.Pop();
				
				//计算逆波兰表达式
				Append(rpn, temp_op);

				//单目运算符的计算
				if ('!' == temp_op)
				{
					float temp_opnd = opnd.Pop();
					//计算结果压入操作数栈opnd
					opnd.Push(Calcu(temp_opnd, temp_op));
				}
				else
				{
					//双目运算符的计算
					float temp_opnd2 = opnd.Pop();
					float temp_opnd1 = opnd.Pop();
					float result = Calcu(temp_opnd1, temp_opnd2, temp_op);
					//计算结果压入操作数栈opnd
					opnd.Push(result);
				}
			}; break;
			default:cout << "It must be wrong!\n"; ; exit(-1); break;
			}
		}
	}

	//逆波兰表达式附加\0作为结束标准符
	rpn[0] = '\0';
	return opnd.Pop();
}

5 栈应用之八皇后问题

已知国际象棋中皇后的势力范围覆盖其所在的水平线、垂直线以及两条对角线。八皇后问题就是在88的棋盘上,放置八个皇后使其不致互相攻击。推广到NN棋盘,设计思路如下,:
1)设计皇后的结构体,包括其行坐标x,列坐标y,及判等依据(也就是如果两个皇后不相等,则其不相互攻击,这样设计可以简化皇后的摆放)。
2)为了满足条件,则可知每一行必有一个皇后,则放置皇后时只需考虑列坐标即可,从棋盘(0,0)摆起,若与栈内的皇后相比均不相等(也就是不相互攻击),则将皇后MyQueen(x,y)入栈。
3.1)上一皇后入栈后,新皇后的坐标为(x+1,0),随着y的逐渐加1逐次与栈内皇后进行比较,如果存在y=y0&&y<N 使得MyQueen(x+1,y0)与栈内其他皇后不相等,则将皇后MyQueen(x+1,y0)入栈,从下一行(x+2,0)重新开始放置。
3.2)上一皇后入栈后,新皇后的坐标为(x+1,0),随着y的逐渐加1逐次与栈内皇后进行比较,如果不存在y=y0&&y<N 使得MyQueen(x+1,y0)与栈内其他皇后不相等,那么与预设相违背,则将栈顶皇后MyQueue(x,y)出栈,寻找处于同行的下一个与栈内皇后不相同的位置,如果不存在则继续出栈,直到找到与之前位置不同的但是满足条件的位置入栈后,再重新按行查找,
4皇后问题的示意图为:
在这里插入图片描述代码实现为:

//皇后类设计
class MyQueen
{
public:

	//定义横竖坐标
	int m_x;
	int m_y;

	MyQueen(int x = 0, int y = 0) :m_x(0),m_y(0) {};
	~MyQueen() {};
	
	//定义判等依据
	//同一行或者同一列或者或同一对角线
	bool operator==(const MyQueen &queen) const
	{
		return (m_x == queen.m_x)
			|| (m_y == queen.m_y)
			|| (m_x + m_y == queen.m_x + queen.m_y)
			|| (m_x - m_y == queen.m_x - queen.m_y);
	}

	//定义不等依据
	//不同一行并且不同一列并且不或同一对角线
	bool operator!=(const MyQueen& queen) const
	{
		return (m_x != queen.m_x)
			&& (m_y != queen.m_y)
			&& (m_x + m_y != queen.m_x + queen.m_y)
			&& (m_x - m_y != queen.m_x - queen.m_y);
	}
};

//N皇后放置方案
//solu  :存放各个满足要求皇后的栈
//num   :皇后的数目
void PlaceQueens(MyStack<MyQueen> &solu,int num)
{
	//定义起始皇后
	MyQueen queen(0, 0);
	do
	{
		//若列坐标小于列数且在栈内找到了相等的皇后,则纵坐标+1,依次尝试,直到queen.m_y >=num
		while ((queen.m_y < num) && (0 <= solu.Find(queen)))
		{
			queen.m_y++;
		}

		//如果跳出循环时列坐标m_y<小于列数,则找到了合适的位置,则将当前皇后压栈,
		//新皇后从下一行的第0列开始尝试
		if (queen.m_y<num)
		{
			solu.Push(queen);
			queen.m_x++;
			queen.m_y = 0;
		}
		else
		{
			//如果跳出循环时,没有合适的位置,则将当前栈顶皇后出栈,并增加其列坐标,寻找本行的下一合适位置
			queen = solu.Pop();
			queen.m_y++;
		}
	} while (solu.Size() < num);
}

6 栈应用之迷宫寻径问题

迷宫寻径问题就是在迷宫中找到一条从出发点都目的点的路径。如下图所示:
在这里插入图片描述基本设计思路为:
0)设迷宫内不能通过的格子状态为WALL,可以选择的格子状态为AVAILABLE,选择后路径不通往回走的格子状态为BACKTRACKED;
1)从出发点开始往上下左右方向尝试,将经过的路径压入到路径栈(设为path)内,如果遇到一个格子其上下左右没有可走的格子(周围的格子状态为WALL或BACKTRACKED,没有AVAILABLE),则将当前格子状态设为BACKTRACKED,并将路径栈栈顶出栈作为路径头,并修改其尝试前进的方向。
2)依次重复上述步骤,直到路径栈的栈顶与目的地相等,则寻径完成。
代码设计如下:

//定义单元格状态
typedef enum{AVAILABLE,ROUTE,BACKTRACKED,WALL} Status; 

//定义方向
typedef enum {
	UNKOWN,DOWN,UP,RIGHT,LEFT,NO_WAY
}Dir;


//定义单元格结构体
typedef struct{
	int m_x;            //当前坐标
	int m_y;            
	Status m_status;    //当前状态
	Dir m_in;           //进入方向       
	Dir m_out;          //出去方向
}Cell;

//迷宫尺寸
#define MAZE_SIZE 13

//障碍符号
#define WALL_CH '#'

//路径符号
#define ROUTE_CH '.'

//迷宫表 1-WALL 0-AVAILABLE
const bool maze_table[MAZE_SIZE][MAZE_SIZE] =
{
	//{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, b},
	/*0*/{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
	/*1*/{ 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1},
	/*2*/{ 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1},
	/*3*/{ 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1},
	/*4*/{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1},
	/*5*/{ 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1},
	/*6*/{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1},
	/*7*/{ 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1},
	/*8*/{ 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1},
	/*9*/{ 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1},
	/*a*/{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1},
	/*b*/{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1},
	/*c*/{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};

//初始化迷宫内各个格子的状态
void Maze_Init(Cell maze[MAZE_SIZE][MAZE_SIZE])
{
	for (int i = 0; i < MAZE_SIZE; i++)
	{
		for (int j = 0; j < MAZE_SIZE; j++)
		{
			//横纵坐标
			maze[i][j].m_x = i;
			maze[i][j].m_y = j;

			//当前状态,WALL还是AVAILABLE
			if (maze_table[i][j])
			{
				maze[i][j].m_status = WALL;
			}
			else
			{
				maze[i][j].m_status = AVAILABLE;
			}

			//设置进出方向
			maze[i][j].m_in = UNKOWN;
			maze[i][j].m_out = UNKOWN;
		}
	}
}

//获取下一个方向
Dir NextDir(Dir dir)
{
	return Dir(dir + 1);
}

//根据当前cell的出方向m_out获取邻居
Cell* Neighbor(Cell* cell)
{
	switch (cell->m_out)
	{
	case LEFT:return cell-1; break;
	case RIGHT:return cell + 1; break;
	case UP:return cell - MAZE_SIZE;; break;
	case DOWN:return cell + MAZE_SIZE; break;
	default:cout << "It msut be wrong!\n";
	}
}

//根据当前cell的出方向返回下一cell
Cell* Advance(Cell *cell)
{
	Cell* next=cell;
	switch (cell->m_out)
	{
	case LEFT:next = cell - 1; next->m_in = RIGHT; break;
	case RIGHT:next = cell + 1; next->m_in = LEFT; break;
	case UP:next = cell - MAZE_SIZE; next->m_in = DOWN; break;
	case DOWN:next = cell + MAZE_SIZE; next->m_in = UP; break;
	default:cout << "It msut be wrong!\n";
	}
	return next;
}

//路径搜索
bool Maze_Route(Cell maze[MAZE_SIZE][MAZE_SIZE], Cell* src, Cell* dst)
{
	if ((AVAILABLE != src->m_status) || (AVAILABLE != dst->m_status))
		return false;
	MyStack<Cell*> path;
	src->m_in = UNKOWN;
	src->m_status = ROUTE;

	//将起始点入栈
	path.Push(src);
	do
	{
		Cell* temp_cell = path.Top();
		//如果路径栈栈顶与目的地址相同则查找完毕
		if (temp_cell == dst)
		{
			//更新迷宫内各个格子的状态
			int path_size = path.Size();
			for (int i = 0; i < path_size; i++)
			{
				Cell* cell = path.Pop();
				maze[cell->m_x][cell->m_y].m_status = ROUTE;
			}
			return true;
		}

		//选择一个方向
		while (NO_WAY > (temp_cell->m_out = NextDir(temp_cell->m_out)))
		{
			//如果选择的方向中有可用的格子,则顺着方向前进1步
			if (AVAILABLE == Neighbor(temp_cell)->m_status)
				break;
		}

		//如果上下左右均没有可用的格子
		if (NO_WAY <= temp_cell->m_out)
		{
			//则标记当前格子为BACKTRACKED
			//弹出路径栈栈顶原始,回退1步尝试其他方向的路径
			temp_cell->m_status = BACKTRACKED;
			temp_cell = path.Pop();
		}
		else
		{
			//如果找到可用的格子,将当前格子压栈并按照选择的方向前进一步
			//新格子继续从UNKOWN状态开始寻找下一个可用路径
			temp_cell = Advance(temp_cell);
			temp_cell->m_out = UNKOWN;
			temp_cell->m_status = ROUTE;
			path.Push(temp_cell);
		}
	} while (!path.IsEmpty());
	return false;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值