递归作为经典的程序结构,是众多算法的核心。
作为一个未经过系统训练的菜鸟,有必要训练一下递归结构。(事实是看到了学长代码里的DFS,倍感精妙,觉得有必要练习一下哈哈哈)
下面是挑选的一些案例。
1. 求阶乘
n个正整数的连乘
n
!
=
n
∗
(
n
−
1
)
∗
(
n
−
2
)
.
.
.
1
n!=n*(n-1)*(n-2)...1
n!=n∗(n−1)∗(n−2)...1
不用递归的话,最直接的想法就是整个for循环:
def factorial(n):
result = 1
for i in range(n):
result = result * (i + 1)
return result
print(factorial(3))
但是不够优雅。那套用递归怎么弄呢?
阶乘递归着写,如下:
n
!
=
n
∗
(
n
−
1
)
!
n!=n*(n-1)!
n!=n∗(n−1)!
所以有
f
a
c
t
o
r
i
a
l
(
n
)
=
n
∗
f
a
c
t
o
r
i
a
l
(
n
−
1
)
factorial(n) = n*factorial(n-1)
factorial(n)=n∗factorial(n−1)
迭代到最后一次
f
a
c
t
o
r
i
a
l
(
1
)
factorial(1)
factorial(1)就不用再往下了,即这一步就是递归基。
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n-1)
print(factorial(3))
(Btw, 这是我写的第一个递归算法,觉得好看的请点赞!哈哈哈)
2. 求斐波那切数列
已知斐波那契数列如下:
不用递归的话,嘶,hmmm,还真不好写
def fibonacci(n):
assert n > 0, f"Positive number only, please."
if n < 3:
return 1
else:
fibo_list = []
fibo_list.append(1)
fibo_list.append(1)
for i in range(2, n):
fibo_list.append(fibo_list[i-1] + fibo_list[i-2])
return fibo_list[n-1]
print(fibonacci(1), fibonacci(4))
小声:初始化列表的时候,fibo_list那一行下面有波浪线,经过查询,发现可以改成如下形式,更加简洁。原链接
def fibonacci(n):
assert n > 0, f"Positive number only, please."
if n < 3:
return 1
else:
fibo_list = [1, 1]
for i in range(2, n):
fibo_list.append(fibo_list[i-1] + fibo_list[i-2])
return fibo_list[n-1]
print(fibonacci(1), fibonacci(4))
递归写法如下:
def fibonacci(n):
assert n > 0, f"Positive number only, please."
if n < 3:
return 1
else:
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(1), fibonacci(4))
可以看到,递归写法更加简洁。(据说算法复杂度比较低,这里就不查了)
3. 汉诺塔问题
汉诺塔问题是指:一块板上有三根针 A、B、C。A 针上套有 64 个大小不等的圆盘,按照大的在下、小的在上的顺序排列,要把这 64 个圆盘从 A 针移动到 C 针上,每次只能移动一个圆盘,移动过程可以借助 B 针。但在任何时候,任何针上的圆盘都必须保持大盘在下,小盘在上。从键盘输入需移动的圆盘个数,给出移动的过程。摘自这个网站
n个盘子本来在A上,现在要把它们移到C上。(姑且叫问题0)
其中必定要经过一步,那就是n-1个盘子落在B上,A上是最大的n号盘。(姑且命名状态1吧)
把A上的n号盘移到C上。
要想达到状态1,需要从原始状态出发,把n-1个盘子移到B上。这个问题和初始问题,问题0相似,只不过数目少了一个,存放盘子的柱子从C变成了B。(姑且叫问题1)
从状态0出发,达到我们最终想要的状态,需要:①把最大的盘子从A移到C。(叫a_move)②把剩下n-1个盘子从B移到C。(姑且叫问题-1)
可见,问题-1和问题1是等价的,只不过移动盘子的方向不同。
总结一下,从原始状态达到状态1需要解决问题-1。从状态1达到最终状态需要a_move+问题1。这样一来,n阶的汉诺塔问题转换成了两个n-1阶的汉诺塔问题和a_move。经过多次降阶,可以降到1阶问题,即只有一个盘子的汉诺塔问题,也就是递归基准。
算法如下:
def Hanoi_tower(n: int, a: str, b: str, c: str):
if n == 1:
return f'No.1 disk was transfered from {a} to {c}'
else:
a_move = f'No.{n} disk was transfered from {a} to {c}'
return Hanoi_tower(n-1, a, c, b) + '\n' + a_move + '\n' + Hanoi_tower(n-1, b, a, c)
print(Hanoi_tower(3, 'A', 'B', 'C'))
4. 最大公约数问题
使用辗转相除法找出最大公约数。两个整数的最大公约数等于其中较小的数和两数相除余数的最大公约数。例如,252和105的最大公约数是21(252 = 21 × 12; 105 = 21 × 5);因为
252
−
2
×
105
=
21
×
(
12
−
5
×
2
)
=
42
252-2\times105=21\times(12-5\times2) =42
252−2×105=21×(12−5×2)=42 ,所以42和105的最大公约数也是21。在这个过程中,较大的数缩小了,所以继续进行同样的计算可以不断缩小这两个数直至其中一个变成零。这时,所剩下的还没有变成零的数就是两数的最大公约数。
摘自这篇帖子
不用递归的话:
#gcd means greatest common diviser
def gcd(a: int, b: int):
max_ = max(a, b)
min_ = min(a, b)
if min_ == 0:
return max_
else:
while min_ != 0:
temp = min_
min_ = max_ % min_
max_ = temp
return max_
print(gcd(252, 105))
用递归的话:
#gcd means greatest common diviser
def gcd(a: int, b: int):
if min(a, b) == 0:
return max(a, b)
else:
return gcd(max(a, b) % min(a, b), min(a, b))
print(gcd(252, 105))
5.八皇后问题
问题表述为:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。百度百科
这个问题最常见的解法是回溯算法,即将列A的皇后放在第一行以后,列B的皇后放在第一行已经发生冲突。这时候不必继续放列C的皇后,而是调整列B的皇后到第二行,继续冲突放第三行,不冲突了才开始进入列C。如此可依次放下列A至E的皇后,如图2所示。将每个皇后往右边横向、斜向攻击的点位用叉标记,发现列F的皇后无处安身。这时回溯到列E的皇后,将其位置由第4行调整为第8行,进入列F,发现皇后依然无处安身,再次回溯列E。此时列E已经枚举完所有情况,回溯至列D,将其由第2行移至第7行,再进入列E继续。按此算法流程最终找到如下图所示的解,成功在棋盘里放下了8个“和平共处”的皇后。继续找完全部的解共92个。百度百科
这种算法的思想是:有冲突解决冲突,没有冲突往前走,无路可走往回退,走到最后是答案。
下面是本人花了一天时间写出来的,没有用到递归的版本:
import numpy as np
def change(x, y, chess_board):
chess_board[x, :] = 0
chess_board[:, y] = 0
for i in range(8):
for j in range(8):
if abs(i - x) == abs(j - y):
chess_board[i, j] = 0
return chess_board
def choose_one(i, chess_board):
for j in range(8):
if chess_board[i][j] == True:
return (i, j)
return False
def back(boards, path_, flag):
chess_board = boards[-1]
chess_board[path_[-1][0]][path_[-1][1]] = 0
if all(chess_board[0, :] == 0) and all(chess_board[1:7, :][1] != 0):
flag = 0
else:
flag = 1
del path_[-1]
del boards[-1]
return boards, path_, chess_board, flag
times = 0
flag = 1
chess_board = np.ones((8, 8))
boards = []
path_ = [(0, 0)]
boards.append(chess_board.copy())
chess_board = change(0, 0, chess_board)
while 1:
while path_.__len__() < 8 and flag == 1:
coods = choose_one(path_.__len__(), chess_board)
if coods == False:
boards, path_, chess_board, flag = back(boards, path_, flag)
else:
boards.append(chess_board.copy())
chess_board = change(coods[0], coods[1], chess_board)
path_.append(coods)
if flag == 1:
times = times + 1
print(path_, '\n')
boards, path_, chess_board, flag = back(boards, path_, flag)
else:
break
print(times)
主要思路是选定一个queen以后,棋盘发生改变,下一个queen的位置只能从有限的位置中选取。
当不能往下走的时候就调用back函数,撤销上一步。为了能返回上一步的棋盘,设置了boards列表存储每一步迭代前的棋盘。
注意:board列表中,同一阶的棋盘只能保留一个,所以在进行撤销操作的过程中,赋值给一个棋盘以后,原棋盘要删掉。
下面是从网上找的回溯算法版本:原链接
class solution(object):
def solveNQueens(self, n):
self.helper([-1]*n, 0, n)
def helper(self, columnPosition, rowindex, n):#ding
# print(rowindex)
if rowindex == n:
self.printSolution(columnPosition, n)
# print(columnPosition)
return
# for column in range(n-rowindex):
for column in range(n):
columnPosition[rowindex] = column
if self.isValid(columnPosition, rowindex):
self.helper(columnPosition, rowindex+1, n)
def isValid(self, columnPosition, rowindex):
if len(set(columnPosition[ :rowindex+1]))!=len(columnPosition[:rowindex+1]):
# print(columnPosition, rowindex)
return False
for i in range(rowindex):
if abs(columnPosition[i]-columnPosition[rowindex]) == int(rowindex-i):
# print(columnPosition, rowindex)
return False
return True
def printSolution(self, columnPosition, n):
# print(columnPosition)
for row in range(n):
line = ""
for column in range(n):
if columnPosition[row] == column:
line += "Q\t"
else:
line += ".\t"
print(line, "\n")
print('\n')
solution().solveNQueens(8)
回溯算法中也用到了递归的思想,其精髓在于for循环与递归调用的结合,return的使用。
6.插入排序
对n个数进行排序,相当于先对n-1个数排序,然后再将第n个数插进来。
#include<stdio.h>
void Insert(int *a,int n)//把数组a的第n个数插入前n-1个数中,注意前n-1个数已经是排好序的了
{
int i=n-1;
int key=a[n];
while((i>=0)&&(key<a[i]))
{
a[i+1]=a[i];
i--;
}
a[i+1]=key;
return;
}
void InsertionSort(int *a,int n)//递归插入,跟求阶乘的思想一样,前n-1个排好序的数组,是建立在前n-2个排好序的数组的基础上插出来的
{
if(n>0)
{
InsertionSort(a,n-1);
Insert(a,n);
}
else
return;
}
void main()
{
int i,n,a[30];
scanf("%d",&n);
for(i=0;i<n;i++)
{
scanf("%d",&a[i]);
}
InsertionSort(a,n-1);
for(i=0;i<n;i++)
{
printf("%d ",a[i]);
}
return;
}