【数据结构】有向图

有向图

一. 有向图的相关术语

在有向图中,边是单向的:每条边连接的两个顶点都是一个有序对,它们的邻接性是单向的。我们开发过程中碰到的很多场景都是有向图:比如任务調度的依赖关系,社交网络的任务关系等等都是天然的有向图。

以下概念都是针对有向图的:

(1)==有向图==:一幅有向图是由一组顶点和一组有方向的边组成的,每条有方向的边都连接着有序的一对顶点。

(2)==顶点的出度==:该顶点指出的边的总数。

(3)==顶点的入度==:指向该顶点的边的总数。

(4)比如对于有序对(w,v) 一般代表w->v 的一条有向边。

(5)==有向路径==:由一系列顶点组成,其中每个顶点都存在一条有向边从它指向序列中的下一个顶点。

(6)==有向环==:为一条至少含有一条边且起点和终点相同的有向路径。

(7)==简单有向环==:是一条除了起点和终点外,不含有重复的顶点和边的环。

二. 有向图的存储数据结构

首先定义有向图的API接口,DiGraph 本质上和Graph是一样的。

public class Digraph
Digraph(int v) 创建一幅含有v个顶点,但是没有边的有向图
Digraph(In in) 从输入流中读取一幅有向图
int E() 边的总数
int V() 边的总数
void addEdge(int v,int w) 添加一条有向边v ->w
Iterble<Integer>adj(int v) 由顶点v出发的有向边所连接的所有顶点
Digraph reverse() 该图的反向图
String toString() 对象的字符串表示形式

1. 有向图的表示

和无向图的表示类似,我们还是使用 ==邻接表== 来存储有向图,其中边 v—>w 表示为顶点v所对应的邻接链表中包含一个w顶点。

2. 有向图取反

上面的API中给出了reverse() 方法,将有向图中的所有有向边反转,并生成副本。

3. 有向图的实现

使用邻接表来存储有向图,用数组来表示图中的每个节点的集合,并且数组的下标就表示节点的标识符。

下面就是有向图的实现方式:

package com.example.algorithm4.graphs;

import edu.princeton.cs.algs4.Bag;

/** * 有向图的实现 * * @author 惜暮 * @email chris.lyt@alibaba-inc.com * @date 2017/12/7 */
public class DiGraph {
    /** * 节点的总个数 */
    private final int V;
    /** * 边的总个数 */
    private int E;
    /** * 有向图的邻接表表示法 */
    private Bag<Integer>[] adj;

    /** * 创建一个含有V个节点,但是0条边的有向图 * @param V */
    public DiGraph(int V) {
        this.V = V;
        this.E = 0;
        adj = (Bag<Integer>[])new Bag[V];
        for (int i=0; i<this.V; i++){
            adj[V] = new Bag<>();
        }
    }


    public int E(){
        return this.E;
    }


    public int V(){
        return this.V;
    }

    /** * 添加一条 v-->w 的边 * @param v 有向边的源在数组中的标识符 * @param w 有向边的终在数组中的标识符 */
    public void addEgge(int v,int w){
        adj[v].add(w);
        this.E++;
    }

    /** * 节点v 的所有出度的终顶点 * @param v 节点v 的标识符 * @return */
    public Iterable<Integer> adj(int v){
        return adj[v];
    }

    /** * 此有向图反转之后的副本 * @return */
    public DiGraph reverse(){
        DiGraph reverse = new DiGraph(this.V);
        for(int i=0 ;i<this.V; i++){
            for (int w : adj[i]){
                //边反转
                reverse.addEgge(w,i);
            }
        }
        return reverse;
    }
}

4. 符号有向图的实现

有向图的邻接表表示和无向图的邻接表表示区别不大,仅仅在于边的处理上有一点区别。如果对于有向图的结点的标识我们也想用字符串来表示呢?其实现方法几乎和无向图一样,增加一个Map用来保存顶点的符号到数组下表的映射关系,然后用字符数组保存所有的符号,数组下标天然表示每个顶点的index。 代码实现上只需要把SymbolGraph中的Graph替换成DiGraph就可,下面也给出实现代码:

package com.example.algorithm4.graphs;

import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.ST;

/** * 符号有向图的实现 * @author 惜暮 * @email chris.lyt@alibaba-inc.com * @date 2017/12/7 */
public class SymbolDiGraph {
    private ST<String,Integer> st; // map,顶点符号名 -> 数组索引
    private String[] keys; // 数组索引 -> 顶点符号名
    private DiGraph G; // 图

