栈(stack)是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈的顶(top),它是后进先出(LIFO)的

4 阅读7分钟

5.1 基本概念 栈(stack)是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈的顶(top),它是后进先出(LIFO)的。对栈的基本操作只有push(进栈)和pop(出栈)两种,前者相当于插入,后者相当于删除最后的元素。

由于栈在本质上是一种受限制的表,所以可以使用任何一种表的形式来实现它,我们最常使用的一般有两种:

链表

数组

它们在复杂度上的优缺点对比如下:

新增和删除元素时的时间复杂度

链表:在动态申请内存(new或者malloc)上的花销非常昂贵。

数组:几乎没有花销,以常数O(1)时间运行,在带有自增和自减寻址功能的寄存器上操作时,编译器会把整数的push和pop操作编译成一条机器指令。

空间复杂度

链表:由于空间是动态申请、释放的,因此不会浪费空间,而且只要物理存储器允许,理论上能够满足最大范围未知的情况。

数组:必须在初始化时指定栈的大小,有可能会浪费空间,也有可能不够空间用。

结论:

如果对运行时的效率要求非常高,并且能够在初始化时预知栈的大小,那么应该首选数组形式;否则就应该选用链表形式。

由于对栈的操作永远都是针对栈顶(top)进行的,因此数组的随机存取的优点就没有了,而且数组必须预先分配空间,空间大小也受到限制,所以一般情况下(对运行时效率的要求不是太高)链表应该是首选。

5.2 代码实现 栈的实现如下:

/// // // FileName : stack.h // Version : 0.10 // Author : Luo Cong // Date : 2005-1-6 11:42:17 // Comment :
// ///

#ifndef STACK_H #define STACK_H

#include "../../slist/src/slist.h"

template class CStack : public CSList { public: int push(T data); int pop(T *data = NULL); int top(T *data) const; };

template inline int CStack::push(T data) { return AddTail(data); }

template inline int CStack::pop(T *data) { if (IsEmpty()) return 0;

if (data)
    top(data);

RemoveTail();
return 1;

}

template inline int CStack::top(T *data) const { ASSERT(data);

if (IsEmpty())
    return 0;

*data = GetTail();
return 1;

}

#endif // STACK_H 调用如下:

/// // // FileName : stack.cpp // Version : 0.10 // Author : Luo Cong // Date : 2005-1-6 11:42:28 // Comment :
// ///

#include #include "stack.h" using namespace std;

static void PrintValue(const int nRetCode, const int nValue) { if (nRetCode) cout << nValue << endl; else cout << "Error occured!" << endl; }

int main() { CStack stack; int nValue; int nRetCode;

#ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif

stack.push(1);
stack.push(2);
stack.push(3);

nRetCode = stack.top(&nValue);
PrintValue(nRetCode, nValue);

nRetCode = stack.pop(&nValue);
PrintValue(nRetCode, nValue);

nRetCode = stack.pop(&nValue);
PrintValue(nRetCode, nValue);

nRetCode = stack.pop(&nValue);
PrintValue(nRetCode, nValue);

}

5.3 说明 上面的代码就是在单链表的基础上实现的栈,您会看到,在C++的继承机制下,栈的实现简单得可怕。 :)

一个影响栈的运行效率的问题是错误检测。我的栈实现中是仔细地检查了错误的——对空栈进行top和pop操作,以及当存储空间不够时进行push操作是会引起异常的,显然,我们不愿意出现这种情况,但是,如果把对这些条件的检测放到代码中,那就很可能要花费像实际栈操作那样多的时间。由于这个原因,除非在错误处理极其重要的场合(例如在操作系统中),一般在栈中省去错误检测就成了普通的惯用手法。

但我认为,一个良好的程序首先应该是健壮的,这比效率还要重要,特别是对于栈这种最基本的数据结构,它很可能会被作为基本的元素而被别的地方大量地使用。所以我并没有因为效率的问题而省去了错误检查机制。

引入错误检查机制的代价是:

对top和pop的操作变得有些繁琐。在代码中我是使用了返回值0或者1来表示成功或者失败,而实际的栈顶元素是通过参数来返回的。这样做必定会有人不满——太麻烦了!但这是我能想到的最好的解决方法,如果你有更好的方法,请告诉我。

运行时效率会降低。如果确实耗费了太多的时间,你可以把错误检查去掉,但前提条件是你能确保整个运行过程中不会出错——其实还是要有错误检查的,只不过这些错误检查会放在外围来做而已。

好了,就说那么多,下面我们来看看栈的应用。

5.4 应用:中缀到后缀表达式的转换 对栈的应用实在是太广泛了(谁让栈是最基本的数据结构元素之一呢?),例如有平衡符号、表达式转换之类的,我们在这里就选择一个比较有实用价值的例子——中缀到后缀表达式的转换。(可以用在编译器等地方)

