经典图算法总结加java实现

汇总了很多博客的内容,作为学习笔记~

图的遍历

从图中的某个顶点出发,按某种方法对图中的所有顶点访问且仅访问一次。为了保证图中的顶点在遍历过程中仅访问一次,要为每一个顶点设置一个访问标志。通常有两种方法:深度优先搜索(DFS)和广度优先搜索(BFS).这两种算法对有向图与无向图均适用。 

深度优先搜索(DFS)

  // 从第i个节点开始深度优先遍历

        private void traverse(int i){

            // 标记第i个节点已遍历

            visited[i] = true;

            // 打印当前遍历的节点

            visit(i);

            // 遍历邻接矩阵中第i个节点的直接联通关系

            for(int j=0;j<vexnum;j++){

                // 目标节点与当前节点直接联通,并且该节点还没有被访问,递归

                if(arcs[i][j]==1 && visited[j]==false){

                    traverse(j);

                }

            }

        }

        // 图的深度优先遍历(递归)

        public void DFSTraverse(){

            // 初始化节点遍历标记

            for (int i = 0; i < vexnum; i++) {

                visited[i] = false;

            }

            // 从没有被遍历的节点开始深度遍历

            for(int i=0;i<vexnum;i++){

                if(visited[i]==false){

                    // 若是连通图,只会执行一次

                    traverse(i);

                }

            }

        }

        // 图的深度优先遍历(非递归)

        public void DFSTraverse2(){

            // 初始化节点遍历标记

            for (int i = 0; i < vexnum; i++) {

                visited[i] = false;

            }

            Stack<Integer> s = new Stack<Integer>();

            for(int i=0;i<vexnum;i++){

                if(!visited[i]){

                    //连通子图起始节点

                    s.add(i);

                    do{

                        // 出栈

                        int curr = s.pop();

                        // 如果该节点还没有被遍历,则遍历该节点并将子节点入栈

                        if(visited[curr]==false){

                            // 遍历并打印

                            visit(curr);

                            visited[curr] = true;

 

                            // 没遍历的子节点入栈

                            for(int j=vexnum-1; j>=0 ; j-- ){

                                if(arcs[curr][j]==1 && visited[j]==false){

                                    s.add(j);

                                }

                            }

                        }

                    }while(!s.isEmpty());

                }

            }

        }

广度优先搜索(BFS)

public class AMWGraph {

    private ArrayList vertexList;//存储点的链表

    private int[][] edges;//邻接矩阵,用来存储边

    private int numOfEdges;//边的数目

    public AMWGraph(int n) {

        //初始化矩阵,一维数组,和边的数目

        edges=new int[n][n];

        vertexList=new ArrayList(n);

        numOfEdges=0;

    }

    //得到结点的个数

    public int getNumOfVertex() {

        return vertexList.size();

    }

    //得到边的数目

    public int getNumOfEdges() {

        return numOfEdges;

    }

    //返回结点i的数据

    public Object getValueByIndex(int i) {

        return vertexList.get(i);

    }

    //返回v1,v2的权值

    public int getWeight(int v1,int v2) {

        return edges[v1][v2];

    }

    //插入结点

    public void insertVertex(Object vertex) {

        vertexList.add(vertexList.size(),vertex);

    }

    //插入边

    public void insertEdge(int v1,int v2,int weight) {

        edges[v1][v2]=weight;

        numOfEdges++;

    }

    //删除结点

    public void deleteEdge(int v1,int v2) {

        edges[v1][v2]=0;

        numOfEdges--;

    }

    //得到第一个邻接结点的下标

    public int getFirstNeighbor(int index) {

        for(int j=0;j<vertexList.size();j++) {

            if (edges[index][j]>0) {

                return j;

            }

        }

        return -1;

    }

    //根据前一个邻接结点的下标来取得下一个邻接结点

    public int getNextNeighbor(int v1,int v2) {

        for (int j=v2+1;j<vertexList.size();j++) {

            if (edges[v1][j]>0) {

                return j;

            }

        }

        return -1;

    }

    //私有函数,深度优先遍历

    private void depthFirstSearch(boolean[] isVisited,int  i) {

        //首先访问该结点,在控制台打印出来

        System.out.print(getValueByIndex(i)+"  ");

        //置该结点为已访问

        isVisited[i]=true;

        int w=getFirstNeighbor(i);//

        while (w!=-1) {

            if (!isVisited[w]) {

                depthFirstSearch(isVisited,w);

            }

            w=getNextNeighbor(i, w);

        }

    }

    //对外公开函数,深度优先遍历,与其同名私有函数属于方法重载