    /** * 构造一颗符号有向图 * @param stream 顶点边的数据源 * @param sp 顶点分隔符 */
    public SymbolDiGraph(String stream,String sp) {
        st = new ST<>();
        In in = new In(stream); // First pass
        while (in.hasNextLine()) {// builds the index
            String[] a = in.readLine().split(sp); // by reading strings
            for (int i = 0; i < a.length; i++) {// to associate each
                if (!st.contains(a[i])) {// distinct string
                    st.put(a[i],st.size()); // with an index.
                }
            }
        }
        keys = new String[st.size()]; //建立索引 --> 结点符号
        for (String name : st.keys()) {// to get string keys
            keys[st.get(name)] = name; // is an array.
        }

        // 创建符号图
        G = new DiGraph(st.size());
        in = new In(stream); // Second pass
        while (in.hasNextLine()) {// builds the graph
            String[] a = in.readLine().split(sp); // by connecting the
            int v = st.get(a[0]); // source vertex
            for (int i = 1; i < a.length; i++) {// on each line
                G.addEdge(v,st.get(a[i])); // to all the others.
            }
        }
    }

    public boolean contains(String s) {
        return st.contains(s);
    }

    public int index(String s) {
        return st.get(s);
    }

    public String name(int v) {
        return keys[v];
    }

    public DiGraph G() {
        return G;
    }
}

三. 有向图中的可达性

我们在无向图中介绍的第一个算法就是DFS,其实无向图是很多算法的基础,解决了单点连通性的问题,可以判断无向图中指定的两个顶点是否连通。其思想对于有向图同样适用~

定义:有向图的单点可达性:给定一个有向图和一个起点s,判断是否存在一条从起点s到指定节点v的有向路径。

针对以上问题,设计DirectedDFS 算法的API:

public class DirectedDFS
DirectedDFS(DiGraph G,int s) 在G中找到从s可达的所有顶点
DirectedDFS(DiGraph G,Iterable<Integer> sources) 在G中找到从sources的所有顶点中可达的所有顶点
boolean reachable(int v) d顶点v是可达的嘛

1. 使用DFS实现有向图的可达性分析

在有向图中,深度优先搜索标记由一个集合的顶点可达的所有顶点所需的时间与被标记的所有顶点的出度之和成正比。

下面是有向图的可达性算法的实现:

package com.example.algorithm4.graphs;

/** * 有向图的深度优先遍历 * * @author 惜暮 * @email chris.lyt@alibaba-inc.com * @date 2017/12/7 */
public class DirectedDFS {
    /** * 从起点s开始DFS访问过的路径标记 */
    private boolean[] marked;

    public DirectedDFS(Digraph digraph,int s) {
        this.marked = new boolean[digraph.V()];
        dfs(digraph,s);
    }

    public DirectedDFS(Digraph digraph,Iterable<Integer> sources) {
        this.marked = new boolean[digraph.V()];
        for(int s : sources){
            if (!marked[s]){
                dfs(digraph,s);
            }
        }
    }

    /** * 从节点v 开始有向图的DFS * * @param digraph 有向图 * @param v 结点 */
    private void dfs(Digraph digraph,int v){
        this.marked[v] = true;
        for (int w : digraph.adj(v)){
            if(!this.marked[w]){
                dfs(digraph,w);
            }
        }
    }

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

这份深度优先搜索能够实现判断一个或则一组(多点可达性)顶点能到达哪些其他顶点。

2. 标记-清除的垃圾收集

多点可达性的一个重要的实际应用是在典型的内存管理上,比如JVM内存管理,在一幅有向图中,一个顶点代表一个对象,一条边代表一个对象对另外一个对象的引用。 有向图的模型很好的可以应用在JVM内存管理上面。当从根节点遍历时候,那么不可达的顶点就代表不会再被使用,就可以被回收以释放内存。

标记-清除的垃圾回收算法会为每个对象保存一个位作为垃圾回收之用,然后周期性的运行一个类似于DirectedDFS的有向图可达性算法来标记所有可以被访问到的对象,然后清理所有对象,回收没有被标记的对象,以达到释放内存的目的。

3. 有向图的寻路

DepthFirstPaths 和 BreadthFirstPaths 也都是有向图处理中的重要算法。可以用处理下面问题:

  • 单点有向路径:给定一个起点s,从s到给定目的顶点v是否存在一条有向路径。如果有就找出这条路径。
  • 单点最短有向路径:给定一个起点s,从s到给定目的顶点v是否存在一条有向路径。如果有就找出最短的路径。

下图给出了一个使用深度优先遍历在一幅有向图中寻找能够从顶点0到达的所有顶点的轨迹的示意图:

四. 环和有向无环图(DFS和拓扑排序)

在有向图的应用场景中对于有向环的检测十分重要,因为有向环就代表着死循环,然后实际的项目中由于依赖关系的复杂,几乎不可能肉眼检测有向环,所以检测有向环是否存在就至关重要。

对于有向环的检测主要有两种方法:DSF和拓扑排序。下面以实际例子来分析

1. 调度问题(拓扑排序)

一个广泛使用的模型就是给定一组任务并安排它们的执行顺序,限制条件是这些任务的执行方法和起始时间。限制条件可能还包括任务的耗时以及消耗的其他资源。最重要的一种限制条件叫做优先级限制,它指明了哪些任务必须在哪些任务之前完成。 不同类型的限制条件会产生不同类型不同难度的调度问题

下面以一个正在安排课程的大学生为例,有些课程是其余课程的先导课程:如图:

再假设该学生一次只能修一门课,问题就可以抽象成以下模型:

优先级限制下的調度问题:给定一组需要完成的任务,以及一组关于任务完成的先后次序的优先级顺序。在满足条件下以最优方案完成任务。为了简化模型,以数字标识顶点,顶点代表任务。可以等价成下图:

拓扑排序:给定一幅有向图,将所有顶点排序,使得所有的有向边均从排在前面元素指向排在后面的元素。

上面的实例的拓扑排序图如下:所有边都朝下,清晰描绘了这幅有向图模型代表的优先级限制下調度问题的解决方法:

拓扑排序有很多典型的应用,比如任务調度,继承等等。

2. 有向环的检测方法一:DFS

定义。有向无环图就是一幅不包含环的有向图。

有向环的检测,我们可以借助于DFS算法,也可以借助拓扑排序。算法DirectedCycle 实现了环的检测,API如下:

public class DirectedCycle
DirectedCycle(Digraph digraph) 寻找有向环的构造函数
boolean hasCycle() digraph是否有环
Iterable<Integer> cycle() 有向环中的所有顶点

下图是一个在有向环中寻找环的过程示意图:

算法实现如下:

package com.example.algorithm4.graphs;

import java.util.Stack;

/** * 有向图环的检测:DFS算法 * * @author 惜暮 * @email chris.lyt@alibaba-inc.com * @date 2017/12/7 */
public class DirectedCycle {
    /** * 从起点开始DFS访问过的路径标记 */
    private boolean[] marked;
    /** * 从起点到一个顶点v的已知路径上的最后一个顶点 */
    private int[] edgeTo;
    /** * 如果存在有向环,就保存有向环中的所有顶点。 */
    private Stack<Integer> cycle;
    /** * 递归调用栈上的所有顶点 * 这里类似于Java里面的一个set,里面保存了所有已经访问过的顶点 */
    private boolean[] onStack;

    /** * 构造器 * * @param digraph 有向图 */
    public DirectedCycle(Digraph digraph) {
        this.marked = new boolean[digraph.V()];
        this.edgeTo = new int[digraph.V()];
        this.onStack = new boolean[digraph.V()];
        for (int v=0; v<digraph.V(); v++) {
            if( !marked[v] ) {
                dfs(digraph,v);
            }
        }
    }

    /** * 从节点v 开始有向图的DFS * * @param digraph 有向图 * @param v 结点 */
    private void dfs(Digraph digraph,int v){
        // 标记当前结点在堆栈路径上
        this.onStack[v] = true;
        this.marked[v] = true;
        for (int w : digraph.adj(v)){
            // 如果当前图已经有环就直接返回
            if (this.hasCycle(w)){
                return;
            } else if(!this.marked[w]){
                // 如果当前结点没有被标记过,递归dfs
                edgeTo[w] = v;
                dfs(digraph,w);
            } else if (onStack[w]){
                // (1)当前结点被标记过来了,(2)而且在已经访问过得堆栈上,则存在环
                cycle = new Stack<>();
                for(int x = v; x!=w; x = this.edgeTo[x]) {
                    cycle.push(x);
                }
                cycle.push(w);
                cycle.push(v);
            }
        }
        // 递归回溯时,当前结点重置为不在环的堆栈上。
        this.onStack[v] = false;
    }

