小橙书阅读指南(十二)——无向图、深度优先搜索和路径查找算法

在计算机应用中,我们把一系列相连接的节点组成的数据结构,叫做图。今天我们将要介绍它的一种形式——无向图,以及针对这种结构的深度优先搜索和路径查找算法。

一、无向图数据结构

接口:

/**
 * 图论接口
 */
public interface Graph {
    /**
     * 顶点数
     *
     * @return
     */
    int vertexNum();

    /**
     * 边数
     *
     * @return
     */
    int edgeNum();

    /**
     * 向图中添加一条v-w的边
     *
     * @param v
     * @param w
     */
    void addEdge(int v, int w);

    /**
     * 和v相邻的所有顶点
     *
     * @param v
     * @return
     */
    Iterable<Integer> adjoin(int v);

    /**
     * v的维度
     *
     * @param v
     * @return
     */
    int degree(int v);
}

实现类:

public class Graph implements algorithms.graphs.ifs.Graph {
    private final int vertex; // 顶点
    private int edge; // 边
    private ArrayList<Integer>[] adj;

    public Graph(int v) {
        this.vertex = v;
        this.edge = 0;
        adj = (ArrayList<Integer>[]) new ArrayList[v];
        for (int i = 0; i < v; i++) {
            adj[i] = new ArrayList<>();
        }
    }

    @Override
    public int vertexNum() {
        return vertex;
    }

    @Override
    public int edgeNum() {
        return edge;
    }

    @Override
    public void addEdge(int v, int w) {
        validateVertex(v);
        validateVertex(w);
        adj[v].add(w);
        adj[w].add(v);
        edge++;
    }

    @Override
    public Iterable<Integer> adjoin(int v) {
        return adj[v];
    }

    @Override
    public int degree(int v) {
        return adj[v].size();
    }

    private void validateVertex(int v) {
        if (v < 0 || v > this.vertex) {
            throw new IllegalArgumentException();
        }
    }
}

二、深度搜索优先算法

对于图的处理我们常常通过系统地检查每一个顶点和每一条边来获取图的各种性质。对于图的问题我们最经常被问及的是:a点和b点连通吗?如果连通如何到达?为了描述方便,我们使用自然数描述图的每一个顶点。

假设有以下图的结构

左侧数组表示节点,右侧代表与节点连接的其他节点。该结构的标准画法如下:

算法描述:深度优先搜索从起点出发(0)遍历(2,1,5)并递归(2)与它链接的点,被搜索到的点将不会被再次递归直到所有的点都被搜索到为止。

深度优先搜索接口与实现:

// 接口
public interface Search {
    boolean marked(int v);
    int count();
}

/**
 * 图论:深度优先搜索
 */
public class DepthFirstSearch implements Search {
    private boolean[] marked;
    private int count;

    public DepthFirstSearch(Graph g, int s) {
        marked = new boolean[g.vertexNum()];
        validateVertex(s);
        dfs(g, s);
    }

    /**
     * 以递归的方式从s起点出发,标记每一个经过的顶点,未被标记的顶点为不连通
     *
     * @param g
     * @param v
     */
    private void dfs(Graph g, int v) {
        marked[v] = true;
        count++;
        for (int x : g.adjoin(v)) {
            if (!marked[x]) {
                dfs(g, x);
            }
        }
    }

    @Override
    public boolean marked(int v) {
        validateVertex(v);
        return marked[v];
    }

    @Override
    public int count() {
        return count;
    }

    // throw an IllegalArgumentException unless {@code 0 <= vertexNum < V}
    private void validateVertex(int v) {
        int V = marked.length;
        if (v < 0 || v >= V)
            throw new IllegalArgumentException();
    }

}

这套算法的核心是dfs函数。我们要理解深度优先算法就必须弄清楚算法递归的过程。marked数组记录节点的访问情况,变量x和v的递归过程如下:

标准画法:

深度优先算法按照上面的路径搜索图,由此我们可以获知深度搜索算法的两个特征:

1.搜索路径沿一条路径向下扩展,每一个节点只会被遍历一次(每一个节点都可以知道在搜索路径上的上一个节点,并唯一确定)。

2.搜索路径上的任意两点代表可达,但并非最短路径。

这样我们就可以回答本文最早提出的有关图的第一个问题:a点和b点连通吗?显然,以a为起点搜索整个图,如果b点在路径上则表示连通。

三、使用深度优先搜索的路径算法

要回答有关图的第二个问题:如果连通如何到达?回忆上一段我们总结的深度搜索算法的第一个特征,我们可以使用数组结构在存储每一个节点的上一个节点。

路径搜索接口和实现:

/**
 * 寻找路径
 */
public interface Paths {
    boolean hasPathTo(int vertex);

    Iterable<Integer> pathTo(int vertex);
}

/**
 * 基于深度优先搜索的路径搜索算法
 */
public class DepthFirstPaths implements Paths {
    private final int s;
    private boolean[] marked;
    private int[] edgeTo;

    public DepthFirstPaths(Graph g, int s) {
        this.s = s;
        marked = new boolean[g.vertexNum()];
        edgeTo = new int[g.vertexNum()];
        dfs(g, s);
    }

    private void dfs(Graph g, int v) {
        marked[v] = true;
        for (int w : g.adjoin(v)) {
            if (!marked[w]) {
                edgeTo[w] = v;
                dfs(g, w);
            }
        }
    }

    @Override
    public boolean hasPathTo(int vertex) {
        return marked[vertex];
    }

    @Override
    public Iterable<Integer> pathTo(int vertex) {
        if (!hasPathTo(vertex)) {
            return null;
        }
        Stack<Integer> path = new Stack<>();
        for (int i = vertex; i != s; i = edgeTo[i]) {
            path.push(i);
        }
        path.push(s);
        return path;
    }
}

