0 前言
最近比较忙,到现在才有时间更新博客,这两天刚好学到了函数递归,这是个很有趣的知识,为什么说有趣呢?因为递归这个东西吧,很多人都对它又爱又恨。爱在递归不仅可以轻松简化代码,增加可读性,还能将一些很难解决的算法问题轻松解决,但它又大大加大了程序复杂度,既浪费内存又运行慢,还容易出现各种类似于栈溢出之类的BUG,但这东西好用又是真的好用,因此让人又爱又恨。今天我也尝试着用递归来打打两个精英怪:汉诺塔问题 与 青蛙过河问题。
1 题目概述
在经典汉诺塔问题中,有3根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
(1) 每次只能移动一个盘子;(2) 盘子只能从柱子顶端滑出移到下一根柱子;
(3) 盘子只能叠在比它大的盘子上。请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。
>> 示例1:
输入:A = [2, 1, 0], B = [], C = [] 输出:C = [2, 1, 0]
>> 示例2:
输入:A = [1, 0], B = [], C = [] 输出:C = [1, 0]
>> 提示:
1. 你需要原地修改栈。
2. A中盘子的数目不大于14个。
2 题目分析
汉诺塔问题文字叙述还是有点抽象,于是我画了个图进行模拟:
根据演示很容易能看出,汉诺塔问题的解决就是在保证下面的盘子比上面大的情况下,最终实现将第一个柱子上所有圆盘转移到最后一个圆盘上。那么很容易发现一个规律:假如设第一个柱子为A、第二个为B、第三个为C,转移其他圆盘前要先把最大那个圆盘转移到目标柱上,也就是说将柱A上面的圆盘全部转移到柱B上后才能转移最大的圆盘到柱C,那么以此类推转移完最大的圆盘后,第二大的圆盘转移也是同理,那么就可以根据这一点写出递归算法:将最大的圆盘转移的算法。
首先为了模拟柱子,先定义一个结构体类型来模拟顺序栈:
//定义简单栈
typedef struct MyStack {
int size; //栈长
int* data; //栈数组
}MyStack;
然后针对我创建的顺序栈(柱子)和上述分析写一个移动圆盘的递归函数:
static int count = 0; //移动次数
//移动操作(移动圆盘数,源栈,辅助栈,目标栈)
void Move_1(int num, MyStack *A, MyStack* B, MyStack* C) {
//如果只要移动一个,则直接将A中盘挪到C中
if (1 == num) {
//C栈在尾部接收数据后栈长+1,A栈栈长-1并将尾部数据发送(size-1为栈中最后一个元素下标)
C->data[C->size++] = A->data[--A->size];
return; //直接跳出循环
}
count++;
//将除最大圆盘以外圆盘从栈A移动到栈B,C作为辅助栈
Move_1(num - 1, A, C, B);
//将最大圆盘从栈A移动到栈C
Move_1(1, A, B, C);
//将剩余圆盘从栈B移动到栈C,A作为辅助栈
Move_1(num - 1, B, A, C);
}
最后填入汉诺塔函数中:
#define MAX_NUM 15 //取提示中最多不超过14块圆盘
void hanota_1(int* A, int* ASize, int* B, int BSize, int** C, int* CSize) {
//创建并分配栈
MyStack X, Y, Z;
X.data = A;
X.size = *ASize;
//Y.data = B;
Y.data = (int*)malloc(MAX_NUM * (sizeof(int))); //要及时释放(辅助栈)
Y.size = 0;
//Z.data = C;
Z.data = (int*)malloc(MAX_NUM * (sizeof(int))); //由于要传回main故不能释放
Z.size = 0;
Move_1(*ASize, &X, &Y, &Z);
*C = Z.data;
*CSize = Z.size;
*ASize = 0;
free(Y.data);
}
最后放入main函数中进行用例测试:
//数组打印函数(为了方便测试写的)
void printArr(int* Arr, int ArrSize) {
int i = 0;
printf(" = [");
if (ArrSize > 0) {
for (i = 0; i < ArrSize; i++) {
printf("%d", Arr[i]);
if (i < ArrSize - 1)
printf(", ");
}
}
printf("]\n");
}
//主函数入口
int main() {
int A[] = { 2, 1, 0 };
int ASize = sizeof(A) / sizeof(A[0]);
int B[3];
int BSize = 0;
int* C = NULL;
int CSize = 0;
printf("交换前:\nA"); printArr(A, ASize); //打印交换前数组
printf("B"); printArr(B, BSize);
printf("C"); printArr(&C, CSize);
hanota_1(A, &ASize, B, BSize, &C, &CSize); //汉诺塔替换
printf("交换%d次后:\nA",count); printArr(A, ASize); //打印交换后数组
printf("B"); printArr(B, BSize);
printf("C"); printArr(C, CSize);
return 0;
}
运行结果如下:
运行结果也很成功。由此可见我前面的分析整体来看是正确的。但再一看后我发现其实该代码内的内聚度较低、耦合度较高,作为一名未来的程序员,我当然要尽可能实现高内聚低耦合的理想代码,于是我开始拆解我的代码并建立模块,进行模块化编程,把栈的操作直接细分为创建、数据入栈、数据出栈三个单一功能的函数:
#include <climits>
#define MAX_NUM 15 //取提示中最多不超过14块圆盘
//定义顺序栈
typedef struct SeqStack {
int size; //栈大小
int* data; //栈内数据
int top; //栈顶指针(用于尾插与尾删)
}SeqStack;
//创建栈操作
SeqStack* CreateSeqStack(int size, int* arr) {
SeqStack* S = (SeqStack*)malloc(sizeof(SeqStack));
S->size = size;
S->top = size - 1; //栈顶指针指向最后一个元素
S->data = (int*)malloc(MAX_NUM * sizeof(int));
for (int i = 0; i < size; i++) {
S->data[i] = arr[i];
}
return S;
}
//入栈操作
void PushStack(SeqStack* S, int val) {
//防止越界
if (S->top == (MAX_NUM - 1)) {
printf("已饱和,无法入栈");
return;
}
S->top++;
S->data[S->top] = val;
S->size++;
}
//出栈操作
int PopStack(SeqStack* S) {
if (S->top == -1) {
printf("栈空,无法出栈");
return INT_MIN;
}
int r = S->data[S->top];
S->top--;
S->size--;
return r;
}
因此咱们的汉诺塔操作也可以进一步简化为:
static int count2 = 0; //移动次数
//移动操作(移动圆盘数,源栈,辅助栈,目标栈)
void Move_2(int num, SeqStack* A, SeqStack* B, SeqStack* C) {
//如果只要移动一个,则直接将A中盘挪到C中
if (1 == num) {
int val = PopStack(A);
PushStack(C, val);
count2++; //计数加一
return; //直接跳出循环
}
//将除最大圆盘以外圆盘从栈A移动到栈B,C作为辅助栈
Move_2(num - 1, A, C, B);
//将最大圆盘从栈A移动到栈C
Move_2(1, A, B, C);
//将剩余圆盘从栈B移动到栈C,A作为辅助栈
Move_2(num - 1, B, A, C);
}
void hanota_2(int* A, int* ASize, int* B, int BSize, int** C, int* CSize) {
//创建并分配栈
SeqStack* X = CreateSeqStack(*ASize, A);
SeqStack* Y = CreateSeqStack(BSize, B);
SeqStack* Z = CreateSeqStack(*CSize, *C);
Move_2(*ASize, X, Y, Z);
*C = Z->data;
*CSize = Z->size;
*ASize = 0;
free(Y);
}
填入测试用例后运行结果:
由此可见,依旧是可以顺利运行。那么让我们记录一下二者的运行时间,对比一下两者的运行速度:
#include <time.h>
//主函数入口
int main() {
clock_t start_time_1 = clock();//记录方法一开始时间
//测试用例1
int A[] = { 2, 1, 0 };
int ASize = sizeof(A) / sizeof(A[0]);
int B[3];
int BSize = 0;
int* C = NULL;
int CSize = 0;
printf("交换前:\nA"); printArr(A, ASize); //打印交换前数组
printf("B"); printArr(B, BSize);
printf("C"); printArr(&C, CSize);
hanota_1(A, &ASize, B, BSize, &C, &CSize); //汉诺塔替换
printf("交换%d次后:\nA",count1); printArr(A, ASize); //打印交换后数组
printf("B"); printArr(B, BSize);
printf("C"); printArr(C, CSize);
clock_t end_time_1 = clock(); //记录方法一结束时间
double elapsed_time_1 = (double)(end_time_1 - start_time_1) / CLOCKS_PER_SEC; //计算运行秒数
printf("方法1运行时间:%f秒\n", elapsed_time_1);
printf("\n---------------分割线----------------\n\n");
clock_t start_time_2 = clock();//记录方法二开始时间
//测试用例2
int AA[] = { 3, 2, 1, 0 };
int AASize = sizeof(AA) / sizeof(AA[0]);
int BB[4];
int BBSize = 0;
int* CC = NULL;
int CCSize = 0;
printf("交换前:\nAA"); printArr(AA, AASize); //打印交换前数组
printf("BB"); printArr(BB, BBSize);
printf("CC"); printArr(&CC, CCSize);
hanota_2(AA, &AASize, BB, BBSize, &CC, &CCSize); //汉诺塔替换
printf("交换%d次后:\nAA", count2); printArr(AA, AASize); //打印交换后数组
printf("BB"); printArr(BB, BSize);
printf("CC"); printArr(CC, CCSize);
clock_t end_time_2 = clock(); //记录方法二结束时间
double elapsed_time_2 = (double)(end_time_2 - start_time_2) / CLOCKS_PER_SEC; //计算运行秒数
printf("方法二运行时间:%f秒\n", elapsed_time_2);
return 0;
}
根据上述结果可以见得,方法二哪怕是在进行过更多步的操作后,运行速度也是远超方法一的,由此也能看出代码模块化的牛逼之处。(注:不同电脑不同状态下运行时间是不同的,性能比较好的电脑这里的秒数可能会越界出现0.000000秒之类的数字)
3 完整代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <climits>
#include <time.h>
//*************① 汉诺塔问题**************
#define MAX_NUM 15 //取提示中最多不超过14块圆盘
static int count1 = 0; //方法1移动次数
static int count2 = 0; //方法2移动次数
//*****************方法一******************
//定义简单栈
typedef struct MyStack {
int size; //栈长
int* data; //栈数组
}MyStack;
//移动操作(移动圆盘数,源栈,辅助栈,目标栈)
void Move_1(int num, MyStack *A, MyStack* B, MyStack* C) {
//如果只要移动一个,则直接将A中盘挪到C中
if (1 == num) {
//C栈在尾部接收数据后栈长+1,A栈栈长-1并将尾部数据发送(size-1为栈中最后一个元素下标)
C->data[C->size++] = A->data[--A->size];
count1++; //计数加一
return; //直接跳出循环
}
//将除最大圆盘以外圆盘从栈A移动到栈B,C作为辅助栈
Move_1(num - 1, A, C, B);
//将最大圆盘从栈A移动到栈C
Move_1(1, A, B, C);
//将剩余圆盘从栈B移动到栈C,A作为辅助栈
Move_1(num - 1, B, A, C);
}
void hanota_1(int* A, int* ASize, int* B, int BSize, int** C, int* CSize) {
//创建并分配栈
MyStack X, Y, Z;
X.data = A;
X.size = *ASize;
//Y.data = B;
Y.data = (int*)malloc(MAX_NUM * (sizeof(int))); //要及时释放(辅助栈)
Y.size = 0;
//Z.data = C;
Z.data = (int*)malloc(MAX_NUM * (sizeof(int))); //由于要传回main故不能释放
Z.size = 0;
Move_1(*ASize, &X, &Y, &Z);
*C = Z.data;
*CSize = Z.size;
*ASize = 0;
free(Y.data);
}
//*****************方法二******************
//定义顺序栈
typedef struct SeqStack {
int size; //栈大小
int* data; //栈内数据
int top; //栈顶指针(用于尾插与尾删)
}SeqStack;
//创建栈操作
SeqStack* CreateSeqStack(int size, int* arr) {
SeqStack* S = (SeqStack*)malloc(sizeof(SeqStack));
S->size = size;
S->top = size - 1; //栈顶指针指向最后一个元素
S->data = (int*)malloc(MAX_NUM * sizeof(int));
for (int i = 0; i < size; i++) {
S->data[i] = arr[i];
}
return S;
}
//入栈操作
void PushStack(SeqStack* S, int val) {
//防止越界
if (S->top == (MAX_NUM - 1)) {
printf("已饱和,无法入栈");
return;
}
S->top++;
S->data[S->top] = val;
S->size++;
}
//出栈操作
int PopStack(SeqStack* S) {
if (S->top == -1) {
printf("栈空,无法出栈");
return INT_MIN;
}
int r = S->data[S->top];
S->top--;
S->size--;
return r;
}
//移动操作(移动圆盘数,源栈,辅助栈,目标栈)
void Move_2(int num, SeqStack* A, SeqStack* B, SeqStack* C) {
//如果只要移动一个,则直接将A中盘挪到C中
if (1 == num) {
int val = PopStack(A);
PushStack(C, val);
count2++; //计数加一
return; //直接跳出循环
}
//将除最大圆盘以外圆盘从栈A移动到栈B,C作为辅助栈
Move_2(num - 1, A, C, B);
//将最大圆盘从栈A移动到栈C
Move_2(1, A, B, C);
//将剩余圆盘从栈B移动到栈C,A作为辅助栈
Move_2(num - 1, B, A, C);
}
void hanota_2(int* A, int* ASize, int* B, int BSize, int** C, int* CSize) {
//创建并分配栈
SeqStack* X = CreateSeqStack(*ASize, A);
SeqStack* Y = CreateSeqStack(BSize, B);
SeqStack* Z = CreateSeqStack(*CSize, *C);
Move_2(*ASize, X, Y, Z);
*C = Z->data;
*CSize = Z->size;
*ASize = 0;
free(Y);
}
//数组打印函数
void printArr(int* Arr, int ArrSize) {
int i = 0;
printf(" = [");
if (ArrSize > 0) {
for (i = 0; i < ArrSize; i++) {
printf("%d", Arr[i]);
if (i < ArrSize - 1)
printf(", ");
}
}
printf("]\n");
}
//主函数入口
int main() {
clock_t start_time_1 = clock();//记录方法一开始时间
//测试用例1
int A[] = { 2, 1, 0 };
int ASize = sizeof(A) / sizeof(A[0]);
int B[3];
int BSize = 0;
int* C = NULL;
int CSize = 0;
printf("交换前:\nA"); printArr(A, ASize); //打印交换前数组
printf("B"); printArr(B, BSize);
printf("C"); printArr(&C, CSize);
hanota_1(A, &ASize, B, BSize, &C, &CSize); //汉诺塔替换
printf("交换%d次后:\nA",count1); printArr(A, ASize); //打印交换后数组
printf("B"); printArr(B, BSize);
printf("C"); printArr(C, CSize);
clock_t end_time_1 = clock(); //记录方法一结束时间
double elapsed_time_1 = (double)(end_time_1 - start_time_1) / CLOCKS_PER_SEC; //计算运行秒数
printf("方法1运行时间:%f秒\n", elapsed_time_1);
printf("\n---------------分割线----------------\n\n");
clock_t start_time_2 = clock();//记录方法二开始时间
//测试用例2
int AA[] = { 3, 2, 1, 0 };
int AASize = sizeof(AA) / sizeof(AA[0]);
int BB[4];
int BBSize = 0;
int* CC = NULL;
int CCSize = 0;
printf("交换前:\nAA"); printArr(AA, AASize); //打印交换前数组
printf("BB"); printArr(BB, BBSize);
printf("CC"); printArr(&CC, CCSize);
hanota_2(AA, &AASize, BB, BBSize, &CC, &CCSize); //汉诺塔替换
printf("交换%d次后:\nAA", count2); printArr(AA, AASize); //打印交换后数组
printf("BB"); printArr(BB, BSize);
printf("CC"); printArr(CC, CCSize);
clock_t end_time_2 = clock(); //记录方法二结束时间
double elapsed_time_2 = (double)(end_time_2 - start_time_2) / CLOCKS_PER_SEC; //计算运行秒数
printf("方法二运行时间:%f秒\n", elapsed_time_2);
return 0;
}
4 总结
总的来说,汉诺塔问题还算一个比较简单的问题,本来我还想再写一个青蛙过河问题的,但看到题目我就发现涉及的太多我还未接触过的东西,还得慢慢继续学习下去!
标签:SeqStack,int,printArr,第四天,C语言,汉诺塔,printf,data,size From: https://blog.csdn.net/2403_87130854/article/details/142761228