    public boolean hasCycle(int v){
        return cycle!=null;
    }

    public Iterable<Integer> cycle(){
        return cycle;
    }
}

上面检测有向图是否有环的算法采用标准的递归dfs算法,添加了一个布尔型数组onStack[] 来保存递归调用期间栈上面的所有顶点。当找到一条边 v->w 且w在栈中时,就代表找到了有向环。环上所有顶点都可以通过edgeTo[]数组得到。

3. 顶点的深度优先次序与拓扑结构

在优先级限制下的調度问题等价于计算有向无环图中的所有顶点的拓扑排序,因此给出如下API:

public class Topological
Topological(Digraph digraph) 拓扑排序的构造函数
boolean isDAG() 图digraph是有向无环图嘛?
Iterable<Integer> order() 拓扑排序的所有顶点

定理:当且仅当一幅图是有向无环图时才能进行拓扑排序。也就是有向无环图才有拓扑排序。

首先我们先来看看 ==有向图中基于深度优先搜索的顶点排序== 的DepthFirstOrder算法。

其基本思想是:深度优先搜索正好只会访问每个顶点一次,如果将dfs()的参数顶点保存在一个数据结构中,遍历这个数据结构实际上就能访问图中的所有顶点,遍历的顺序取决于这个数据结构的性质以及是在递归调用之前还是调用之后进行保存。在应用中我们关注的是以下3种排列顺序:

  • 前序:在递归调用之前将顶点加入队列
  • 后序:在递归调用之后将顶点加入队列
  • 逆后续:在递归调用之后将顶点压入栈。

下图是用DepthFirstOrder 算法处理有序无环图产生的轨迹。实现简单,支持处理图的高级算法中十分有用的pre()、post()和reversePost()方法。 例如Topological类中order()方法就调用了reversePost()方法。

算法实现如下:

package com.example.algorithm4.graphs;

import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

/** * 有向图中基于深度优先搜索的顶点排序 * * @author 惜暮 * @email chris.lyt@alibaba-inc.com * @date 2017/12/7 */
public class DepthFirstOrder {
    /** * 从起点开始DFS访问过的路径标记 */
    private boolean[] marked;
    /** * 所有顶点的前序排列 */
    private Queue<Integer> pre;
    /** * 所有顶点的后序排列 */
    private Queue<Integer> post;
    /** * 所有顶点的逆后序排列 */
    private Stack<Integer> reversePost;

