数据结构——栈

书本知识整理。
把线性表的插入和删除操作放在同一端进行就能得到栈数据结构。我们可以从相应线性表类派生出栈类,也可以直接创建基于数组和链表的栈类。直接派生的优点是减少编码量,提高程序可靠性,缺点是运行效率低。

8.1 定义和应用

:是特殊线性表,插入和删除只能在表的同一端进行,允许插入和删除的这一端栈顶,另一端叫栈底,是一个后进先出的数据结构。

8.2 抽象数据类型

栈的抽象数据类型给出了栈的最基础常用操作.

C++ 抽象类栈

template < class T>
class stack
{
    public:
    virtual ~stack () {}
    virtual bool empty () const = 0;
    //返回true,当且仅当栈为空
    virtual int size () const = 0;
    //返回栈中元素个数
    virtual T & top() = 0;
    //返回栈顶元素的引用
    virtual void pop() = 0;
    //删除栈顶元素
    virtual void push( const T & theElement) = 0;
    //将元素theElement压入栈顶
}

8.3 数组描述

把数组线性表的右端定义为栈顶,那么入栈和出栈就是线性表在最好情况下的插入和删除,两个操作的时间都为O(1)。

8.3.1 作为一个派生类实现

从类arrayList和stack派生类derivedArrayStack,其成员函数通过调用基类成员函数实现。

一个从类arrayList 派生的数组栈类

template <class T>
class derivedArrayStack: private arrayList<T>,public stack <T>
{
    public:
    derivedArrayStack(int initialCapacity = 10)
         :arrayList< T > (initialCapacity) {}
    bool empty () const
       { return arrayList <T>::empty(); }
    int size () const
         { return  arrayList <T> ::size(); }
    T & top()
    {//返回栈顶元素
        if (arrayList <T>::empty())
           throw stackEmpty();
        return get(arrayList <T>::size()-1 );
    }
    void & pop()
    {//删除栈顶元素
        if (allayList <T>::empty())
           throw stackEmpty();
        erase(arrayList <T>:: size()-1);
    }
    void push(const T & theElement)
    {//插入
        insert(allayList <T>::size(),theElement)
    }
}

时间复杂度:构造函数是O(initialCapacity),插入操作在数组长度不增加时为Θ (1),在增加时为O(stack size)。其他复杂度都是为Θ(1)。

因为get和erase遇到空栈都会抛出异常,所以在top和pop中可以删除对空栈的检查。可以用try—catch结构代替对空栈的检查。

8.3.2 类arrayStack

为了减少不必要的下标检查和往回复制,我们可以定制数组栈。每种操作的时间复杂度和derivedArrayStack的相应操作一样。

类arrayStack:

template<class T>
class arrayStackpublic  stack<T>
{
    public:
       arrayStack(int initialCapacity = 10);
       ~ arrayStack() { delete [] stack; }
       bool empty() const { return stackTop == -1; }
       int size() const { return stackTop+1; }
       T & top();
       void pop();
       void push(const T& theElement);
    private:
       int stackTop;  //当前栈顶的索引值
       int arrayLength;  //栈容量
       T * stack;     //元素数组
};

构造函数:

template < class T >
arrayStack<T> :: arrayStack(int initialCapacity = 10)
{ // 构造函数
    if (initialCapacity < 1) //输出错误信息,抛出异常
    {
        ostringstream s;
        s<<"initialCapacity ="<< initialCapacity <<"Must be >0";
        throw illegalParameterValue(s.str());
    }
    arrayLength = initialCapacity;//初始化
    stack = new T[arrayLength];
    stackTop = -1;//!不是从0开始
}

得到栈顶元素:

template < class T >
T & arrayStack<T> :: top ()
    {
	if (stackTop == -1)//当栈为空时
            throw StackEmpty();
	return stack[ stackTop ];
    }

删除栈顶元素:

template<class T>
void arrayStack<T> :: pop()
{
   if (stackTop == -1)
      throw StackEmpty();
   stack [stackTop--].~T();
   // !调用T的析构函数,而不是栈的析构函数,然后stackTop-1
}

在栈顶插入:

template<class T>
void arrayStack<T>:: push(const T & theElement)
 {
   //如果栈已满,则容量加倍.
   if (stackTop == arrayLength-1)
   {
        changeLength1D(stack, arrayLength, 2*arrayLength);
        arrayLength*=2;
   }
     //在栈顶插入元素
     stack[++stackTop] = theElement;
}

8.4 链表描述

用链表描述,如果链表右端作为栈顶,每次top、pop、push操作都需要从左往右找到最后一个节点,时间复杂度为O(size()),如果把链表左端作为栈顶,则每次top、pop、push操作的时间复杂度为Θ(1),所以我们选择链表左端作为栈顶。

