C语言使用循环与数组替代递归实现汉诺塔

向北走
向北走
发布时间:2026-10-16| 3人浏览过
原创

汉诺塔问题是数据结构中的经典案例,常规解法依赖于递归调用,导致栈空间占用随层数增加而线性增长,且执行路径复杂。本文探讨一种在C语言中利用循环结构与动态数组来模拟递归过程的非递归实现方案。该方法通过迭代生成每一步的磁盘移动指令并存入数组,从而避免函数调用的开销机制,虽然其时间与空间复杂度仍受限于问题本身的指数级特性,但提供了另一种算法视角。

汉诺塔问题与递归原理

汉诺塔问题的核心规则如下:

  1. 初始状态:有三根柱子(设为A、B、C),A柱上有n个大小不一的圆盘,按从上到下序号1至n递增排列。
  2. 目标状态:将所有n个圆盘从A柱移动到C柱。
  3. 约束条件:每次只能移动一个圆盘;任何时候大盘子不能放置在比它小的盘子上方。

递归解题的核心思想是“分治”:要将n个盘子从源柱(From)移动到目标柱(To),需借助辅助柱(Aux):

  • 当 n=1 时:直接将圆盘从 From 移到 To。
  • 当 n>1 时:
    • 先将 n-1 个圆盘从 From 移至 Aux(此时To作为辅助)。
    • 将第 n 个(最大的)圆盘从 From 直接移至 To。
    • 再将那 n-1 个圆盘从 Aux 移至 To(此时From作为辅助)。

基于此逻辑,标准的C语言递归实现代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

void towers(int n, char from, char to, char aux);
int main() {
	towers(3, 'A', 'C', 'B');
	return 0;
}

void showMove (int order, char from, char to) {
	printf("move disk %d from %c to %c\n", order, from, to);
}

void towers(int n, char from, char to, char aux) {
	if (n==1) {
		showMove(n, from, to);
		return;
	}
	towers(n-1, from, aux, to);
	    showMove(n, from, to);
	towers(n-1, aux, to, from);
}

性能分析:递归解法的时间复杂度为 $O(2^n)$,因为总共需要执行 $2^n - 1$ 次移动操作。空间复杂度为 $O(n)$,主要由系统调用栈的深度决定,最多同时保存 n 个函数的上下文信息。

基于循环与数组的非递归实现思路

既然所有的递归都可以转化为循环,关键在于如何管理“调用栈”。在非递归实现中,我们可以显式地维护一个数组,用来存储每一次迭代生成的“移动动作序列”。

核心逻辑推导:

  1. 规模扩展:假设我们已经知道了移动 i-1 个盘子从 A 到 B 的所有步骤集合,保存在数组的前半部分。
  2. 插入最大盘:对于 i 个盘子,中间必然有一个步骤是将第 i 号大盘从源柱直接移到目标柱。
  3. 映射剩余步骤:原本用于移动 i-1 个小盘的步骤,现在需要根据当前的源、目标、辅助柱角色进行坐标变换,并填入数组的后半部分。

实现步骤:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

// 定义移动动作结构体
typedef struct {
    int order;
    char from;
    char to;
} MoveAction;

// 更新动作数组中的某一项
void setMoveAction(MoveAction *p, int order, char from, char to) {
    p->order = order;
    p->from = from;
    p->to = to;
}

// 辅助函数:根据当前柱子角色转换源/目标字符
char compare_map(char c, char left, char right) {
    if (c == left) return right;
    else if (c == right) return left;
    return c;
}

