减治法-插入排序和图的两种查找方式

减治技术利用了一个问题给定实例的解和同样问题较小实例的解之间的某种关系。一旦建立了这种关系,就可以从顶至下(递归式),也可以从底至上(非递归式)地来运用该关系。减治法的三个主要变种:

  • 减去一个常量
  • 减去一个常量因子
  • 减去的规模是可变的

示例一,减(一)治技术:

clip_image001

实例二,减(半)治技术:

clip_image001[7]

5.1 插入排序

先从减治的思路考虑排序的问题,要对A[0..n-1]进行排序,我们可以假定A[0..n-2]已经排好序了,那么现在所需要做的就是将A[n-1]这个数插到前面n-1个数中一个合适的位置。至于如何插入,有三种方式,如下:

  • 从左至右扫描直接插入
  • 从右至左扫描直接插入(书上说这种效率比第一种要高一些)
  • 折半插入

折半插入排序是基于递归思想的,但从底至上(迭代)实现的话效率更高。下面的代码是从右至左扫描直接插入。

代码实现:

/**
     * 5.1 插入排序
     * @author xiaofeig
     * @since 2015.9.20
     * @param array 目标排序数组
     * */
    public static void insertSort(int[] array){
        for(int i=1;i<array.length;i++){
            for(int j=i-1;j>=0&&array[j]>array[j+1];j--){
                int temp=array[j];
                array[j]=array[j+1];
                array[j+1]=temp;
            }
        }
    }

算法分析:

该算法的基本操作是键值比较array[j]>array[j+1],最坏的情况是一个严格递减的数组。对于这种情况,键值的比较次数是:

image

因此,在最坏的情况下,插入排序和选择排序的键值比较次数是完全一致的。最好的情况是输入的数组已经按升序排好序了。因此对于升序的数组,键值比较的次数是

image

对于有序数组这种最优输入,虽然有非常好的性能,但这种情况本身没有太大的意义,应为不能指望这么简便的输入。

对该算法平均效率的精确分析主要基于对无序元素对的研究。这种分析表明,对于随即序列的数组,插入排序的平均比较次数是降序数组的一半,即

image

相比基本排序算法领域的选择排序和冒泡排序,插入排序还是较领先的,能够表现出一定优异性能。

5.2 深度优先查找和广度优先查找

图的表示

图的主要变种有无向图、有向图和加权图,它的主要表示方法有邻接矩阵和邻接链表。

clip_image001[1]

image

上面右图是无向图的邻接链表表示法,用数组来保存图的每个节点,链表表示边关系,链表中保存的值为节点下标,如a节点链表中的2表示节点a与节点c存在边关系。

5.2.1 深度优先查找

介绍两个名词,树向边回边

如果第一次遇到一个新的未访问节点,它是从哪个节点被访问到的,就把它附加给那个节点的子节点,而连接这两个节点的边成为树向边

如果遇到一条指向已访问节点的边,并且这个节点不是它的直接前驱(父节点),称这条边为回边

在深度优先查找遍历的时候需要构造一个深度优先查找森林,下图是示例的深度优先查找森林,其中灰线表示回边。

clip_image001[13]

遍历的初始节点可以是第一个棵树的根节点。比如上面的左图中,我们可以构造有两棵树的森林,第一棵树的根节点是a,第二颗树的根节点是g,遍历的时候就可以先从a节点开始。它的第一个相邻节点下标为2,即访问节点c,再访问节点c的相邻节点,直到所访问节点的相邻节点都已被访问再退回至上一层的节点,访问该节点的其他相邻节点,最后退回至节点a,访问a的其他未访问的相邻节点。这样,第一棵树就已遍历完成。

依次类推访问其他树。下图是实例的访问过程:

clip_image001[7]

 

代码实现:

/**
     * 5.2.1 深度优先遍历
     * @author xiaofeig
     * @param g 要遍历的图
     * @since 2015.9.24
     * */
    public static <T> void depthFirstSearch(Graph<T> g){
        if(g!=null){
            for(int i=0;i<g.getNodes().length;i++){
                dfs(g,i);
            }
        }
    }
    /**
     * 递归方式访问每个next节点
     * @author xiaofeig
     * @param g 目标图
     * @param index 要访问的节点下标
     * @since 2015.9.24
     * */
    public static <T> void dfs(Graph<T> g,Integer index){
        if(index<g.getNodes().length&&index>=0){
            if(!g.getNodes()[index].isVisit()){
                System.out.print(g.getNodes()[index].getValue()+",");
                g.getNodes()[index].setVisit(true);
            }
            Node<Integer> next=g.getNodes()[index].getNext();
            while(next!=null){
                if(!g.getNodes()[next.getValue()].isVisit()){
                    dfs(g,next.getValue());
                }
                next=next.getNext();
            }
        }
    }

图类Graph(getter和setter方法略):

/**
 * 图(邻接表表示法)
 * @author xiaofeig
 * @since 2015.9.24
 * */
public class Graph<T> {