8.4.1 类derivedLinkyStack

①将类derivedArrayStack中的private arrayList替换为private chain,②用名称derivedLinkyStack替代derivedArrayStack,③insert和erase的调用中索引实参改为0 。

8.4.2 类linkStack

为提高性能,我们可以定制链表栈。

定制链表栈:

template<class T>
class LinkedStackpublic  stack<T>
{
    public:
        LinkedStack(int initialCapacity = 10)
            {stackTop=NULL; stackSize=0}
        ~ LinkedStack();
        bool empty() const { return stackSize == 0; }
        int size() const { return stackSize; }
        T & top();
        void pop();
        void push(const T & theElement);
    private:
        chainNode<T>* stackTop;  //栈顶指针
        int  stackSize;     //栈中元素个数
};

析构函数:

template <class T>
LinkedStack<T>::~LinkedStack()
{//删除栈中所有节点
   while(stackTop!=NULL)
   {//删除栈顶节点
      chainNode <T> * nextNode = stackTop->next;
      delete stackNode;
      stackNode = nextNode;
   }
}

时间复杂度:O(sackSize)

得到栈顶元素:

template <class T>
T & LinkedStack<T> ::top()
{
    if(stackSize == 0)
      throw  stackEmpty();
    return stackTop->Element;
}

时间复杂度:Θ(1)

删除栈顶节点:

template <class T>
void LinkedStack<T> ::pop()
{
    if(stackSize == 0)
      throw  stackEmpty();
    chainNode <T> * nextNode = stackTop->next;
    delete stackTop;
    stackTop = nextNode;
    stackSize--;
}

时间复杂度:Θ(1)

在栈顶插入:

template <class T>
void LinkedStack<T> ::push(const T & theElement)
{
    stackTop = new chainNode (theElement,stackTop);
    stackSize++;
}

时间复杂度:Θ(1)

8.5应用

某些应用满足LIFO方式,可以用栈数据结构解决。

8.5.1 括号匹配

目的:输入一个字符串,输出匹配的括号和不匹配的括号
思路:从左到右扫描字符串,扫描到左括号保存到栈中,每当扫描一个右括号,就将它与栈顶的左括号(如果存在)相匹配,并将匹配到的左括号从栈顶删除。

void PrintMatchedPairs (string expr)
{// 括号匹配
    arrayStack<int> s;
    int length = (int) expr.size(); //也可以用strlen(expr)
    // 扫描表达式expr ,寻找( 、)
    for ( int i = 0; i<length; i++)
    {
        if ( expr.at(i) == ' ( ' )  //at是STL中的函数
            s.push(i);  //保存的是索引值
        else if (expr.at(i) ==' ) ' )
            try
            { //从栈中删除匹配的左括号,输出匹配括号的索引值
              cout << s.top() <<'  ' << i << endl;
              s.pop();
            }
            catch (stackEmpty)
            { //栈空,没有匹配的左括号
              cout<<"No match for right parenthesis"<<" at "<<i<<endl;
            }
}
    //右括号匹配结束后,栈不为空,栈中所剩下的左括号都是未匹配的
    while( !s.empty() )
   {
       cout << "No match for left parenthesis at " << s.top() < endl;
       s.pop();
   }
}

时间复杂度:O(n),n为字符串长度

8.5.2 汉诺塔

目的:假设有n个碟子和三座塔。初始时所有碟子从小到大堆在塔1上,我们要把碟子都移动到塔2,每次移动一个,而且任何时候都不能把大碟子压在小碟子上。
思路:利用递归,先把塔1上面n-1个碟子移到塔3,再把最大的碟子从塔1移到塔2,最后把n-1个碟子从塔3移到塔2。

第一种实现方法:输出碟子从塔1到塔2的移动次序

void towersOfHanoi ( int n, int x, int y, int z )
{//把n 个碟子从塔x 移动到塔y,可借助于塔z
    if (n > 0)
    {
        towersOfHanoi(n-1, x,z,y);  //把塔x上面n-1个碟子移到塔z
        cout << "Move top disk from tower " << x
            <<" to top of  tower " << y << endl;
        //把最大的碟子从塔x移到塔y
        towersOfHanoi(n-l, z, y, x);
        //把n-1个碟子从塔z移到塔y
    }
}

时间复杂度:Θ(2n)

第二中实现方法:要显示每次移动后三座塔的布局,所以要在内存中保存这些布局。因为碟子的移动是按照LIFO方式进行的,所以可以将每座塔表示为一个栈,因为数组栈运行速度比链表栈快,所以我们采用数组栈。

