本关任务:编写代码实现希尔排序。
为了完成本关任务,你需要掌握: 1.如何实现希尔排序; 2.希尔排序的算法分析。
希尔排序
对于插入排序,最好的情况是列表已经基本有序,此时比较次数的时间复杂度是O(n)
。列表越接近有序,插入排序的比较次数就越少。因此,希尔排序以插入排序为基础,将待排序的列表划分为一些子列表,再对每一个子列表执行插入排序,从而实现对插入排序性能的改进。
希尔排序又叫缩小增量排序,划分子列表的特定方法是希尔排序的关键。我们并不是将原始列表直接分成若干个含有连续元素的子列表,而是首先确定一个增量 i 来作为子列表的划分间隔,然后把每间隔为 i 的所有元素选出来组成子列表。在希尔排序的过程中,每一趟排序都将增量不断减小,随着子列表数量的越来越少,无序表整体越来越接近有序,从而能够减少整体排序的比较次数。
图 1 展示的是以 3 为增量的希尔排序。
转存失败重新上传取消
图1 以 3 为增量的希尔排序
若对这个含有 9 个数据项的列表以 3 为间隔来划分,则会分成三个子列表:
将列表中下标为 0、3、6 的数据项分成一组,得到子列表 [54,17,44];
将列表中下标为 1、4、7 的数据项分成一组,得到子列表 [26,77,55];
将列表中下标为 2、5、8 的数据项分成一组,得到子列表 [93,31,20]。
然后分别对每一个子列表执行插入排序,得到如图 2 所示的列表。
转存失败重新上传取消
图2 对每个子列表排序后的结果
这三次插入排序的过程可描述为:
对于子列表 [54,17,44],首先将 17 与 54 进行比较,17 小于 54,于是将 17 插入到 54 之前。然后将 44 与 54、17 比较,于是将 44 插入到 17 与 54 之间,最终得到有序列表 {17,44,54};
对于子列表 [26,77,55],首先将 77 与 26 进行比较,77 大于 26,于是将 77 插入到 26 之后(不需要移动位置)。然后将 55 与 77、26 比较,于是将 55 插入到 26 与 77 之间,最终得到有序列表 {26,55,77};
对于子列表 [93,31,20],首先将 31 与 93 进行比较,31 小于 93,于是将 31 插入到 93 之前。然后将 20 与 93、31 比较,于是将 20 插入到 31 之前,最终得到有序列表 {20,31,93}。
这就完成了第 1 趟希尔排序,虽然这个列表还没有完全排好序,但经过这一趟对子列表的排序之后,列表中的每个元素更加靠近它最终应该处在的位置。
希尔排序的最后一趟排序一定是将增量减少到 1,图 3 是对图 2 中得到的列表以 1 为增量进行希尔排序,即执行标准的插入排序过程。
转存失败重新上传取消
图3 以 1 为增量的排序
通过之前对子列表进行的排序,列表比最开始更加接近有序,此时再进行标准插入排序,能够在一定程度上减少比较和移动的次数。此时,仅需要再进行四次移动就可以完成排序。最后一次插入排序过程中的移动操作有:
将插入项 20 与 26 进行比较,20 小于 26,于是将 26 向右移动一个位置;再将 20 与 17 进行比较,20 大于 17,最终将 20 插入到 17 与 26 之间;
将插入项 31 与 55 进行比较,31 小于 55,于是将 55 向右移动一个位置;再将 31 与 44 进行比较,31 小于 44,于是将 44 向右移动一个位置;再将 31 与 26 进行比较,31 大于 26,最终将 31 插入到 26 与 44 之间;
将插入项 54 与 55 进行比较,54 小于 55,于是将 55 向右移动一个位置;再将 54 与 44 进行比较,54 大于 44,最终将 54 插入到 44 与 55 之间。
对于含有 n 个数据项的列表,希尔排序的增量一般从 n/2 开始,之后的每趟减少到 n/4、n/8……直到 1。图 4 展示了对含有 9 个数据项的列表以 4 为增量划分子列表的一个示例。
转存失败重新上传取消
图4 以 4 为增量的情况
此外,增量序列中的值不应该有除 1 之外的公因子,否则可能会造成前面某一趟分在同一组已经比较过的数据项,在本趟继续分在同一组,此时这些数据项再次相互比较毫无意义,同时还会增加算法的时间,例如 8、4、2、1 这样的序列就不要选取(8、4、2 有公因子 2)。
希尔排序的算法分析
可能你会觉得希尔排序并不会比插入排序好,因为它最后一步执行了一次完整的插入排序。但事实上,最后的一次排序并不需要很多次的比较和移动,因为已经在之前对子列表的排序中实现了部分排序,这使得最后的排序非常高效。
希尔排序的复杂度分析十分复杂,大致是介于O(n)
和O(n2)
之间。使用某些增量值时,它的时间复杂度为O(n2)
。通过改变增量的大小,比如将增量保持在2k−1
(1、3、7、15、31 等),希尔排序的时间复杂度可以达到O(n3/2)
。
根据提示,在右侧编辑器中的 Begin-End 区间补充代码,根据希尔排序的算法思想完成shellSort
和gapInsertionSort
方法,从而实现对无序表的排序。
平台会对你编写的代码进行测试,比对你输出的数值与实际正确的数值,只有所有数据全部计算正确才能通过测试:
测试输入:
54,26,93,17,77,31,44,55,20
输入说明:输入为需要对其进行排序的无序表。
预期输出:
增量为 4 : [20, 26, 44, 17, 54, 31, 93, 55, 77]
增量为 2 : [20, 17, 44, 26, 54, 31, 77, 55, 93]
增量为 1 : [17, 20, 26, 31, 44, 54, 55, 77, 93]
输出说明:输出的是对无序表进行希尔排序的每一趟排序的结果,以列表的形式展现。其中增量的取值从 n/2 开始,之后的每趟减少到 n/4……直到 1。在本例测试数据中,数据项个数为 9,则增量序列为:4、2、1。
测试输入:
49,38,65,97,76,13,27
预期输出:
增量为 3 : [27, 38, 13, 49, 76, 65, 97]
增量为 1 : [13, 27, 38, 49, 65, 76, 97]
提示:
for i in range(0, 30, 5): # 步长为 5
print(i, end=" ")
print('\n')
for j in range(1, 30, 5):
print(j, end=" ")
输出:
0 5 10 15 20 25
1 6 11 16 21 26
'''请在Begin-End之间补充代码, 完成shellSort和gapInsertionSort函数'''
# 希尔排序
def shellSort(alist):
sublistcount = len(alist) // 2 # 设定初始增量为n/2
while sublistcount > 0: # 不断缩小增量,进行多趟排序
for startposition in range(sublistcount): # 每进行一次循环就对某一个子列表进行排序
# 调用gapInsertionSort函数对子列表进行排序
# ********** Begin ********** #
gapInsertionSort(alist,startposition,sublistcount)
# ********** End ********** #
print("增量为", sublistcount, ":", alist)
sublistcount = sublistcount // 2
# 带间隔的插入排序
def gapInsertionSort(alist, start, gap):
for i in range(start + gap, len(alist), gap): # 循环的次数表示插入排序的趟数
currentvalue = alist[i] # 当前插入项的值
position = i # 当前插入项所在的位置
# 当 position-gap 位置有数据项 且 当前插入项小于 position-gap 位置的数据项,
# 就不断地进行以下操作
# 将 position-gap 位置的数据项在子列表中向右移动一个位置
# position 指向 position-gap 位置
# ********** Begin ********** #
while position>=gap and alist[position-gap]>currentvalue:
alist[position]=alist[position-gap]
position=position-gap
# ********** End ********** #
alist[position] = currentvalue # 找到当前插入项的插入位置
本关任务:编写代码实现快速排序。
为了完成本关任务,你需要掌握: 1.如何实现快速排序; 2.快速排序的算法分析。
快速排序
快速排序使用了和归并排序一样的分而治之策略,它的思路是依据一个“基准值”数据项来把列表分为两部分:小于基准值的一部分和大于基准值的一部分,然后每部分再分别进行快速排序,这是一个递归的过程。基准值的作用在于协助拆分列表,一般选择列表的第 1 项作为基准值。基准值在最后排序好的列表里的实际位置,我们通常称之为分割点,是用来对拆分成的两部分分别进行快速排序的关键位置点。
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
找到基准值的位置,设置左标记 leftmark 和右标记 rightmark。
不断地移动左右标记,进行多次比较和交换: ① 左标一直向右移动,遇到比基准值大的就停止; ② 右标一直向左移动,遇到比基准值小的就停止; ③ 把左右标记所指的数据项交换。
继续移动,直到左标记移到右标记的右侧,停止移动。这时右标记所指的位置就是基准值应处的位置,将基准值和这个位置的数据项交换。此时,基准值左边部分的数据项都小于或等于基准值,右边部分的数据项都大于或等于基准值。
然后递归地对左边和右边的部分进行快速排序,当待排序部分只有一个数据项时,递归过程结束。
以下为快速排序的一个简单示例。首先将图 1 中列表的第 1 个数据项 54 作为基准值,然后设置两个位置标记 leftmark 和 rightmark,左标记 leftmark 指向列表的第一项 26,右标记 rightmark 指向列表的最后一项 20。
转存失败重新上传取消
图1 快速排序初始状态
接下来需要不断地把左标记向右移动,直到它指向了一个比基准值更大的数据项。同时不断地把右标记向左移动,直到它指向了一个比基准值更小的数据项。最后交换左、右标记位置的数据项。
首先看左标记,左标记当前所指的 26 小于基准值 54,则继续右移。右移一个位置后指向的 93 大于基准值 54,则停止移动。当前列表状态如图 2 所示。
转存失败重新上传取消
图2 左标记指向的 93 大于基准值 54
接下来看右标记,右标记当前所指的 20 小于基准值 54,则停止移动。当前列表状态如图 3 所示。
转存失败重新上传取消
图3 右标记指向的 20 小于基准值 54
相对于最终的分割点,当前左、右标记所指的两个数据项正位于错误的位置,因此需要对其进行交换。在此示例中,需要将 93 和 20 进行交换,交换后的列表状态如图 4 所示。
转存失败重新上传取消
图4 将 93 和 20 进行交换
重复执行以上步骤:
左标记当前所指的 20 小于基准值 54,则继续右移;右移一个位置后指向的 17 小于基准值 54,则继续右移;右移一个位置后指向的 77 大于基准值 54,则停止移动。
右标记当前所指的 93 大于基准值 54,则继续左移;左移一个位置后指向的 55 大于基准值 54,则继续左移;左移一个位置后指向的 44 小于基准值 54,则停止移动。
当前左、右标记的位置如图 5 所示,将所指向的 77 和 44 进行交换。
转存失败重新上传取消
图5 左、右标记分别指向 77 和 44
接下来继续移动左右标记:
左标记当前所指的 44 小于基准值 54,则继续右移;右移一个位置后指向的 31 小于基准值 54,则继续右移;右移一个位置后指向的 77 大于基准值 54,则停止移动。
右标记当前所指的 77 大于基准值 54,则继续左移;左移一个位置后指向的 31 小于基准值 54,则停止移动。
如图 6 所示,此时右标记移动到了左标记的前方,则右标记所在的位置就是分割点的位置。
转存失败重新上传取消
图6 右标记小于左标记
将基准值 54 与分割点位置的数据项 31 交换后,第 1 趟排序结束,当前列表状态如图 7 所示。可以看到,列表被分成两部分,左部分的所有数据项都比基准值 54 小,右部分的所有数据项都比基准值 54 大。接下来递归地对这两部分再执行快速排序过程,直到列表中只剩一个数据项。
转存失败重新上传取消
图7 第 1 趟排序结果
快速排序的递归算法的“递归三要素”可总结如下:
基本结束条件:列表中仅有 1 个数据项时,自然是排好序的;
缩小规模:根据基准值将列表分为两部分,最好的情况是分为相等规模的两半;
调用自身:将拆分成的两部分分别调用自身进行排序。
快速排序的算法分析
我们可以将快速排序分为两个过程来分析:
拆分的过程,如果对列表的拆分总是发生在列表的中央,那么时间复杂度就是O(logn)
;
移动的过程,每次左右标记移动时都需要将所指的数据项与基准值进行比较,所以时间复杂度是O(n)
。
综合考虑,时间复杂度为O(nlogn)
。但是,如果基准值所在的分割点过于偏离中部,造成左右两部分数量不平衡,则会使算法效率降低。最坏的情况是,拆分成的某一部分始终没有数据,这时时间复杂度就退化到O(n2)
。
在右侧编辑器中的 Begin-End 区间补充代码,根据快速排序的算法思想完成quickSortHelper
和partition
方法,从而实现对无序表的排序。其中quickSortHelper
方法用于对从 first 到 last 位置的数据项所在的列表进行快速排序;partition
方法用于对列表进行拆分,同时返回分割点位置。
平台会对你编写的代码进行测试,比对你输出的数值与实际正确的数值,只有所有数据全部计算正确才能通过测试:
测试输入:
54,26,93,17,77,31,44,55,20
输入说明:输入为需要对其进行排序的无序表。
预期输出:
[17, 20, 26, 31, 44, 54, 55, 77, 93]
输出说明:输出的是对无序表进行快速排序后的结果,以列表的形式展现。
测试输入:
49,38,65,97,76,13,27
预期输出:
[13, 27, 38, 49, 65, 76, 97]
'''请在Begin-End之间补充代码, 完成quickSortHelper和partition函数'''
# 快速排序
def quickSort(alist):
quickSortHelper(alist,0,len(alist)-1)
# 指定当前排序列表开始(first)和结束(last)位置的快速排序
def quickSortHelper(alist,first,last):
if first=pivotvalue and rightmark>=leftmark:
rightmark=rightmark-1
# ********** End ********** #
# 右标小于左标时,结束移动
if rightmark < leftmark:
done = True
# 否则将左、右标所指位置的数据项交换
else:
# ********** Begin ********** #
temp=alist[leftmark]
alist[leftmark]=alist[rightmark]
alist[rightmark]=temp
# ********** End ********** #
# 将基准值就位
temp = alist[first]
alist[first] = alist[rightmark]
alist[rightmark] = temp
return rightmark # 返回基准值位置,即分割点
from binaryTree import BinaryTree
'''请在Begin-End之间补充代码, 完成 preorder()、inorder()、postorder()'''
# 前序遍历
def preorder(tree):
if tree: # 如果当前树的根结点为空,就递归结束
# ********** Begin ********** #
print(tree.key,end=' ')
preorder(tree.leftChild)
preorder(tree.rightChild)
# ********** End ********** #
# 中序遍历和后序遍历跟前序遍历的语句是一样的,只是次序不一样
# 中序遍历
def inorder(tree):
if tree != None:
# ********** Begin ********** #
inorder(tree.leftChild)
print(tree.key,end=' ')
inorder(tree.rightChild)
# ********** End ********** #
# 后序遍历
def postorder(tree):
if tree != None:
# ********** Begin ********** #
postorder(tree.leftChild)
postorder(tree.rightChild)
print(tree.key,end=' ')
# ********** End ********** #
import operator
from parsetree import buildParseTree
'''请在Begin-End之间补充代码, 完成函数postordereval()'''
# 后序遍历法进行表达式求值
def postordereval(parseTree):
opers = {'+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv}
res1 = None # 用来存储左子树求得的值
res2 = None # 用来存储右子树求得的值
if tree: # 如果当前树根结点不为空才进行以下求值操作
# ********** Begin ********** #
fn=opers[tree.getRootVal()]
fn(res1,res2)
res1=postordereval(tree.leftChild)
res2=postordereval(tree.rightChild)
# ********** End ********** #
if res1 and res2: # 如果左子树和右子树都成功返回值
# ********** Begin ********** #
return tree.key
# ********** End ********** #
else:
# ********** Begin ********** #
return
# ********** End ********** #
# ( ( 7 + 4 ) * 9 )
pt = buildParseTree(input())
class Stack():
# 创建空列表实现栈
def __init__(self):
self.__list = []
# 判断是否为空
def is_empty(self):
return self.__list == []
# 压栈,添加元素
def push(self,item):
self.__list.append(item)
# 弹栈,弹出最后压入栈的元素
def pop(self):
if self.is_empty():
return
else:
return self.__list.pop()
class BinaryTree:
# 创建左右子树为空的根结点
def __init__(self,rootObj):
self.key = rootObj # 成员key保存根结点数据项
self.leftChild = None # 成员leftChild初始化为空
self.rightChild = None # 成员rightChild初始化为空
# 把newNode插入到根的左子树
def insertLeft(self,newNode):
if self.leftChild == None:
self.leftChild = BinaryTree(newNode) # 左子树指向由newNode所生成的BinaryTree
else:
t = BinaryTree(newNode) # 创建一个BinaryTree类型的新结点t
t.leftChild = self.leftChild # 新结点的左子树指向原来根的左子树
self.leftChild = t # 根结点的左子树指向结点t
# 把newNode插入到根的右子树
def insertRight(self,newNode):
if self.rightChild == None:
self.rightChild = BinaryTree(newNode) # 右子树指向由newNode所生成的BinaryTree
else:
t = BinaryTree(newNode)
t.rightChild = self.rightChild # 新结点的右子树指向原来根的右子树
self.rightChild = t
# 取得右子树
def getRightChild(self):
return self.rightChild # 返回值是一个BinaryTree类型的对象
# 取得左子树
def getLeftChild(self):
return self.leftChild
# 设置根结点的值
def setRootVal(self,obj):
self.key = obj
# 取得根结点的值
def getRootVal(self):
return self.key
'''请在Begin-End之间补充代码, 完成表达式解析树的建立'''
# 创建表达式解析树
def buildParseTree(fpexp):
fplist = fpexp.split()
pStack = Stack() # 存储树结点的栈
eTree = BinaryTree('') # 创建一个空的二叉树
pStack.push(eTree) # 把刚建立的结点入栈,该结点将最后出栈
currentTree = eTree # 把当前结点设成刚才建立的结点
for i in fplist: # 对每个单词进行从左到右扫描
# 表达式开始
if i == '(':
# 创建一个当前结点的左孩子结点
# 将当前结点入栈,这样等下上升的时候才知道返回到哪里
# 将当前结点下降到左孩子结点
# ********** Begin ********** #
currentTree.insertLeft('')
pStack.push(currentTree)
currentTree=currentTree.getLeftChild()
# ********** End ********** #
# 操作数
elif i not in ['+', '-', '*', '/', ')']:
# 将当前结点的根值设置成把i转化为整数后的结果
# 出栈一个结点作为当前结点的父结点
# 将当前结点上升到 父结点
# ********** Begin ********** #
currentTree.key=int(i)
parent=pStack.pop()
currentTree=parent
# ********** End ********** #
# 操作符
elif i in ['+', '-', '*', '/']:
# 将当前结点的根值设置成操作符i
# 创建一个当前结点的右孩子结点
# 将当前结点入栈
# 将当前结点下降到右孩子结点
# ********** Begin ********** #
currentTree.setRootVal(i)
currentTree.insertRight('')
pStack.push(currentTree)
currentTree=currentTree.getRightChild()
# ********** End ********** #
# 表达式结束
elif i == ')':
# 出栈上升,返回到父结点
# ********** Begin ********** #
parent=pStack.pop()
currentTree=parent
# ********** End ********** #
else:
raise ValueError
return eTree
# 中序遍历法生成全括号中缀表达式
def printexp(tree):
sVal = "" # 用来存储中缀表达式字符串
if tree: # 如果当前树根结点不为空才进行以下操作
# ********** Begin ********** #
if tree.getLeftChild():
sVal='('+printexp(tree.getLeftChild())
sVal = sVal + str(tree.getRootVal())
# ********** End ********** #
if tree.getRightChild():
# ********** Begin ********** #
sVal = sVal + printexp(tree.getRightChild())+')'
# ********** End ********** #
return sVal
pt = buildParseTree(input())
# print(printexp(pt))
from binaryTree import BinaryTree
# 对二叉树进行前序序列化
def serialize(tree):
sVal = "" # 用来存储序列化字符串
if tree: # 如果当前树根结点不为空才进行以下操作
# 按前序遍历的顺序将各个结点值放入到序列化字符串中,同时在每个值后加上字符'!'
# ********** Begin ********** #
sVal=str(tree.key)+"!"
sVal+=serialize(tree.leftChild)
sVal+=serialize(tree.rightChild)
# ********** End ********** #
else:
# 空结点位置放入'#!'
# ********** Begin ********** #
return '#!'
# ********** End ********** #
return sVal
class Stack():
# 创建空列表实现栈
def __init__(self):
self.__list = []
# 判断是否为空
def is_empty(self):
return self.__list == []
# 压栈,添加元素
def push(self,item):
self.__list.append(item)
# 弹栈,弹出最后压入栈的元素
def pop(self):
if self.is_empty():
return
else:
return self.__list.pop()
class BinaryTree:
# 创建左右子树为空的根结点
def __init__(self,rootObj):
self.key = rootObj # 成员key保存根结点数据项
self.leftChild = None # 成员leftChild初始化为空
self.rightChild = None # 成员rightChild初始化为空
# 把newNode插入到根的左子树
def insertLeft(self,newNode):
if self.leftChild == None:
self.leftChild = BinaryTree(newNode) # 左子树指向由newNode所生成的BinaryTree
else:
t = BinaryTree(newNode) # 创建一个BinaryTree类型的新结点t
t.leftChild = self.leftChild # 新结点的左子树指向原来根的左子树
self.leftChild = t # 根结点的左子树指向结点t
# 把newNode插入到根的右子树
def insertRight(self,newNode):
if self.rightChild == None:
self.rightChild = BinaryTree(newNode) # 右子树指向由newNode所生成的BinaryTree
else:
t = BinaryTree(newNode)
t.rightChild = self.rightChild # 新结点的右子树指向原来根的右子树
self.rightChild = t
# 取得右子树
def getRightChild(self):
return self.rightChild # 返回值是一个BinaryTree类型的对象
# 取得左子树
def getLeftChild(self):
return self.leftChild
# 设置根结点的值
def setRootVal(self,obj):
self.key = obj
# 取得根结点的值
def getRootVal(self):
return self.key
'''请在Begin-End之间补充代码, 完成表达式解析树的建立'''
# 创建表达式解析树
def buildParseTree(fpexp):
fplist = fpexp.split()
pStack = Stack() # 存储树结点的栈
eTree = BinaryTree('') # 创建一个空的二叉树
pStack.push(eTree) # 把刚建立的结点入栈,该结点将最后出栈
currentTree = eTree # 把当前结点设成刚才建立的结点
for i in fplist: # 对每个单词进行从左到右扫描
# 表达式开始
if i == '(':
# 创建一个当前结点的左孩子结点
# 将当前结点入栈,这样等下上升的时候才知道返回到哪里
# 将当前结点下降到左孩子结点
# ********** Begin ********** #
currentTree.insertLeft('')
pStack.push(currentTree)
currentTree=currentTree.getLeftChild()
# ********** End ********** #
# 操作数
elif i not in ['+', '-', '*', '/', ')']:
# 将当前结点的根值设置成把i转化为整数后的结果
# 出栈一个结点作为当前结点的父结点
# 将当前结点上升到 父结点
# ********** Begin ********** #
currentTree.key=int(i)
parent=pStack.pop()
currentTree=parent
# ********** End ********** #
# 操作符
elif i in ['+', '-', '*', '/']:
# 将当前结点的根值设置成操作符i
# 创建一个当前结点的右孩子结点
# 将当前结点入栈
# 将当前结点下降到右孩子结点
# ********** Begin ********** #
currentTree.setRootVal(i)
currentTree.insertRight('')
pStack.push(currentTree)
currentTree=currentTree.getRightChild()
# ********** End ********** #
# 表达式结束
elif i == ')':
# 出栈上升,返回到父结点
# ********** Begin ********** #
parent=pStack.pop()
currentTree=parent
# ********** End ********** #
else:
raise ValueError
return eTree
flag = -1
def deserialize(s):
global flag
flag = flag + 1
lst = s.split('!')
# flag每次加1,从0开始对字符进行判断
root = None
# 新建一个树对象来反序列化字符串
if lst[flag] != '#':
# 往树中存值,可递归输入s是因为flag是不断增大的
# 反序列过程中先对根结点进行操作,然后才是左右子树
# ********** Begin ********** #
root=BinaryTree(int(lst[flag]))
# ********** End ********** #
print(root.key, end=' ') # 打印出先序反序列化重构二叉树的过程
# ********** Begin ********** #
root.leftChild=deserialize(s)
root.rightChild=deserialize(s)
# ********** End ********** #
return root
# -*- coding: utf-8 -*-
'''请在Begin-End之间补充代码, 完成Vertex类和Graph类'''
class Vertex:
def __init__(self,key):
self.id = key # 顶点的键值
self.connectedTo = {} # 顶点的邻接列表
# 增加一条连接顶点nbr的边,边上的权重为weight
def addNeighbor(self,nbr,weight=0):
self.connectedTo[nbr] = weight
# 将顶点字符串化
def __str__(self):
return str(self.id) + ' connectedTo: ' + str([x.id for x in self.connectedTo])
# 得到与该顶点所连接的其他顶点
def getConnections(self):
# ********** Begin ********** #
return self.connectedTo.keys()
# ********** End ********** #
# 返回顶点的key
def getId(self):
# ********** Begin ********** #
return self.id
# ********** End ********** #
# 返回连接到顶点nbr的边的权重
def getWeight(self,nbr):
# ********** Begin ********** #
return self.connectedTo[nbr]
# ********** End ********** #
class Graph:
def __init__(self):
self.vertList = {} # 初始化主列表为空
self.numVertices = 0 # 初始化顶点数量
# 将顶点key插入图中
def addVertex(self,key):
# 顶点数量加一
# 新建一个键值为key的顶点newVertex
# ********** Begin ********** #
self.numVertices = self.numVertices + 1
newVertex = Vertex(key)
# ********** End ********** #
self.vertList[key] = newVertex
return newVertex
# 查找顶点n
def getVertex(self,n):
if n in self.vertList:
# ********** Begin ********** #
# 通过key查找顶点
return self.vertList[n]
# ********** End ********** #
else:
return None
def __contains__(self,n):
return n in self.vertList
# 添加从顶点f到顶点t的一条边,边上权重设置为cost
def addEdge(self,f,t,cost=0):
# 首先判断这两个顶点是否存在,若不存在就调用addVertex方法,将它加入到图中
# ********** Begin ********** #
if f not in self.vertList:
nv=self.addVertex(f)
# ********** End ********** #
if t not in self.vertList:
nv = self.addVertex(t)
# 在这条边的起始顶点f相关联的列表上,调用addNeighbor方法将边添加进去
# ********** Begin ********** #
self.vertList[f].addNeighbor(self.vertList[t],cost)
# ********** End ********** #
# 返回图中所有顶点列表
def getVertices(self):
return self.vertList.keys()
# 迭代的特殊方法,使用for循环取出图中顶点
def __iter__(self):
return iter(self.vertList.values())
from graphs import Graph, Vertex
'''请在Begin-End之间补充代码, 完成buildGraph()'''
def buildGraph(wordFile):
d = {}
g = Graph()
wfile = open(wordFile,'r') # 打开单词列表文件
# 把单词分成几组,每组单词之间相差一个字母
for line in wfile:
word = line[:-1]
for i in range(len(word)):
bucket = word[:i] + '_' + word[i+1:]
# 如果d中存在标签为bucket的桶,就把该单词word添加到对应的桶中
# 否则,创建一个标签为bucket的桶,且将word以列表形式存入
# ********** Begin ********** #
if bucket in d:
# 4字母单词可属于4个桶
d[bucket].append(word)
else:
d[bucket] = [word]
# ********** End ********** #
# 在同一个存储桶中为单词添加顶点和边
for bucket in d.keys():
for word1 in d[bucket]:
for word2 in d[bucket]:
# 对桶中两个不相等的单词调用addEdge()进行添加边操作
# ********** Begin ********** #
if word1 != word2:
g.addEdge(word1, word2)
# ********** End ********** #
return g
from graphs import Graph, Vertex
from queue import Queue
'''请在Begin-End之间补充代码, 完成bfs和traverse函数'''
def bfs(g,start):
start.setDistance(0) # 设置距离为0
start.setPred(None) # 设置前驱结点为None
vertQueue = Queue()
vertQueue.enqueue(start) # 将当前结点入队
while (vertQueue.size() > 0): # 若队列不为空,循环执行以下操作
currentVert = vertQueue.dequeue() # 出队一个结点作为当前顶点
for nbr in currentVert.getConnections(): # 对当前顶点邻接列表中的顶点进行迭代检查
# 若颜色是白色,则该结点未被探索
if (nbr.getColor() == 'white'):
# 将nbr其设置为灰色'gray'
# nbr的距离被设置为当前结点的距离加一
# nbr的前驱结点被设置为当前结点currentVert
# nbr入队,被加入队尾
# ********** Begin ********** #
nbr.setColor('gray')
nbr.setDistance(currentVert.getDistance()+1)
nbr.setPred(currentVert)
vertQueue.enqueue(nbr)
# ********** End ********** #
currentVert.setColor('black')
# 通过前驱结点链接来打印出整个词梯。
def traverse(y):
x = y
while (x.getPred()):
# 打印顶点x的key
# 前驱结点成为新的x
# ********** Begin ********** #
print(x.getId())
x = x.getPred()
# ********** End ********** #
print(x.getId())
第1关:骑士周游问题
from graphs import Graph, Vertex
'''请在Begin-End之间补充代码, 完成genLegalMoves、knightGraph和knightTour函数'''
# 合法走棋位置函数
def genLegalMoves(x, y, bdSize):
# 存储八个合法走棋位置
newMoves = []
# 马走日8个格子的坐标偏移值
moveOffsets = [(-1, -2), (-1, 2), (-2, -1), (-2, 1),
(1, -2), (1, 2), (2, -1), (2, 1)]
for i in moveOffsets:
newX = x + i[0]
newY = y + i[1]
# 调用legalCoord方法判断newX和newY是否走出棋盘
# 只有落在棋盘里的才通过append加到newMoves里
# ********** Begin ********** #
if legalCoord(newX,bdSize) and legalCoord(newY,bdSize):
newMoves.append((newX,newY))
# ********** End ********** #
return newMoves
# 确认不会走出棋盘
def legalCoord(x, bdSize):
if x >= 0 and x < bdSize: # 不得超出正方形棋盘的边界
return True
else:
return False
# 构建走棋关系图
def knightGraph(bdSize):
# 建立空图ktGraph
ktGraph = Graph()
# 遍历每个格子
for row in range(bdSize):
for col in range(bdSize):
# 将每个格子都编号为nodeId
nodeId = posToNodeId(row, col, bdSize)
# 单步合法走棋
newPositions = genLegalMoves(row, col, bdSize)
# 对每个位置进行判断
for e in newPositions:
nid = posToNodeId(e[0], e[1], bdSize)
# 将顶点和形成的边加到图ktGraph中
# ********** Begin ********** #
ktGraph.addEdge(nodeId,nid)
# ********** End ********** #
return ktGraph
# 根据棋盘行、列确定索引值
def posToNodeId(row, col, bdSize):
return row * bdSize + col
def knightTour(n, path, u, limit):
# n:层次; path:路径; u:当前顶点; limit:搜索总深度
u.setColor('gray') # 当前顶点设为灰色,表示正在探索
path.append(u) # 当前顶点加入路径
if n < limit:
nbrList = list(u.getConnections()) # 对当前顶点连接的所有合法移动逐一深入
i = 0
done = False
while i < len(nbrList) and not done:
# 选择白色未经过的顶点深入
# 层次加1,递归调用knightTour深入
# ********** Begin ********** #
if nbrList[i].getColor()=='white':
done=knightTour(n+1,path,nbrList[i],limit)
# ********** End ********** #
i = i + 1
# 都无法完成总深度,回溯,试本层下一个顶点
if not done:
path.pop()
u.setColor('white')
else:
done = True
return done
from graphs import Graph, Vertex
'''请在Begin-End之间补充代码, 完成orderByAvail和knightTourBetter函数'''
# 合法走棋位置函数
def genLegalMoves(x, y, bdSize):
newMoves = []
moveOffsets = [(-1, -2), (-1, 2), (-2, -1), (-2, 1),
(1, -2), (1, 2), (2, -1), (2, 1)]
for i in moveOffsets:
newX = x + i[0]
newY = y + i[1]
if legalCoord(newX, bdSize) and legalCoord(newY, bdSize):
newMoves.append((newX, newY))
return newMoves
# 确认不会走出棋盘
def legalCoord(x, bdSize):
if x >= 0 and x < bdSize:
return True
else:
return False
# 构建走棋关系图
def knightGraph(bdSize):
ktGraph = Graph()
for row in range(bdSize):
for col in range(bdSize):
nodeId = posToNodeId(row, col, bdSize)
newPositions = genLegalMoves(row, col, bdSize)
for e in newPositions:
nid = posToNodeId(e[0], e[1], bdSize)
ktGraph.addEdge(nodeId, nid)
return ktGraph
# 根据棋盘行、列确定索引值
def posToNodeId(row, col, bdSize):
return row * bdSize + col
def orderByAvail(n):
resList = []
for v in n.getConnections():
if v.getColor() == 'white':
c = 0
for w in v.getConnections():
# 若w未被搜索过,颜色是白色,则c的值加1
# ********** Begin ********** #
if w.getColor()=='white':
c=c+1
# ********** End ********** #
resList.append((c, v))
# 对有合法移动目标格子数量的顶点进行从小到大排序
# ********** Begin ********** #
resList.sort(key=lambda x:x[0])
# ********** End ********** #
return [y[1] for y in resList]
def knightTourBetter(n, path, u, limit): # use order by available function
u.setColor('gray')
path.append(u)
if n < limit:
# 调用orderByAvail函数,将当前结点的有序合法移动位置存入nbrList
# ********** Begin ********** #
nbrList=orderByAvail(u)
# ********** End ********** #
i = 0
done = False
while i < len(nbrList) and not done:
# 选择白色未经过的顶点深入
# 层次加1,递归调用knightTourBetter深入
# ********** Begin ********** #
if nbrList[i].getColor()=='white':
done=knightTourBetter(n+1,path,nbrList[i],limit)
# ********** End ********** #
i = i + 1
if not done: # prepare to backtrack
path.pop()
u.setColor('white')
else:
done = True
return done
from graphs import Graph
'''请在Begin-End之间补充代码, 完成DFSGraph中的dfs和dfsvisit函数'''
class DFSGraph(Graph):
def __init__(self):
super().__init__()
self.time = 0 # 时间实例变量
self.resList = [] # 存储遍历序列
def dfs(self):
for aVertex in self:
aVertex.setColor('white')
aVertex.setPred(-1)
for aVertex in self:
# 如果顶点的颜色为白色'white',调用dfsvisit函数探索顶点
# ********** Begin ********** #
if aVertex.getColor()=='white':
self.dfsvisit(aVertex)
# ********** End ********** #
def dfsvisit(self, startVertex):
startVertex.setColor('gray')
self.resList.append(startVertex)
self.time += 1
startVertex.setDiscovery(self.time)
for nextVertex in startVertex.getConnections():
# 如果顶点nextVertex的颜色为白色'white',则表示未被探索
# 设置其前驱为startVertex
# 递归调用dfsvisit函数进行更深层次的探索
# ********** Begin ********** #
if nextVertex.getColor()=='white':
nextVertex.setPred(startVertex)
self.dfsvisit(nextVertex)
# ********** End ********** #
startVertex.setColor('black')
self.time += 1
startVertex.setFinish(self.time)