    public void depthFirstSearch() {

        for(int i=0;i<getNumOfVertex();i++) {

            //因为对于非连通图来说,并不是通过一个结点就一定可以遍历所有结点的。

            if (!isVisited[i]) {

                depthFirstSearch(isVisited,i);

            }

        }

    }

    //私有函数,广度优先遍历

    private void broadFirstSearch(boolean[] isVisited,int i) {

        int u,w;

        LinkedList queue=new LinkedList();

        //访问结点i

        System.out.print(getValueByIndex(i)+"  ");

        isVisited[i]=true;

        //结点入队列

        queue.addlast(i);

        while (!queue.isEmpty()) {

            u=((Integer)queue.removeFirst()).intValue();

            w=getFirstNeighbor(u);

            while(w!=-1) {

                if(!isVisited[w]) {

                        //访问该结点

                        System.out.print(getValueByIndex(w)+"  ");

                        //标记已被访问

                        isVisited[w]=true;

                        //入队列

                        queue.addLast(w);

                }

                //寻找下一个邻接结点

                w=getNextNeighbor(u, w);

            }

        }

    }

    //对外公开函数,广度优先遍历

    public void broadFirstSearch() {

        for(int i=0;i<getNumOfVertex();i++) {

            if(!isVisited[i]) {

                broadFirstSearch(isVisited, i);

            }

        }

    }

}

拓扑排序:

寻找一个入度为0的顶点,该顶点是拓扑排序中的第一个顶点序列,将之标记删除,然后将与该顶点相邻接的顶点的入度减1,再继续寻找入度为0的顶点,直至所有的顶点都已经标记删除或者图中有环。从上可以看出,关键是寻找入度为0的顶点。

先将入度为0的顶点放在栈或者队列中。当队列不空时,删除一个顶点v,然后更新与顶点v邻接的顶点的入度。只要有一个顶点的入度降为0,则将之入队列。此时,拓扑排序就是顶点出队的顺序。该算法的时间复杂度为O(V+E).

第一步:遍历图中所有的顶点,将入度为0的顶点 入队列。

第二步:从队列中出一个顶点,打印顶点,更新该顶点的邻接点的入度(减1),如果邻接点的入度减1之后变成了0,则将该邻接点入队列。

第三步:一直执行上面 第二步,直到队列为空。

最后结果可能不同,因为拓扑排序有多解

public ArrayList<Integer> topo() {

ArrayList<Integer> result = new ArrayList<>();

Queue<Integer> que = new LinkedList<>();

int[] ind = new int[V];

for(int i=0;i<V;i++)

ind[i] = 0;

for(LinkedList<Integer> nodes:adj)

for(int x:nodes)

ind[x]++; for(int i=0;i<V;i++)

if(ind[i]==0)

que.offer(i);

while(!que.isEmpty()){

int k = que.poll();

result.add(k);

for(int x:adj(k)) {

ind[x]--;

if(ind[x]==0)

que.offer(x);

}

}

return result;

}

定义: 拓扑排序是对有向无环图(DAG)的顶点的一种排序, 使得如果存在一条从v到w的路径,那么在排序中w就出现在v的后面。 
如果图含有环,那么拓扑排序是不可能的。试想有3个正整数,a比b大,b比c大,c比a大,我们无法对abc排序

重构二叉树

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

/*class TreeNode

 {

     int val;

     TreeNode left;

         TreeNode right;

     TreeNode(int x) { val = x; }

 }

*/

public class BulidTree

{

 public TreeNode reConstructBinaryTree(int [] pre,int [] in)

 {

 if(pre == null || in == null || pre.length != in.length )//如果先序或者中序数组有一个为空的话,就无法建树,返回为空

 return null;

 else

 {

 return reBulidTree(pre,0,pre.length-1,in,0,in.length-1);

 }

 }

 private TreeNode reBulidTree(int[] pre,int startPre,int endPre,int[] in,int startIn,int endIn)

 {

 if(startPre > endPre || startIn > endIn)//先对传的参数进行检查判断

 return null;

 int root = pre[startPre];//数组的开始位置的元素是跟元素

 int locateRoot = locate(root,in,startIn,endIn);//得到根节点在中序数组中的位置 左子树的中序和右子树的中序以根节点位置为界

 if(locateRoot == -1) //在中序数组中没有找到跟节点,则返回空

   return null;

 TreeNode treeRoot = new TreeNode(root);//创建树根节点

 treeRoot.left = reBulidTree(pre,startPre + 1,startPre + locateRoot - startIn,in,startIn,locateRoot-1);//递归构建左子树

 treeRoot.right = reBulidTree(pre,startPre+locateRoot-startIn+1,endPre,in,locateRoot+1,endIn);//递归构建右子树

 return treeRoot;

 }

