递归思想是算法编程中的重要思想。
作为初学者,对递归编程表示很蒙逼,每次遇到需要递归的问题,心里就有一万头草泥马飞过~~~~~~(此处略去一万头草泥马)
在B站看数据结构与算法的视频时,视频中给了两个非常典型的例子——《汉诺塔》和《八皇后问题》,就希望自己用Python实现一下这两个递归程序,其中汉诺塔问题比较简单,还是能够理解,这里就不讲了。
《八皇后问题》:说要在一个棋盘上放置8个皇后,但是不能发生战争,皇后们都小心眼,都爱争风吃醋,如果有人和自己在一条线上(水平、垂直、对角线)就会引发撕13大战,所以我们就是要妥当的安排8位娘娘,以保后宫太平。[1]
与网上普遍搜到的利用yield函数完成递归方法不同,注意时因为对于初学Python的我来说,完全搞不懂yield怎么用,更不要说将其放在递归程序里了, [捂脸]
下面我会对我的代码进行逐一解释,希望更多的像我这样的初学者能够看懂。!!!敲黑板!!!------------不仅要看懂,还要自己会写代码-----------------
首先声明:
代码中位置状况采用list列表的形式, 如,chess[0] = 4代表在棋盘第1行的第5个位置放置皇后。
count = 0 # 定义一个全局变量count, 用于八皇后方案的统计 # conflict 函数为冲突函数,主要用于判断新添加的位置是否与前面的位置冲突 # pos 为新添加的位置, chess为添加pos位置前的皇后位置分布 def conflict(pos, chess): len_chess = len(chess) # 现在已经分配了几行,几有几个皇后在棋盘上,因为初始chess为空,每次判断一个符合规则的位置后再加入到棋盘中 for i in range(len_chess):if abs(chess[i] - pos) in (0, len_chess - i): # 这一行为关键,太多了就放后面了 return True return False # Queen函数为递归函数 def Queens(num,chess): global count # python 中要再函数中使用之前定义的全局变量,必须再函数中用 global 声明一下 if len(chess) == 8: # 判断chess的长度,当chess长度为8的时候,说明8个皇后都已经全部放好位置了,就可以直接输出相应的方案了 count += 1 print(chess) else: # chess长度小于8,说明还有没安排好的皇后,所以继续安排剩下的位置 for pos in range(num): # 对接下来的一行的8个位置进行遍历 if not conflict(pos, chess): # 对每个位置进行冲突判断 chess1 = [] #关键是这三行,用于构造棋盘的副本,这三句非常重要,具体原因放在后面解释 for i in chess: chess1.append(i) chess1.append(pos) # 符合规则,则将其附在chess副本的后面 Queens(num,chess1) # 递归调用Queens函数 board = [] # 初始化空棋盘 Queens(8,board) # 调用函数 print(count) # 打印总共有多少中方案
冲突判断
def conflict(pos, chess): len_chess = len(chess) # 现在已经分配了几行,几有几个皇后在棋盘上,因为初始chess为空,每次判断一个符合规则的位置后再加入到棋盘中 for i in range(len_chess):if abs(chess[i] - pos) in (0, len_chess - i): # 这一行为关键,太多了就放后面了 return True return False
根据规则:有三种情况存在冲突,我i们一一来列举分析一下:
- 在同一列的情况,若两个皇后在同一列,那么这两行的纵坐标相同,反映到我们的棋盘定义上则未:chess[i] = chess[j] 即第i行与第j行的两个皇后在同一列了
- 第二种情况则为在“\” 对角线上,那么各位置的(横坐标 - 纵坐标)的值时一样的,假设chess[i] = a1,chess[j] = a2在对角线"\"上,i - a1 = j - a2。 如(0,1),(1,2),(2,3)横纵坐标之差都为1
- 第三种情况则为在" / " 对角线上,那么各位置的(横坐标 + 纵坐标)的值时一样的,假设chess[i] = a1,chess[j] = a2在对角线"\"上,i + a1 = j + a2。 如(0,3),(1,2),(2,1)横纵坐标之和都为1
我们再观察,就会发现(这个我自己想时想不出来的,也是对照着代码思考出来的):
对于被判断是否符合要求的元素,其索引恰好为现在chess的长度,因为最后一个元素的索引为len(chess)-1,那么新的元素的索引恰为len(chess)
我们另 len_chess 为当前chess的长度,则三种情况对应的表达式为:
- chess[i] - pos = 0
- chess[i] - i = pos - len_chess ---------> chess[i] - pos = i-len_chess
- chess[i] + i = pos + len_chess ---------> chess[i] - pos = len_chess - i
由于 len_chess > i, 所以可以得到当abs(chess[i] - pos)==0 或len_chess - i时则不符合规则,返回True :
棋盘副本建立
for pos in range(num): #对接下来的一行的8个位置进行遍历 if not conflict(pos, chess): #对每个位置进行冲突判断 chess1 = [] #关键是这三行,用于构造棋盘的副本,这三句非常重要,具体原因放在后面解释 for i in chess: chess1.append(i) chess1.append(pos) # 符合规则,则将其附在chess副本的后面 Queens(num,chess1) # 递归调用Queens函数
这里首先提一点的是,python里面所有如果要将一个list拷贝一份,不能直接像C语言里面一样用幅值语句,需要重新初化,
我个人的理解是:因为list的名称为一个地址,当我们将一个地址幅值给一个新的变量时,新的变量指向的还是原来那个数组,所以并没有起到拷贝的作用。 比较典型的例子就是规定大小的二维数组的建立
言归正传,我们为什么要新建立一个副本呢?
- 因为我们需要使用副本来进行深层次的递归,如果不使用副本chess1而直接使用原始数据chess,那么随着递归的深入,chess所对应的地址内的值在不断变化,当返回到递归点的时候,原始数据已经不存在了,
只会从当前chess对应的数组继续进行递归。
- 比如,当 chess = [0,2,4,1,3]时,此时我们需要填充第6行皇后的位置,但是通过判断,发现第6行所有位置都不符合要求,那么此时程序应该退回chess = [0,2,4,1,4]再继续判断第6行是否有符合的位置,但是若不用副本
的话,程序会退回,但是chess所对应的值还是[0,2,4,1,3],这样是错误的。
QAQ: 这一段我也没有讲的特别清楚,主要关键就是这个副本的建立使原始数据不受破坏,可以在递归从深层跳出的时候保持为原来的状态继续进行,可以在编译器中单步运行看一看再理解会更好
[1]. 摘自http://www.cnblogs.com/littleseven/p/5362791.html, 斯认为该博主写的还可以,