数据结构 - 栈

栈一种常见的特殊线性数据结构,其特殊之处在于其操作顺序,下面会详细介绍,也正因为其特性,因此栈可以轻松解决表达式求值、括号匹配、递归算法、回溯算法等等问题。

01、定义

栈的特殊性表现为操作受限,其一只允许在栈的一端进行元素插入和删除运算,其二栈的运算操作遵循后进先出(Last In First Out,简称LIFO)的原则。

入栈:当把元素插入到栈,这一行为叫做入栈,也称进栈或压栈;

出栈:当把元素从栈中移除,这一行为叫做出栈,也称退栈;

栈顶:允许元素进行插入和删除操作的一端称为栈顶;

栈底:与栈顶对应的另一个端称为栈底,并且不允许进行元素操作;

空栈:当栈中没有元素时叫做空栈。

满栈:当栈是有限容量,并且容量已用完,则称为满栈。

栈容量:当栈是有限容量,栈容量表示栈可以容纳的最大元素数量。

栈大小:表示当前栈中的元素数量。

02、分类

栈是逻辑结构,因此以存储方式的不同可以分为顺序栈和链栈。

顺序栈就是使用连续的地址空间存储所有栈元素,通常采用数组实现,因此导致栈的大小是固定的,不易扩扩容,容易浪费空间同时还要注意元素溢出等问题。

链栈顾名思义就是采用链式方式存储,通常采用单向链表实现,因此链栈可以无限扩容,按需使用,内存利用高效,同时也不存在满栈的情况。

03、实现(顺序栈)

我们借助数组来实现顺序栈,其核心思想是把数组的起始位置作为栈底,把数组尾方向当作栈顶。

我们知道数组对插入、删除元素是不友好的,因为涉及到已存在元素移动的问题,但是如果直接在数组尾端插入、删除元素还是很方便的,因为不涉及元素移动问题,我们正是利用这一特点,把数组起始位置做为栈底,而插入、删除方便的数组尾端作为栈顶。

下面我们将一步一步实现一个泛型顺序栈。

1、ADT定义

我们首先来定义顺序栈的ADT。

ADT Stack{

数据对象:D 是一个非空的元素集合,D = {a1, a2, ..., an},其中 ai 表示栈中的第i个元素,n是栈的长度。

数据关系:D中的元素通过它们的索引(位置)进行组织,索引是从0到n-1的整数,并且遵循元素只能在栈顶操作,元素后进先出的原则。

基本操作:[

Init(n) :初始化一个指定容量的空栈。

Capacity:返回栈容量。

Length:返回栈长度。

Top:返回栈顶元素,当为空栈则报异常。

IsEmpty():返回是否为空栈。

IsFull():返回是否为满栈。

Push():入栈即添加元素,当为满栈则报异常。

Pop():出栈即返回栈顶元素并把其从栈中移除,当为空栈则报异常。

]

}

定义好栈ADT,下面我们就可以开始自己实现的栈。

2、初始化Init

初始化结构主要做几件事。

  • 初始化栈的容量;

  • 初始化存放栈元素数组;

  • 初始化栈顶索引;

具体实现代码如下:

//存放栈元素
private T[] _array;
//栈容量
private int _capacity;
//栈顶索引,为-1表示空栈
private int _top;
//初始化栈为指定容量
public MyselfArrayStack<T> Init(int capacity)
{
    //初始化栈容量为capacity
    _capacity = capacity;
    //初始化指定长度数组用于存放栈元素
    _array = new T[_capacity];
    //初始化为空栈
    _top = -1;
    //返回栈
    return this;
}

3、获取栈容量 Capacity

这个比较简单直接把栈容量私有字段返回即可。

//栈容量
public int Capacity
{
    get
    {
        return _capacity;
    }
}

4、获取栈长度 Length

因为栈顶索引表示数组下标,因此可以通过栈顶索引加1转为栈长度,同时因为我们定义了空栈是栈顶索引为-1,因此此时栈长等于[-1+1]为0,所以栈长度即为[栈顶索引+1]。

//栈长度
public int Length
{
    get
    {
        //栈长度等于栈顶元素加1
        return _top + 1;
    }
}

5、获取栈顶元素 Top

获取栈顶元素可以通过栈顶索引私有字段从数组中直接获取,同时要注意判断栈是否为空栈,如果为空栈则报异常。具体代码如下:

//获取栈顶元素
public T Top
{
    get
    {
        if (IsEmpty())
        {
            //空栈,不可以进行获取栈顶元素操作
            throw new InvalidOperationException("空栈");
        }
        return _array[_top];
    }
}

6、获取是否空栈 IsEmpty

是否空栈只需判断栈顶索引是否为小于0即可。