 //找到根节点在中序数组中的位置,根节点之前的是左子树的中序数组,根节点之后的是右子树的中序数组

 private int locate(int root,int[] in,int startIn,int endIn)

 {

 for (int i = startIn; i < endIn; i++)

{

if(root == in[i])

return i;

}

 return -1;

 }

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

图的最短路径算法

问题:从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径(最短路径)。解决最短路径有以下算法,Dijkstra算法,Bellman-Ford算法,Floyd算法和SPFA算法,另外还有启发式搜索算法A*。

Dijkstra(迪杰斯特拉)算法(贪心、动态规划)

典型的单源最短路径算法,用于计算从某个源点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。

问题:Dijkstar能否处理负权边?(来自《图论》)

答案:不能,这与贪心选择性质有关,每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径;但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin'),再通过这个负权边L(L<0),使得路径之和更小(dmin'+L<dmin),则dmin'+L成为最短路径,并不是dmin,这样dijkstra就被囧掉了。

 (1) Dijkstra算法按路径长度递增次序产生最短路径。先把V分成两组:

S:已求出最短路径的顶点的集合

V-S=T:尚未确定最短路径的顶点集合

将T中顶点按最短路径递增的次序加入到S中,依据V0到T中顶点Vk的最短路径,或是从V0到Vk的直接路径或是从V0经S中顶点到Vk的路径权值之和。

  1. 求最短路径步骤

初使时令S={V0},T={其余顶点},T中顶点对应的距离值, 若存在<V0,Vi>,为<V0,Vi>弧上的权值,若不存在<V0,Vi>,为Inf。从T中选取一个其距离值为最小的顶点W(贪心),加入S,对T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值比不加W的路径要短,则修改此距离值。重复上述步骤,直到S中包含所有顶点,即S=V为止。

Floyd算法(Floyd-Warshall algorithm)(动态规划)

又称为弗洛伊德算法、插点法,是解决给定的加权图中顶点间的最短路径的一种算法,计算每一对顶点之间的最短路径,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。

Floyd算法的基本思想如下:从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点到B,所以,我们假设dist(AB)为节点A到节点B的最短路径的距离,对于每一个节点K,我们检查dist(AK) + dist(KB) < dist(AB)是否成立,如果成立,证明从A到K再到B的路径比A直接到B的路径短,我们便设置 dist(AB) = dist(AK) + dist(KB),这样一来,当我们遍历完所有节点K,dist(AB)中记录的便是A到B的最短路径的距离。

void floyd() {

      for(int i=1; i<=n ; i++){

        for(int j=1; j<= n; j++){

          if(map[i][j]==Inf){

               path[i][j] = -1;//表示  i -> j 不通

          }else{

               path[i][j] = i;// 表示 i -> j 前驱为 i

          }

        }

      }

      for(int k=1; k<=n; k++) {//对k的遍历要放在最外层

        for(int i=1; i<=n; i++) {

          for(int j=1; j<=n; j++) {

            if(!(dist[i][k]==Inf||dist[k][j]==Inf)

  &&dist[i][j] > dist[i][k] + dist[k][j]) {

               dist[i][j] = dist[i][k] + dist[k][j];

               //path[i][k] = i;//删掉

               path[i][j] = path[k][j];

            }

          }

        }

      }

    }

Bellman-Ford算法

 为了能够求解边上带有负值的单源最短路径问题,Bellman(贝尔曼,动态规划提出者)和Ford(福特)提出了从源点逐次绕过其他顶点,以缩短到达终点的最短路径长度的方法。 Bellman-ford算法是求含负权图的单源最短路径算法,效率很低,但代码很容易写。即进行不停地松弛,每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,否则就成功完成。Bellman-ford算法有一个小优化:每次松弛先设一个flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。Bellman-ford算法浪费了许多时间做无必要的松弛,所以SPFA算法用队列进行了优化,效果十分显著,高效难以想象。SPFA还有SLF,LLL,滚动数组等优化。

递推公式(求顶点u到源点v的最短路径):

        dist 1 [u] = Edge[v][u]

        dist k [u] = min{ dist k-1 [u], min{ dist k-1 [j] + Edge[j][u] } }, j=0,1,…,n-1,j≠u

 Dijkstra算法和Bellman算法思想有很大的区别:Dijkstra算法在求解过程中,源点到集合S内各顶点的最短路径一旦求出,则之后不变了,修改的仅仅是源点到T集合中各顶点的最短路径长度。Bellman算法在求解过程中,每次循环都要修改所有顶点的dist[ ],也就是说源点到各顶点最短路径长度一直要到Bellman算法结束才确定下来。

 

SPFA算法

 用一个队列来进行维护。初始时将源加入队列。每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。直到队列为空时算法结束;这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。

 

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值