在说什么是线性结构之前,我想还是先提前学习具体的结构,然后再一一总结为好。 栈(stack)应该是线性结构中最容易的一种数据结构了,这种结构的主要操作有两种:将一个新的值添加到栈称为推送(push)该值; 删除堆栈中的最顶层的值称为弹出(pop)栈。以后我们就直接用英文来表示了。在stack中这种的顺序处理方式我们通常称为LIFO,意思是后进先出

栈的模型

我们用下面的模型来模拟栈的这个结构: 假设你在一个自助餐厅,客人在自助餐线上开始时拿起自己的盘子,那些盘子就放在弹簧柱上,这样子是为了方便顾客从自助餐线上拿起顶部的盘子,就像下图的模式那样:

数据结构——线性结构(1)——顺序栈结构_main函数

当放碗机添加新的盘子时,它会放在堆叠的顶部,推动弹簧的压缩,其他的略微下降,如图所示:

数据结构——线性结构(1)——顺序栈结构_#include_02

对于顾客来说,他们只能从stack的顶部取走盘子,一旦当他们这么做之后,其余的盘子就会弹回原来的位置上去。因此,最后加进来的盘子就是第一个被取走的盘子。 stack在编程中如此重要还有一个小原因,那就是嵌套函数(nested function)的调用形式就是以这种方式进行的。举个例子,如果main函数中我们调用f函数,那么一个f函数的栈框架就会放在main函数栈框架的顶部,就像这样:

数据结构——线性结构(1)——顺序栈结构_数据结构_03

如果f函数中还调用了g函数,那么就会在f的栈框架(stack frame)的上方为g函数开辟一个全新的栈框架。就像这样:

数据结构——线性结构(1)——顺序栈结构_#include_04

当g函数返回的时候,它的栈结构就会被pop回f函数中,同样的f函数也会pop会main函数中,最终返回初始的模式。 所以总结起来就是:

调用顺序(入栈):
main -> f()->g();
返回顺序(出栈):
g() -> f() ->main;

栈的基本操作

一般对栈的操作比较少,常见的有(这里的方法名是笔者自己所定,是在vs2015中,STL自带):

数据结构——线性结构(1)——顺序栈结构_#include_05

我们也知道我们可以用上面的方法写一些常用的。很好用的函数。下面的是我自己写的一些好用的函数:

/*移除并返回栈顶的元素*/
int popAndReturn(stack<int> & operandStack) {
    int n = operandStack.top();
    operandStack.pop();
    return n;
    }
/*清空栈顶的元素*/
void clear(stack<int> & operandStack) {
    while(!operandStack.empty()) {
    operandStack.pop();
    }
}

栈的实现

这篇文章的内容可能比较多(主要是代码)。我个人认为这些可实现的数据结构,一定要结合实际的代码来学习是最佳的。 现在明确一点,我们的任务,是实现上述所说的数据结构,其接口中主要的方法有:

数据结构——线性结构(1)——顺序栈结构_数组_06

在编写这个类之前,我们新建一个空项目,空项目下需要建立下面五个文件(三个头文件,两个Cpp文件):

数据结构——线性结构(1)——顺序栈结构_#include_07

 

error类

这里,error的目的是输出错误信息,类似于下面代码的作用:

cout << "错误信息" << endl;

以后的代码不再贴此部分的详细内容:

  • error.h代码
#ifndef _error_h
#define _error_h
#include <string>
/*
*函数:error
*用法: error("string")
*----------------------
*返回错误信息,并终止程序
*/ 
void error(std::string msg);

#endif
  • error.cpp代码
#include <iostream>
#include <string>
#include <cstdlib> //EXIT_FAILURE
#include "error.h"
using namespace std;
void error(string msg) {
    cerr << msg << endl;
    exit(EXIT_FAILURE);
}

实现方法的选择

我们可以考虑用vector来实现charstack,因为这样的话我们不用考虑怎么删除元素,怎么为元素分配空间。但是更重要的是,依靠Vector类会让我们更难分析CharStack类的性能,因为Vector类隐藏了许多的复杂性(封装了很多细节)。因为我们目前还不知道Vector类是如何实现的,这就导致我们不知道在push和pop方法需要的情况下添加或删除元素涉及多少工作量。 接下来,我们要论的的主要目的是分析数据表示如何影响算法的效率。如果所有工作量都可见,那么这种分析就更容易。所以我们以后要用底层的东西来表示。

使用内置数组类型来存储元素的优点是数组不隐藏具体的细节。从数组中选择一个元素需要几个机器语言指令,在现代计算机上花费多少的时间还有在堆上分配数组存储或在不再需要时回收该存储通常的时间,这些操作都在在恒定时间运行。在典型的内存管理系统中,需要相同的时间来分配一个1000字节的块,就像分配一个10的块一样。 我们知道。对于数组来说,一旦为数组分配了空间,就无法改变它的大小。但是,我们可以做的是分配一些固定的初始大小的数组,然后只要旧的空间用尽,将其替换为全新的数组。在此过程中,你必须将所有元素从旧数组复制到新的数组,然后确保旧数组使用的内容被循环回到堆中。 下面先看看我们需要实现的功能有哪些:

charstack.h代码

/*
 *这个文件定义了charstack类,它用来实现char类型的栈抽象
 */ 
#ifndef _charstack_h
#define _charstack_h

/*
 *这个类模拟char型的栈,它的基本类型类似于
 * stack <char>,我们现在用指定的基本类型去实现
 *这些抽象的操作,然后我们可以利用前面的模板知识
 *将这些转换为一般的模板
 */ 
class CharStack{
    public/*
    * 构造函数: CharStack
    * 用法: CharStack cstk;
    * ----------------------
    * 初始化一个新的空栈,使其能够装下一系列的字符 
    */
    CharStack();
    /*
    * 析构函数: ~CharStack
    * 用法: 常常隐式调用
    * -------------------------
    * 释放这个结构在堆中占用的空间 
    */
    ~CharStack();
    /*
    * 方法: size
    * 用法: int nElems = cstk.size();
    * --------------------------------
    * 返回栈中的字符数量 
    */
    int size();
    /*
    * 方法: getCapacity()
    * 用法: int n = cstk.getCapacity();
    * --------------------------------
    * 返回栈中的容量 
    */
    int getCapacity();
    /*
    * 方法: isEmpty
    * 用法: if (cstk.isEmpty()) . . .
    * --------------------------------
    * 当栈中没有字符元素的时候,返回true 
    */

    bool isEmpty();
    /*
    * 方法: clear
    * 用法: cstk.clear();
    * --------------------
    * 移除栈中所有的元素 
    */
    void clear();
    /*
    * 方法: push
    * 用法: cstk.push(ch);
    * ---------------------
    * 将一个元素压入栈中.
    */
    void push(char ch);
    /*
    * 方法: pop
    * 用法: char ch = cstk.pop();
    * ----------------------------
    * 移除栈顶元素并返回其值.
    */
    char pop();
    /*
    * 方法: peek
    * 用法: char ch = cstk.peek();
    * -----------------------------
    * 返回堆栈上的最上面的值,而不删除它。
    * 在空栈上调用查看会产生错误
    */
    char peek();

    #include "charstackpriv.h"
};