//是否空栈
public bool IsEmpty()
{
    //栈顶索引小于0表示空栈
    return _top < 0;
}

7、获取是否满栈 IsFull

是否满栈只需判断栈顶索引是否与栈容量减1相等,代码如下:

//是否满栈
public bool IsFull()
{
    //栈顶索引等于容量大小表示满栈
    return _top == _capacity - 1;
}

8、入栈 Push

入栈则是在把栈顶索引向数组后移动一位后,再把新元素赋值到栈顶索引所对应的元素上,同时还需要检查是否为满栈,如果是满栈则报错,具体实现代码如下:

//入栈
public void Push(T value)
{
    if (IsFull())
    {
        //栈顶索引大于等于容量大小减1,表明已经满栈,不可以进行入栈操作
        throw new InvalidOperationException("满栈");
    }
    //栈顶索引先向后移动1位,然后再存放栈顶元素
    _array[++_top] = value;
}

9、出栈 Pop

出栈则是首先取出栈顶元素后,然后把栈顶索引向数组前移动一位,最后返回取出的栈顶元素,同时还需要检查是否为空栈,如果是空栈则报错,具体实现代码如下:

//出栈
public T Pop()
{
    if (IsEmpty())
    {
        //栈顶索引小于1表示空栈,不可以进行出栈操作
        throw new InvalidOperationException("空栈");
    }
    //返回栈顶元素后,栈顶索引向前移动1位
    return _array[_top--];
}

04、实现(链栈)

我们借助链表来实现链栈,其核心思想是把链表尾节点作为栈底,把链表首元节点当作栈顶。

这是因为如果我们想拿到链表的尾节点需要变量整个链表才可以拿到,但是要想获取首元节点则可以通过头指针直接获取到(无头节点链表),因此对于链表尾节点来说操作时不友好的适合来做栈底,链表首元节点对操作友好适合做为栈顶。

下面我们将一步一步实现一个泛型链栈。

1、ADT定义

相对于顺序栈的ADT来说,链栈的ADT少了两个方法即获取栈容量和是否满栈,这也是链表特性带来的好处。

2、初始化Init

初始化结构主要初始化栈顶节点为空和栈长度为0,具体实现如下:

public class MyselfStackNode<T>
{
    //数据域
    public T Data;
    //指针域,即下一个节点
    public MyselfStackNode<T> Next;
    public MyselfStackNode(T data)
    {
        Data = data;
        Next = null;
    }
}
public class MyselfStackLinkedList<T>
{
    //栈顶节点即首元节点
    private MyselfStackNode<T> _top;
    //栈长度
    private int _length;
    //初始化栈
    public MyselfStackLinkedList<T> Init()
    {
        //初始化栈顶节点为空
        _top = null;
        //初始化栈长度为0
        _length = 0;
        //返回栈
        return this;
    }
}

3、获取栈长度 Length

这个比较简单直接把栈长度私有字段返回即可。

//栈长度
public int Length
{
    get
    {
        return _length;
    }
}

4、获取栈顶元素 Top

获取栈顶元素可以通过栈顶节点直接获取,但是要注意判断栈是否为空栈,如果为空栈则报异常。具体代码如下:

//获取栈顶元素
public T Top
{
    get
    {
        if (IsEmpty())
        {
            //空栈,不可以进行获取栈顶元素操作
            throw new InvalidOperationException("空栈");
        }
        //返回首元节点数据域
        return _top.Data;
    }
}

5、获取是否空栈 IsEmpty

是否空栈只需判断栈顶节点是否为空即可。

//是否空栈
public bool IsEmpty()
{
    //栈顶节点为null表示空栈
    return _top == null;
}

6、入栈 Push

入栈则是首先创建一个新节点,然后把原栈顶节点赋值给新节点的指针域,最后把新节点变更为栈顶节点,同时栈长加1,具体实现代码如下:

//入栈
public void Push(T value)
{
    //创建新的栈顶节点
    var node = new MyselfStackNode<T>(value);
    //将老的栈顶节点赋值给新节点的指针域
    node.Next = _top;
    //把栈顶节点变更为新创建的节点
    _top = node;
    //栈长度加1
    _length++;
}

7、出栈 Pop

出栈则是首先取出栈顶节点对应的数据后,然后把栈顶节点指向原栈顶节点对应的下一个节点,同时栈长度减1,当然如果空栈则报错,具体实现代码如下:

//出栈
public T Pop()
{
    if (IsEmpty())
    {
        //空栈,不可以进行出栈操作
        throw new InvalidOperationException("空栈");
    }
    //获取栈顶节点数据
    var data = _top.Data;
    //把栈顶节点变更为原栈顶节点对应的下一个节点
    _top = _top.Next;
    //栈长度减1
    _length--;
    //返回栈顶数据
    return data;
}

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner