栈(Stack)是一种基于后进先出(LIFO,Last-In-First-Out)原则的数据结构。它可以理解为一种容器,其中的元素按照一种特定的顺序进行插入和删除操作。
栈有两个主要的操作:
- 入栈(Push) :将元素添加到栈的顶部。新的元素被放置在所有已存在元素的上方。
- 出栈(Pop) :从栈的顶部移除元素。只能移除最近添加的元素,即最后入栈的元素。 栈的特点是只能访问和操作位于栈顶的元素,即最后一个入栈的元素。其他元素必须先出栈才能访问到。这种特性使得栈非常适合处理具有顺序关系的问题,例如函数调用、表达式求值、括号匹配等。 栈的实现可以使用不同的数据结构,如数组或链表。在数组实现中,栈的顶部通常对应于数组的末尾,而在链表实现中,栈的顶部对应于链表的头部。
那我们一起来解决一下这个爬楼梯的题目,并且浅浅的聊一下什么是“栈”把!
那我们一起来解决一下这个爬楼梯的题目,并且浅浅的聊一下什么是“栈”把!
解1:
-
读题 -数学运算思维 2种走法 f(n) n 节台阶有多少种走法 函数式思想 f(n-1) f(n-2) f(n)=f(n-1)+f(n-2) -递归 并不是一种算法,而是一种实现的方式 内存的不断使用吗,函数栈的入栈 f(n) 意思就是执行了函数 推入执行栈 let a = 1;内存里开辟了一个数执行的内存空间,并把1值 放进去了 代码的执行是一个栈数据结构
-
函数运行有比较大的内存开销
-
这段代码是一个递归函数
climStairs(n),用于计算爬楼梯的方法数。它基于以下规则: -
如果楼梯的阶数
n等于 1,那么只有一种方式可以爬到楼顶,即直接爬一步。 -
如果楼梯的阶数
n等于 2,那么有两种方式可以爬到楼顶,即一次爬一步或者一次爬两步。 -
对于
n > 2的情况,爬楼梯的方法数等于爬到倒数第一阶的方法数加上爬到倒数第二阶的方法数。这是因为在最后一步,可以选择爬一阶到达楼顶,或者选择爬两阶到达楼顶。因此,爬楼梯的方法数等于前面两种情况的总和。
函数的逻辑如下:
- 如果
n等于 1,即只有一阶楼梯,直接返回 1。 - 如果
n等于 2,即有两阶楼梯,直接返回 2。 - 对于其他
n的情况,调用climStairs(n-1)和climStairs(n-2)分别计算爬到倒数第一阶和倒数第二阶的方法数,并将它们相加作为结果返回。
函数通过递归调用自身来解决较大的问题,将问题不断分解为规模更小的子问题,并将子问题的结果累加得到最终的结果。
function climbStairs(n){
if(n === 1) return 1;
if(n === 2) return 2;
return climStairs(n-1) + climStairs(n-2);
}
但是由于递归函数运行的时候会产生多余的内存问题,他的结果产生会这样的
这是由于递归产生的将之前的函数运行都保存引起的,执行栈pushle太多函数,那么我们可以对他进行一个简单的优化
在此之前我们通过递归问题来聊一聊“栈”的有关问题
递归具有以下几个优点:
-
简洁性和可读性:递归能够用更简洁、直观的方式来表达问题的解决思路。递归代码通常比较简洁,易于理解和阅读。通过递归调用自身,可以将复杂的问题分解成更小的子问题,使代码结构更清晰。
-
问题分解:递归允许将一个大问题分解为一个或多个相同类型的子问题。这种分解能够使问题更易于理解和解决。递归的思想适用于许多问题,特别是那些可以通过重复应用相同的操作来解决的问题。
-
代码复用:递归可以促进代码的复用。通过定义一个通用的递归函数,可以在不同的场景和不同的输入上重复使用该函数。这样可以减少代码的冗余和重复编写的工作。
-
解决复杂问题:递归是解决某些复杂问题的有效方法。一些问题的解决方案往往需要按照递归的方式来思考和实现。这些问题可能涉及树结构、图结构、排列组合等等。递归能够通过分解问题和利用子问题的解来有效地解决这些复杂问题。
递归具有以下几个缺点:
-
性能损耗:递归可能会导致性能损耗。每次递归调用都需要在内存中创建新的函数调用栈帧,这会增加内存消耗和函数调用的开销。递归的性能通常比迭代(循环)方式要差。在某些情况下,递归可能导致堆栈溢出,特别是当递归深度很大时。
-
重复计算:在一些情况下,递归可能会导致重复计算。由于递归调用自身,可能会出现相同的子问题被重复解决的情况。这会导致不必要的计算开销。可以通过使用记忆化技术(如缓存中间结果)来避免重复计算。
-
难以理解和调试:递归的实现可能比较抽象和难以理解,特别是对于复杂的递归算法。递归可能会导致代码的执行顺序变得难以跟踪和调试。递归的错误和异常处理也可能比较困难。
-
堆栈溢出:如果递归的深度过大,超过了系统或语言的堆栈大小限制,就可能导致堆栈溢出错误。这通常发生在没有正确设置递归终止条件或递归深度过深的情况下。
-
空间复杂度高:递归通常需要额外的内存空间来存储每一层递归的函数调用栈帧。在某些情况下,递归的空间复杂度可能比较高,特别是当递归深度很大时。
执行栈遵循先进后出(Last-In-First-Out)的原则,即最后被推入栈的栈帧首先被执行和弹出。这意味着函数的嵌套调用会形成多个嵌套的栈帧,后调用的函数会先执行完毕并被弹出。当执行栈为空时,程序执行结束。 执行栈在程序的调试和错误追踪中扮演重要的角色,它记录了函数的调用顺序和执行状态,帮助开发人员理解代码的执行流程。 当说到代码的执行是一个栈数据结构时,指的是在程序执行过程中,使用栈(Stack)这种数据结构来管理函数的调用和返回。
在程序执行时,函数调用的过程可以看作是一个栈的操作。当一个函数被调用时,它的执行上下文(包括函数的参数、局部变量等)被推入栈的顶部,形成一个栈帧(Stack Frame)。这个栈帧保存了函数的执行信息。然后,函数内部可能会调用其他函数,导致更多的栈帧被推入栈中。
前面聊了很多的“栈”,那么“栈”的操作怎么样的呢?
栈的操作遵循先进后出(Last-In-First-Out)的原则,即最后被推入栈的栈帧首先被处理。当一个函数执行完毕后,它的栈帧会被弹出栈,程序继续执行上一个栈帧所在的函数。
通过使用栈数据结构,程序可以跟踪函数的调用顺序,并在适当的时候恢复到上一个函数的执行状态。这种栈结构的管理方式使得函数的嵌套调用和返回成为可能,并确保程序能够正确地处理函数的执行顺序和状态。
栈还广泛用于其他编程语言中的一些常见场景,如表达式求值、递归函数、异常处理等。在这些场景中,栈被用于跟踪和管理代码的执行顺序和状态,以便程序可以正确地执行和处理相应的操作。 如果不往执行栈里push太多层函数,是不是就可以解决递归带来的内存问题呢 入栈过的函数是没有必要再入栈,以下是优化过的递归函数
const f = []; // 全局变量
const climbStairs = function(n){
//退出条件
if(n === 1) return 1;
if(n === 2) return 2; //
if (f[n] === undefined){
//什么是递归函数 函数嵌套函数
f[n]= climbStairs(n-1) + climbStairs(n-2); //递归公式
}
return f[n];
}
这段代码是一个改进过的递归函数 climbStairs(n),用于计算爬楼梯的方法数。与之前的代码相比,它引入了一个全局变量 f,用于缓存已经计算过的结果,以避免重复计算。
函数的逻辑如下:
- 定义一个全局变量
f,用于存储已经计算过的楼梯方法数,初始化为空数组。 - 如果
n等于 1,即只有一阶楼梯,直接返回 1。 - 如果
n等于 2,即有两阶楼梯,直接返回 2。 - 如果
f[n]不存在(即之前没有计算过),则进行递归调用climbStairs(n-1)和climbStairs(n-2)来计算爬到倒数第一阶和倒数第二阶的方法数,并将它们相加作为结果,并将结果存储在f[n]中。 - 返回
f[n]作为结果。
通过引入全局变量 f 并进行结果缓存,函数在计算过程中会首先检查是否已经计算过某个楼梯阶数的方法数,如果计算过则直接返回缓存结果,避免了重复的递归计算。这种改进可以显著提高递归函数的性能,避免不必要的重复计算。
递归函数是一种从上而下的数学思维,接下来是用for循环做的自底而上的动态规划。
const climbStairs = function(n){
// 自底向上 不递归 递推 dp(自顶向上) 动态规划
const f = []; //进行优化
f[1] = 1;
f[2] = 2;
for(let i = 3; i<=n;i++){
//递推公式
f[i] = f[i-1]+f[i-2];
}
return f[n];
}
这就避免了递归函数造成的重复计算,堆栈溢出。
希望笔者的文章能够对你有帮助,可以对你尽快的了解“栈”的意义,并且能够熟练掌握和运用!