图算法
1、图的表示
1.1、邻接矩阵(有向图、无向图、带权图、代码实现)
1、无向图的邻接矩阵
2、有向图的邻接矩阵
3、带权值的图
有了上述的理解,我们可以设计数据结构,并实现了。C++实现如下:
#include<iostream>
#include <iomanip>//setw格式化函数头文件
using namespace std;
template<typename T1,typename T2>
class graph{
private:
const static int MAXVEX = 100;
const static int MaxValue = 65535; //要加static
bool flag;//标记是否创建无向图 ,默认是
int numVertexes,numEdges;//顶点个数、边数;
T2 arc[MAXVEX][MAXVEX]; //邻接矩阵
T1 vexs[MAXVEX];//顶点数组
public:
graph(int numVertexes,int numEdges,bool flag);//构造函数 ,这里无需写默认参数
public:
void createMGraph();//构建图
void displayEdge();//展示邻接矩阵
};
template<typename T1,typename T2>
graph<T1,T2>::graph(int numVertexes,int numEdges,bool flag=true){ //默认参数必须放最右边
this->numEdges = numEdges;
this->numVertexes = numVertexes;//初始化值
this->flag = flag;
createMGraph();
}
template<typename T1,typename T2>
void graph<T1,T2>::createMGraph(){
cout<<"开始创建图,顶点个数为"<<this->numVertexes
<<" ;边数为"<<this->numEdges<<":"<<endl;
cout<<"输入顶点编号:";
for(int i=0;i<this->numVertexes;i++){//顶点表初始化
cin>>vexs[i];
}
for(int i=0;i<this->numVertexes;i++){
for(int j=0;j<this->numVertexes;j++)
this->arc[i][j] = this->MaxValue;//邻接矩阵权值初始化
}
cout<<"输入边(vi,vj)的下标i,j和权值w:"<<endl;
int i,j,w;
for(int k=0;k<this->numEdges;k++){
cin>>i>>j>>w;
this->arc[i][j]=w;
if(this->flag==true) //是否构建无向图
this->arc[j][i]=w;//对称矩阵
}
cout<<"创建完成!"<<endl;
return;
}
template<typename T1,typename T2>
void graph<T1,T2>::displayEdge(){
for(int i=0;i<this->numVertexes;i++){
for(int j=0;j<this->numVertexes;j++)
cout<<setw(5)<<this->arc[i][j]<<"\t";//5位对齐
cout<<endl;
}
return;
}
int main(){
graph<int,int> mygraph(4,5,false);
mygraph.displayEdge();
return 0;
}
1.2、 邻接表
1、邻接表的提出
2、无向图的邻接表
3、有向图的邻接表(分出边表、入边表)
4、带权图的处理
有了上面的邻接表的理解,我们可以实现代码(java):
package dataStruct;
import java.util.Scanner;
/**
* 邻接表_无向图、有向图、带权图实现实现
*/
public class GraphAdjList {
private static final int MAXSIZE = 100;//默认最大顶点个数为100
private int numVertexes;//顶点个数
private int numEdges;//边数
public boolean flag;//标记是否为无向图,java类里边默认初始化为true
public VertexNode[] adjList;
/**
* 构造器
* @param numVertexes 顶点个数
* @param numEdges 边数
*/
public GraphAdjList(int numVertexes,int numEdges,boolean flag) {
this.numVertexes = numVertexes;
this.numEdges = numEdges;
this.flag = flag;
adjList = new VertexNode[MAXSIZE];//为了后面的插入
}
public void CreateALGraph(){
Scanner input = new Scanner(System.in);
String info = "无向图";
if(!flag)
info = "有向图";
System.out.println("创建"+info+",顶点个数:"+this.numVertexes+"边数:"+this.numEdges);
System.out.println("开始输入顶点信息:");
for(int i=0;i<this.numVertexes;i++){
int data = input.nextInt();
this.adjList[i] = new VertexNode(data,null);
}
System.out.println("开始输入边(vi,vj)的顶点序号和边上的权值w:");
for(int k=0;k<this.numEdges;k++){
int i=input.nextInt();
int j=input.nextInt();//1 3 2,1 2 4
int weight = input.nextInt();
EdgeNode e = new EdgeNode(j, weight);
e.next = this.adjList[i].firstEdge;//头插法,e节点指向原来头结点指向的地址
this.adjList[i].firstEdge = e;//链接!
if(flag){//如果是无向图
e = new EdgeNode(i,weight);
e.next = this.adjList[j].firstEdge;
this.adjList[j].firstEdge = e;
}
}
System.out.println("创建完成!");
}
public void displayALGraph(){
for(int i=0;i<this.numVertexes;i++){
if(adjList[i].firstEdge!=null){
EdgeNode e = adjList[i].firstEdge;
while(e!=null){
System.out.print("顶点:"+this.adjList[i].data+"邻接顶点:");//打印顶点信息
System.out.println(e.adjvex+"权值:"+e.weight);//打印该顶点其邻接顶点信息
e = e.next;
}
}
}
}
static class VertexNode{//顶点结构
int data;//顶点信息
EdgeNode firstEdge;//边表头指针
public VertexNode(int data,EdgeNode firstEdge){
this.data = data;
this.firstEdge = firstEdge;
}
}
static class EdgeNode{//边表
public int adjvex;//邻接顶点的下标
public int weight;//权重,可以考虑初始化为无穷大代表不存在。
public EdgeNode next;//链域,指向下一个邻接顶点
public EdgeNode(int adjvex,int weight){
this.adjvex = adjvex;
this.weight = weight;
}
}
public static void main(String[]args){
GraphAdjList myGraph = new GraphAdjList(5,6,false);//创建5顶点6边的有向图
myGraph.CreateALGraph();
myGraph.displayALGraph();
}
}
1.3、 十字链表与邻接多重表
1、十字链表——解决有向图邻接表结构缺点
2、邻接多重表——解决无向图邻接表结构,边的删除麻烦问题
1.4、边集数组
2、图的遍历
2.1、DFS(深度优先搜索、递归算法)
基于邻接矩阵的DFS
template<typename T1,typename T2>
void graph<T1,T2>::DFS(int i){
this->visited[i]=true;//标记当前顶点已经遍历
cout<<this->vexs[i];//打印顶点
for(int j=0;j<this->numVertexes;j++){
if(this->arc[i][j]!=this->MaxValue && !this->visited[j])//未遍历过
DFS(j);
}
}
template<typename T1,typename T2>
void graph<T1,T2>::DFSTraverse(){
cout<<endl<<"遍历结果:";
for(int i=0;i<this->numVertexes;i++){
this->visited[i]=false;//所有点都标记未访问过
}
for(int i=0;i<this->numVertexes;i++){//开始遍历
if(!this->visited[i])
DFS(i);
}
}
由于是邻接矩阵存储结构,算法时间复杂度O(n2)
基于邻接表的DFS
template<typename T1,typename T2>
void graph<T1,T2>::DFS(int i){//邻接表的话
this->visited[i]=true;//标记当前顶点已经遍历
EdgeNode *p;//临时变量
cout<<this->adjList[i].data<<" ";打印顶点值
p = this->adjList[i].firstedge;//获取邻接顶点
while(p){
if(!visited[p->adjvex])//该顶点没有遍历过
DFS(p->adjvex);
p = p->next;//遍历下一个邻接顶点
}
}
template<typename T1,typename T2>
void graph<T1,T2>::DFSTraverse(){//基本不变
cout<<endl<<"遍历结果:";
for(int i=0;i<this->numVertexes;i++){
this->visited[i]=false;//所有点都标记未访问过
}
for(int i=0;i<this->numVertexes;i++){//开始遍历
if(!this->visited[i])
DFS(i);
}
}
邻接表使得算法复杂度为O(n+e),n为顶点个数,e为边数。
2.2、BFS(宽度优先搜索、优先队列)
对边搜索、不断延展。
邻接矩阵的BFS
void BFSTraverse(MGraph G){
int i,j;
Queue Q;
for(i=0;i<G.numVertexes;i++)
visited[i] = false;
InitQueue(&Q);//初始化队列
for(i=0;i<G.numVertexes;i++){
if(!visited[i]){
visited[i] = true;//标记已经访问过了
cout<<G.vexs[i];//打印顶点
EnQueue(&Q,i);//入队
while(!QueueEmpty(Q))//队列不空
{
DeQueue(&Q,&i);//队头元素出队
for(j=0;j<G.numVertexes;j++){
if(G.arc[i][j] !=this.MaxValue && !visited[j])//当前边的顶点未访问过
{ visited[j] = true;
cout<<G.vexs[j];//打印顶点
EnQueue(&Q,j);//该顶点入队
}
}
}
}
}
}
邻接表的BFS
void BFSTraverse(GraphAdjList G){
int i,j;
Queue Q;
EdgeNode *p;//临时变量
for(i=0;i<G.numVertexes;i++)
visited[i] = false;
InitQueue(&Q);//初始化队列
for(i=0;i<G.numVertexes;i++){
if(!visited[i]){
visited[i] = true;//标记已经访问过了
cout<<G.vexs[i];//打印顶点
EnQueue(&Q,i);//入队
while(!QueueEmpty(Q))//队列不空
{
DeQueue(&Q,&i);//队头元素出队
p = G->adjList[i].firstedge;//当前顶点的邻接表头指针
while(p){
if(!visited[p->adjvex])//当前边的顶点未访问过
{ visited[p->adjvex] = true;
cout<<G->adjList[p->adjvex].data;//打印顶点
EnQueue(&Q,p->adjvex);//该顶点入队
}
p = p->next;//指向下一个邻接顶点
}
}
}
}
}
2.3、小结
3、寻找最小生成树(两个贪心算法)
实际问题:
3.1、Prim算法(分割法、贪心策略:寻找集合中的顶点所连接边中最小权值边)
贪心策略、算法分析:
template<typename T1,typename T2>
void graph<T1,T2>::MinSpendTree_Pirm(){
int min,i,j,k;
int adjvex[this->MAXVEX];//用来保存顶点的下标
int lowcost[this->MAXVEX];//保存相关顶点之间的权值
lowcost[0]=0;//表示v0加入生成树,只要lowcost=0表示对于的顶点加入了生成树
adjvex[0]=0;//第一个顶点为0
for(i=1;i<this->numVertexes;i++){
lowcost[i] = this->arc[0][i];//与V0顶点有边的点权值存入
adjvex[i] = 0;//初始化为V0下标
}
for(i=1;i<this->numVertexes;i++){
min = this->MaxValue;//初始化最小值权值,一般为无穷大
j=1;k=0;
while(j<this->numVertexes){//寻找当前顶点中最小的权值,存到k
if(lowcost[j]!=0 && lowcost[j]<min){
min = lowcost[j];
k=j;
}
j++;
}
//找到最小权值的顶点,加入最小生成树
cout<<"当前顶点边中最小权值的是:"<<adjvex[k]<<"-->"<<k;
lowcost[k]=0;//将其加入最小生成树
for(j=1;j<this->numVertexes;j++){
if(lowcost[j]!=0 && this->arc[k][j]<lowcost[j]){
lowcost[j]= this->arc[k][j];
adjvex[j]=k;
}
}
}
}
3.2、 Kruskal算法(边集数组、每次从剩余边选择最小权值边)
图解分析:
基于分析我们可以写出代码:
template<typename T1,typename T2>
void graph<T1,T2>::MinSpanTree_Krusal(){
if(!this->flag){
cout<<"该方法暂时仅限无向图使用";
return;
}
int parent[this->MAXVEX]={0};//创建一个数组,下标i对于边的起点start,存放的值对应end
int i,n,m,count=0;//count记录最小权值
//将无向图的邻接矩阵转化为边集数组,注意权值排序。
typedef struct{
int begin;
int end;
int weight;
}Edge;
Edge edges[this->numEdges];//定义边集数组
int edgenum = this->numEdges,e=0;
for(i=0;i<this->numVertexes;i++){
for(int j=0;j<this->numVertexes && e<edgenum;j++){
if(this->arc[i][j]!=this->MaxValue &&i<j)//边存在,i<j是无向图里边只计算一次
{
edges[e].begin=i;
edges[e].end = j;
edges[e].weight = this->arc[i][j];
e++;
}
}
}
//将边集数组按照权值排序
for(int i=1;i<edgenum;i++){//冒泡排序
for(int j=0;j<edgenum-i;j++){
if(edges[j].weight>edges[j+1].weight){
int temp = edges[j].begin;
edges[j].begin = edges[j+1].begin;
edges[j+1].begin = temp;
temp = edges[j].end;
edges[j].end = edges[j+1].end;
edges[j+1].end = temp;
temp = edges[j].weight;
edges[j].weight = edges[j+1].weight;
edges[j+1].weight = temp;
}
}
}
for(int i=0;i<this->numEdges;i++){
n = Find(parent,edges[i].begin);
m = Find(parent,edges[i].end);
if(n!=m)//不形成闭环
{
parent[n] = m;//将其加入生成树
cout<<" "<<edges[i].begin<<"-->"<<edges[i].end ;
count+=edges[i].weight;
}
}
cout<<endl<<"(kruskal)最小生成树代价:"<<count;
}
运行结果:
4、最短路径问题
4.1、单源最短路径问题(给定一个点,求到其余各个点的距离)
4.1.1、迪杰特斯拉(Dijkstra)算法(贪心算法)
很像prim算法,利用集合,寻找最短路径。但是有区别。而且用途也不同。这是一个贪心算法。
基于理解上的算法实现 :
template<typename T1,typename T2>
void graph<T1,T2>::MinPath_Dijkstra(int v0){
int D[this->numVertexes];//最短路径数组,D[v]表示源点v0到V的最短路径
bool flag[this->numVertexes] ;//标记集合,flag[v]=true表示点V0到V的最短路径已经求得
int p[this->numVertexes];//记录到V的最短路径中倒数第二个顶点
int k;
//初始化
for(int V=0;V<this->numVertexes;V++){
D[V] = this->arc[v0][V];//默认初始化为其边的权值
flag[V]=false;
p[V]=0;
}
D[v0]=0;
flag[v0]=true;//源点到源点路径不需要记录
p[v0]=0;
int min;
//开始查找最短路径
for(int V=1;V<this->numVertexes;V++){//控制选择到V的最短路径
min = this->MaxValue-1;
for(int w=0;w<this->numVertexes;w++){//找到当前离点v0最近的点,且其最短路径还没找到
if(!flag[w]&&D[w]<min){
min=D[w];
k=w;
}
}
flag[k]=true;//加入集合里边
//调整D[W],即每找到一个点加入路径,就调整一次源点到其余未加入最短路径的顶点的D[W]
for(int w=0;w<this->numVertexes;w++){
if(!flag[w]&& (min+this->arc[k][w]<D[w]))
{
D[w]=min+this->arc[k][w];//记录下来
p[w]=k;
}
}//找到最短路径、倒数第二个顶点k
}
//打印最短路径
for(int i=0;i<this->numVertexes;i++){
k=i;
cout<<""<<v0<<"--->"<<i<<"的最短路径为:";
while(k!=v0){
cout<<k<<"<--";
k=p[k];
}
cout<<v0<<" 长度为"<<D[i]<<endl;
}
return ;
}
未优化的算法复杂度很高
4.1.2、Floyd算法(动态规划)
4.1.3、Bellman-Ford 算法