这两天在复习栈操作,从顺序结构,链式结构,到最后通过一个应用场景来进行检验栈操作,即表达式的四则运算,希望看了之后能够有所收获。
栈是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端成为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表,简称LIFO结构。
由此可知,栈的本质,就是线性表,只要前面的线性表章节掌握好了,栈的操作也就水道渠成的事。线性表的具体操作,可以参考线性表。
(1)顺序存储结构定义
private static int MAXSIZE = 100;//栈的存储量
public int COUNT; //栈中实际存放的元素个数
public T[] datas = new T[MAXSIZE]; //存储栈中元素
public int top; //用于栈顶指针
(2)进栈操作
//入栈,时间复杂度O(1)
public bool Push(T value)
{
//栈满
if (top == datas.Length - 1)
{
return false;
}
top++;
COUNT++;
datas[top] = value;
return true;
}
(3)出栈操作
//返回出栈的值,时间复杂度O(1)
public T Pop()
{
T value;
//空栈
if (top == -1)
return default(T);
value = datas[top];
top--;
COUNT--;
return value;
}
事实上,使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也就是一个栈增长时,另一个栈在缩短的情况。特别地,这是针对两个具有相同数据类型的栈。
(1)共享栈存储结构定义
private static int MAXSIZE=100; //共享栈最大的存储量
public int[] datas = new int[MAXSIZE]; //共享栈存储方式
private int top1 = -1;//栈1,栈顶指针
private int top2 = MAXSIZE;//栈2,栈顶指针
public int COUNT1 = 0;//栈1中元素个数
public int COUNT2 = 0;//栈2中元素个数
(2)入栈操作
///
/// 入栈操作
///
/// 入栈元素
/// 栈号参数,指把入栈元素放到哪个栈
public void Push(int value, int stackNumber)
{
//共享栈已满
if (top1 + 1 == top2)
{
Console.WriteLine("共享栈已满!");
}
if (stackNumber == 1)
{
++top1;
datas[top1] = value;
COUNT1++;
}
else if (stackNumber == 2)
{
--top2;
datas[top2] = value;
COUNT2++;
}
}
(3)出栈操作
public int Pop(int stackNumber)
{
int value = default(int);
if (stackNumber == 1)
{
if (top1 == -1)
{
Console.WriteLine("取数据失败,栈1为空!");
}
value = datas[top1];
top1--;
COUNT1--;
}
else if (stackNumber == 2)
{
if (top2 == MAXSIZE)
{
Console.WriteLine("取数据失败,栈2为空!");
}
value = datas[top2];
top2++;
COUNT2--;
}
return value;
}
从上面代码可以明显看到,所谓的共享栈,其实就是根据栈号参数进行系列栈操作,与普通栈并无区别。唯一的区别就是,栈满与栈空的判断条件发生了变化,这个细节注意一下就OK了。
(1)链式栈的存储结构
链式栈的结点定义类StackNode.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 栈_链式存储
{
class StackNode
{
public T data; //结点数据
public StackNode next; //结点指针
public StackNode()
{ }
public StackNode(T value,StackNode nextNode)
{
this.data = value;
this.next = nextNode;
}
}
}
(2)入栈与出栈操作
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 栈_链式存储
{
class LinkStackTest
{
private StackNode top;
public int count = 0 ;
private static LinkStackTest _instance;
public static LinkStackTest Instance {
get {
if (_instance == null)
_instance = new LinkStackTest();
return _instance;
}
}
//创建一个空栈
public void CreateLinkStack()
{
top = new StackNode();
}
//入栈
public void Push(T value)
{
if (top == null)
CreateLinkStack();
StackNode newNode = new StackNode(value,top);
top = newNode; //将新节点赋值给栈顶指针
count++; //栈中个数元素增加
}
//出栈
public T Pop()
{
T value = default(T);
//判断栈是否为空
if (top == null)
return value;
value = top.data; //取出栈顶元素
top = top.next; //栈顶下移
count--; //栈中元素个数减少
return value;
}
}
}
共同点:二者时间复杂度一致,均为O(1)
区别:顺序栈需要开始就确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都要指针域。这同时增加了一些内存开销,但长度无限制。
结论:如果栈的使用过程中,元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。
下面以四则运算表达式求值为例进行说明栈的应用场景。主要解题思路如下:
(1) 将中缀表达式转化为后缀表达式(栈用来进出运算的符号):从左到右遍历中缀表达式的每个数字和符号,若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号则栈顶元素一次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
(2)将后缀表达式进行运算得出结果(栈用来进出运算的数字):从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果
第一步,转换为后缀表达式。
//将中缀表达式转换为后缀表达式
public string TranformExpression(string str)
{
string strTemp = "";
//原始数据中提取出操作数
string[] subStrs = str.Split('+', '-', '*', '/', '(', ')');
List newStrs = new List();
foreach (string temp in subStrs)
{
if (temp != "")
newStrs.Add(temp);
}
Stack<char> charStack = new Stack<char>();
//将表达式转换为后缀表达式
//规则如下:1.若是数字直接输出
//2.若是符号,判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号(乘除优先加减),则栈顶元素一次出栈并输出
for (int i = 0,j=0; i < str.Length; i++)
{
bool currentNum = false;//当前字符与下一个字符是否均为数字
//当前字符为运算符
if (str[i] < '0' || str[i] > '9')
{
/*符号输出操作*/
//为左边括号直接进栈
if (str[i] == '(')
charStack.Push(str[i]);
else if (str[i] == ')')
{
//括号匹配,依次出栈
while (charStack.Count > 0&&charStack.Peek() != '(')
{
strTemp += charStack.Pop() + " ";
}
if (charStack.Count > 0)
{
charStack.Pop(); //取出左小括号
}
}
else if (str[i] == '+' || str[i] == '-' || str[i] == '*' || str[i] == '/')
{
//若当前栈为空
if (charStack.Count == 0)
charStack.Push(str[i]);
else
{
//比较当前字符与栈顶字符的优先级
while (charStack.Count > 0 && !IsPriority(str[i], charStack.Peek()))
{
strTemp += charStack.Pop() + " ";
}
charStack.Push(str[i]);
}
}
}
//当前字符为数字
else
{
//下一个字符仍为数字
if ((i+1)<str.Length &&str[i + 1] >= '0' && str[i + 1] <= '9')
currentNum = true;
if (!currentNum&&j" ";
}
}
}
//顺序输出栈中剩余操作符
while(charStack.Count>0)
{
if (charStack.Count == 1)
strTemp += charStack.Pop();
else
strTemp += charStack.Pop() + " ";
}
return strTemp;
}
//ch1优先级高,返回false,否则,返回true
//ch1为当前元素,ch2为栈顶元素
//优先级不高于栈顶元素,则栈顶元素依次出栈并输出,并将当前符号进栈
public bool IsPriority(char ch1,char ch2)
{
if ((ch1 == '+' || ch1 == '-')&&(ch2=='*'||ch2=='/'|| ch2 != '('))
{
return false;
}
return true;
}
第二步,计算后缀表达式。
//计算后缀表达式
public string CalculateExpression(string transStr)
{
string result = "";
string[] strs = transStr.Split(' ');
Stack<string> calStack = new Stack<string>();
//判断字符串是否为数字
foreach (string str in strs)
{
if (isNumberic(str))
{
calStack.Push(str);
}
else
{
string op1 = calStack.Pop();
string op2 = calStack.Pop();
double temp = CalculateAllOperation(str,op2,op1);
calStack.Push(temp + "");
}
}
return result = calStack.Peek();
}
//判断字符串能否转换成数字
private bool isNumberic(string str)
{
try
{
double.Parse(str);
return true;
}
catch {
return false;
}
}
//计算操作
private double CalculateAllOperation(string operation,string op1,string op2)
{
double operation_1 = double.Parse(op1);
double operation_2 = double.Parse(op2);
if (Math.Abs(operation_2 - 0.0f)<0.000001f)
return default(double);
double result = 0.0f;
switch(operation)
{
case "+":
result = operation_1 + operation_2;
break;
case "-":
result = operation_1 - operation_2;
break;
case "*":
result = operation_1 * operation_2;
break;
case "/":
result = operation_1 / operation_2;
break;
}
return result;
}
最后,主函数测试:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 栈应用_计算四则表达式
{
class Program
{
static void Main(string[] args)
{
string str = "9+(6+(3-1))*3+10/2"; //中缀表达式
//将表达式按照一定规则入栈,转换为后缀表达式
Console.Write(str+"=");
string str2 = Tool.Instance.TranformExpression(str);
string result = Tool.Instance.CalculateExpression(str2);
Console.WriteLine(result);
Console.ReadKey();
}
}
}
实验程序结果截图如下: