栈
可以从生活中来举例, 比如 你家里一共有 10个人在吃饭, 吃完饭后 也就说至少有10个碗清洗和整理到碗橱(假设一个人一个碗,大小都一样.) 现在每清洗好后一个碗, 就依次叠在一起.
当下次只有你一个人在家吃饭时,你现在需要取一个碗来吃饭, 按照正常逻辑 你会从那碟碗上 取第一个.(从下面取或者从中间取都是不明智的) 这就是一个生活中 栈的例子.
![](http://hi.csdn.net/attachment/201109/5/0_1315231320lr5Z.gif)
从程序设计角度来谈谈,看下面的一段测试程序.
void testStack(int a,char b,int c) { } int main() { testStack(2, 'b', 1); //研究下函数参数的入栈和出栈顺序 return 0; }
我们objdump -d这段程序,由于长度关系 只取出一部分..
<main>:
400489: ba 01 00 00 00 mov $0x1,%edx 40048e: be 61 00 00 00 mov $0x61,%esi 400493: bf 02 00 00 00 mov $0x2,%edi
<testStack>
400478: 89 7d fc mov %edi,-0x4(%rbp) 40047b: 89 f0 mov %esi,%eax 40047d: 89 55 f4 mov %edx,-0xc(%rbp) 400480: 88 45 f8 mov %al,-0x8(%rbp)
从汇编代码可以看出 ,分别把$0x1,$0x61,$0x2 三个立即数存入不同的寄存器中,然后再通过寄存器传参,可以看出 由于栈是由高到低(栈底->栈顶), 所以入栈的顺序为 1, 'b', 2
说到这,函数参数从右往左入栈好处是 由于出栈是先pop最左边的 也就是离函数名最近的.所以每次取参数就很方便.
深入认识栈
栈可以说是一个残缺的线性表, 因为元素的添加和删除都是在一端进行的.都遵循着 "后进先出" (LIFO) ,跟上文说的碗一样 ,最后的放的碗总是先被使用.
来看下栈的图示结构.
通过图示可以大概看出两种操作 Push 和 Pop 也就是 入栈 和 出栈.Step 1 开始初始化为一个空栈,Step 2 ,向栈中压入一个值 a, Step 3继续压栈 b ,Step 4中进行 Pop b.只剩下 a. 逻辑比较简单.很容易懂.
程序设计时,可以使用Array 和 List 两种数据结构来实现, Array实现的 为 顺序栈 ,它在内存中为连续存放的,List实现的为链式栈,随机存放.
顺序栈:
实现原理: 使用一个数组 vessel 和 大小 size, 每次push前 先判断 vessel的大小是否小于 size ,如果小于则进行入栈操作, 每次Pop前 先检查 vessel的大小是否为 0 如果不为0 则进行Pop操作.
设计类如下:
template <class T> class StackArray { int size; int tos; /**< 索引 */ T* vessel; public: StackArray(int s); void Push(T& elm); T& Pop(); }; /* --- 构造函数 --- */ template <class T> StackArray<T>::StackArray(int s) { size = s; tos = -1; vessel = new T[s]; }
Push操作实现:
template <class T> void StackArray<T>::Push(T& elm) { if (tos == size-1) { return; } /**< 判断栈是否满 */ vessel[tos++] = elm; }
Pop操作实现:
template <class T> T& StackArray<T>::Pop() { if (tos == -1) { return; } /**< 判断栈是否为空 */ return vessel[tos--]; }
链式栈
实现原理:顺序栈是个效率很高的实现方法,但是对内存利用很不灵活,所以可以进一步使用链式来进行内存管理,利用设计链表的方法,先设计出链式栈节点类 ,然后在设计出 链式栈类. 链式栈节点类 , 需要 保存数据 和 指向下一个节点的指针 两个成员变量.
template <class T> class StackLinkNode { T data; StackLinkNode<T>* link; StackLinkNode(T& value):data(value),link(NULL){} };
链式栈设计 和 顺序栈不同,不需要容器来保存 只需要一个 栈顶指针.
template <class T> class StackLink { StackLinkNode<T>* tos; public: StackLink():tos(NULL){} void Push(T& value); T& Pop(); }; Push操作实现如下:
template <class T> void StackLink<T>::Push(T& value) { StackLinkNode<T>* node = new StackLinkNode<T>(value); node->link = tos; /**< 新创建的结点指向栈顶结点 */ tos = node; /**< tos++ 指向新添加的节点 */ } 看下面的图会比较好理解点:
Pop操作如下:
template <class T> void StackLink<T>::Pop() { StackLinkNode<T>* old = tos; tos = tos->link; /**< 向栈底移动 */ T data = old->data; /**< 保存原栈顶元素 */ delete old; return data; }
如图:
实现Pop时需要注意 资源释放问题. 所以需要个临时的来进行保存.
其它知识点:
由于栈操作是常数时间,所以影响一个栈的执行效率是在冗长的错误检测上,比如链式栈, 但是忽略错误检测是不行的, 我们应该尽量把冗长错误检测进行重新整理. 使其尽可能的高效率.