C#算法系列(9)——栈

       这两天在复习栈操作,从顺序结构,链式结构,到最后通过一个应用场景来进行检验栈操作,即表达式的四则运算,希望看了之后能够有所收获。

一、栈的定义

       栈是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端成为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈栈又称为后进先出的线性表,简称LIFO结构。
       由此可知,栈的本质,就是线性表,只要前面的线性表章节掌握好了,栈的操作也就水道渠成的事。线性表的具体操作,可以参考线性表

二、栈存在的几种形式

1.顺序栈

(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;
}

2.共享栈

       事实上,使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也就是一个栈增长时,另一个栈在缩短的情况。特别地,这是针对两个具有相同数据类型的栈。
(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)入栈操作

/// <summary>
/// 入栈操作
/// </summary>
/// <param name="value">入栈元素</param>
/// <param name="stackNumber">栈号参数,指把入栈元素放到哪个栈</param>
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了。

3.链式栈

(1)链式栈的存储结构
链式栈的结点定义类StackNode.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 栈_链式存储
{
    class StackNode<T>
    {
        public T data; //结点数据
        public StackNode<T> next; //结点指针

        public StackNode()
        { }

        public StackNode(T value,StackNode<T> 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<T>
    {
        private StackNode<T> top;
        public int count = 0 ;

        private static LinkStackTest<T> _instance;
        public static LinkStackTest<T> Instance {
            get {
                if (_instance == null)
                    _instance = new LinkStackTest<T>();
                return _instance;
            }
        }

        //创建一个空栈
        public void CreateLinkStack()
        {
            top = new StackNode<T>();
        }

        //入栈
        public void Push(T value)
        {
            if (top == null)
                CreateLinkStack();
            StackNode<T> newNode = new StackNode<T>(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;
        }
    }
}

3.顺序栈与链式栈的区别

       共同点:二者时间复杂度一致,均为O(1)
       区别:顺序栈需要开始就确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都要指针域。这同时增加了一些内存开销,但长度无限制。
       结论:如果栈的使用过程中,元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。

4.栈的应用

       下面以四则运算表达式求值为例进行说明栈的应用场景。主要解题思路如下:
       (1) 将中缀表达式转化为后缀表达式(栈用来进出运算的符号):从左到右遍历中缀表达式的每个数字和符号,若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号则栈顶元素一次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
       (2)将后缀表达式进行运算得出结果(栈用来进出运算的数字):从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果

第一步,转换为后缀表达式。

//将中缀表达式转换为后缀表达式
public string TranformExpression(string str)
{
    string strTemp = "";
    //原始数据中提取出操作数
    string[] subStrs = str.Split('+', '-', '*', '/', '(', ')');
    List<string> newStrs = new List<string>();
    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<newStrs.Count)
            {
                strTemp += newStrs[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();
        }
    }
}

实验程序结果截图如下:


这里写图片描述

       以上就是今天栈的总结,如有疑问,欢迎留言!!!谢谢浏览,喜欢的关注或收藏一下!

猜你喜欢

转载自blog.csdn.net/qq_24642743/article/details/78752091