这里似乎有两个不同的问题。
首先 - 优化编译器可以注意到这一点并重写它吗?如果不测试所有编译器和所有版本,很难回答这个问题。我在以下代码上使用 gcc 4.8.4 进行了尝试:
size_t find(size_t uf[], size_t index) {
if (index != uf[index]) {
uf[index] = find(uf, uf[index]);
}
return uf[index];
}
void link(size_t uf[], size_t i, size_t j) {
uf[find(uf, i)] = uf[find(uf, j)];
}
这没有实现按秩优化,但支持路径压缩。我使用优化级别 -O3 编译了它,程序集如下所示:
find:
.LFB23:
.cfi_startproc
pushq %r14
.cfi_def_cfa_offset 16
.cfi_offset 14, -16
pushq %r13
.cfi_def_cfa_offset 24
.cfi_offset 13, -24
pushq %r12
.cfi_def_cfa_offset 32
.cfi_offset 12, -32
pushq %rbp
.cfi_def_cfa_offset 40
.cfi_offset 6, -40
pushq %rbx
.cfi_def_cfa_offset 48
.cfi_offset 3, -48
leaq (%rdi,%rsi,8), %rbx
movq (%rbx), %rax
cmpq %rsi, %rax
je .L2
leaq (%rdi,%rax,8), %rbp
movq 0(%rbp), %rdx
cmpq %rdx, %rax
je .L3
leaq (%rdi,%rdx,8), %r12
movq %rdx, %rax
movq (%r12), %rcx
cmpq %rcx, %rdx
je .L4
leaq (%rdi,%rcx,8), %r13
movq %rcx, %rax
movq 0(%r13), %rdx
cmpq %rdx, %rcx
je .L5
leaq (%rdi,%rdx,8), %r14
movq %rdx, %rax
movq (%r14), %rsi
cmpq %rsi, %rdx
je .L6
call find // <--- Recursion!
movq %rax, (%r14)
.L6:
movq %rax, 0(%r13)
.L5:
movq %rax, (%r12)
.L4:
movq %rax, 0(%rbp)
.L3:
movq %rax, (%rbx)
.L2:
popq %rbx
.cfi_def_cfa_offset 40
popq %rbp
.cfi_def_cfa_offset 32
popq %r12
.cfi_def_cfa_offset 24
popq %r13
.cfi_def_cfa_offset 16
popq %r14
.cfi_def_cfa_offset 8
ret
.cfi_endproc
鉴于中间存在递归调用,看起来这个尾调用并没有被消除。公平地说,这是因为您所描述的转换非常重要,所以我对它没有找到它并不感到惊讶。这并不意味着没有优化编译器可以找到它,但一个主要的编译器不会。
您的第二个问题是为什么我们以这种方式呈现算法。作为同时教授算法和编程的人,我认为使用尽可能简单的演示文稿来讨论算法非常有价值,即使这意味着抽象出一些特定的实现细节。这里,算法背后的关键思想是更新在通往代表的路上遇到的所有节点的父指针。递归恰好是描述该想法的一种非常简洁的方式,即使在天真地实现时它也存在堆栈溢出的风险。但是,通过以这种特定方式表达伪代码,更容易描述和讨论它,并证明它会像宣传的那样工作。我们可以用另一种方式来描述它以避免堆栈溢出,但在 Theoryland 我们通常不会
在查看更高级算法和数据结构的伪代码时,通常会忽略非常重要的实现细节,并暗示某些任务可以在特定时间范围内完成。当讨论建立在更复杂的算法和数据结构之上的算法或数据结构时,通常不可能为所有内容编写伪代码,因为您在层层之上有层,在层之上还有一层层被掩盖的细节。从理论的角度来看,这很好——如果读者确实想实现它,他们可以填补空白。另一方面,如果读者对论文中的关键技术和理论(在学术环境中很常见)更感兴趣,他们就不会陷入实现细节的困境。