如何描述一個複雜的連接配接關系?如圖,很容易判斷緊鄰的2個人關系,但中間的連接配接很多很亂,怎麼判斷出兩個人的關系呢?
并查集就是一種結構,通過儲存節點以及節點上的标簽,來判斷這兩個節點是否連接配接在一起。當兩個節點綁定時,可以任選其中一個節點的标簽,指定另一個節點。當判斷兩個節點是不是連接配接時,可以上溯節點的祖宗節點,如果祖宗節點相同,那麼節點相連。此時,節點上的标簽可了解為指向父親節點。
并查集的并&查
1.節點清單
并查集中的節點隻需要儲存父親節點的資訊,那麼線性結構字典、清單都可以。我們用一維數組,索引是自身id,值指向父親。
初始化時每個節點指向自身。
class UnionFind:
def __init__(self,size):
self.size = size
self.parent = np.arange(size)
def union(self,p,q): ##将兩個節點連接配接在一起
def isConnected(self,p,q): ## 判斷兩個節點是否相連
2.判斷兩個節點相連
當兩個節點的祖宗節點相同時,兩個節點就是連接配接節點。
def find(self,p):
assert p>=0 and p<self.size
while (self.parent[p]!=p):
##向上周遊,當父節點不是自己時,
##那麼還存在父節點,繼續周遊。
p = self.parent[p]
return p
def isConnected(self,p,q):
##當祖宗節點相同時,兩個節點是連接配接節點
return self.find(p)==self.find(q)
3.節點連接配接
當節點連接配接時,需要将2個節點的祖宗節點相連,可任選一個節點連接配接另一個節點。
def union(self,p,q):
pRoot = self.find(p)
qRoot = self.find(q)
if pRoot == qRoot:
return
else:
## 第一個節點祖宗節點指向第二節點的祖宗節點
self.parent[pRoot] = qRoot
4.優化連接配接
第3小節,我們任意選擇一個節點連接配接,這樣的選擇有問題。舉例:一層和二層節點集合合并:
- 如果二層節點的祖宗節點連接配接到一層節點上,那麼就形成了一個三層節點集。
- 另一種可能,一層節點連接配接到二層祖宗節點,新集還是二層。
層數越少,查詢祖宗節點的代價越小,應讓節點層數少的連接配接到層數高的。
class UnionFind:
def __init__(self,size):
self.size = size
self.parent = np.arange(size)
self.rank = np.ones(size) //記錄該節點下面的層次,預設都是1層
def unionByRank(self,p,q):
assert p>=0 and p<self.size and q>=0 and q<self.size
pRoot = self.find(p)
qRoot = self.find(q)
if pRoot == qRoot:
return self
else:
## rank小的,添加到rank大的,這樣的合并不增加rank。rank節點向上周遊的步數。
if(self.rank[pRoot] > self.rank[qRoot]):
self.parent[qRoot] = pRoot
elif(self.rank[qRoot] > self.rank[pRoot]):
self.parent[pRoot] = qRoot
else:
self.parent[pRoot] = qRoot
self.rank[qRoot] +=1
return self
5.路徑壓縮
進一步優化,使每個節點直接指向它的祖宗節點。
def findCompress2(self,p):
if p!=self.parent(p):
self.parent[p] = self.findCompress2(self.parent[p])
return self.parent[p]
通過遞歸調用,函數從某一點出發,上溯到祖宗節點,傳回值傳遞祖宗節點。函數傳回時,相當于祖宗節點向下周遊,對每一個節點父節點重新指派。
總結:
本文兩個重點:介紹了并查集和路徑壓縮;單向清單的反向周遊。