返回介绍

5.3 使用数据结构对真实世界建模

发布于 2024-01-22 21:44:06 字数 7023 浏览 0 评论 0 收藏 0

甚至在因特网之前,人们也有办法与世界另一边的某人下一盘国际象棋。每个棋手在自己家里放好一个棋盘,然后轮流向对方寄出明信片,描述每一着棋。要做到这一点,棋手需要一种方法,无二义地描述棋盘的状态,以及他们的着法。

在“代数记谱法”中,棋盘空间由一个数字和字母坐标确定,如图5-1所示。

图5-1 代数记谱法中棋盘的坐标

棋子用字母表示:K表示王,Q表示后,R表示车,B表示象,N表示马。描述一次移动,用棋子的字母和它的目的地坐标。一对这样的移动表示一个回合(白方先下),例如,棋谱2. Nf3 Nc6表明在棋局的第二回合,白方将马移动到f3,黑方将马移动到c6。

代数记谱法还有更多内容,但要点是你可以用它无二义地描述象棋游戏,不需要站在棋盘前。你的对手甚至可以在世界的另一边!实际上,如果你的记忆力很好,甚至不需要物理的棋具:只需要阅读寄来的棋子移动,更新心里想的棋盘。

计算机有很好的记忆力。现在计算机上的程序,很容易存储几百万个像'2. Nf3 Nc6'这样的字符串。这就是为什么计算机不用物理棋盘就能下象棋。它们用数据建模来表示棋盘,你可以编写代码来使用这个模型。

这里就可以用到列表和字典。可以用它们对真实世界建模,例如棋盘。作为第一个例子,我们将使用比国际象棋简单一点的游戏:井字棋。

5.3.1 井字棋盘

井字棋盘看起来像一个大的井字符号(#),有9个空格,可以包含X、O或空。要用字典表示棋盘,可以为每个空格分配一个字符串键,如图5-2所示。

图5-2 井字棋盘的空格和它们对应的键

可以用字符串值来表示,棋盘上每个空格有什么:'X'、'O'或' '(空格字符)。因此,需要存储9个字符串。可以用一个字典来做这事。带有键'top-R'的字符串表示右上角,带有键'low-L'的字符串表示左下角,带有键'mid-M'的字符串表示中间,以此类推。

这个字典就是表示井字棋盘的数据结构。将这个字典表示的棋盘保存在名为theBoard的变量中。打开一个文件编辑器窗口,输入以下代码,并保存为ticTacToe.py:

theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ',
            'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ',
            'low-L': ' ', 'low-M': ' ', 'low-R': ' '}

保存在theBoard变量中的数据结构,表示了图5-3中的井字棋盘。

图5-3 一个空的井字棋盘

因为theBoard变量中每个键的值都是单个空格字符,所以这个字典表示一个完全干净的棋盘。如果玩家X选择了中间的空格,就可以用下面这个字典来表示棋盘:

theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ',
            'mid-L': ' ', 'mid-M': 'X', 'mid-R': ' ',
            'low-L': ' ', 'low-M': ' ', 'low-R': ' '}

theBoard变量中的数据结构现在表示图5-4中的井字棋盘。

图5-4 第一着

一个玩家O获胜的棋盘上,他将O横贯棋盘的顶部,看起来像这样:

theBoard = {'top-L': 'O', 'top-M': 'O', 'top-R': 'O',
            'mid-L': 'X', 'mid-M': 'X', 'mid-R': ' ',
            'low-L': ' ', 'low-M': ' ', 'low-R': 'X'}

theBoard变量中的数据结构现在表示图5-5中的井字棋盘。

图5-5 玩家O获胜

当然,玩家只看到打印在屏幕上的内容,而不是变量的内容。让我们创建一个函数,将棋盘字典打印到屏幕上。将下面代码添加到ticTacToe.py(新代码是黑体的):

theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ',
            'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ',
            'low-L': ' ', 'low-M': ' ', 'low-R': ' '}
def printBoard(board):
    print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R'])
    print('-+-+-')
    print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R'])
    print('-+-+-')
    print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R'])
printBoard(theBoard)

运行这个程序时,printBoard()将打印出空白井字棋盘。

| |
-+-+-
| |
-+-+-
| |

printBoard()函数可以处理传入的任何井字棋数据结构。尝试将代码改成以下的样子:

theBoard = {'top-L': 'O', 'top-M': 'O', 'top-R': 'O', 'mid-L': 'X', 'mid-M':
'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': 'X'}

def printBoard(board):
print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R'])
print('-+-+-')
print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R'])
print('-+-+-')
print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R'])
printBoard(theBoard)

现在运行该程序,新棋盘将打印在屏幕上。

O|O|O
-+-+-
X|X|
-+-+-
| |X