    private Node<T>[] nodes;
    /**
     * 添加边
     * @param i 边的始节点序号
     * @param j 边的末节点序号
     * */
    public void addEdge(int i,int j){
        if(i<nodes.length&&i>=0&&j<nodes.length&&j>=0){
            Node node=nodes[i];
            while(node.getNext()!=null){
                node=node.getNext();
            }
            Node<Integer> next=new Node<Integer>();
            next.setValue(j);
            node.setNext(next);
        }
    }
}

节点类Node(getter和setter方法略):

/**
 * 节点元素
 * @author xiaofeig
 * @since 2015.9.24
 * */
public class Node<T> {

    /** 节点的值 */
    private T value;
    /** 是否已被访问 */
    private boolean visit;
    /** 下一个节点的下标 */
    private Node<Integer> next;

    /**
     * 增加节点到当前节点的尾部
     * @param t 增加的目标节点值
     * */
    public void add(Integer i) {
        if (i != null) {
            Node<Integer> node = new Node<Integer>();
            while (next != null) {
                next = next.getNext();
            }
            next = node;
        }
    }
    
}

测试代码:

        Graph<String> g=new Graph<String>();
        Node<String>[] nodes=new Node[10];
        for(char c='a';c<'k';c++){
            nodes[c-'a']=new Node<String>();
            nodes[c-'a'].setValue(new String(new char[]{c}));
        }
        g.setNodes(nodes);
        
        g.addEdge(0, 2);
        g.addEdge(0, 3);
        g.addEdge(0, 4);
        g.addEdge(1, 3);
        g.addEdge(1, 4);
        g.addEdge(1, 5);
        g.addEdge(2, 0);
        g.addEdge(2, 3);
        g.addEdge(2, 5);
        g.addEdge(3, 0);
        g.addEdge(3, 1);
        g.addEdge(3, 2);
        g.addEdge(4, 0);
        g.addEdge(4, 1);
        g.addEdge(4, 5);
        g.addEdge(5, 1);
        g.addEdge(5, 2);
        g.addEdge(5, 4);
        g.addEdge(6, 7);
        g.addEdge(6, 9);
        g.addEdge(7, 6);
        g.addEdge(7, 8);
        g.addEdge(8, 7);
        g.addEdge(9, 6);
        
        Chapter5_2.depthFirstSearch(g);

效率分析:

深度优先查找遍历实际上是非常高效的,因为它锁消耗的时间和用来表示图的数据结构的规模是成正比的。对于邻接矩阵表示法,它遍历的时间效率属于Θ(|V|2),而对于邻接链表表示法,它属于Θ(|V|+|E|),其中|V|和|E|分别是图的顶点和边的数量。

我们可以使用深度优先遍历来检查一个图中是否包含回路。

5.2.2 广度优先查找

刚刚在深度优先查找的内容中有说到回边,在这里,回边被称为交叉边

广度优先查找遍历最先也是需要构建这样一个广度优先查找森林的,下图是示例的广度优先查找森林,灰线表示交叉线。

clip_image001[17]

思路也很简单,先选取某个节点,如节点a,遍历完所有它的子节点后,在依次访问它所有子节点的子节点,当然访问之前要判断一下是否已被访问过了。

下图是示例广度优先查找的顺序。

clip_image001[11]

代码实现:

/**
     * 5.2.2 广度优先遍历
     * @author xiaofeig
     * @param g 要遍历的图
     * @since 2015.9.24
     * */
    public static <T> void breadthFirstSearch(Graph<T> g){
        if(g!=null){
            for(int i=0;i<g.getNodes().length;i++){
                if(!g.getNodes()[i].isVisit()){
                    System.out.print(g.getNodes()[i].getValue()+",");
                    g.getNodes()[i].setVisit(true);
                    List<Node<Integer>> result=new LinkedList<Node<Integer>>();
                    result.add(g.getNodes()[i].getNext());
                    for(int j=0;j<result.size();j++){
                        result.addAll(bfs(g,result.get(j)));//将下一层的边关系添加到集合中,类似队列
                    }
                }
            }
        }
    }
    /**
     * 递归方式访问每个next节点
     * @author xiaofeig
     * @since 2015.9.25
     * @param g 目标图
     * @param next 要访问的边关系
     * @return 树下一层的边关系
     * */
    public static <T> List<Node<Integer>> bfs(Graph<T> g,Node<Integer> next){
        List<Node<Integer>> result=new LinkedList<Node<Integer>>();
        while(next!=null){
            Node<T> node=g.getNodes()[next.getValue()];
            if(!node.isVisit()){
                System.out.print(node.getValue()+",");
                node.setVisit(true);
                if(node.getNext()!=null){
                    result.add(node.getNext());
                }
            }
            next=next.getNext();
        }
        return result;
    }

算法分析:

广度优先查找和深度优先查找的效率是相同的。不同的是,它产生了顶点的一种序列(FIFO的结构),而深度优先查找以栈的结构跟踪节点会比较合适。

广度优先查找不仅可以检查图的连通性和无环性,还可以求出两个给定顶点之间的最短距离。

posted @ 2015-09-25 21:04  wokelon  阅读(1714)  评论(0编辑  收藏  举报