題號(hào):500, 鍵盤(pán)行
給定一個(gè)單詞列表,只返回可以使用在鍵盤(pán)同一行的字母打印出來(lái)的單詞。鍵盤(pán)如下圖所示。鍵盤(pán)分布圖如下:

這是一個(gè)比較容易但稍微有點(diǎn)麻煩的問(wèn)題。
class Solution:
def findWords(self, words: List[str]) -> List[str]:
keyboardtab = ["qwertyuiop", "asdfghjkl", "zxcvbnm"]
kbdict = {}
res = words
for k, v in enumerate(keyboardtab):
for i in v:
kbdict[i] = k
for word in words:
for s in word[1:]:
if kbdict[s.lower()] != kbdict[word[0].lower()]:
res.remove(word)
break
return res
測(cè)試用例中,運(yùn)行得還可以,但提交后遇到一個(gè)案例輸出有誤。
>>> findWords(["abdfs", "ccdd","a", "qwwewm"])
['ccdd', 'a']
很顯然,正確答案不應(yīng)該包括"ccdd"上。代碼的整體邏輯問(wèn)題不大,可能是循環(huán)過(guò)程中出現(xiàn)的一些問(wèn)題。添加print的語(yǔ)句,查看words和word的變化。
def findWords(self, words: List[str]) -> List[str]:
keyboardtab = ["qwertyuiop", "asdfghjkl", "zxcvbnm"]
kbdict = {}
res = words
for k, v in enumerate(keyboardtab):
for i in v:
kbdict[i] = k
for word in words:
print(words, word)
for s in word[1:]:
if kbdict[s.lower()] != kbdict[word[0].lower()]:
res.remove(word)
break
return res
運(yùn)行樣例后結(jié)果如下:
>>> findWords(["abdfs", "ccdd","a", "qwwewm"])
['abdfs', 'ccdd', 'a', 'qwwewm'] abdfs
['ccdd', 'a', 'qwwewm'] a
['ccdd', 'a', 'qwwewm'] qwwewm
['ccdd', 'a']
問(wèn)題很明顯了,我們的res、words引用了同一個(gè)列表,當(dāng)調(diào)用res.remove(word)時(shí),words也被修改掉了。因此造成外層循環(huán)的錯(cuò)誤,最終導(dǎo)致結(jié)果出錯(cuò)。解決問(wèn)題的方法也很簡(jiǎn)單,將,把for語(yǔ)句中的res=words修改成res=words.copy()即可words改成words[:]更Pythonic一些。在循環(huán)過(guò)程中不要對(duì)正在循環(huán)的對(duì)象做修改,在很多入門(mén)教程中都提到過(guò)很多次,但不經(jīng)意間總還是會(huì)犯這種錯(cuò)誤,引以為鑒吧!
那么,當(dāng)在for循環(huán)中修改正在迭代的對(duì)象,實(shí)際上又發(fā)生了些什么呢?
從上面的錯(cuò)誤案例中,看到的似乎循環(huán)中記住了某個(gè)固定的索引,然后根據(jù)這個(gè)索引返回變化中的列表中對(duì)應(yīng)的元素。但實(shí)際上是這樣么?借助Python官方文檔,嘗試尋找一下答案。
for語(yǔ)句做了些什么?
引用自官方文檔
for語(yǔ)句用于對(duì)序列(例如字符串、元組或列表)或其他可迭代對(duì)象中的元素進(jìn)行迭代:for-stmt :: = for 目標(biāo)列表 in 表達(dá)式列表: 執(zhí)行子句 else:執(zhí)行子句
- 表達(dá)式列表調(diào)用
__iter__()函數(shù),生成一個(gè)迭代器對(duì)象(iterator); - 為迭代器的每一項(xiàng)執(zhí)行一次子句,具體次序與迭代器的次序相同(迭代器通過(guò)調(diào)用
__next__()來(lái)獲取下一項(xiàng)),每一項(xiàng)按照標(biāo)準(zhǔn)賦值語(yǔ)句給目標(biāo)列表。 - 當(dāng)所有項(xiàng)耗盡時(shí)(表達(dá)式列表為空或者迭代器引發(fā) StopIteration),執(zhí)行else 的子句,并退出循環(huán)。
因此當(dāng)在循環(huán)中修改表達(dá)式列表時(shí),每次循環(huán)都會(huì)根據(jù)新列表生成新的迭代器對(duì)象,即便不看官方文檔,這和我們的印象也是符合的。但不斷變化的迭代器是如何保留一個(gè)類(lèi)似索引的變量來(lái)從新列表中進(jìn)行賦值操作的又是一個(gè)問(wèn)題。
好在文檔中連這個(gè)問(wèn)題也考慮到了
序列在循環(huán)過(guò)程中會(huì)有一個(gè)內(nèi)部計(jì)數(shù)器來(lái)追蹤下一個(gè)要使用的項(xiàng),每一次迭代都會(huì)使這個(gè)項(xiàng)增遞增,當(dāng)計(jì)數(shù)器的值達(dá)到列表的長(zhǎng)度時(shí)循環(huán)即停止。這意味著如果在子句中刪除了序列當(dāng)前或之前的一項(xiàng),其后一項(xiàng)再下次循環(huán)中將會(huì)被跳過(guò)。
這將會(huì)帶來(lái)相當(dāng)多的困擾,文檔也貼心的介紹了使用索引 [:] 創(chuàng)建副本作為表達(dá)式列表的方法。
之前關(guān)注的只是循環(huán)中序列取值時(shí)受到的影像,實(shí)際上在循環(huán)中修改序列對(duì)迭代次數(shù)的影像也挺值得關(guān)注,以下就是一個(gè)例子。
>>> l = list(range(10))
>>> for i in l:
print(l, i)
l.pop(i)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 0
[1, 2, 3, 4, 5, 6, 7, 8, 9] 2
[2, 3, 4, 5, 6, 7, 8, 9] 4
[3, 4, 5, 6, 7, 8, 9] 6
[4, 5, 6, 7, 8, 9] 8
每次循環(huán)len(l) - 1,內(nèi)部計(jì)數(shù)器 + 1,因此當(dāng)進(jìn)行到第5次的時(shí)候,len(l) == 內(nèi)部計(jì)數(shù)器,循環(huán)結(jié)束。剛好輸出了所有偶數(shù)項(xiàng)。