栈stack
栈是一种后进后出机制,它只允许访问访问一个数据项,即 栈顶(最后插入的数据项)。它有主要的三种操作:push,向栈内压入值;pop,弹出栈顶的值,即返回栈顶的值,并把它从栈内删除;peek,只返回但不删除栈顶。
概念很容易理解,无非就像给弹匣压子弹等等这种类比,但是像我这样的新手在刚接触到栈的时候总是很迷茫,认为它很难,其实这只是错觉,主要是因为没有搞清楚栈主要用在那些场景。
栈普遍应用于编译器、文本检测、科学计算等等,在编译器中,它用来检测一个函数体等是否为封闭的(括号是否成对等等),在文本检测中,想想你的vs,如果最后没有大括号它就会检测出来,和编译器中一样,在科学计算中,就像商店能买到的那些高级计算器,输入一个算式可以直接计算出来结果,而不像普通计算器中,只能输入数字,然后在按键一步一步计算,以上这些应用场景,归根结底都是对文本的检测,现在知道栈的用途了吧,后面将结合队列一起练练手。
队列queue
顾名思义,就是一堆东西成队排列了,它是先进先出机制(FIFO)与栈相反,栈是后进后出(LIFO)。它的主要操作有:Enqueue,向队尾添加值;Dequeue,返回并移除对头的值;peek,返回但不移除对头的值。
队列很容易理解,它主要应用在网络通信、系统等。windows的所有事件都是放在队列里面的。
最典型的,就是系统的任务分配了,每个进程或线程都分配在某些队列里,方便cpu时间片的分配调度,任务的运行不可能是你最后打开的程序先运行吧,当然它有优先级,这个排除在外。
栈的应用
现在搞清楚了栈和队列的应用场景,那就趁热打铁,练练手。
现在,按部就班的实现一个科学表达式的计算。
首先了解一下计算机是怎么计算类似于 1*(2+3)+4/2 这样的计算。对于大脑来说,这个算式简单到不能再简单了,但是电脑却不是如此,得让它明白那个先算,那个后算,以及哪些是操作符(运算符)哪些不是(例如括号)。
像我们经常使用的这种算式称为 中缀式 ,就是运算符都在两个操作符中间了,这种中缀式对于我们人脑来说,并不是顺序执行的,它可以从中间先开始,比如1+2*3 ,这正是电脑很难理解中缀式的原因,因为他总是顺序执行的。那么,我们就得把中缀式换成 后缀式(postfix) 或称作 逆波兰记法(reverse Polish notation)。
中缀式就是把运算符放到两个操作数后边,让算式保持顺序计算(对于我们人脑也是如此)。具体怎么放,大家看看下面的例子就明白了。
中缀式 |
后缀式 |
A+B+C-D | A B+C+D- |
A*B/C+D | A B*C/D+ |
A*(B+C*D)+E | A B C D * + * E + |
(A+B)*C/(D+E-F) | A B + C * D E + F - / |
A+B*C+(D*E+F)*G | A B C * + D E * F + G * + |
结合上边的示例,会发现后缀式并没有描述优先级的括号,这是因为括号并不是运算符。大家在自己动手做两个,就掌握这种方法了。
程序中如何实现这种转换呢?那就要依靠强大的栈了。
中追到后缀的转换
下面是实现中缀转后缀的程序:
示例 2:
{
Dictionary<char, int> opreater = new Dictionary<char, int>(); //这个用来存放操作符和他们的优先级
//3最高
opreater.Add('+', 1);
opreater.Add('-', 1);
opreater.Add('*', 2);
opreater.Add('/', 2);
opreater.Add('(', 3);
opreater.Add(')', 3);
string m = Console.ReadLine(); //读取一个算式
Stack<char> opreaters = new Stack<char>(); //这个栈用来存放操作符
Queue<char> mq = new Queue<char>(); //这个队列存放算式
//将算式字符串的字符一个个插入队列
for (int i = 0; i < m.Length; i++)
{
mq.Enqueue(m[i]);
}
//开始将中缀转换为后缀算式
Console.WriteLine("开始转换");
while (mq.Count > 0) //当算式队列没用完则执行
{
if (opreater.ContainsKey(mq.Peek()) == true) //如果下一个字符是操作符的话执行
{
if (mq.Peek() == ')') //如果为右括号,则弹出所有元素,直到遇到一个与之相对应的左括号
{
mq.Dequeue(); //让右括号弹出,它已经没什么用了
while (opreaters.Count > 0)
{
if (opreaters.Peek() == '(') //如果遇到了左括号,则不打印,直接弹出
{
opreaters.Pop();
break;
}
else
{
Console.Out.Write(opreaters.Pop() + " "); //不是左括号则继续弹出打印
}
}
}
else if (opreaters.Count == 0 || opreater[mq.Peek()] > opreater[opreaters.Peek()] || mq.Peek() == '(') //如果栈为空或者操作符优先级高,则入栈
{
opreaters.Push(mq.Dequeue());
continue;
}
else //如果操作符优先级相等或者比较低,则输出所有操作符知道遇到左括号
{
if (opreaters.Peek() != '(') //这个程序写的逻辑性不太好,写完了到很难在回头分析了,大家不要学我,最忌讳这样了
{
Console.Out.Write(opreaters.Pop() + " ");
}
while (opreaters.Count != 0 && opreater[mq.Peek()] >= opreater[opreaters.Peek()]) //如果遇见其他操作符时则弹出打印,直到遇见更低的操作符
{
if (opreaters.Peek() == '(') //如果为左括号则直接弹出不打印
{
opreaters.Pop();
break;
}
else //打印直到遇到左括号
{
Console.Out.Write(opreaters.Pop() + " ");
if (opreaters.Count != 0 && opreater[mq.Peek()] < opreater[opreaters.Peek()])
{
Console.Out.Write(opreaters.Pop() + " ");
}
}
}
opreaters.Push(mq.Dequeue()); //最后,将当前操作符入栈
}
}
else
{
Console.Write(mq.Dequeue() + " ");
}
}
//读完数据,则将栈内元素全部弹出
while (opreaters.Count != 0)
{
Console.Write(opreaters.Pop() + " ");
}
}
上面这段代码按照总结的那些方法写出来的,注意,它并没有错误检测,如果括号不成对,就会出错,假设所有的输入均合法,则所有右括号遇完之后,栈中也就没有左括号了,所以最后一个输出循环并没有检测左括号。
程序首先用一个字典(键值对集合)来存放四个运算符的优先级,而输入的字符串则转储到队列,方便while循环进行比对,当然,不用队列而用for循环也可以,这里主要考虑给队列也练练。
程序写的不是很好,也不规范,大家只要自己动手写一下就明白了。
现在我们已经实现了从中缀式转为后缀式了,紧接着来实现计算部分。
示例 3:
{
Dictionary<char, int> opreater = new Dictionary<char, int>(); //这个用来存放操作符和他们的优先级
//3最高
opreater.Add('+', 1);
opreater.Add('-', 1);
opreater.Add('*', 2);
opreater.Add('/', 2);
opreater.Add('(', 3);
opreater.Add(')', 3);
string m = Console.ReadLine(); //读取一个算式
Stack<char> opreaters = new Stack<char>(); //这个栈用来存放操作符
Stack<int> numStack = new Stack<int>(); //这个栈又来存放算数
Queue<char> mq = new Queue<char>(); //这个队列存放算式
//将算式字符串的字符一个个插入队列
for (int i = 0; i < m.Length; i++)
{
mq.Enqueue(m[i]);
}
//开始将中缀转换为后缀算式
Console.WriteLine("开始转换");
string nums = ""; //存放数字的字符串
while (mq.Count > 0) //当算式队列没用完则执行
{
if (opreater.ContainsKey(mq.Peek()) == true) //如果下一个字符是操作符的话执行
{
if (nums != "")
{
int num = int.Parse(nums); //将数字字符串转为整形
nums = "";
numStack.Push(num); //将这个数字入栈
}
if (mq.Peek() == ')') //如果为右括号,则弹出所有元素,直到遇到一个与之相对应的左括号
{
mq.Dequeue(); //让右括号弹出,它已经没什么用了
while (opreaters.Count > 0)
{
if (opreaters.Peek() == '(') //如果遇到了左括号,则不打印,直接弹出
{
opreaters.Pop();
break;
}
else
suan(opreaters.Pop(), ref numStack);
}
}
else if (opreaters.Count == 0 || opreater[mq.Peek()] > opreater[opreaters.Peek()] || mq.Peek() == '(') //如果栈为空或者操作符优先级高,则入栈
{
opreaters.Push(mq.Dequeue());
continue;
}
else //如果操作符优先级相等或者比较低,则输出所有操作符直到遇到左括号
{
if (opreaters.Peek() != '(') //这个程序写的逻辑性不太好,写完了到很难在回头分析了,大家不要学我,最忌讳这样了
suan(opreaters.Pop(), ref numStack);
while (opreaters.Count != 0 && opreater[mq.Peek()] >= opreater[opreaters.Peek()]) //如果遇见其他操作符时则弹出打印,直到遇见更低的操作符
{
if (opreaters.Peek() == '(') //如果为左括号则直接弹出不打印
{
opreaters.Pop();
break;
}
else //打印直到遇到左括号
{
suan(opreaters.Pop(), ref numStack);
if (opreaters.Count != 0 && opreater[mq.Peek()] < opreater[opreaters.Peek()])
{
suan(opreaters.Pop(), ref numStack);
}
}
}
opreaters.Push(mq.Dequeue()); //最后,将当前操作符入栈
}
}
else
nums += mq.Dequeue(); //提取字符
}
while (opreaters.Count != 0)
{
if (nums != "") //判断算数字符串是否为空
{
int num = int.Parse(nums); //将数字字符串转为整形
nums = "";
numStack.Push(num);
}
suan (opreaters.Pop(),ref numStack);
}
//读完数据,则将栈内元素全部弹出
Console.Write(numStack.Pop());
}
/// <summary>
/// 按照运算符运算两个操作数
/// </summary>
/// <param name="opt">运算符</param>
/// <param name="numStack">算数栈</param>
private void suan(char opt,ref Stack<int> numStack)
{
int i = 0;
int j = 0;
switch (opt)
{
case '+':
i = numStack.Pop();
j = numStack.Pop();
numStack.Push(j + i); //将运算结果入栈
break;
case '-':
i = numStack.Pop();
j = numStack.Pop();
numStack.Push(j - i);
break;
case '*':
i = numStack.Pop();
j = numStack.Pop();
numStack.Push(j * i);
break;
case '/':
i = numStack.Pop();
j = numStack.Pop();
numStack.Push(j / i);
break;
}
}
这段代码只不过比 示例2 多了个计算工能,其他代码完全一样。
在 示例2 原有的基础上,多了一个栈,这个栈是用来存放操作数的。将示例二中所有输出操作数的地方,全部换成了计算操作数,每次计算的时候,都是对两个操作数进行计算的,函数suan就是负责计算的,把它给单独提出来了。
大家动手写后,就掌握了科学计算式的方法了,顺便也搞清楚栈的用途了。
补充:
其实,栈最普遍最重要的作用还是体现在函数调用中,这就是为什么每次调试程序的时候,总有个“调用堆栈”的窗口显示一些不知所云的东东。
在运行一个函数的时候,其局部变量都会被寄存在cpu的寄存器中(计算是在cpu中完成,而cpu是对它的那些寄存器进行操作,而不是直接在内存中),当这个函数内又调用另外一个函数时,它的局部变量会在寄存器中把外部函数的局部变量给占据,当这个函数调用完毕,主函数的局部变量岂不是完蛋了。这个时候栈就起到大作用了。
栈内每个元素均为已经调用但并没有返回函数的描述(就假当一个元素是一个函数的所有局部变量吧,具体情况我也不太清楚),而栈顶(活动记录(activation record) 或称为 栈帧(stack frame))即为当前环境,即当前的函数描述。这样,在层层调用函数并返回后,函数调用者的变量就都又恢复了,这点在递归中尤为明显,递归就是一层又一层的跟洋葱皮似的,一直剥洋葱皮,剥完后,还得再把刚才剥掉的那些从里往外再数一便。
调用堆栈现在大概的知道是怎么一回事了,现在说说尾递归,什么是尾递归,跟名字一样,就是函数的那个调用自己的递归语句在函数体的末尾,也就是这个递归语句最终走完,紧接着下一步就是函数体的结束了。
尾递归是种极差的递归,因为每次将要执行递归语句的时候,前面的变量可以说是已经用不着了,但是按照调用堆栈的原则,它们还会被存在调用堆栈里,如果递归足够多的话,栈说不定就会溢出,程序就崩溃了。
发表评论
-
ASP.NET Process Model之一:IIS 和 ASP.NET ISAPI
2010-01-07 22:04 1037前几天有一个朋友在MSN上问我“ASP.NET 从最初的接收到 ... -
深入ASP.NET数据绑定(下)——多样的绑定方式
2010-01-06 19:58 1018在这个系列的上篇中介 ... -
深入ASP.NET数据绑定(中)——数据双向绑定机理
2010-01-06 19:57 1167在上一篇《深入ASP.NET数据绑定(上)》中,我们分析了在. ... -
深入ASP.NET数据绑定(上)
2010-01-06 19:55 1058在ASP.NET我们在使用Repeater,DetailsVi ... -
C#与数据结构--二叉树的遍历
2009-07-09 10:30 22206.2.2 二叉树的存储结构 二叉树的存储可分为两种:顺序 ... -
前序遍历二叉树,中序遍历二叉树,后序遍历二叉树 c#实现
2009-07-08 23:31 2241using System.Collections.Generi ... -
如何自己实现IEnumerable和IEnumerator接口以支持foreach语句
2009-07-08 23:25 2428在C#中,凡是实现了IEnumerator接口的数据类型都可以 ... -
IEnumerable与IEnumerator区别
2009-07-08 23:11 2619public interface IEnumerable{ ... -
IList、ICollection、IEnumerable 之辨析
2009-07-08 23:06 3149祖宗:IEnumerable 此接口只有一个方法 GetEn ... -
使用ASP.NET Global.asax 文件
2009-05-05 20:00 1144Global.asax 文件,有时候 ... -
ASP.NET VIEWSTATE初探
2009-05-05 17:55 1648一、 ViewState 的作用 与刚接触 ASP. ...
相关推荐
### C#数据结构和算法分析知识点详解 #### 一、数据结构教材的选择与编写背景 在探讨具体的C#数据结构和算法之前,我们先来了解一下本书的编写背景和选择C#作为教学语言的原因。 - **编写背景**:本书的编写是在...
数据结构与算法分析是计算机科学中的核心领域,它关乎如何高效地存储和处理数据,以及设计和实现高效的算法。在C语言中学习数据结构与算法分析,能够让你深入理解底层机制,这对于提升编程技能和优化代码性能至关...
《武汉大学 C#数据结构与算法》是一门深入探讨计算机科学基础的课程,主要针对C#编程语言,涵盖了数据结构和算法这两个核心概念。在学习这门课程时,你将有机会掌握C#语言如何用于实现高效的数据管理和计算方法。 1...
《C#数据结构和算法分析》是一本专为C#初学者设计的教材,它强调了数据结构和算法在编程中的核心地位。程序不仅仅是代码的集合,而是算法和数据结构的有效结合,这使得理解这两者变得至关重要。这本书通过C#语言来...
1. **内容概览**:全书共分为8章,覆盖了数据结构和算法的基本概念、线性表、栈和队列、串和数组、树型结构、图结构、排序以及查找等方面的内容。每一章都会介绍相关的数据结构或算法,并结合.NET框架中的具体实现...
- **需求**: 随着C#语言的应用日益广泛,特别是对于那些采用C#作为主要开发语言的专业而言,迫切需要一本专门介绍C#数据结构与算法分析的教材。 #### 二、C#语言的特点与.NET Framework的发展 - **C#语言特性**: -...
### C# 数据结构与算法分析知识点概述 #### 一、数据结构教材的选择与编写背景 在选择编写一本关于数据结构的教材时,作者面临了两个主要的问题:一是市场上已有的数据结构教材种类繁多,包括使用多种编程语言编写...
本文旨在填补这一空白,通过分析一篇关于C#数据结构与算法的经典资料,探讨其核心内容和技术特点。 #### 二、C#数据结构与算法的重要性 1. **提高编程效率**:理解并掌握高效的数据结构和算法对于提高程序性能至关...
李元波的教程《C#数据结构与算法》第三部分,可能会涵盖上述概念的实际应用和案例分析,帮助读者深化理解和实践能力。通过深入学习和实践,开发者能够更好地运用这些知识,编写出更加高效、可维护的C#代码。
通过以上介绍,我们可以看出,《最佳学习教材-C#-数据结构-算法分析》不仅是一本详尽的数据结构与算法教程,更是C#语言及其在.NET框架中应用的重要参考资料。无论是对于初学者还是有一定经验的开发者来说,这本书都...
本资源"**c#数据结构和算法源码**"提供了一个实践学习的平台,通过C#代码来理解和应用各种数据结构和算法。 首先,我们来看二叉树(Binary Tree)。二叉树是一种特殊的图,每个节点最多有两个子节点,通常分为左子...
总的来说,"C#数据结构与算法"的学习涵盖了许多核心编程概念,包括数据结构的实现、操作和应用,以及算法的设计与分析。通过这个课程,你不仅可以提升编程技能,还能为面试和项目开发打下坚实的基础。记住,对数据...
学习C#数据结构与算法,不仅需要理解它们的原理,还要通过实践加深理解,例如编写代码实现这些数据结构和算法,参与编程竞赛或解决实际项目中的问题。这将有助于提升编程思维,使你在面对复杂问题时能更有条理地分析...
李元波主讲的“C#数据结构与算法5”系列视频,针对这一主题进行了深入讲解,旨在帮助开发者提升在C#环境下的算法设计和分析能力。 首先,我们要理解数据结构是什么。数据结构是指组织和存储数据的方式,它决定了...
《数据结构与算法——C#语言描述》是一本专为C#程序员设计的教材,它深入探讨了数据结构和算法的基础知识,通过C#语言来实现各种数据结构和算法,帮助开发者提升编程技能和解决问题的能力。书中涵盖的内容广泛,包括...