void towers(int n, char from, char to, char aux) {
    MoveAction *actions, action;
    int i, k, size;
    char f, t;

    // 计算理论上的总步数上限并分配内存
    // 注意:实际有效步数为 2^n - 1,此处分配稍作调整以适配索引
    actions = (MoveAction *)calloc(pow(2, n), sizeof(MoveAction));
    
    // 初始化第1个盘子(i=1)的移动动作
    setMoveAction(&actions[0], 1, from, to);
    size = 1; // 当前已记录的步数对应 2^1 - 1 = 1

    // 迭代处理每一个盘子的加入
    for (i = 2; i <= n; i++) {
        /*
         * 循环逻辑说明:
         * 现有 size 步记录了移动 i-1 个盘子的过程。
         * 新的序列由三部分组成:
         * 1. 原序列经过变换后,表示将 i-1 个盘从 From 移到 Aux。
         * 2. 新增一步:将第 i 号盘从 From 移到 To。
         * 3. 原序列经过变换后,表示将 i-1 个盘从 Aux 移到 To。
         */

        // 第一部分:倒序处理或变换原序列前半部分,映射到 Aux 场景
        // 这里为了填充数组后半段,先准备中间的大盘位置,再填充尾部
        
        // 求解集合 {actions[x] | size + 1 <= x <= 2*size }
        // 这部分对应:将 i-1 个盘从 Aux 移动到 To (即最终的第三步)
        for (k = 0; k < size; k++) {
            action = actions[k];
            // 变换坐标系:原(from, to) 变为 (aux, to)
            f = compare_map(action.from, from, aux);
            t = compare_map(action.to, from, aux);
            setMoveAction(&actions[k + size + 1], action.order, f, t);
        }

        // 第二部分:记录第 i 个盘子的移动
        setMoveAction(&actions[size], i, from, to);

        // 第三部分:求解集合 {actions[x] | 0 <= x <= size -1 }
        // 这部分对应:将 i-1 个盘从 From 移动到 Aux (即第一步)
        // 需要原地覆盖原数组内容,或者重新映射。由于后续不再需要旧数据,可直接覆盖
        for (k = 0; k < size; k++) {
            action = actions[k];
            // 变换坐标系:原(from, to) 变为 (from, aux)
            f = compare_map(action.from, to, aux);
            t = compare_map(action.to, to, aux);
            setMoveAction(&actions[k], action.order, f, t);
        }

        // 更新当前步数规模:size_new = size_old * 2 + 1
        size = size * 2 + 1;
    }

    // 输出所有生成的动作
    for (i = 0; i < size; i++) {
        showMove(actions[i].order, actions[i].from, actions[i].to);
    }
    
    free(actions); // 释放内存
}

int main() {
    towers(3, 'A', 'C', 'B');
    return 0;
}

void showMove(int order, char from, char to) {
    printf("move disk %d from %c to %c\n", order, from, to);
}

代码详解:

  • 数据结构:使用 MoveAction 结构体数组存储每一步的操作。注意:在实际生产环境中,建议对内存分配进行检查(如检查 calloc 返回值是否为 NULL)以避免空指针异常。
  • 坐标映射:compare_map 函数用于在逻辑转换中修正柱子的标识符。例如,当我们将“从A到B”的子任务转换为“从A到C”的子任务的一部分时,原本的“目标B”可能变成了现在的“辅助C”。
  • 迭代构建:外层循环从 i=2 到 n,逐层增加盘子数量。内层循环负责将现有的步骤集合按照汉诺塔的逻辑拆分、重定向并合并。

性能评估: 该循环解法同样无法改变汉诺塔问题本质上的指数级步数要求,时间复杂度仍为 $O(2^n)$。由于使用数组显式存储所有步骤,空间复杂度也达到 $O(2^n)$,相比递归的 $O(n)$ 栈空间,这种方式在内存消耗上更为高昂。然而,这种实现方式完全避免了函数调用带来的栈溢出风险,且逻辑流程对于理解状态机的转换具有直观的教学意义。

总结

本文展示了如何在 C 语言中通过循环与数组手动模拟汉诺塔问题的递归逻辑。虽然从工程效率角度看,递归解法更简洁且空间更优,但非递归实现揭示了算法底层的状态转移过程,适用于需要规避深层递归限制或在受限环境中运行场景的学习与研究。

以上就是C语言使用循环与数组替代递归实现汉诺塔的详细内容,更多关于C语言汉诺塔算法的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。