相对于一般序列结构,栈和队列的操作仅限于逻辑上特定的某端,二者元素接受操作的次序不同,简而言之,栈是先进后出,而队列为先进先出。在信息处理领域,栈和队列的身影随处可见,不少程序设计语言即建立在栈结构之上,我们日常生活中对自然资源和社会资源分配、调度银行和医院服务窗口可借助队列实现合理和优化的分配。
栈 栈是存放数据对象的一种特殊容器,其中的数据元素按线性的逻辑次序排列,但是插入操作仅限于栈的某一特定端,只能在栈顶插入和删除。栈中元素接受操作的次序必然遵循所谓的后进先出规律:从栈的整个生命周期来看,更晚出栈的元素应为更早入栈者。
栈和递归
调用栈的基本单位是帧,每次函数调用时都会相应地创建一帧,记录该函数在二进制程序中的返回地址,以及局部变量、传入参数等,并将该帧压入调用栈。在任一时刻,调用栈中的各帧依次对应于那些尚未返回的调用的实例,即当时的活跃函数实例
栈的应用 逆序输出 输出次序和处理过程颠倒,递归深度和输出长度不易预知
进制转换
给定任意10进制非负整数,将其转换为d进制表示形式
迭代模式
1 2 3 4 5 6 7 8 void convert ( Stack<char >& S, __int64 n, int base ) { static char digit[] = { '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , 'A' , 'B' , 'C' , 'D' , 'E' , 'F' }; while ( n > 0 ) { int remainder = ( int ) ( n % base ); S.push ( digit[remainder] ); n /= base; } }
递归模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void convert ( Stack<char >& S, __int64 n, int base ) { static char digit[] = { '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , 'A' , 'B' , 'C' , 'D' , 'E' , 'F' }; if ( 0 < n ) { S.push ( digit[n % base] ); convert ( S, n / base, base ); } } int main ( int argc, char * argv[] ) { Stack<char > S; convert ( S, n, base ); while ( !S.empty() ) printf ( "%c" , ( S.pop() ) ); } return 0 ; }
递归嵌套 具有自相似性的问题可递归描述,但分支位置和嵌套深度不固定
括号匹配
检查表达式括号是否匹配
递归实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void trim ( const char exp [], int & lo, int & hi ) { while ( ( lo <= hi ) && ( exp [lo] != '(' ) && ( exp [lo] != ')' ) ) lo++; while ( ( lo <= hi ) && ( exp [hi] != '(' ) && ( exp [hi] != ')' ) ) hi--; } int divide ( const char exp [], int lo, int hi ) { int mi = lo; int crc = 1 ; while ( ( 0 < crc ) && ( ++mi < hi ) ) { if ( exp [mi] == ')' ) crc--; if ( exp [mi] == '(' ) crc++; } return mi; } bool paren ( const char exp [], int lo, int hi ) { trim ( exp , lo, hi ); if ( lo > hi ) return true ; if ( exp [lo] != '(' ) return false ; if ( exp [hi] != ')' ) return false ; int mi = divide ( exp , lo, hi ); if ( mi > hi ) return false ; return paren ( exp , lo + 1 , mi - 1 ) && paren ( exp , mi + 1 , hi ); }
迭代实现
1 2 3 4 5 6 7 8 9 10 11 12 bool paren ( const char exp [], int lo, int hi ) { Stack<char > S; for ( int i = lo; i <= hi; i++ ) { switch ( exp [i] ) { case '(' : case '[' : case '{' : S.push ( exp [i] ); break ; case ')' : if ( ( S.empty() ) || ( '(' != S.pop() ) ) return false ; break ; case ']' : if ( ( S.empty() ) || ( '[' != S.pop() ) ) return false ; break ; case '}' : if ( ( S.empty() ) || ( '{' != S.pop() ) ) return false ; break ; default : break ; } return S.empty(); }
栈混洗
考察栈$A=<a_1,a_2,…,a_n]$,$B$为空栈,A左端为栈顶
只允许将A的顶元素弹出并压入S,或将S的顶元素弹出并压入B
经过上述一系列操作后,A中元素全部转入B中,
$B=[a_{k1},a_{k2},…,a_{kn} >$,B右端为栈顶
同一输入序列,可有多种栈混洗
$[1,2,3,4>,[4,3,2,1>,[3,2,4,1>$
长度为$n$的栈混洗,可能的栈混洗个数 $$ SP(n)=\frac{(2n)!}{n!(n+1)!} $$ 设s在$k$次pop之后首次重新变空,则$k$无非$n$种情况
$SP(n)=\sum_{k=1}^{n}SP(k-1)SP(n-k)=catalan(n)$
设B为A的任意排列,则B为A的一个栈混洗,当且仅当对于任意的1<=i<j<k<=n,不存在{…,k,…,i,…,j…}
先证明仅当,首先,对于输入序列中的任意三个元素,其在输出序列中是否存在一个可行的相对排列次序与其它元素无关。不妨只关注这三个元素${i,j,k}$
无论如何,元素$i,j$必然先于$k$(弹出A并随即压入B中)压入中转栈S,若输出序列${k,i,j}$存在,则意味着这三个元素中,k必然首先从栈S中弹出,并且根据先进后出的规律,此时$i,j$存在于栈S中,且顺序只能为$j,i$,三者的次序必然是${k,j,i}$则$k$率先从栈S弹出,则三者压入输出栈B的次序必然是${k,j,i}$,而不是${k,i,j}$。
再证明当,对于任何不含禁形的输出序列,都可给出对应的栈混洗过程。
对任意1<=i<j<=n,B中都不含模式{…,j+1,…,i,…,j…},则B必定为A的一个栈混洗
将${j+1,i,j}$视作新一类的禁形,称为615禁形,${k,i,j}$称为915禁形。
接下来证明,只要B中含有915禁形,必然也含有615禁形,当然,两者中的$i,j$未必一致
假定对于任何的$k-i<d$,以上命题均成立,接下来考虑$k-i=d$的情况
不妨设$i<j<k-1$,于是元素$k-1$在B中的相对于$i$的位置无非两种可能
k-1位于i的左方(前方) 此时${k-1,i,j}$即915禁形,$k-1-i<d$,所以必然含有615禁形
k-1位于i的右侧 ${k,i,k-1}$即构成一个615禁形
若对任意1<j<k<=n,B中都不含模式{…,k,…,j-1,…,j…},则B未必为A的栈混洗
945特征${k,j-1,j}$不称作禁形,915禁形未必含有945模式。
例如,B={2,4,1,3}
其中{3,1,2},{4,1,2},{4,2,3},不含有任何945模式,但是却含有915模式{4,2,3},同时也是615模式。
延迟缓冲 在一些应用问题中,输入可分解为多个单元并通过迭代依次扫描处理,但过程中的各步计算往往滞后于扫描的进度,需要待到必要信息已完整到一定程度时才能做出判断。在这类场合,栈结构可以扮演缓冲区的角色。
表达式求值
自左向右扫描表达式,用栈记录已经扫描的部分(含执行计算的结果)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #define N_OPTR 9 typedef enum { ADD, SUB, MUL, DIV, POW, FAC, L_P, R_P, EOE } Operator; const char pri[N_OPTR][N_OPTR] = { '>' , '>' , '<' , '<' , '<' , '<' , '<' , '>' , '>' , '>' , '>' , '<' , '<' , '<' , '<' , '<' , '>' , '>' , '>' , '>' , '>' , '>' , '<' , '<' , '<' , '>' , '>' , '>' , '>' , '>' , '>' , '<' , '<' , '<' , '>' , '>' , '>' , '>' , '>' , '>' , '>' , '<' , '<' , '>' , '>' , '>' , '>' , '>' , '>' , '>' , '>' , ' ' , '>' , '>' , '<' , '<' , '<' , '<' , '<' , '<' , '<' , '=' , ' ' , ' ' , ' ' , ' ' , ' ' , ' ' , ' ' , ' ' , ' ' , ' ' , '<' , '<' , '<' , '<' , '<' , '<' , '<' , ' ' , '=' }; float evaluate ( char * S, char *& RPN ) { Stack<float > opnd; Stack<char > optr; char * expr = S; optr.push ( '\0' ); while ( !optr.empty() ) { if ( isdigit ( *S ) ) { readNumber ( S, opnd ); append ( RPN, opnd.top() ); } else switch ( orderBetween ( optr.top(), *S ) ) { case '<' : optr.push ( *S ); S++; break ; case '=' : optr.pop(); S++; break ; case '>' : { char op = optr.pop(); append ( RPN, op ); if ( '!' == op ) { float pOpnd = opnd.pop(); opnd.push ( calcu ( op, pOpnd ) ); } else { float pOpnd2 = opnd.pop(), pOpnd1 = opnd.pop(); opnd.push ( calcu ( pOpnd1, op, pOpnd2 ) ); } break ; } default : exit ( -1 ); } } return opnd.pop(); }
试改进evaluate()算法,检查表达式的语法是否正确
确认每个操作符与其所对应操作数之间的相对位置符合中缀表达式的语法,在每个操作数入栈时,操作数的规模应该刚好比操作符栈的规模大一。注意,这里的操作符不包括括号和头尾标识符。
逆波兰表达式
逆波兰表达式在由运算符和操作数组成的表达式中,不使用括号即可表达带优先级的运算关系。
RPN转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void append ( char *& rpn, float opnd ) { int n = strlen ( rpn ); char buf[64 ]; if ( opnd != ( float ) ( int ) opnd ) sprintf ( buf, "%.2f \0" , opnd ); else sprintf ( buf, "%d \0" , ( int ) opnd ); rpn = ( char * ) realloc ( rpn, sizeof ( char ) * ( n + strlen ( buf ) + 1 ) ); strcat ( rpn, buf ); } void append ( char *& rpn, char optr ) { int n = strlen ( rpn ); rpn = ( char * ) realloc ( rpn, sizeof ( char ) * ( n + 3 ) ); sprintf ( rpn + n, "%c " , optr ); rpn[n + 2 ] = '\0' ; }
RPN表达式无需括号即可确定运算优先级,是否意味着所占空间必少于常规表达式
未必,尽管RPN表达式可省去括号,但是必须在相邻的操作数、操作符之间插入特定的分隔符(通常为空格)。这种分隔符必须事先约定,且不能用以表示操作符和操作数,故称为元字符。引入元字符的数目与操作数和操作符的数目相当,故所占空间未必少于原表达式。
既然evaluate()算法已经可以求值,同时完成RPN转换有何意义
同样长度(指同样多操作数)的 RPN 比中缀表达式算得快。
若每个操作数都是一个具体的数,把中缀表达式转成 RPN 的过程已经足以得到中缀表达式的值,这种情况下转成 RPN 再计算得不偿失。
但是,操作数可能是一个未知数。例如中缀表达式 (a + b) * c
分别代入 n 组具体数字计算表达式值,例如代入 (a=1,b=2,c=3)
、 (a=4,b=5,c=6)
、 (a=10,b=11,c=12)
求值。不转就要算 n 次中缀表达式,转就只要转 1 次 + 算 n 次 RPN 。
逆波兰表达式的优点
当有操作符时就计算,因此,表达式并不是从右至左整体计算而是每次由中心向外计算一部分,这样在复杂运算中就很少导致操作符错误。
堆栈自动记录中间结果,这就是为什么逆波兰计算器能容易对任意复杂的表达式求值。与普通科学计算器不同,它对表达式的复杂性没有限制。
逆波兰表达式中不需要括号,用户只需按照表达式顺序求值,让堆栈自动记录中间结果;同样的,也不需要指定操作符的优先级。
机器状态永远是一个堆栈状态,堆栈里是需要运算的操作数,栈内不会有操作符。
试探回溯法 根据候选解的某些局部特征,即可判断其是否合理,以候选解的子集为单位批量地删除称为剪枝。
试探回溯模式
从0开始,逐渐增加候选解长度,一旦发现注定要失败,则收缩至前以长度,并继续试探。
N皇后问题
在n*n的棋盘上放置n个皇后,使得她们互不攻击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void placeQueens ( int N ) { Stack<Queen> solu; Queen q ( 0 , 0 ) ; do { if ( N <= solu.size() || N <= q.y ) { q = solu.pop(); q.y++; } else { while ( ( q.y < N ) && ( 0 <= solu.find ( q ) ) ) { q.y++; nCheck++; } if ( N > q.y ) { solu.push ( q ); if ( N <= solu.size() ) nSolu++; q.x++; q.y = 0 ; } } } while ( ( 0 < q.x ) || ( q.y < N ) ); }
迷宫寻径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bool labyrinth ( Cell Laby[LABY_MAX][LABY_MAX], Cell* s, Cell* t ) { if ( ( AVAILABLE != s->status ) || ( AVAILABLE != t->status ) ) return false ; Stack<Cell*> path; s->incoming = UNKNOWN; s->status = ROUTE; path.push ( s ); do { displayLaby(); getchar(); Cell* c = path.top(); if ( c == t ) return true ; while ( NO_WAY > ( c->outgoing = nextESWN ( c->outgoing ) ) ) if ( AVAILABLE == neighbor ( c )->status ) break ; if ( NO_WAY <= c->outgoing ) { c->status = BACKTRACKED; c = path.pop(); } else { path.push ( c = advance ( c ) ); c->outgoing = UNKNOWN; c->status = ROUTE; } } while ( !path.empty() ); return false ; }
队列 与栈一样,队列也是存放数据对象的一种容器,其中的数据对象也按线性逻辑次序排列。同样只允许在队列两端进行操作,若插入对象只能从其中某一端,而删除只能从另外一端,将允许取出元素的一头称为队首,另外一端称为队尾。队列中各对象的操作次序遵循先进先出的规律:更早出队的元素应为更早入队者。
队列的应用 循环分配器 一群客户共享同一资源时,按照先来后到的顺序分配资源,例如多个应用程序共享cpu,实验室成员共享打印机
1 2 3 4 5 6 7 8 9 RoundRobin{ Queue Q (clients) ; while (!ServiceClosed()){ e= Q.enqueue(); serve(e); Q.enqueue(e); } }
银行服务模拟 以银行这一典型场景为例,利用队列结构实现队顾客服务的调度和优化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void simulate ( int nWin, int servTime ) { Queue<Customer>* windows = new Queue<Customer>[nWin]; for ( int now = 0 ; now < servTime; now++ ) { if ( rand() % ( 1 + nWin ) ) { Customer c ; c.time = 1 + rand() % 98 ; c.window = bestWindow ( windows, nWin ); windows[c.window].enqueue ( c ); } for ( int i = 0 ; i < nWin; i++ ) if ( !windows[i].empty() ) if ( -- windows[i].front().time <= 0 ) windows[i].dequeue(); } delete [] windows; } int bestWindow ( Queue<Customer> windows[], int nWin ) { int minSize = windows[0 ].size(), optiWin = 0 ; for ( int i = 1 ; i < nWin; i++ ) if ( minSize > windows[i].size() ) { minSize = windows[i].size(); optiWin = i; } return optiWin; }