汉诺塔描述:
该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
老师的代码:
//
// Created by A on 2023/4/24.
//
#include <iostream>
using namespace std;
/**
* Hanoi.
*/
void hanoi(int paraN, char paraSource, char paraDestination, char paraTransit) {
if (paraN <= 0) {
return;
} else {
hanoi(paraN - 1, paraSource, paraTransit, paraDestination);
printf("%c -> %c \r\n", paraSource, paraDestination);
hanoi(paraN - 1, paraTransit, paraDestination, paraSource);
}// Of if
}// Of hanoi
/**
* Test the hanoi function.
*/
void hanoiTest() {
printf("---- HanoiTest begins. ----\r\n");
printf(" \r\n 3 plates\r\n");
hanoi(3, 'A', 'B', 'C');
printf("\r\n 4 plates\r\n");
hanoi(4, 'A', 'B', 'C');
printf("\r\n---- HanoiTest ends. ----\r\n");
}// Of addToTest
/**
The entrance.
*/
int main(){
hanoiTest();
return 0;
}// of main
运行结果:
---- HanoiTest begins. ----
3 plates
A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B
4 plates
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
A -> B
C -> B
C -> A
B -> A
C -> B
A -> C
A -> B
C -> B
---- HanoiTest ends. ----
当盘子的个数是N是,我们可以先不看最下面的一个,把上面的N-1个看成一个整体。
第一步:那么就需要我们把N-1个盘子想办法从A通过C这个中转站放到B上。
第二步:将最后的一个最大的盘子放到C上 也就是A-->C。
第三步:将这个N-1个盘子想办法从B通过A这个中转站放到C上,结束。
汉诺塔问题分解:
1.前n-1个移到辅助杆子上。
2.把最后一个移到目标杆子上。
3.把辅助杆子上的移到目标杆子上。
函数设计如下:
hanoi(int paraN, char paraSource, char paraDestination, char paraTransit) {
hanoi(paraN - 1, paraSource, paraTransit, paraDestination);
printf("%c -> %c \r\n", paraSource, paraDestination);
hanoi(paraN - 1, paraTransit, paraDestination, paraSource);
}
除此之外还有非递归实现,也是栈顶操作的体现,代码如下:
//
// Created by A on 2023/4/19.
//
#include<iostream>
#include <math.h>
#include<time.h>
using namespace std;
const int MAX = 64;
struct stack {
int s[MAX];
int top;
char name;
int Top() {
return s[top];
}
int Pop() {
return s[top--];
}
void Push(int x) {
s[++top] = x;
}
};
void Creat(stack ta[], int n) {
ta[0].name = 'A';
ta[0].top = n - 1;
for (int i = 0; i < n; i++) {
ta[0].s[i] = n - i;
}
ta[1].top = ta[2].top = 0;
for (int i = 0; i < n; i++) {
ta[1].s[i] = ta[2].s[i] = 0;
}
if (n % 2 == 0) {
ta[1].name = 'B';
ta[2].name = 'C';
} else {
ta[1].name = 'C';
ta[2].name = 'B';
}
}
void Hanoi(stack ta[], long max) {
int k = 0;
int i = 0;
int ch;
while (k < max) {
ch = ta[i % 3].Pop();
ta[(i + 1) % 3].Push(ch);
cout << ++k << ": " <<
"Move disk " << ch << " from " << ta[i % 3].name << "(地址为" << &ta[i % 3] << ")" <<
" to " << ta[(i + 1) % 3].name << "(地址为" << &ta[(i + 1) % 3] << ")" << endl;
i++;
if (k < max) {
if (ta[(i + 1) % 3].Top() == 0 ||
ta[(i - 1) % 3].Top() > 0 &&
ta[(i + 1) % 3].Top() > ta[(i - 1) % 3].Top()) {
ch = ta[(i - 1) % 3].Pop();
ta[(i + 1) % 3].Push(ch);
cout << ++k << ": " << "Move disk "
<< ch << " from " << ta[(i - 1) % 3].name << "(地址为" << &ta[(i - 1) % 3] << ")"
<< " to " << ta[(i + 1) % 3].name << "(地址为" << &ta[(i + 1) % 3] << ")" << endl;
} else {
ch = ta[(i + 1) % 3].Pop();
ta[(i - 1) % 3].Push(ch);
cout << ++k << ": " << "Move disk "
<< ch << " from " << ta[(i + 1) % 3].name << "(地址为" << &ta[(i + 1) % 3] << ")"
<< " to " << ta[(i - 1) % 3].name << "(地址为" << &ta[(i - 1) % 3] << ")" << endl;
}
}
}
}
int main() {
clock_t start, finish;
int n;
cout << "请输入汉诺塔的阶数:";
cin >> n;
start = clock();
stack ta[3];
Creat(ta, n);
int max = pow(2, n) - 1;
Hanoi(ta, max);
finish = clock();
printf("解决此 %d 阶汉诺塔所需的时间为:%.2f ms\n", n, (double) (finish - start));
system("pause");
return 0;
}
运行结果如下:
请输入汉诺塔的阶数:3
1: Move disk 1 from A(地址为0x61faf0) to C(地址为0x61fbf8)
2: Move disk 2 from A(地址为0x61faf0) to B(地址为0x61fd00)
3: Move disk 1 from C(地址为0x61fbf8) to B(地址为0x61fd00)
4: Move disk 3 from A(地址为0x61faf0) to C(地址为0x61fbf8)
5: Move disk 1 from B(地址为0x61fd00) to A(地址为0x61faf0)
6: Move disk 2 from B(地址为0x61fd00) to C(地址为0x61fbf8)
7: Move disk 1 from A(地址为0x61faf0) to C(地址为0x61fbf8)
解决此 3 阶汉诺塔所需的时间为:5.00 ms
其他计算机思想的理解:
1.自顶向下,逐步求精
“自顶向下, 逐步求精”是结构化程序设计常见的思路。“自顶向下”是将复杂、大的问题划分为小问题,找出问题的关键、重点所在,然后用精确的思维定性、定量地去描述问题。“逐步求精” 是将现实世界的问题经抽象转化为逻辑空间或求解空间的问题。复杂问题经抽象化处理变为相对比较简单的问题。经若干步抽象(精化)处理,最后到求解域中只是比较简单的编程问题。总的来说,就是采用模块分解与功能抽象和分而治之的方法分解细化大问题到足以解决的小问题。
2.递归与分治
递归的定义:
程序调用自身的编程技巧称为递归。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。边界条件与递归方程是递归函数的两个要素,递归函数只有具备了这两个要素,才能在有限次计算后得出结果。
递归优点:结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,为设计算法、调试程序带来很大方便。
递归缺点:递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多,容易导致栈的溢出。优点:结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,为设计算法、调试程序带来很大方便。
缺点:递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多,容易导致栈的溢出。
分治的定义:
分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。
3.形参与实参
形参变量:
形参变量是功能函数里的变量,只有在被调用的时候才分配内存单元,调用结束后立即释放。所以形参只在函数内部有效。
实参变量:
实参可以是常量,变量,表达式,函数等等,但无论是何类型,在进行函数调用是,他们必须有确定的值,以便把这些值拷贝给形参。
形参和实参在内存中有不同的位置:
在函数运行时,形参和实参是不同的变量,他们在内存中处于不同的位置。形参将实参的内容拷贝一份,在该函数运行结束的时候释放,实参内容不变。
4.时间复杂度分析
1.只关注循环执行次数最多的一段代码
2.加法法则:总复杂度等于量级最大的那段代码的复杂度
3.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
(1)可以忽略加法常数:O(2n + 3) = O(2n)
(2)与最高次项相乘的常数可忽略:O(2n^2) = O(n^2)
(3) 最高次项的指数大的,函数随着 n 的增长,结果也会变得增长得更快:O(n^3) > O(n^2)
(4)判断一个算法的(时间)效率时,函数中常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数:O(2n^2) = O(n^2+3n+1)、O(n^3) > O(n^2)
多项式阶:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长。包括,
O(1)(常数阶)、O(logn)(对数阶)、O(n)(线性阶)、O(nlogn)(线性对数阶)、O(n^2)(平方阶)、O(n^3)(立方阶)。
非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。包括,
O(2^n)(指数阶)、O(n!)(阶乘阶)。
O(1) :常量级时间复杂度,只要代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度我们都记作 O(1)。
O(logn)、O(nlogn):如下程序所示,x=log2n,所以,这段代码的时间复杂度就是 O(log2n)。
i=1;
while(i<=n) {
i = i*2;
}
O(m+n)、O(m*n):从代码中可以看出,m和n是表示两个数据规模。我们无法事先评估m和n谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复 杂度就是0(m+n)。针对这种情况,原来的加法法则就不正确了,我们需要将加法规则改为: T1(m) + T2(m) = O(f(m) + g(n))。但是乘法法则继续有效: T1(m)*T2(n) = O(f(m) * f(n))。
int cal(int m, int n) {
int sum_1=0;
int i=1;
for(;i<m;++i){
sum_1 = sum_1 + i;
}
int sum_2 = 0;
int j=1;
for (;j<n;++j){
sum_2 = sum_2 + j;
}
return sum_1 + sum_2;
}
5.空间复杂度分析
空间复杂度:表示算法的存储空间与数据规模之间的增长关系。
void print(int n) {
inti=0;
int[] a = new int[n];
for (i; i <n; ++i) {
a[i] =i* i;
}
for(i=n-1;i>=0;--i){
print out a[i]
}
}
跟时间复杂度分析一样,我们可以看到,第2行代码中,我们申请了一个空间存储变量i,但是它是常最阶的,跟数据规模n没有关系,所以我们可以忽略。第3行申请了一个大小为n的int类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是O(n)。我们常见的空间复杂度就是O(1)、O(n)、 O(n2), 像O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。而且,空间复杂度分析比时间复杂度分析要简单很多。