因为你创建了一个数据结构来表示井字棋盘,编写了printBoard()中的代码来解释该数据结构,所以就有了一个程序,对井字棋盘进行了“建模”。也可以用不同的方式组织数据结构(例如,使用'TOP-LEFT'这样的键来代替'top-L'),但只要代码能处理你的数据结构,就有了正确工作的程序。

例如,printBoard()函数预期井字棋数据结构是一个字典,包含所有9个空格的键。假如传入的字典缺少'mid-L'键,程序就不能工作了。

O|O|O
-+-+-
Traceback (most recent call last):
  File "ticTacToe.py", line 10, in <module>
    printBoard(theBoard)
  File "ticTacToe.py", line 6, in printBoard
    print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R'])
KeyError: 'mid-L'

现在让我们添加代码,允许玩家输入他们的着法。修改ticTacToe.py程序如下所示:

 theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': '
 ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '}

 def printBoard(board):
     print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R'])
     print('-+-+-')
     print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R'])
     print('-+-+-')
     print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R'])
 turn = 'X'
 for i in range(9):
❶     printBoard(theBoard)
     print('Turn for ' + turn + '. Move on which space?')
❷     move = input()
❸     theBoard[move] = turn
❹     if turn == 'X':
         turn = 'O'
     else:
         turn = 'X'
 printBoard(theBoard)

新的代码在每一步新的着法之前,打印出棋盘❶,获取当前棋手的着法❷,相应地更新棋盘❸,然后改变当前棋手❹,进入到下一着。

运行该程序,它看起来像这样:

 | |
-+-+-
 | |
-+-+-
 | |
Turn for X. Move on which space?
mid-M
 | |
-+-+-
 |X|
-+-+-
 | |
Turn for O. Move on which space?
low-L
 | |
-+-+-
 |X|
-+-+-
O| |

--_snip_--

O|O|X
-+-+-
X|X|O
-+-+-
O| |X
Turn for X. Move on which space?
low-M
O|O|X
-+-+-
X|X|O
-+-+-
O|X|X

这不是一个完整的井字棋游戏(例如,它并不检查玩家是否获胜),但这已足够展示如何在程序中使用数据结构。

注意

如果你很好奇,完整的井字棋程序的源代码在网上有介绍,网址是http://nostarch.com/automatestuff/

5.3.2 嵌套的字典和列表

对井字棋盘建模相当简单:棋盘只需要一个字典,包含9个键值对。当你对复杂的事物建模时,可能发现字典和列表中需要包含其他字典和列表。列表适用于包含一组有序的值,字典适合于包含关联的键与值。例如,下面的程序使用字典包含其他字典,用于记录谁为野餐带来了什么食物。totalBrought()函数可以读取这个数据结构,计算所有客人带来的食物的总数。

 allGuests = {'Alice': {'apples': 5, 'pretzels': 12},
              'Bob': {'ham sandwiches': 3, 'apples': 2},
              'Carol': {'cups': 3, 'apple pies': 1}}

 def totalBrought(guests, item):
     numBrought = 0
❶     for k, v in guests.items():
❷         numBrought = numBrought + v.get(item, 0)
     return numBrought

 print('Number of things being brought:')
 print(' - Apples ' + str(totalBrought(allGuests, 'apples')))
 print(' - Cups ' + str(totalBrought(allGuests, 'cups')))
 print(' - Cakes ' + str(totalBrought(allGuests, 'cakes')))
 print(' - Ham Sandwiches ' + str(totalBrought(allGuests, 'ham sandwiches')))
 print(' - Apple Pies ' + str(totalBrought(allGuests, 'apple pies')))

在totalBrought()函数中,for循环迭代guests中的每个键值对❶。在这个循环里,客人的名字字符串赋给k,他们带来的野餐食物的字典赋给v。如果食物参数是字典中存在的键,它的值(数量)就添加到numBrought❷。如果它不是键,get()方法就返回0,添加到numBrought。

该程序的输出像这样:

Number of things being brought:
- Apples 7
- Cups 3
- Cakes 0
- Ham Sandwiches 3
- Apple Pies 1

这似乎对一个非常简单的东西建模,你可能认为不需要费事去写一个程序来做到这一点。但是要认识到,这个函数totalBrought()可以轻易地处理一个字典,其中包含数千名客人,每个人都带来了“数千种”不同的野餐食物。这样用这种数据结构来保存信息,并使用totalBrought()函数,就会节约大量的时间!

你可以用自己喜欢的任何方法,用数据结构对事物建模,只要程序中其他代码能够正确处理这个数据模型。在刚开始编程时,不需要太担心数据建模的“正确”方式。随着经验增加,你可能会得到更有效的模型,但重要的是,该数据模型符合程序的需要。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文