前言
继前一篇文章后面,本文这里聊一聊一种非常重要的数据结构—栈,提到栈大家都会想到先进后出或者后进先出。正是因为栈的这种特点,使得栈这种数据结构具有非常广泛的应用,比如在浏览器进行上网时,当你需要
撤回
到前面一个网页时,你需要点击后退键返回到上一级网页进行浏览;当你使用PPT、Word进行图像或者文字处理时,如果出现了错误,需要前面的结果时,这时候也需要撤回功能;此外,还有很多地方均用到了这种撤回的功能。这种撤回的实现正是栈来实现的。
栈的初步了解
什么是栈?
栈是一种仅仅能在一端进行插入和删除的数据结构,这一端被称为栈顶
,而另一端相对应的被称为栈底
。如下图所示,是一本压着一本摆在桌子上的一坨书,当我们想要取中间的某一本的时候,先将压在这本书上上面的书一本一本的取下来,然后才能取下我们想要的书。通俗来说,先放的书,要靠后才能取到;后放的书,靠前能取到;最上面的书处在栈顶
,最下面的书处在栈底
。这就是栈的典型模型。
栈的基本操作
栈的基本操作分为进栈(也叫压栈、入栈),出栈这两种。进栈与出栈的详细操作如下图所示。进栈对应数据的插入,出栈对应数据的删除;并且我们需要注意的是栈的基本操作只能在栈顶,不管是进栈还是出栈。
栈的基本实现
栈作为数据结构的重要组成之一,它的实现方式分为两种,第一种是顺序存储结构,第二种是链式存储结构。对于第一种对应的实现方式是使用数组实现,也就是顺序栈;第二种是链表实现,也就是链式栈。
栈的顺序存储结构与实现
使用定义一个一定大小的整型数组arr
,假设数组的大小是5,则arr[0]
存储栈底元素,arr[4]
存储栈顶元素。使用top
指针指向栈顶位置,当数据进栈,指针位置+1;当数据出栈,指针位置-1。
class Stack
{
public:
Stack();
~Stack();
void push(int data);
int pop();
int size();
bool isEmpty();
private:
int* arr;
const int arrSize=5;
int top;
}
Stack::Stack()
{
arr=new int[arrSize];
top=-1;
}
Stack::~Stack()
{
delete [] arr;
arr=null;
}
//数据进栈
void Stack::push(int data)
{
if(top==maxSize-1)
{
cout<<"栈已经满了";
return;
}
arr[++top]=data;
}
//数据出栈
int Stack::pop()
{
if(top==-1)
{
cout<<"空栈";
return;
}
return arr[top--];
}
//判断是否为空
bool Stack::isEmpty()
{
if(top==-1)
return true;
return false;
}
//返回栈的大小
int Stack::size()
{
return top+1;
}
栈的链式存储结构与实现
对于链式栈来说,一般将栈顶放在链表的头部位置,如下图所示,栈顶中的节点指向下一个节点,并依次指向指向栈底节点,所以可以看出当栈中没有节点时,top
指针指向null
Struct Node{
int value;
Node *next;
}
class Stack
{
public:
Stack();
~Stack();
void push(int data);
int pop();
int size();
bool isEmpty();
private:
Node *top;
int Count=0;
}
Stack::Stack()
{
top=null;
}
Stack::~Stack()
{
}
//数据进栈
void Stack::push(int data)
{
Node* p=new Node;
p->value=data;
p->next=top;
top=p;
Count++;
}
//数据出栈
int Stack::pop()
{
if(top==null)
{
cout<<"栈为空";
return -1;
}
else{
Node* p=top;
int data=p->value;
top=top->next;
delete p;
count--;
return data;
}
}
//判断是否为空
bool Stack::isEmpty()
{
if(top==null)
return true;
return false;
}
//返回栈的大小
int Stack::size()
{
return Count;
}
两种栈性能分析
通过上面基本可以理解栈这种数据结构了,那么这两种栈有什么优劣之处呢?下面将从下面几个角度进行分析:时间性能与空间性能。
时间性能
无论是顺序栈还是链式栈,这两种栈在push
和pop
操作上均很比较简单,时间复杂度均为O(1)
空间性能
对比顺序栈和链式栈,我们发现对于顺序栈来说,首先要确定数组的大小,从而确定顺序栈的大小,与此同时,这个大小的不确定性可能会导致内存空间的浪费或者栈的数据溢出,但是由于数组本身具有访问数据的优势,所以顺序栈的数据存取相对很方便;对于链式栈,由于链表本身固有的特点,所以链式栈没有空间大小限制,这一点明显要优于顺组栈。因此如果在使用过程中不能确定数据量的大小,最好要选择链式栈;相反,如果可以预先确定数据量的大小,优先选择顺序栈。
栈的实际应用
四则运算表达式
在计算机中进行加减乘除是通过后缀表达式和中缀表达式得到运算结果的,这两种表达式正是通过栈来得到的。下面对这两种表达式进行分析。在计算机中,中缀表达式就是四则运算表达式
后缀表达式
比如一个简单的四则运算表达式17+(5-3)X4-6
,那么这个四则运算表达式的后缀表达式是什么呢?步骤入下:
- 首先对于数据17,17不是符号,不进栈,表达式为17
- 接着是
+
号,+
是符号,进栈 - 接着是
(
,(
是符号,由于没有配对,所以进栈 - 接着是数字5,5不是符号,不进栈,表达式为17、5
- 接着是
—
号,—
是符号,进栈 - 接着是数字3,3不是符号,不进栈17、5、3
- 接着是
)
号,)
是 符号,此时需要与前面入栈的(
进行配对,所以将(
与)
之间的符号依次输出,此时仅仅有一个符号—
,所以将其弹出,表达式为17、5、3、—
- 接着是符号
X
,由于X
号的优先级大于+
号,所以*
进栈 - 接着是数字4,4不是符号,不进栈,表达式为17、5、3、
—
、4 - 接着是符号
—
,—
是符号,但是此时栈顶的*号优先级大于—
号,所以将栈中的元素弹出,并将—
号进栈 - 此时表达式的结果为此时表达式为17、5、3、
—
、*
、+
- 接着是数字6,6不是符号,不进栈,此时表达式是17、5、3、
—
、*
、+
、6 - 最后没有数据,将
—
号弹出,此时表达式是17、5、3、—
、*
、+
、6、—
、该结果就是中缀表达式
后缀表达式的计算
- 首先数字17,入栈
- 接着数字5,入栈
- 接着数字3,入栈
- 接着是符号
—
,将栈顶数据3、5依次弹出,3是被减数,5是减数,并相减,得到数字2,并将2入栈 - 接着是数字4,入栈
- 接着是符号
*
,将栈顶数据4、2依次弹出,并相乘,得到结果8,并将8入栈 - 接着是符号
+
,将栈顶数据8、17依次弹出,并相加,得到结果25,并将25入栈 - 接着是数字6,入栈
- 接着是符号
—
,将栈顶数据6、25依次弹出,6是被减数,25是减数,并相减,得到数字19 - 最后结果就是19
除此之外,还有与栈相关的实际应用,如函数的调用,递归运算等等,在这里就不详细一一讲述了,后面有机会在其它文章中再进行讲解。
接着是符号*
,将栈顶数据4、2依次弹出,并相乘,得到结果8,并将8入栈
- 接着是符号
+
,将栈顶数据8、17依次弹出,并相加,得到结果25,并将25入栈 - 接着是数字6,入栈
- 接着是符号
—
,将栈顶数据6、25依次弹出,6是被减数,25是减数,并相减,得到数字19 - 最后结果就是19
本人在公众号(公众号:草帽小子学编程)坚持输出技术文章,欢迎读者扫描下方二维码进行关注