//全局变量,tower[1:3]表示三个塔
arrayStack<int> tower[4];
void moveAndShow ( int n, int x, int y, int z);//函数声明
void towersOfHanoi(int n)
{ // 函数moveAndShow的预处理程序
    for (int d = n; d > 0; d--) // 初始化
    tower[1].push(d); // 把碟子d从大到小放到塔1上
   //把塔1上的n个碟子移动到塔2上,借助于塔3的帮助
    moveAndShow(n, 1, 2, 3);
}
void moveAndShow(int n, int x, int y, int z)
{ // 把n 个碟子从塔x 移动到塔y,可借助于塔z
    if (n > 0)
    {
        moveAndShow(n-l, x, z, y);//把塔x上面n-1个碟子移到塔z
        int d= tower[x].top(); // 碟子编号
        tower[x].pop(); //从x中移走一个碟子
        tower[y].push(d); //把这个碟子放到y 上
        showState(); //显示塔的布局
        moveAndShow(n-l, z, y, x);
        }//把n-1个碟子从塔z移到塔y
}

时间复杂度:Θ(2n)

8.5.3 列车车厢重排

目的:借助缓冲轨道,将入轨道上非有序车厢,按照车厢号递增方式移到出轨道。
思路:从前至后检查入轨道上的车厢,如果正在检查的车厢是满足排列要求的下一节车厢,直接把它移到出轨道(此时缓冲轨道上可能也有满足条件的车厢,将其移到出轨道),否则将它移到缓冲轨道。
对缓冲轨道选择的基本要求是:编号为u的车厢应该直接进入的缓冲轨道其顶部车厢是大于u的最小者,即最小上限。因为缓冲轨道符合LIFO方式,所以可以数组栈表示缓冲轨道。

函数railroad:

//全局变量
arrayStack<int> * track;  //缓冲轨道数组,数组类型是数组栈
int numberOfCars;         //车箱数
int numberOfTracks;       //缓冲轨道数
int smallestCar;          //在缓冲轨道中编号最小的车厢
int itsTrack;             //停靠最小编号车厢的缓冲轨道

bool railRoad ( int inputOrder[], int theNumberOfCars, int theNumberOfTracks )
{//从初始顺序开始重排车厢

    numberOfCars = theNumberOfCars;
    numberOfTracks = theNumberOfTracks;
    //创建用于缓冲轨道的栈
    track = new arrayStack <int> [numberOfTrack+1];
    int nextCarToOutput=1;      //当前能进入出轨道的车厢
    smallestCar = numberOfCars +1;  //初始化,取一个比最大车厢号大的值

    //重排车厢
    for( int i =1; i<=numberOfCars; i++)
    {
       if( inputOrder[i] == nextCarToOutput)
       {//满足排列要求的下一节车厢,直接移到出轨道
            cout<<"Move car"<<inputOrder[i]
               << "from input track to output track"<<endl;
            nextCarToOutput++;

            while(smallestCar == nextCarToOutput)
            {//缓冲轨道上满足出轨条件的车厢,将其移到出轨道
                outputFromHoldingTrack();
                nextCarToOutput++;
            }
       }
        else
        //将车厢inputOrder[i]移到一个缓冲轨道
            if(!putInHoldingTrack(inputOrder[i]))
               return false;
    }
    return true;
}

时间复杂度:O(numberOfTracks*numberOfCars)

函数outputFromHoldingTrack:

void outputFromHoldingTrack()
{//把编号最小的车厢从缓冲轨道移到出轨道

    track[itsTrack].pop(); //删除编号最小车厢
    cout <<"Move car"<<smallestCar<<"from holding track"
         <<itsTrack <<"to output track"<<endl;

    //检查所有栈的栈顶,寻找编号最小的车厢和它所属的栈
    smallestCar = numberOfCar + 2;
    //因为不能确定编号最小的车厢编号,所以取一个较大值,方便寻找
    for( int i=1; i< = numberOfTracks; i++)
       if( !track[i].empty() && ( track[i].top() < smallestCar) )
       {
           smallestCar = track[i].top();
           itsTrack = i;
       }
}

时间复杂度:O(numberOfTracks)

函数putInHoldingTrack:

bool putInHoldingTrack(int c)
{//将车厢c移到一个缓冲轨道。返回false,当且仅当没有可用的缓冲轨道

    //为车厢c寻找最合适的缓冲轨道
    //初始化
    int bestTrack = 0;        //最适合的轨道
    bestTop = numberOfCars + 1;
    //因为不能确定编号最适合的轨道,所以最适合轨道的顶部车厢号取一个较大值,方便寻找

    //扫描缓冲轨道
    for (int i =1; i <=numberOfTracks; i++)
       if(!track[i].empty())
       {//缓冲轨道不为空
           int topCar = track[i].top();
           if( c<topCar && topCar < bestCar)
           {//缓冲轨道i具有编号更小的车厢,同时满足比c编号大,且又是比c编号大的车厢号中最小的
               bestCar = topCar;
               bestTrack = i;
           }
       }
       //缓冲轨道为空,必须先考虑有车厢的轨道,因为空轨道要留给比所有轨道顶部车厢号都大的车厢
        else if(bestTrack = 0) bestTrack = i;

    //没有满足条件的缓冲轨道
    if(bestTrack == 0)   return false;

    //把车厢c移到轨道bestTrack
    track[bestTrack].push(c);
    cout << "Move car " <<c<< "from input track "
         << "to holding track "<< bestTrack <<endl;

    //如果需要更新smallestCar 和itsTrack
    if( c < smallestCar )
    {
        smallestCar = c;
        itsTrack = bestTrack;
    }
    return true;
}

时间复杂度:O(numberOfTracks)

8.5.6 迷宫老鼠

目的:寻找一条从入口到出口的路径。
思路:a.用 n×m 的矩阵来描述迷宫,在位置(i,j)处有一个障碍物时其值为1,否则其值为0。
   b.为了避免处理内部位置和边界位置时存在差别,在迷宫的周围增加一圈障碍物。
   c.如果当前位置不是迷宫出口,则在当前位置上放置障碍物,以便阻止搜索过程又绕
    回到这个位置。
   d.用系统的方式来确定从当前位置要向哪一个相邻位置移动。
   e.检查相邻的位置中是否有空闲的,如果有就移动到这个新的相邻位置上,然后从这
    个位置开始搜索通往出口的路径。如果不成功,选择另一个相邻的空闲位置,并从
    它开始搜索通往出口的路径。如果相邻的位置中没有空闲的,则回退到上一位置。
   f.为了方便移动,在进入新的相邻位置之前,把当前位置保存在一个堆栈中。在堆栈
    中始终包含从入口到当前位置的路径。
   g.如果所有相邻的空闲位置都已经被探索过,并且未能找到路径,则表明在迷宫中不
    存在从入口到出口的路径。

bool findPath()
{//寻找一条从入口(1,1)到出口(n,n)的路径
 //如果找到,返回true,否则返回false

    //动态建立栈用来保存路径,position类中有数据成员col,row
    path = new arrayStack <position>;

    //初始化偏移量
    position offset[4];
    offset[0].row = 0;  offset[0].col = 1;  //向右
    offset[1].row = 1;  offset[1].col = 0;  //向下
    offset[2].row = 0;  offset[1].col = -1; //向左
    offset[1].row = -1;  offset[1].col = 0; //向上

    //初始化迷宫外围的障碍墙
    for( int i = 0; i<= size +1; i++)
    {
        maze[0][[i]] = maze[size+1][i] = 1; //底部和顶部
        maze[i][0] = maze[i][sie+1] = 1;    //左和右
    }


    position here;  //当前位置
    here.row = 1;   //在起点
    here.col = 1;
    maze [1][1] = 1;  //在起点放上障碍物,防止再次回到起点
    int option = 0;   //下一步要往哪个方向走,0~3分别对应右下左上
    int lastOption = 3;

    //寻找一条路径
    while( here.row != size || here.col != size)
    {//还没有到达出口
        int r,c;  //临时row,col
        while(option <= lastOption)
        {//查看往哪个方向走没有障碍物
            r = here.row + offset[option].row;
            c = here.col + offset[option].col;
            if( maze[r][c] == 0 ) break;
            //有相邻位置没有障碍物,就到这个位置,跳出循环
            option++;
        }

        if( option <= lastOption)
        {//有相邻位置可走
            path->push(here);//把当前位置放入栈
            here.row = r;    //移到可行的相邻位置
            here.col = c;
            maze[r][c] = 1;  //放上障碍物,防止重复访问
            option = 0;   //!为寻找下一个位置准备
        }
        else
        {//如果没有相邻的一步可走,返回上一步
            if(path->empty())
               return false;
               //链表为空,无路可返,即不存在这样一条路径,结束循环
            position next = path->top();
            path->pop();
            //返回到上一步,要把上一步从栈中删了,直到确定它有可行的下一步再放入栈

            if(next.row == here.row)
              option = 2 + next.col - here.col;
              //上一步和当前位置行数相同,返回到上一步后可能要向下走或者向上走
            else option = 3 + next.row - here.row;
            //上一步和当前位置列数相同,返回到上一步后只能要向左走,
            //否则只能返回到上上步,如果存在上上步的话
            //因为按照移动规则,第一次进入下一步,一定是先考虑向右走。
            here = next;
        }
    }
    return true;
}

时间复杂度:O(size2) = O(m2)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值