5.4.1 代码实现 /// // // FileName : postfix.cpp // Version : 0.10 // Author : Luo Cong // Date : 2005-1-6 16:00:54 // Comment :
// ///

// 算法: // 1)检查输入的下一元素。 // 2)假如是个操作数,输出。 // 3)假如是个开括号,将其压栈。 // 4)假如是个运算符,则 // i) 假如栈为空,将此运算符压栈。 // ii) 假如栈顶是开括号,将此运算符压栈。 // iii) 假如此运算符比栈顶运算符优先级高,将此运算符压入栈中。 // iv) 否则栈顶运算符出栈并输出,重复步骤4。 // 5)假如是个闭括号,栈中运算符逐个出栈并输出,直到遇到开括号。开括号出栈并丢弃。 // 6)假如输入还未完毕,跳转到步骤1。 // 7)假如输入完毕,栈中剩余的所有操作符出栈并输出它们。

#include <stdio.h> #include "stack.h"

// 返回操作符的优先级 // +和-的优先级是一样的,和/的优先级也是一样的,但+和-的优先级要比和/的低。 static int GetPRI(const char optr) { switch (optr) { case '+': return 1; case '-': return 1; case '*': return 2; case '/': return 2; default : return 0; } }

// 在这个函数中完成对栈顶的操作符和当前操作符的优先级对比, // 并决定是输出当前的操作符还是对当前的操作符进行入栈处理。 static void ProcessStackPRI( CStack &stack, const char optr, char **szPostfix ) { ASSERT(*szPostfix);

int i;
int nRetCode;
char chStackOptr;
int nCount = stack.GetCount();

for (i = 0; i <= nCount; ++i)
{
    nRetCode = stack.top(&chStackOptr);
    if (
        (0 == nRetCode) ||                  // 栈顶为空,新操作符添加到栈顶
        (GetPRI(chStackOptr) < GetPRI(optr))// 栈顶操作符优先级比当前的要低
    )
    {
        stack.push(optr);
        break;
    }
    else
    {
        // 如果栈顶操作符优先级不低于当前的,则栈顶元素出栈并输出:
        stack.pop();
        *(*szPostfix)++ = chStackOptr;
    }
}

}

static void Infix2Postfix( const char *szInfix, char *szPostfix ) { ASSERT(szPostfix);

char chOptr;
int nRetCode;
CStack<char> stack;

while (*szInfix)
{
    switch (*szInfix)
    {
    // 忽略空格和TAB:
    case ' ':
    case '\t':
        break;

    // 对操作符进行优先级判断,以便决定是入栈还是输出:
    case '+':
    case '-':
    case '*':
    case '/':
        nRetCode = stack.IsEmpty();
        if (!nRetCode)
            ProcessStackPRI(stack, *szInfix, &szPostfix);
        else
            stack.push(*szInfix);   // 当栈为空时,毫无疑问操作符应该入栈
        break;

    // 遇到左括号时,无条件入栈,因为它的优先级是最高的
    case '(':
        stack.push(*szInfix);
        break;

    // 遇到右括号时,逐个把栈中的操作符出栈,直到遇到左括号为止
    case ')':
        do
        {
            nRetCode = stack.pop(&chOptr);
            if (nRetCode && ('(' != chOptr))            // 左括号本身不输出
                *szPostfix++ = chOptr;
        } while (!stack.IsEmpty() && ('(' != chOptr));  // 遇到左括号为止
        break;

    // 其余的情况,直接输出即可
    default:
        *szPostfix++ = *szInfix;
        break;
    }
    ++szInfix;
}
// 如果输入的内容已经分析完毕,那么就把栈中剩余的操作符全部出栈
while (!stack.IsEmpty())
{
    nRetCode = stack.pop(&chOptr);
    *szPostfix++ = chOptr;
}
*szPostfix = '\0';

}

int main() { char szInfix = "a+bc+(d*e+f)*g"; char szPostfix[255];

#ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif

Infix2Postfix(szInfix, szPostfix);

printf("Infix   : %s\n", szInfix);
printf("Postfix : %s\n", szPostfix);

}

5.4.2 说明 源代码里面已经有了详细的注释,我就不再罗嗦了。我只做了+、-、*、/四种操作符的转换,另外,如果括号不匹配,例如有左括号但是没有右括号,或者反过来,程序就可能会运行不正确,但这不是我写这个例子的重点,我写它只是为了掌握栈的用法,如果您有兴趣,可以试着完善它。

下面给出两个例子:

中缀表达式:a + b * c + (d * e + f) * g 后缀表达式:abc*+def+g+ 中缀表达式:2 * (x + y) / (1 - x) 后缀表达式:2xy+*1x-/ ————————————————

                        版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                    

原文链接:blog.csdn.net/qq_24700495…