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;
}