前言:本文笔者详细介绍了并查集的概念和实现, 图的基本概念和存储结构。
并查集
引入:以社交网络为例,并查集被用于处理人际关系、社群发现等问题。通过将每个人看作一个节点,并使用并查集来管理他们之间的社交关系,当两个人成为朋友时,即执行合并(Union)操作,将它们放在同一个连通分量中。当需要判断两个人是否属于同一个社群时,只需执行发现(Find)操作,判断它们是否属于同一个连通分量即可。并查集主要应用于连通性问题中。
所有位置抽象到一个数组中时,
并查集的实现
class unionFindSet
{
public:
unionFindSet(size_t sz)
:_ufs(sz,-1)
{}
void Union(int x, int y)
{
int rootX = findRoot(x);
int rootY = findRoot(y);
/*当x和y在同一个集合中的时候,就不用合并直接返回即可*/
if (rootX != rootY)
{
_ufs[rootX] += _ufs[rootY];
_ufs[rootY] = rootX;
}
}
int findRoot(int x)
{
int root = x;
while (_ufs[root] >= 0)
{
root = _ufs[root];
}
return root;
}
/*显示当前并查集中有几个集合*/
size_t size()const
{
int _count = 0;
for (auto x : _ufs)
{
if (x < 0) ++_count;
}
return _count;
}
private:
vector<int> _ufs;
};
图的基本概念:
非人话:图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成的一种数据结构,用于表示多对多的关系。图可以表示为G(V, E),其中G表示一个图,V是图G中顶点的集合,E是图G中边的集合。线性表和树都可以看作是图的特殊形式。(树是一种特殊的图),更多的图的分类之后提到再细说。
人话:简单来说,图是一种表示型数据结构,意在表示某种场景,为了表示不同的场景衍生出了各种类型的图,图的顶点可以看作是一个元素,图的边可以看作元素与元素之间的某种关系(并不一定是单一的关系).
图的存储结构的实现(邻接矩阵)
邻接矩阵:
图解:1表示相连,0表示不相连(以无向图为例)
代码实现
namespace adjacencyMatrices
{
template<class V, class W, W WMAX = INT_MAX, bool judgMent = false>
class Graph
{
public:
Graph(const V* vectex, size_t n)
{
_vectex.reserve(n);
for (int i = 0; i < n; i++)
{
_vectex.push_back(vectex[i]);
_indexMap[vectex[i]] = i;
}
_edges.resize(n);
for (int i = 0; i < n; i++)
{
_edges[i].resize(n, WMAX);
}
}
size_t getIndex(const V& queriedNum)
{
auto it = _indexMap.find(queriedNum);
if (it != _indexMap.end())
{
return it->second;
}
else
{
throw "输入的顶点错误!";
return -1;
}
}
void addEdges(const V& src, const V& des, const W& edge)
{
size_t targetSrc = getIndex(src);
size_t targetDes = getIndex(des);
_edges[targetSrc][targetDes] = edge;
if (judgMent == false)//判断是否是无向图
{
_edges[targetDes][targetSrc] = edge;
}
}
void printTest()
{
for (int i = 0; i < _vectex.size(); i++)
{
cout << i << "-->" << _vectex[i] << endl;
}
cout << endl;
/*打印横下标*/
cout << " ";
for (int i = 0; i < _edges.size(); i++)
{
cout << i <<" ";
}
cout << endl;
/*打印矩阵*/
for (int i = 0; i < _edges.size(); ++i)
{
/*打印竖下标*/
cout << i << " ";
for (int j = 0; j < _edges[i].size(); ++j)
{
if (_edges[i][j] != WMAX)
cout << _edges[i][j] << " ";
else
cout << "#" << " ";
}
cout << endl;
}
cout << endl << endl;
// 打印所有的边
for (int i = 0; i < _edges.size(); ++i)
{
for (int j = 0; j < _edges[i].size(); ++j)
{
if (i < j && _edges[i][j] != WMAX)
{
cout << _vectex[i] << "-" << _vectex[j] << ":" <<
_edges[i][j] << endl;
}
}
}
}
private:
vector<V> _vectex;
map<V, int> _indexMap;
vector<vector<int>> _edges;
};
}
邻接矩阵的优势:
1.邻接矩阵的存储方式适合稠密图。
2.O(1)的时间复杂度判断2个顶点的连接关系,并取到权值。
邻接矩阵的缺点:
1.不适合稀疏图
2.不适合判断一个顶点连接的边数 -->O(N)
邻接表
为了解决邻接矩阵不擅长的问题,引入了邻接表的结构
图示:使用链表将自己链接顶点的边都挂在自己下面(以无向图为例)
代码实现:
namespace LinkTable
{
template<class W>
struct LinkEdge
{
int _srcIndex;
int _dstIndex;
W _w;
LinkEdge<W>* _next;
LinkEdge(const W& w)
: _srcIndex(-1)
, _dstIndex(-1)
, _w(w)
, _next(nullptr)
{}
};
template<class V, class W, bool Direction = false>
class Graph
{
typedef LinkEdge<W> Edge;
public:
Graph(const V* vertexs, size_t n)
{
_vertexs.reserve(n);
for (size_t i = 0; i < n; ++i)
{
_vertexs.push_back(vertexs[i]);
_vIndexMap[vertexs[i]] = i;
}
_linkTable.resize(n, nullptr);
}
size_t GetVertexIndex(const V& v)
{
auto ret = _vIndexMap.find(v);
if (ret != _vIndexMap.end())
{
return ret->second;
}
else
{
throw invalid_argument("不存在的顶点");
return -1;
}
}
void AddEdge(const V& src, const V& dst, const W& w)
{
size_t srcindex = GetVertexIndex(src);
size_t dstindex = GetVertexIndex(dst);
// 0 1
Edge* sd_edge = new Edge(w);
sd_edge->_srcIndex = srcindex;
sd_edge->_dstIndex = dstindex;
sd_edge->_next = _linkTable[srcindex];
_linkTable[srcindex] = sd_edge;
// 1 0
// 无向图
if (Direction == false)
{
Edge* ds_edge = new Edge(w);
ds_edge->_srcIndex = dstindex;
ds_edge->_dstIndex = srcindex;
ds_edge->_next = _linkTable[dstindex];
_linkTable[dstindex] = ds_edge;
}
}
private:
map<string, int> _vIndexMap;
vector<V> _vertexs; // 顶点集合
vector<Edge*> _linkTable;
};
}
邻接表的缺点:不适合判断2个定点是否相连。