算法的标准画法:

相关链接:

Algorithms for Java

原文地址:https://www.cnblogs.com/learnhow/p/9784144.html

时间: 2024-11-07 23:36:39

小橙书阅读指南(十二)——无向图、深度优先搜索和路径查找算法的相关文章

小橙书阅读指南(二)——选择排序

算法描述:一种最简单的排序算法是这样的:首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置.再次,再剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置.如此往复,知道将整个数组排序.这种方法叫做选择排序,因为它在不断地选择剩余元素之中的最小者. 算法图示: Java代码示例: import common.ArraysGenerator; import common.Sortable; import java.io.IOException; import java.uti

小橙书阅读指南(十一)——散列表

算法描述:散列表是一种在时间和空间上做出权衡的查找算法.使用查找算法分为两步.第一步是通过散列函数将被查找的键转化未数组的一个索引.理想情况下,不同的键都能转为不同的索引值.当然,这只是理想情况,所以我们需要面对两个或多个键都被散列到相同索引值的情况.因此,散列查找的第二部就是处理碰撞冲突的过程. 一个比较令人满意的散列函数能够均匀并独立地将所有键散布于0到M-1之间. 一.基于拉链法的散列表 算法图示: 拉链散列表算法的本质是将哈希值相同的键保存在一个普通链表中,当我们需要调整数组长度的时候,

小橙书阅读指南(十)——二叉查找树

算法描述:二叉查找树时一种能够将链表插入的灵活性和有序数组查找的高效性结合起来的符号表(SymbolTable)实现.具体来说,就是使用每个节点含有两个链接的二叉树来高效地实现符号表.一颗二叉查找树时一颗二叉树,其中每个节点都含有一个Comparable的键且每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键. 一.查找 一般来说,在符号表中查找一个键只可能出现命中和未命中两种情况.一般通过递归算法在二叉树中查找,如果树时空的则查找未命中:如果被查找的键和根节点相等,查找命中,

小橙书阅读指南(九)——红黑平衡树(2)

从标准二叉树的极端情况我们推导出2-3树这样的数据结构具备自平衡的特性,但是要实现这个特性在算法上相当复杂.考虑在大部分情况下,对于检索的指数级时间消费O(lgN)要求并不严格.因此,我们会看到如何将一颗标准的2-3树转变成红黑树的过程. 一.局部变换 考虑如果在2-节点上挂新的键并不会破坏2-3树的平衡结构.可是在3-节点上挂新的键,可能的变化却多达6种.这个临时的4-节点可能是根节点,可能是一个2-节点的左子节点或者右子节点,也可能是3-节点的左子节点.中子节点或者右子节点.2-3树插入算法

小橙书阅读指南(五)——归并排序的两种实现

算法描述:将两个较小的有序数组合并成为一个较大的有序数组是比较容易的事情.我们只需要按照相同的顺序依次比较最左侧的元素,然后交替的放进新数组即可.这就是自顶向下的归并排序的实现思路.与之前的算法不同的是,归并排序需要使用额外的存储空间,用空间换时间的做法也是在排序算法中经常需要做的选择. 算法图示: 算法解释:把一个较大的数组不断划分为较小的两个数组,直到无法再切分之后再做逆向合并,并再合并的过程中调整顺序.归并算法的难点是如何尽可能的减少额外存储空间的使用. Java代码示例: package

小橙书阅读指南(七)——优先队列和索引优先队列

算法描述:许多应用程序都需要按照顺序处理任务,但是不一定要求他们全部有序,或是不一定要一次就将他们排序.很多情况下我们只需要处理当前最紧急或拥有最高优先级的任务就可以了.面对这样的需求,优先队列算法是一个不错的选择. 算法图示: 算法解释:上图所展示的是最大优先队列(大顶堆)的算法逻辑,在这个标准的二叉树中,任意节点的元素都大于其叶子节点的元素.利用数组表示该二叉树即Array[2]和Array[3]是Array[1]的叶子节点,Array[4]和Array[5]是Array[2]的叶子节点,A

小橙书阅读指南(三)——插入排序

算法描述:通常人们在整理扑克的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置.在算法的实现中,为了给要插入的元素腾出1个空间,我们需要将其余所有元素在插入之前都向右移动1位.这种算法叫插入算法. 算法图示: 算法解释:在基础版本中通常的做法是,当新元素需要被插入有序数组的时候,从右向左依次交换.直到新元素到达它合适的位置. Java代码示例: import common.ArraysGenerator; import common.Sortable; import java.i

小橙书阅读指南(六)——快速排序和三向切分快速排序

算法描述:快速排序是一种分治的排序算法.它将数组分为两个子数组,并将两部分独立的排列.快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将子数组归并以将整个数组排序:而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了. 算法图示: 算法解释:选择标的元素(5)并且便利数组,将素有小于5的元素都安排在它的左侧,而大于5的元素都安排在它的右侧.之后再通过递归的方法分别处理左边的子数组和右边的子数组. 快速排序的算法难点在于尽量不要使用额外的存储空间(即保证原地

寒假阅读笔记十二

架构之美--最终用户应用架构(二) 今天,我阅读的是<架构之美>的第十二章,这一章主要讲的是Akonadi框架,让我充分了解了Akonadi框架是什么?怎么用? kde 4.1中的Akonadi是一个以mysql为存储管理的 KDE 4 存储接口.它分为两个部分,一个称之为 Akonadi服务器,一个是为用户程序提供的和Akonadi服务器打交道的库,Akonadi服务器是单独提供的程序,属于kde的支持部分的一个软件.用户库包含在kdepimlibs之中.Akonadi目前的主要应用是做为k