WEEK2-Recursion and invariant programming

本文探讨了递归函数与循环之间的深层联系,通过实例讲解如何利用不变量编程技术编写高效、正确的递归函数,并介绍了尾递归的概念及其优势。

Paradigms of Computer Programming - Fundamentals (EDX)

WEEK2-Recursion and invariant programming 笔记



1. Overview of invariant programming

declare
fun {SumDigitsR N}
   if (N==0) then 0
   else (N mod 10) + {SumDigitsR (N div 10)} end
end

In the previous lesson we saw a simple example of a recursive function, namely SumDigitsR. This function is able to calculate the sum of all digits of any integer, no matter how many digits it has (within the limits of the system's representation). The recursive call and the conditional together are similar to a while loop: an execution that is repeated while some condition is satisfied (like N==0). Each function call does one iteration of the loop.

上节课看了一个递归函数的简单例子(SumDigitsR),这个函数可以计算任何整数的所有位数之和,无论有多少位,只要在系统的能力范围内。“递归调用和条件”与“while”循环(某个条件满足时就反复执行)类似。每个函数调用做一次循环的重复。


In this lesson we will go to the root of this intuition. We will understand in a deep way the connection between recursion and loops, and we will see how to write correct and efficient programs that use recursion. We will see that a recursive function is exactly the same as a while loop, if one essential condition is satisfied: if the single recursive call that is executed is the last operation done by the function (this is calledtail recursion). To write a tail recursive function, we need to introduce one or more extra arguments, to help calculate the result. Anaccumulator is an extra argument in which the result is calculated piece by piece (it is accumulated).

这节课继续探索。我们会更深入了解递归和循环的联系,以及怎样用递归写出正确的、高效的程序。我们会看到递归函数和while循环一模一样在满足一个必要条件(执行的递归调用是函数上一次的操作(尾递归))时。为了写出尾递归函数,我们需要引入一个或多个额外的变量来帮助计算结果。累积器就是一个结果是一点点累积起来的变量。
(argument 字典跟我说有什么自变量、引数的意思,这里先翻译成变量了。)


We will give a technique, called invariant programming, to program correct and efficient loops. This technique applies both to the functional paradigm and to the imperative paradigm. (An imperative paradigm allows variables to be assigned more than once; many widely used languages, such as Java and C++, are based on imperative paradigms.) As part of this technique, we define two formal concepts, thespecification of a function (a mathematical formula that defines what the function calculates) and theinvariant of a recursive function (a mathematical formula that is true at each recursive call). The right way to program a function is to start with the specification. The right way to program a loop (a recursive function or while loop) is by starting with the invariant.

我们会用一种技术(不变量编程)来写出正确高效的循环。这种技术应用于函数范式和命令范式。(命令范式允许变量被多次赋值,很多常用语言比如JAVA和C++都是命令范式。)这种技术的两个概念:函数规范(定义函数计算什么的数学式),递归函数的不变量(每次递归调用都为TRUE的数学式)。正确的写函数方式是从规范开始,而正确的写循环(递归函数或while循环)是从不变量开始。

(术语可能会翻译的有问题,我的笔记自己看得懂最重要。)


2. Factorial with communicating vases


The sum of digits function we did in the last lesson seems to be very natural, so natural that it's hard to imagine another way! But this intuition is wrong. There is a second approach to write the function, quite different that leads to another, and better definition. To introduce this approach, let's start with a simpler function, namely factorial. This will let us focus on the key insight. Afterwards, we will come back to sum of digits.

上节课写的求位数和的函数简直太完美了,但是这种直觉是错的。还有第二种方式来写这个函数,和第一种很不一样,而且更好。为了引入这种方式,先从简单的求阶乘函数开始,这会帮助我们关注重点,之后会回到求位数和。



We first give the specification of the factorial function, a mathematical formula that defines it. Our first implementation of factorial follows exactly this specification. This definition is in the same style as the sum of digits function. It is fine from a mathematical point of view, but quite bad from a programming language point of view.

先给出阶乘函数的规范,用数学式定义它。第一种完全按照这个规范来。这个定义和位数和函数的形式一样。从数学角度看还好,但是从编程语言角度来看就不太好了。


% Factorial function
% 0! = 1
% n! = n * (n-1)! when n>0
declare
fun {Fact1 N}
   if N==0 then 1
   else N*{Fact1 N-1} end
end


The second implementation uses an approach called the principle of communicating vases. We start with a formula that separates the factorial into two parts: n!=i!*a. We know n but we don't know n! (yet). The other variables, i and a, are introduced together. Initially, i=n and a=1, so the formula holds. Now, we come to the key insight: we will change i and a together, in such a way that the formula still holds. If we can arrive at a point where i=0, then a is equal to n!, the answer.

第二种使用一种叫做沟通容器法则的方式。先把阶乘分成两部分:n!=i!*a。我们知道n但是还不知道n!。其余的变量i和a是一起引入的。首先,i=n,a=1,所以式子成立。现在,关键的是:我们要一起改变i和a,用一种让式子仍然成立的方式。当i=0时,a就等于n!,得到了答案。




For factorial, we will decrease i and increase a together. We will decrease i by 1 and increase a by multiplying it by i. By doing this, the formula remains true. We can easily show this rigorously: i!*a = i*(i-1)!*a = (i-1)! * (i*a). Let i'=i-1 and a'=i*a. Then the last expression is equal to i'!*a', which is exactly our formula. Now we can write the second implementation.

对阶乘来说,同时减少i,增加a。i -= 1,a *= i。这样,式子一直成立。i!*a = i*(i-1)!*a = (i-1)! * (i*a)。i'=i-1 and a'=i*a。最后一个表达式是i'!*a',完全就是我们的式子。现在可以写第二种了。


% Principle of communicating vases
% n! = i! * a
%    = i * (i-1)! * a
%    = (i-1)! * (i*a)
% We have: i'=i-1 and a'=i*a
declare
fun {Fact2 I A}
   if I==0 then A
   else {Fact2 I-1 I*A} end
end


3. Sum of digits with communicating vases




fun {SumDigits2 S A}
   if S==0 then A
   else
      {SumDigits2 (S div 10) A+(S mod 10)}
   end
end


4. The golden rule of tail recursion

#We will see why Fact2 (with tail recursion) is more efficient than Fact1 (no tail recursion):
1. Fact1 is based on a simple mathematical definition
2. Fact2 is designed with invariant programming

#Comparing Fact1 and Fact2
1. Tail recursion is when the recursive call is the last operation in the function body
2. N * {Fact1 N-1} % No tail recursion
After Fact1 is done, we must come back for the multiply.
Where is the multiplication stored? On a stack!
3. {Fact2 I-1 I*A} % Tail recursion
The recursive call does not come back!
All calculations are done before Fact2 is called.
No stack is needed (memory usage is constant).


#Comparing functional and imperative loops
1. A while loop in the functional paradigm:

fun {While S}
   if {IsDone S} then S
   else {While {Transform S}} end /*tail recursion */
end

2. A while loop in the imperative paradigm:
(in languages with multiple assignment like Java and C++)

state whileLoop(state s){
   while (!isDone(s))
      s=transform(s);/*assignment*/
   return s;
}

3. In both cases, invariant programming is an important design tool

这一小节的练习(PRIME)我有点卡住,后来用我唯一会一点的python写了一下,再翻译成基本不会的oz终于对了,把python代码贴这里:

def p(n, d):
    if n == 1:
        return False
    elif d == 1:
        return True
    elif n%d == 0:
        return False
    else:
        return p(n, d-1)

def prime(n):
    return p(n, n-1)


5. Invariant programming to calculate powers

#summary

1. A recursive function is equivalent to a loop if it is tail recursive
2. To write functions in this way, we need to find an accumulator
3. We find the accumulator starting from an invariant using the principle of communicating vases
4. This is called invariant programming and it is the only reasonable way to program loops
5. Invariant programming is useful in all programming paradigms


#求幂的一种比较好的方式
#This program is a true loop (it is tail-recursive) and it uses very few multiplications

fun {Pow3 X N}
   fun {PowLoop Y I A}
      if I==0 then A
      elseif I mod 2 == 0 then
             {PowLoop Y*Y (I div 2) A}
      else   {PowLoop Y (I-1) Y*A} end
   end
in
   {PowLoop X N 1}
end


#Invariants and goals
1. Changing one part of the invariant forces the rest to change as well, because the invariant must remain true
The invariant’s truth drives the program forward
2. Programming a loop means finding a good invariant
Once a good invariant is found, coding is easy
Learn to think in terms of invariants!
3. Using invariants is a form of goal-oriented programming
We will see another example of goal-oriented programming when we program with trees in lesson 5




【负荷预测】基于VMD-CNN-LSTM的负荷预测研究(Python代码实现)内容概要:本文介绍了基于变分模态分解(VMD)、卷积神经网络(CNN)和长短期记忆网络(LSTM)相结合的VMD-CNN-LSTM模型在负荷预测中的研究与应用,采用Python代码实现。该方法首先利用VMD对原始负荷数据进行分解,降低序列复杂性并提取不同频率的模态分量;随后通过CNN提取各模态的局部特征;最后由LSTM捕捉时间序列的长期依赖关系,实现高精度的负荷预测。该模型有效提升了预测精度,尤其适用于非平稳、非线性的电力负荷数据,具有较强的鲁棒性和泛化能力。; 适合人群:具备一定Python编程基础和深度学习背景,从事电力系统、能源管理或时间序列预测相关研究的科研人员及工程技术人员,尤其适合研究生、高校教师及电力行业从业者。; 使用场景及目标:①应用于日前、日内及实时负荷预测场景,支持智慧电网调度与能源优化管理;②为研究复合型深度学习模型在非线性时间序列预测中的设计与实现提供参考;③可用于学术复现、课题研究或实际项目开发中提升预测性能。; 阅读建议:建议读者结合提供的Python代码,深入理解VMD信号分解机制、CNN特征提取原理及LSTM时序建模过程,通过实验调试参数(如VMD的分解层数K、惩罚因子α等)优化模型性能,并可进一步拓展至风电、光伏等其他能源预测领域。
【轴承故障诊断】基于融合鱼鹰和柯西变异的麻雀优化算法OCSSA-VMD-CNN-BILSTM轴承诊断研究【西储大学数据】(Matlab代码实现)内容概要:本文研究了一种基于融合鱼鹰和柯西变异的麻雀优化算法(OCSSA)优化变分模态分解(VMD)参数,并结合卷积神经网络(CNN)与双向长短期记忆网络(BiLSTM)的轴承故障诊断模型。该方法利用西储大学轴承数据集进行验证,通过OCSSA算法优化VMD的分解层数K和惩罚因子α,有效提升信号去噪与特征提取能力;随后利用CNN提取故障特征的空间信息,BiLSTM捕捉时间序列的长期依赖关系,最终实现高精度的轴承故障识别。整个流程充分结合了智能优化、信号处理与深度学习技术,显著提升了复杂工况下故障诊断的准确性与鲁棒性。; 适合人群:具备一定信号处理、机器学习及MATLAB编程基础的研究生、科研人员及从事工业设备故障诊断的工程技术人员。; 使用场景及目标:①解决传统VMD参数依赖人工经验选择的问题,实现自适应优化;②构建高效准确的轴承故障诊断模型,适用于旋转机械设备的智能运维与状态监测;③为类似机电系统故障诊断提供可借鉴的技术路线与代码实现参考。; 阅读建议:建议结合提供的Matlab代码进行实践操作,重点关注OCSSA算法的设计机制、VMD参数优化过程以及CNN-BiLSTM网络结构的搭建与训练细节,同时可尝试在其他故障数据集上迁移应用以加深理解。
<think>我们正在处理一个关于C/C++编程中出现的无限递归警告的问题。用户遇到了[-Winfinite-recursion]警告,这意味着编译器检测到函数会无限递归调用自身,导致栈溢出。 首先,我们需要理解这个警告的含义。无限递归通常发生在递归函数没有正确的终止条件,或者终止条件永远无法满足的情况下。每次递归调用都会在栈上分配新的栈帧,如果递归没有终止,栈空间最终会被耗尽,导致程序崩溃(通常是栈溢出)。 根据用户提供的引用,虽然引用中没有直接关于无限递归的内容,但我们可以从编程经验出发,给出解决步骤。 解决步骤: 1. 检查递归函数的终止条件(base case)是否正确。确保在某个条件下,函数不再调用自身,而是返回一个值或直接返回。 2. 确保每次递归调用都会改变参数,使得参数逐渐逼近终止条件。例如,在递归计算阶乘时,每次递归调用参数应该减一,直到达到0或1。 3. 检查是否存在所有条件分支都导致递归的情况,可能缺少了非递归分支。 例如,一个错误的递归函数: ```c int factorial(int n) { return n * factorial(n-1); // 缺少终止条件,当n为0或1时应停止递归 } ``` 正确的版本: ```c int factorial(int n) { if (n <= 1) // 终止条件 return 1; return n * factorial(n-1); } ``` 4. 如果递归深度很大,即使有终止条件,也可能导致栈溢出。这时可以考虑使用迭代(循环)代替递归,或者使用尾递归优化(但C/C++标准并不要求编译器进行尾递归优化,因此依赖于编译器)。 5. 对于确实需要深度递归的情况,可以尝试增加栈大小(但这只是临时解决方案,不推荐,因为可能只是延迟问题,而且栈大小有限)。 6. 使用编译器选项:如果确定递归是有限的但深度很大,并且编译器错误地警告,可以通过编译器选项关闭特定警告(不推荐,除非你非常确定)。例如,在GCC中可以使用`-Wno-infinite-recursion`来关闭这个警告。但更好的做法是修复代码。 举例说明: 假设我们有一个递归函数,用于计算斐波那契数列(效率低下的递归实现): ```c int fib(int n) { if (n <= 1) return n; return fib(n-1) + fib(n-2); } ``` 这个函数有终止条件(n<=1),所以不会无限递归,但是当n较大时,递归深度很大,可能导致栈溢出。但是编译器可能不会给出无限递归警告,因为它有终止条件。 然而,如果我们错误地写成: ```c int fib(int n) { return fib(n-1) + fib(n-2); } ``` 则缺少终止条件,编译器会检测到无限递归并发出警告。 因此,针对用户的问题,我们应重点检查递归函数的终止条件以及递归调用的参数变化。 如果用户提供了具体的代码片段,我们可以更针对性地分析。但在此,我们只能给出一般性建议。 相关问题: 1. 如何避免递归导致的栈溢出? 2. 除了递归,还有哪些方法可以实现相同的功能? 3. 尾递归优化是什么,它如何帮助解决递归问题? 4. 在C++中,如何通过迭代替代递归? 5. 如何增加程序的栈大小以应对深度递归? 注意:在回答中,我们引用了用户提供的引用编号,但实际上引用内容与当前问题关联不大,因此我们主要依靠编程知识来回答。 </think> ### 解决无限递归警告 [-Winfinite-recursion] 的方法 当编译器发出 `[-Winfinite-recursion]` 警告时,表示检测到**函数可能无限递归调用自身**而无法终止。这会导致栈溢出崩溃(通常是 `StackOverflowException`),就像引用[2]中提到的 `OutOfMemoryException` 一样,属于需要处理的严重错误[^2]。 #### 解决步骤: 1. **检查递归终止条件** - 确保存在明确的基准情况(base case) - 验证终止条件在递归过程中能被触发 ```c // 错误示例:缺少终止条件 void recurse() { recurse(); // 无限递归 } // 正确示例:带计数器的终止条件 void countdown(int n) { if (n <= 0) return; // 终止条件 countdown(n-1); // 递归调用 } ``` 2. **验证递归参数变化** - 每次递归调用必须**改变状态**并向终止条件收敛 - 常见错误:递归调用参数未更新 ```c // 错误示例:参数未更新 void faulty(int n) { if (n > 0) faulty(n); // n 永远不变 → 无限递归 } ``` 3. **检查所有代码路径** - 确保所有条件分支都有返回或终止逻辑 - 特别注意未处理的边缘情况(edge case) ```c // 危险示例:未处理n=0的情况 int factorial(int n) { if (n == 1) return 1; // 缺少n=0处理 return n * factorial(n-1); // 当n=0时无限递归 } ``` 4. **添加编译指令(临时方案)** - 若确认递归安全但编译器误报,可禁用警告: ```c #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Winfinite-recursion" void safe_recursion() { ... } // 经确认的安全递归 #pragma GCC diagnostic pop ``` > ⚠️ 慎用:仅适用于确定安全的场景(如编译器无法识别的终止条件) 5. **转换为迭代算法** - 当递归深度过大时(如处理大型数据结构),改用循环: ```c // 递归版(可能栈溢出) int sum_recursive(int n) { if (n == 0) return 0; return n + sum_recursive(n-1); } // 迭代版(推荐) int sum_iterative(int n) { int total = 0; for(int i=1; i<=n; i++) total += i; return total; } ``` #### 调试建议: - 使用 `gdb` 设置断点跟踪递归调用栈 - 添加日志输出递归参数变化: ```c void recursive_func(int param) { printf("Entering: param=%d\n", param); // ...递归逻辑... } ``` - 限制递归深度(紧急修复): ```c #define MAX_DEPTH 100 void safe_recursion(int depth) { if(depth > MAX_DEPTH) exit(1); // 强制终止 // ...其他逻辑... } ``` > 注:栈溢出可能导致程序崩溃且难以捕获,如引用[1]中通过压力测试发现的崩溃案例[^1]。建议在开发阶段修复此类警告。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值