    public DepthFirstOrder(Digraph digraph) {
        marked = new boolean[digraph.V()];
        pre = new LinkedList<>();
        post = new LinkedList<>();
        reversePost = new Stack<>();

        for (int v=0; v<digraph.V(); v++){
            if ( !marked[v] ) {
                dfs(digraph,int v){
        pre.add(v);
        this.marked[v] = true;
        for (int w : digraph.adj(v)){
            if(!this.marked[w]){
                dfs(digraph,w);
            }
        }
        post.add(v);
        reversePost.push(v);
    }

    public Iterable<Integer> pre(){
        return pre;
    }

    public Iterable<Integer> post(){
        return post;
    }

    public Iterable<Integer> reversePost(){
        return reversePost;
    }
}

该类允许使用各种顺序遍历DFS经过的顶点。这在高级的有向图处理算法中非常有用,因为搜索的递归性是的我们能够证明这段计算的许多性质。

下面给出拓扑排序==Topological==的实现算法:

package com.example.algorithm4.graphs;

/** * 有向无环图的 拓扑排序 * * @author 惜暮 * @email chris.lyt@alibaba-inc.com * @date 2017/12/7 */
public class Topological {
    /** * 顶点的拓扑排序 */
    private Iterable<Integer> order;

    public Topological(Digraph digraph){
        DirectedCycle cycleFinder = new DirectedCycle(digraph);
        if( !cycleFinder.hasCycle() ) {
            DepthFirstOrder dfs = new DepthFirstOrder(digraph);
            order = dfs.reversePost();
        }
    }

    public Iterable<Integer> order() {
        return order;
    }

    public boolean isDAG(){
        return order!=null;
    }
}

Topological的实现借助了DirectedCycle 检测环是否存在,借助DepthFirstOrder的逆后续来获取拓扑排序。

==定理:一幅有向无环图的拓扑排序就是所有顶点的逆后续排列。==

证明:

在任意一条边v->w, 在调用dfs(v) 时,下面三种情况,必有一种成立:

(1)dfs(w) 已经被调用过了,且已经返回了。 (w节点已经被标记true了)

(2)dfs(w)还没有被调用(w还没被标记), 因此v->w 会直接或则间接调用并返回dfs(w),且dfs(w)会在dfs(v)之前返回。

(3)dfs(w)已经被调用但还没返回。证明的关键在于,在DAG中,这种情况是不可能出现的。由于递归调用链的特性意味着存在从w到v的路径,但又存在v->w的边,所以存在一个环。

在上面(1)(2)两种情况中dfs(w) 会在dfs(v)之前完成,也就是后续排列中w排在v之前。 所以在逆后序中w排在v之后,因此任意一条边v->w 都如我们所愿地从排名比较靠前的顶点指向排名靠后的顶点。

==定理:使用深度优先算法对有向无环图进行拓扑排序所需要时间和 (V+E) 成正比。==

证明:从DirectedCycle和DepthFirstOrder的实现可知,两次搜索都访问了所有顶点和边,所以时间和(V+E)成正比。

下面给出了一个示例 DAG 的逆后续是拓扑排序的轨迹图:

五. 有向图中的强连通性

定义:有向图中,如果两个顶点v和w是互相可达的,则称它们为强连通的。也就是说存在一条从v到w的有向路径,也存在一条从w到v的有向路径。 如果一幅有向图中任意两个顶点都是强连通的,则称这幅有向图也是强连通的。

1. 强连通分量

有向图的强连通性也是一种顶点之间的平等关系,因为其有着如下性质:

  • 自反性:任意顶点v和自己都是强连通的。
  • 对称性:如果v和w是强连通的, 那么w和v也是强连通的。
  • 传递性:如果v和w是强连通的且w和x也是强连通的 ,那么v和x也是强连通的。

强连通分量就是:有向图的极大强连通子图

2.强连通分量应用

最典型应用就是网络,以顶点代表网页,超链接代表边。 下面给强连通分量的API:

public class SCC
SCC(Digraph digraph) 预处理构造函数
boolean stronglyConnected(int v,int w) v和w是强连通的吗?
int count() 图中强连通分量的总数
int id(int v) v所在强连通分量的标识符(0至count()-1 之间)

3. 计算强连通分量的Kosaraju算法(无)

4. 再谈可达性(无)

六. 总结(无)

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


【啊哈!算法】算法3:最常用的排序——快速排序       上一节的冒泡排序可以说是我们学习第一个真正的排序算法,并且解决了桶排序浪费空间的问题,但在算法的执行效率上却牺牲了很多,它的时间复杂度达到了O(N2)。假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序则只需要0.1秒,而冒泡排序则需要1千万秒,达到115天之久,是不是很吓人。那有没有既不浪费空间又可以快一点的排序算法
匿名组 这里可能用到几个不同的分组构造。通过括号内围绕的正则表达式就可以组成第一个构造。正如稍后要介绍的一样,既然也可以命名组,大家就有考虑把这个构造作为匿名组。作为一个实例,请看看下列字符串: “08/14/57 46 02/25/59 45 06/05/85 18 03/12/88 16 09/09/90 13“ 这个字符串就是由生日和年龄组成的。如果需要匹配年两而不要生日,就可以把正则
选择排序:从数组的起始位置处开始,把第一个元素与数组中其他元素进行比较。然后,将最小的元素方式在第0个位置上,接着再从第1个位置开始再次进行排序操作。这种操作一直到除最后一个元素外的每一个元素都作为新循环的起始点操作过后才终止。 public void SelectionSort() { int min, temp;
public struct Pqitem { public int priority; public string name; } class CQueue { private ArrayList pqueue; public CQueue() { pqueue
在编写正则表达式的时候,经常会向要向正则表达式添加数量型数据,诸如”精确匹配两次”或者”匹配一次或多次”。利用数量词就可以把这些数据添加到正则表达式里面了。 数量词(+):这个数量词说明正则表达式应该匹配一个或多个紧紧接其前的字符。 string[] words = new string[] { "bad", "boy", "baad", "baaad" ,"bear", "b
来自:http://blog.csdn.net/morewindows/article/details/6678165/归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列
插入排序算法有两层循环。外层循环会啄个遍历数组元素,而内存循环则会把外层循环所选择的元素与该元素在数组内的下一个元素进行比较。如果外层循环选择的元素小于内存循环选择的元素,那么瘦元素都想右移动以便为内存循环元素留出位置。 public void InsertionSort() { int inner, temp;
public int binSearch(int value) { int upperBround, lowerBound, mid; upperBround = arr.Length - 1; lowerBound = 0; while (lowerBound <= upper
虽然从表内第一个节点到最后一个节点的遍历操作是非常简单的,但是反向遍历链表却不是一件容易的事情。如果为Node类添加一个字段来存储指向前一个节点的连接,那么久会使得这个反向操作过程变得容易许多。当向链表插入节点的时候,为了吧数据复制给新的字段会需要执行更多的操作,但是当腰吧节点从表移除的时候就能看到他的改进效果了。 首先需要修改Node类来为累增加一个额外的链接。为了区别两个连接,这个把指
八、树(Tree)树,顾名思义,长得像一棵树,不过通常我们画成一棵倒过来的树,根在上,叶在下。不说那么多了,图一看就懂:当然了,引入了树之后,就不得不引入树的一些概念,这些概念我照样尽量用图,谁会记那么多文字?树这种结构还可以表示成下面这种方式,可见树用来描述包含关系是很不错的,但这种包含关系不得出现交叉重叠区域,否则就不能用树描述了,看图:面试的时候我们经常被考到的是一种叫“二叉树”的结构,二叉
Queue的实现: 就像Stack类的实现所做的一样,Queue类的实现用ArrayList简直是毋庸置疑的。对于这些数据结构类型而言,由于他们都是动态内置的结构,所以ArrayList是极好的实现选择。当需要往队列中插入数据项时,ArrayList会在表中把每一个保留的数据项向前移动一个元素。 class CQueue { private ArrayLis
来自:http://yingyingol.iteye.com/blog/13348911 快速排序介绍:快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地
Stack的实现必须采用一种基本结构来保存数据。因为再新数据项进栈的时候不需要担心调整表的大小,所以选择用arrayList.using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Collecti
数组类测试环境与排序算法using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Data_structure_and_algorithm{ class CArray { pr
一、构造二叉树 二叉树查找树由节点组成,所以需要有个Node类,这个类类似于链表实现中用到的Node类。首先一起来看看Node类的代码。 public class Node { public int Data; public Node Left; public Node Right; public v
二叉树是一种特殊的树。二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。 下面这棵树就是一棵二叉树。         二叉树的使用范围最广,一棵多叉树也可以转化为二叉树,因此我们将着重讲解二叉树。二叉树中还有连两种特殊的二叉树叫做满二叉树和
上一节中我们学习了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据结构它叫做栈。栈限定只能在一端进行插入和删除操作。比如说有一个小桶,小桶的直径只能放一个小球,我们现在向小桶内依次放入2号、1号、3号小球。假如你现在需要拿出2号小球,那就必须先将3号小球拿出,再拿出1号小球,最后才能将2号小球拿出来。在刚才取小球的过程中,我们最先放进去的小球最后才能拿出来,而最后放进去的小球却可以最先拿
msdn中的描述如下:(?= 子表达式)(零宽度正预测先行断言。) 仅当子表达式在此位置的右侧匹配时才继续匹配。例如,w+(?=d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。(?(零宽度正回顾后发断言。) 仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?此构造不会回溯。msdn描述的比较清楚,如:w+(?=ing) 可以匹配以ing结尾的单词(匹配结果不包括ing),(
1.引入线索二叉树 二叉树的遍历实质上是对一个非线性结构实现线性化的过程,使每一个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。但在二叉链表存储结构中,只能找到一个节点的左、右孩子信息,而不能直接得到节点在任一遍历序列中的前驱和后继信息。这些信息只有在遍历的动态过程中才能得到,因此,引入线索二叉树来保存这些从动态过程中得到的信息。 2.建立线索二叉树 为了保
排序与我们日常生活中息息相关,比如,我们要从电话簿中找到某个联系人首先会按照姓氏排序、买火车票会按照出发时间或者时长排序、买东西会按照销量或者好评度排序、查找文件会按照修改时间排序等等。在计算机程序设计中,排序和查找也是最基本的算法,很多其他的算法都是以排序算法为基础,在一般的数据处理或分析中,通常第一步就是进行排序,比如说二分查找,首先要对数据进行排序。在Donald Knuth 的计算机程序设