哄好面試官系列-1: 比較2個python dict(多級)是否相同
工作面試是個很有意思的過程, 面試經常是一個對未知領域初步瞭解的最好時機(對雙方都是), 面試官和麵試人通常也會盡力在最短的時間裡表達/接受盡可能多的資訊.
因此面試題一般也是比較有趣的: 它濃縮了日常工作中的典型和有挑戰性的問題, 而又不會帶有太多日常工作中的繁瑣.
在技術面試中, 要哄好面試官, 最重要的無疑是能把一個問題解釋的完善嚴謹.
於是打算收集一些 有趣 的問題, 跟大家分享. 本次先嘮嘮這個:
問題: 比較2個多級dict是否相同
2個多級 dict
可以看成2個樹的對比, 此類問題應該在刷題網站上有不少了, 似乎用樹的遍歷就可以了, 然而可達鴨認為事情並不簡單. 在深入答案之前, 我們先明確下問題的描述:
-
一個
dict
中有多個key, 每個key對應一個子dict
. 為了簡化問題, 假設dict
中的key對應的value都是dict
, 沒有其他型別. -
如果能夠用來訪問
a
的所有的key也可以用來訪問b
, 就認為2個dict相同, 例如:a = {'foo':{'bar':{}}}; b = {'foo':{'bar':{}}}
就是一對相同的dict:-
a['foo']
和b['foo']
都存在. -
a['foo']['bar']
和b['foo']['bar']
也都存在.
-
思路: 遞迴
比較2個 dict
同構的思路很直接:
-
對於2個
dict
:a
和b
, 先比較這2個dict
各自的key的集合一致, 如果不一致肯定2個dict
不一樣. -
再逐個對比每個key對應的子
dict
是否一樣. 直到遍歷完所有的子dict
.
def eq(a, b): for k in set(a) | set(b): if k not in a or k not in b: return False if not eq(a[k], b[k]): return False return True print eq({}, {'x':{}}) # False print eq({'x':{}}, {'x':{}}) # True
上面的程式碼差不多可以把大部分面試官哄到6成滿意度.
但在實際使用中, 上面的程式碼還不太完善, 因為 dict
構成的圖的節點之間可能存在 環形引用的情況 , 如果有環, 上面的程式碼就會出現呼叫棧溢位. 所以在上面的程式碼基礎上, 還需要加入有對環的處理.
幾乎所有的語言對函式的遞迴呼叫層數都有限制, 例如python的限制是1000.
處理有環的情況
用python來舉例描述2個有環的 dict
結構, 如下:
a = {} b = {} a['x'] = {}# a1 a['x']['x'] = a b['x'] = {}# b1 b['x']['x'] = {}# b2 b['x']['x']['x'] = b
畫出上面2個圖的引用關係是醬的(其中a1, b1, b2等用來表示a, b中其他的子table):
這裡我們認為 a
和 b
是 訪問相等 的: 因為對於訪問者來說, 無法區分 a
和 b
的差別: 能用來訪問 a
的路徑, 也可以用來訪問 b
, 反之也一樣 :
對於 a
和 b
來說:
a和 b都是合法的 a['x']和 b['x']都是合法的 a['x']['x']和 b['x']['x']都是合法的 a['x']['x']['x'] 和 b['x']['x']['x'] 都是合法的
在這個有環的例子中, 可以看出:
肯定存在一些公共路徑是無限長的.
現在我們需要改進演算法, 檢查出環形的路徑並及時終止遞迴遍歷.
-
如果某個路徑在
a
中走到一個環上, 但在b
中沒有在一個環上, 就不用做特殊處理, 這種情況會自然的結束遞迴. -
需要處理的是一個路徑p在
a
,b
上都對應到一個環的情況.
還是拿上面的各自成環的 a
, b
為例, 對於一個無限長的公共路徑p, p = [x, x, x...]
, 它的每一步都通過 key=x
訪問到下一個子 dict
. 這有, 它的每一步分別訪問到的 a
, b
上的 節點 如下 ( a0
和 b0
分別是a和b的根節點. a1
, b2
等是其他的節點:
a->a0, b0<- b a['x']->a1, b1<- b['x'] a['x']['x']->a0, b2<- b['x']['x'] a['x']['x']['x'] ->a1, b0<- b['x']['x']['x'] ...->a0, b1<- ... a1, b2 a0, b0 ...
觀察下上面的步驟可以發現, 最後路徑p又會回到a0, b0的位置.
因為節點對的數量是有限的, 最多不超過 |a| * |b|
個( |a|
是a中的節點數), 那麼如果一個路徑p是無限長的, 那最終一定會在再次回到一個已經訪問過的 節點對 (上例中的 a0, b0
).
找到這個規律, 我們就有了剔除無限長路徑的思路:
比較2個(可能有環型引用的)dict的演算法:
-
遍歷(廣度優先/深度優先都可以), 枚舉出所有查詢路徑p
-
對一個路徑p, 檢查它的一步
p[i]
是否都能在a
和b
中走通, 如果不能, 則a
和b
存在一個不一致的路徑, 失敗退出. -
過濾出無限長的路徑:
記錄路徑p在圖
a
,b
中經過的 節點對 , 如果p[i]
訪問到一個已經經過的 節點對 , 則認為這個路徑是環的, 不需要繼續檢查了, 回溯去檢查其他路徑.
python實現
def eq(a, b, walked=None): walked = walked or {} if (id(a), id(b)) in walked: return True walked[(id(a), id(b))] = True for k in set(a) | set(b): if k not in a or k not in b: return False if not eq(a[k], b[k], walked): return False return True print eq({}, {'x':{}}) # False print eq({'x': {}}, {'x':{}}) # True a = {} b = {} a['x'] = a b['x'] = {} b['x']['x'] = b print eq(a, b) # True
上面程式碼中, id()
用來取得一個物件的唯一id(原始型別的int, string等, 引用型別的dict, list object都可以使用), 可以理解為c語言中的指標的角色.
效率分析
假設 a
, b
的節點數分別是m和n, 那麼, 因為整個遍歷過程最多經一個 節點對 一次, 並且最多也需要記錄所有的 節點對 的被訪問的歷史, 在上面的遞迴實現中, 最差情況是遇到一個經過了所有 節點對 的環, 因此:
-
時間效率:
O(n*m)
. -
空間效率:
O(n*m)
(節點對記錄的空間和遞迴呼叫棧的空間, 都是同樣的級別).
更多
到此為止, 在這個問題上我希望我已經盡我所能把面試官哄好了.
如果還沒哄夠, 關於這個問題有一些相關的方面可以繼續擴充套件下:
-
上面提到的 訪問相等 是一個直觀的說法, 在大學裡學過的編譯原理中, 它有更嚴謹的定義.
2個
dict
各自組成的圖可以認為是兩個 自動機 , 而1個圖中所有的路徑就是這個自動機表達的語言. 這個題目的本質也就是判斷2個自動機表達的語言是否等價.在有些場合, 這個問題也會表達成判斷2個正則表示式是否等價.
關於2個自動機是否等價的比較, 網上直接可以搜到非常成熟的演算法.
-
如果把所有路徑經過的 節點對 合起來看做1個節點, 那麼這個組合的節點對和節點對之間的關聯關係會組成一個新的圖.
這個新的圖是2個圖的 張量積 . 如果2個圖是 訪問相等 的, 那麼他們跟這個新的張量積的圖也是 訪問相等 的.
而這個問題的解法, 也可以看成對這個張量積圖的一次遍歷(雖然實際上沒有生成這個圖).
張量積的圖中:
-
點集是:
a
和b
的點集的笛卡爾積:{ (a[i], b[j]) }
-
邊集的定義:
如果 ai 到 aj 有一條名為k的路徑, bk 到 bl 也有一條名為k的路徑, 則(ai, bk) 到 (aj, bl) 有一條名為k的路徑.
-
上面2個分支也是有趣話題, 值得深入, 相信對技術人的職業生涯或業餘興趣都會有不少幫助:)