主要介紹並查集算法實現以及相關優化。c++
圖相關算法的實現。算法
一種不同的樹形結構數組
可視化的來看鏈接問題:bash
左上右下是不是鏈接的呢?微信
網絡中節點間的鏈接狀態網絡
社交網絡:Facebook中用戶a和b中的聯繫(好友關係)。是否能聯繫到。函數
音樂電影書籍,多媒體之間造成網絡。性能
互聯網網頁之間造成的網絡測試
路由器和路由器之間造成的也是網絡優化
道理交通,航班調度都是網絡
數學中的集合類實現
並就是實現並集。& 查詢
鏈接問題 & 路徑問題
比路徑問題要回答的問題少(路徑是什麼,鏈接問題只問有沒有連)
除了回答問題自己以外是否是額外的回答了別的問題。頗有可能就存在
更高效的算法。:由於高效算法不須要回答額外的問題。
對於一組數據,主要支持兩個動做:
用來回答一個問題
最簡單的表示方式;
數組。0,1.
0-4 5-9
0-4是一組,5-9是一組。組內之間有聯繫,一組內的元素有相同的id
奇偶
奇數是一組,偶數是一組。
namespace UF1 {
class UnionFind {
private:
int *id;
int count;
public:
UnionFind(int n) {
count = n;
id = new int[n];
//初始條件每一個元素都是一組
for (int i = 0; i < n; i++)
id[i] = i;
}
~UnionFind() {
delete[] id;
}
//傳入元素p,返回元素對應的id。
int find(int p) {
assert(p >= 0 && p < count);
return id[p];
}
bool isConnected(int p, int q) {
return find(p) == find(q);
}
//傳入兩個元素,並
void unionElements(int p, int q) {
//找到兩個元素的id
int pID = find(p);
int qID = find(q);
//比較id
if (pID == qID)
return;
for (int i = 0; i < count; i++)
//從頭至尾的掃描時間複雜度O(n)
if (id[i] == pID)
id[i] = qID;
}
};
}
複製代碼
Testhelper.h:
namespace UnionFindTestHelper{
//n是數據量
void testUF1( int n ){
//
srand( time(NULL) );
UF1::UnionFind uf = UF1::UnionFind(n);
time_t startTime = clock();
//O(N*N)的時間複雜度
for( int i = 0 ; i < n ; i ++ ){
int a = rand()%n;
int b = rand()%n;
uf.unionElements(a,b);
//O(n)
}
for(int i = 0 ; i < n ; i ++ ){
int a = rand()%n;
int b = rand()%n;
uf.isConnected(a,b);
//時間複雜度只有O(1)
}
time_t endTime = clock();
cout<<"UF1, "<<2*n<<" ops, "<<double(endTime-startTime)/CLOCKS_PER_SEC<<" s"<<endl;
}
}
複製代碼
main.cpp:
int main() {
int n = 100000;
UnionFindTestHelper::testUF1(n);
return 0;
}
複製代碼
運行結果:
UF1, 200000 ops, 32.3533 s
[Finished in 39.7s]
複製代碼
quick find 查找時只須要O(1)級別。可是並確很慢
常規實現思路
將每個元素,看作是一個節點。
元素節點
每一個元素擁有一個指向父節點的指針。而後最上面的父節點指針指向本身。
數組存放父親
parent(i) = i;
union 3 4
union 3 8
union 6 5
union 9 4
要將9鏈接到4的根節點8上去。數組中:4-3-8-8 8是4的根節點。9指向8.
4和9鏈接在一塊兒:由於根相同。
成果
namespace UF2{
class UnionFind{
private:
int* parent;
int count;
public:
UnionFind(int count){
parent = new int[count];
this->count = count;
for( int i = 0 ; i < count ; i ++ )
parent[i] = i;
}
~UnionFind(){
delete[] parent;
}
//不斷向上找父親
int find(int p){
assert( p >= 0 && p < count );
while( p != parent[p] )
p = parent[p];
return p;
}
//看是否能找到一樣的根
bool isConnected( int p , int q ){
return find(p) == find(q);
}
//找到p的根,和q的根
void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
//把根掛到另外一個的根
parent[pRoot] = qRoot;
}
};
}
複製代碼
運行結果:
UF1, 20000 ops, 0.246341 s
UF2, 20000 ops, 0.059387 s
複製代碼
當n大的時候,方法1更優了。
union 9,4 & union 4 9
union 9 4
9的元素少,將它指向4的根節點。造成的樹層數低。
// 咱們的第三版Union-Find
namespace UF3{
class UnionFind{
private:
int* parent; // parent[i]表示第i個元素所指向的父節點
int* sz; // sz[i]表示以i爲根的集合中元素個數
int count; // 數據個數
public:
// 構造函數
UnionFind(int count){
parent = new int[count];
sz = new int[count];
this->count = count;
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
sz[i] = 1;
}
}
// 析構函數
~UnionFind(){
delete[] parent;
delete[] sz;
}
// 查找過程, 查找元素p所對應的集合編號
// O(h)複雜度, h爲樹的高度
int find(int p){
assert( p >= 0 && p < count );
// 不斷去查詢本身的父親節點, 直到到達根節點
// 根節點的特色: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所屬一個集合
// O(h)複雜度, h爲樹的高度
bool isConnected( int p , int q ){
return find(p) == find(q);
}
// 合併元素p和元素q所屬的集合
// O(h)複雜度, h爲樹的高度
void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根據兩個元素所在樹的元素個數不一樣判斷合併方向
// 將元素個數少的集合合併到元素個數多的集合上
if( sz[pRoot] < sz[qRoot] ){
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}
else{
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
};
}
複製代碼
運行結果:
UF2, 200000 ops, 19.3316 s
UF3, 200000 ops, 0.0184 s
複製代碼
上面合併4和2 依靠集合的size來決定誰指向誰並不徹底合理。根據層數才最合理。
用rank[i] 表示根節點爲i的樹的高度
namespace UF4{
class UnionFind{
private:
int* rank; // rank[i]表示以i爲根的集合所表示的樹的層數
int* parent; // parent[i]表示第i個元素所指向的父節點
int count; // 數據個數
public:
// 構造函數
UnionFind(int count){
parent = new int[count];
rank = new int[count];
this->count = count;
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 析構函數
~UnionFind(){
delete[] parent;
delete[] rank;
}
// 查找過程, 查找元素p所對應的集合編號
// O(h)複雜度, h爲樹的高度
int find(int p){
assert( p >= 0 && p < count );
// 不斷去查詢本身的父親節點, 直到到達根節點
// 根節點的特色: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所屬一個集合
// O(h)複雜度, h爲樹的高度
bool isConnected( int p , int q ){
return find(p) == find(q);
}
// 合併元素p和元素q所屬的集合
// O(h)複雜度, h爲樹的高度
void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根據兩個元素所在樹的元素個數不一樣判斷合併方向
// 將元素個數少的集合合併到元素個數多的集合上
if( rank[pRoot] < rank[qRoot] ){
parent[pRoot] = qRoot;
}
else if( rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此時, 我維護rank的值
}
}
};
}
複製代碼
對於UF3來講, 其時間性能依然是O(n*h)的, h爲並查集表達的樹的最大高度,但因爲UF3能更高几率的保證樹的平衡, 因此性能更優
UF4雖然相對UF3進行有了優化, 但優化的地方出現的狀況較少,因此性能更優表現的不明顯, 甚至在一些數據下性能會更差,由於判斷更多了。
運行結果
2000000 ops, 0.313945 s
複製代碼
前面咱們都在優化union。其實Find咱們也能夠進行優化。因爲每一個節點存的都是它的父親節點,全部每一個節點均可以有無數個(多個)孩子。在search值的時候,對於沒有找到的根的節點,能夠往上挪一挪。
好比咱們要find4
咱們將4的父親節點鏈接爲4的父親的父親(若是出現3就是根節點,也沒有關係,由於對於根節點來講,3的父親仍是3)
下面考慮4的parent:2 (此時跳過了3,跳2級是沒有問題的)
最後的結果:
修改 find函數
int find(int p){
assert( p >= 0 && p < count );
// path compression 1
while( p != parent[p] ){
parent[p] = parent[parent[p]];
p = parent[p];
}
}
複製代碼
//path compression 2, 遞歸算法
if( p != parent[p] )
parent[p] = find( parent[p] );
return parent[p];
複製代碼
最後的狀況
優化狀況並不明顯。甚至由於遞歸的消耗。因此理論最優不必定實際好。
通過並查集的優化,並查集的操做,時間複雜度近乎是O(1)的
-------------------------華麗的分割線--------------------
看完的朋友能夠點個喜歡/關注,您的支持是對我最大的鼓勵。
想了解更多,歡迎關注個人微信公衆號:番茄技術小棧