十大算法 - Java -韩顺平 图解Java数据结构和算法

程序员使用的十大算法

    • 1. 二分查找算法
    • 2. 分治算法
    • 3. 动态规划算法
    • 4. KMP算法 (字符串匹配问题)
    • 5. 贪心算法(集合覆盖问题)
    • 6. 普利姆算法(修路问题)
    • 7. 克鲁斯卡尔算法(公交站问题)
    • 8. 迪杰斯特拉算法
    • 9. 弗洛伊德算法
    • 10. 骑士周游问题

Java -韩顺平 图解Java数据结构和算法最后的讲解总结

1. 二分查找算法

可以使用非递归和递归的方式进行,代码的区别在于while()内的条件。
调用递归

public static ArrayList<Integer> binarySearch2(int[] arr, int left, int right, int findVal){
    int mid = (left+right)/2;
    int midVal = arr[mid];

    if (left>right){
        return new ArrayList<Integer>();
    }

    if (findVal>midVal){
        //向右递归
        return binarySearch2(arr,mid+1,right,findVal);
    }else if (findVal<midVal){
        return binarySearch2(arr,left,mid-1,findVal);
    }else {
        ArrayList<Integer> resIndexList = new ArrayList<>();
        int temp = mid-1;
        //左
        while (true){
            if (temp<0 || arr[temp]!=findVal){
                break;
            }
            resIndexList.add(temp);
            temp--;
        }
        //中
        resIndexList.add(mid);
        //右
        temp = mid+1;
        while (true){
            if (temp>arr.length-1 || arr[temp]!=findVal){
                break;
            }
            resIndexList.add(temp);
            temp++;
        }

        return resIndexList;
    }
}

不调用递归

public static int binarySearch(int[] arr,int targetVal){
        int left=0;
        int right = arr.length-1;
        while (left<=right){
            int mid = (left+right)/2;
            if (targetVal==arr[mid]){
                return mid;
            }else if (arr[mid]>targetVal){
                right=mid-1;
            }else {
                left=mid+1;
            }
        }
        return -1;
    }

2. 分治算法

就是把一个复杂的问题分成两个或者更多相同、相似的子问题。(汉诺塔,移动)
n=1 的时候,直接移动。
n>=2的时候,就看做是两个盘,1是最下边的盘。2是它上面的盘。
先把上面的移动到A->B,再最下边到C,把B->C.

//汉诺塔的移动方法
    public static void hanmoTower(int num,char a,char b,char c){
        if (num==1){
            System.out.println("第一个盘从"+a+"->"+c);
        }else {
            //看成最下边一个和上边所有的算一个
            //1.先把最上边的所所有盘A-->B,移动过程会使用到c
            hanmoTower(num - 1, a, c, b);
            //2.把最下边的盘A--C
            System.out.println("第" + num + "个 从" + a + "->" + c);
            //3.在把所有盘从B-C
            hanmoTower(num - 1, b, a, c);
        }
    }

3. 动态规划算法

将大问题划分为小问题,从而一步步获取最优解决的处理算法。
与分治算法不同的是,子问题不是相互独立的
背包问题,【无限背包可以转为01背包】
第i个物品重量和价值–>w[i]和v[i]
二维数组V[i][j]–>前i号物品放入j最大的容量的价值V[i][j]

	①v[0][j]=v[i][0]=0.
	②当w[i]>j时,v[i][j]=v[i-1][j].  就是你装不下第i个物品。
	③当w[i]<=j时,就是第i个物品可以装下的时候,判断是装这个价值大,还是按照之前的价值大。
	v[i][j]=max{v[i-1][j], v[i]+v[j-w[i]]}

难点1是在判断价值大小,2是记录结果

    public static void main(String[] args) {
        int[] weight = {1,4,3};
        int[] val = {1500,3000,2000};
        int m=4; //背包容量
        int n = val.length; // 物品的个数
        //创建二维数组,v[i][j]表示将前i个物品装入容量j背包的最大价值
        int[][] v = new int[n+1][m+1];
        int[][] path = new int[n+1][m+1];

        //第一行第一列,默认为0.也可以进行一下初始化
        for (int i = 0; i < v.length; i++) {
            v[i][0] = 0;
        }

        for (int i = 0; i < v[0].length; i++) {
            v[0][i] = 0;
        }

        for (int i = 1; i <v.length; i++) {
            for (int j = 1; j <v[0].length; j++) {
                //套用公式
                if (weight[i-1]>j){
                    v[i][j]=v[i-1][j];
                }else {
//                    v[i][j]=Math.max(v[i-1][j],val[i-1]+v[i-1][j-weight[i-1]]);
                    //因为i是从1开始的,val是从0开始的
                    //为了记录商品存放到背包的情况,我们不能直接使用上面的公式,需要使用if-else来判断
                    if (v[i-1][j]<val[i-1]+v[i-1][j-weight[i-1]]){
                        v[i][j] = val[i-1]+v[i-1][j-weight[i-1]];
                        path[i][j]=1;
                    }else {
                        v[i][j] = v[i-1][j];
                    }
                }
            }
        }      

        for (int i = 0; i < v.length; i++) {
            for (int j = 0; j < v[0].length; j++) {
                System.out.print(v[i][j]+" ");
            }
            System.out.println();
        }
        System.out.println("-----------------");
        //输出最后我们放入的商品
//        以下的是错误的,因为需要的最终结果,中间有过程也会使得等于1.会输出所有情况。
        for (int i = 0; i < path.length; i++) {
            for (int j = 0; j < path[0].length; j++) {
                System.out.print(path[i][j]+" ");
//                if (path[i][j]==1){
//                    System.out.printf("第%d个商品放入背包%n",i);
//                }
            }
            System.out.println();
        }
        // path可以看到,横坐标是能加入几个,纵坐标是相对于前一行对比,如果大于等于他就置1.
        //事实上,我们需要寻找当给定m的容量,最下边的置1的情况返回。

        int i= path.length-1; //行的最大下标
        int j = path[0].length-1; //列的最大下标
        while(i>0&&j>0){
            //从后边遍历
            if (path[i][j]==1){
                System.out.printf("第%d个商品放入背包%n",i);
                j=j-weight[i-1];
                //就是,这里放过背包,放过背包以后,肯定有weight[i-1]+X 重量,把weight[i-1]减掉,就是X,看X可以再存多少。
            }
            i--;//表示的是第i个已经放过了,现在要往前找i-1个放入容量为X的背包
        }
    }

4. KMP算法 (字符串匹配问题)

①暴力匹配算法

//暴力匹配
public static int violenceMatch(String str1,String str2){
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int s1Len = s1.length;
int s2Len = s2.length;
int i = 0;
int j = 0;

while (i

}

②KMP算法
KMP方法就是利用之间判断过的信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,就通过next数组找到前面匹配过的位置。省去大量时间
重点:部分匹配表。是由前缀、后缀的相同长度决定。
如:ABCDA==>[A,AB,ABC,ABCD],[BCDA,CDA,DA,A] ====A 长度为1

    /**
     *
     * @param str1 原字符串
     * @param str2 查找的字符串
     * @param next 部分匹配表
     * @return
     */
    public static int kmpSearch(String str1,String str2, int[] next){

        //遍历str1
        for (int i = 0,j=0; i < str1.length(); i++) {
            //需要处理不同的时候,调整j的大小 【KMP的核心】
            while (j>0&&str1.charAt(i)!=str2.charAt(j)){
                j=next[j-1];
            }

            if (str1.charAt(i)==str2.charAt(j)){
                j++;
            }

            if (j==str2.length()){
                return i-j+1;
                //因为j先j++而i后i++
            }
        }
        return -1;
    }

    //获取一个字符串的部分匹配值表
    public static int[] kmpNext(String dest){
        int[] next = new int[dest.length()];

        next[0]=0;//如果字符串的长度是1,匹配值就是0
        int j=0;
        for (int i = 1; i < dest.length(); i++) {
//            当dest.charAt(i)!=dest.charAt(j),我们需要从next[j-1]获取新的j
//            直到发现有dest.charAt(i)==dest.charAt(j)才推出【KMP的核心】
            while (j>0&&dest.charAt(i)!=dest.charAt(j)){
                j=next[j-1];
            }
            //如果发现不相等,就从部分匹配表的j-1的位置取
            if (dest.charAt(i)==dest.charAt(j)){
            //当dest.charAt(i)!=dest.charAt(j) 相等时,部分匹配值才加1.
                j++;
            }
            next[i]=j;
        }
        return next;
    }

5. 贪心算法(集合覆盖问题)

在每一步选择中都采取最好或者最优的选择
得到的结果不一定是最优的结果,但是都是相对近似最优解的结果。使用贪婪算法,效率高。
例子:广播电台覆盖最多的区域。将区域写成一个集合,遍历电台,遍历找到覆盖最大的,取出。将已经覆盖的区域删除,再重新遍历选择。知道所有区域都覆盖。
代码:

ArrayList<String> selects = new ArrayList<>();
        //定义一个临时的集合,在遍历的过程中,存放电台区域和未覆盖区域的交集,方便判断个数
        HashSet<String> tempSet = new HashSet<String>();

        //定义一个maxKey,保存在遍历的过程中,覆盖数最多的key,不为空,就加入结果selects
        String maxKey = null;
        while (allAreas.size() != 0) {
            //每次循环要maxKey置空!!!【重要】
            maxKey=null;

            for (String key : broadcasts.keySet()) {
                //把临时的集合清空!!!【重要】
                tempSet.clear(); 
                //当前这个key能够覆盖的地区
                HashSet<String> areas = broadcasts.get(key);
                tempSet.addAll(areas);
                //求交集.tempSet和allAreas的交集,并赋值给tempSet
                tempSet.retainAll(allAreas);
                //如果,交集比原来maxKey的数量还多,就替换。
                if (tempSet.size() > 0 && (maxKey == null || tempSet.size() > broadcasts.get(maxKey).size())) {
                    maxKey=key;
                }
            }

            if (maxKey!=null){
                selects.add(maxKey);
                //将存入后的地区,从All区域中去掉
                allAreas.removeAll(broadcasts.get(maxKey));
            }

        }
        System.out.println(selects);

6. 普利姆算法(修路问题)

满足都连通的情况下,怎么使得边的权值总和最小
(最小生成树,带权的无向连通图,N个顶点,N-1条边)
普利姆算法求最小生成树

	在包含n个顶点的连通图中,找出只有n-1条边包含所有n个顶点的连通子图(极小连通子图)
	
	就是先从一个节点开始,找与它相连的权值最小的值。
	然后下一步是在这两个节点为起始点,找与他们相连的权值最小的点。
	下一次,以三个为起始点...最后共找到k-1条边。权值的总和为最小,起始点不同,找到的路径不同。
public void prim(MGraph graph, int v) {
        int[] visited = new int[graph.verx]; //是否被访问过,默认都是0
        //把当前节点标记为已经访问
        visited[v] = 1;

        //记录两个顶点的下标
        int h1 = -1;
        int h2 = -1;
        int minWeight = 10000;//将minWeight初始成一个大数,后边遍历会被替换
        for (int k = 1; k < graph.verx; k++) {
            //k-1条边,普利姆算法结束后,有这么多条边.相当于一条边一条边去找,
            for (int i = 0; i < graph.verx; i++) {
                //先找访问过的
                for (int j = 0; j < graph.verx; j++) {
                    //再找未访问的
                    if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight) {
                        //替换minWeight的意义就是找最小的两个点的权值
                        minWeight = graph.weight[i][j];
                        h1 = i;
                        h2 = j;
                    }
                }
            }
            System.out.println("找到第" + k + "条边| " + graph.data[h1] + "---" + graph.data[h2] + " 权值:" + minWeight);
            visited[h2] = 1;
            minWeight = 10000;
        }
    }

新建图类和最小生成树类代码未复制。

7. 克鲁斯卡尔算法(公交站问题)

	①所有边的权值排序,按照从小到大。
	②依次选择权值小的边的两个顶点,但是要保证不构成回路。
	【加入的边的两个顶点不能都指向同一个终点】

p

ublic void kruskal(){
        int index = 0; //表示最后结果数组的索引
        int[] ends = new int[edgeNum]; //用于保存在 “已经生成的最小生成树”中 每个顶点的终点(不重复,只有一个);
        //创建结果数组.,保存最后的最小生成树
        EData[] results = new EData[edgeNum];

        //获取图中所有边的集合,
        EData[] edges = getEdges();
        System.out.println("图的边的集合"+Arrays.toString(edges)+"共多少条边"+edges.length);

        sortEdge(edges);
        System.out.println(Arrays.toString(edges));
        //遍历edges,将边添加到最小生成树,并判断即将加入的边是否生成了回路
        for (int i = 0; i < edgeNum; i++) {
            //获取到第i条边的第一个顶点
            int p1 = getPosition(edges[i].start);
            int p2 = getPosition(edges[i].end);

            //获取p1顶点在已有生成树的终点;找的是终点。A-B-C,输入A,输出C。
            int m = getEnd(ends,p1);
            int n = getEnd(ends,p2);

            if (m!=n){
                //没有构成回路
                ends[m]=n;//设置m点的终点是n 在已有最小生成树的终点
                results[index++] = edges[i];//有一条边加入数组
            }
        }
        System.out.println(Arrays.toString(results));
    }
/**
     *  获取下标为i的顶点的终点,用于后面判断两个顶点的终点是否相同
     * @param ends 就是每个点对应的终点的数组,数组是在遍历的过程中,逐步形成的
     * @param i 表示传入的顶点对应的下标
     * @return 返回i对应的终点的下标
     */
    private int getEnd(int[] ends,int i){
        while (ends[i]!=0){
            i=ends[i];  //就是返回了输入顶点的终点。输入顶点找与它相连的点,再找相连的点,知道找到顶点
        }
        return i;
    }

8. 迪杰斯特拉算法

典型的最短路径算法,从一个节点到其他节点的最短路径

	它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点位置。
	
	最初的起始点到每个点的距离找到最小值,以最小距离的另一个节点为新起始点,新起始点到每个节点的距离,
	加上前边一段最小的距离,再与最初的节点到现在每个节点的距离比较,选择最小的加入到距离数组。
	再将新的起始点的最小距离的另一端节点为新新起始点。加上前边的距离,与直接距离进行比较,
	选择最小的加入数组。直到所有节点都遍历完。
class Graph {
    private char[] vertex;
    private int[][] matrix;
    private VisitedVertex vv;

    public Graph(char[] vertex, int[][] matrix) {
        this.vertex = vertex;
        this.matrix = matrix;
    }

    public void showGraph() {
        for (int[] link : matrix
        ) {
            System.out.println(Arrays.toString(link));
        }
    }

    /**
     * @param index 出发顶点的下标
     */
    public void dijkstra(int index) {
        vv = new VisitedVertex(vertex.length, index);
        update(index); //更新index节点到周围顶点的距离和它的前驱节点
        //走完,还需要vertex.length-2条边
        for (int i = 1; i < vertex.length; i++) {
            index = vv.updateArr();
            update(index);
        }

    }

    private void update(int index) {
        int len = 0;
        //需要遍历 index的那一行 ,找到节点之间的关系
        for (int i = 0; i < matrix[index].length; i++) {
            //len是 出发顶点到index顶点的距离加上从index到i顶点的距离的和
            len = vv.getDis(index) + matrix[index][i];
            if (!vv.in(i) && len < vv.getDis(i)) {
                vv.updatePre(i, index);//是把下一个节点i的前驱节点置为index。
                vv.updateDis(i, len);
                //是把下一个节点的,到index的距离替换到DIS,DIS原来都是最大值,现在对应的数值就是到index的距离。
            }
        }
        System.out.println("=============");
        System.out.println(Arrays.toString(vv.dis));

    }

    public void showDijstra(){
        vv.show();
    }
}

class VisitedVertex {
    public int[] already_arr;//已经访问的
    public int[] pre_visited;//你访问的,前一个顶点
    public int[] dis; //距离
    /**
     * @param length 点的个数
     * @param index  开始访问的顶点下标
     */
    public VisitedVertex(int length, int index) {
        this.already_arr = new int[length];
        this.pre_visited = new int[length];
        this.dis = new int[length];
        //dis全置为大的数
        Arrays.fill(dis, 65535);
        this.already_arr[index] = 1;//初始化的时候就置自己为1
        this.dis[index] = 0;//初始化dis,自己到自己为0
    }

    /**
     * @param index
     * @return 判断这个点是不是被访问过
     */
    public boolean in(int index) {
        return already_arr[index] == 1;
    }

    /**
     * 更新出发顶点到index的距离
     *
     * @param index 更新的下标
     * @param len   更新的值 (只有小于那个值才更新)
     */
    public void updateDis(int index, int len) {
        dis[index] = len;
    }

    /**
     * 更新pre顶点的前驱顶点为index顶点 就是
     *
     * @param pre   已经访问的顶点,index与pre是相连的
     * @param index 可能需要选择的节点
     */
    public void updatePre(int pre, int index) {
        pre_visited[pre] = index;
    }

    /**
     * 返回出发顶点到index顶点的距离
     *
     * @param index
     */
    public int getDis(int index) {
        return dis[index];
    }

    //在给定出发顶点后继续走,返回下一次的新的访问节点。
    /**
     * 遍历所有节点,找没有访问过的节点。然后判断距离是不是最小的。
     *
     * @return
     */
    public int updateArr() {
        int min = 65535, index = 0;
        for (int i = 0; i < already_arr.length; i++) {
            if (already_arr[i] == 0 && dis[i] < min) {
                min = dis[i];
                index = i;
            }
        }
        already_arr[index] = 1;
        return index;
    }

    //最后结果
    //那三个数组
    public void show() {
        System.out.println("=============");
        System.out.println("already_arr");
        for (int i : already_arr) {
            System.out.print(i + " ");
        }

        System.out.println("pre_visited");
        for (int i : pre_visited) {
            System.out.print(i + " ");
        }

        System.out.println("dis");
        for (int i : dis) {
            System.out.print(i + " ");
        }
        System.out.println();
        char[] vertex ={'A','B','C','D','E','F','G'};
        int count=0;
        for (int i:dis) {
            if (i!=65535){
                System.out.println(vertex[count]+"("+i+")");
            }else {
                System.out.println("N");
            }
            count++;
        }
    }

9. 弗洛伊德算法

迪杰斯特拉算法通过选定的被访问顶点,求出从出发顶点到其他顶点的最短路径。
弗洛伊德算法是每个顶点都是出发访问点,所以需要将每个顶点都看做是被访问顶点,求出从每一个顶点到其他顶点的最短路径。(中间顶点,出发顶点,终点,三层for循环,修改距离表和前驱关系表)
核心:min{L(i,k)+L(k,j),L(i,j)} 这样获得vi到vj的最短路径

//弗洛伊德算法,同意理解,容易实现
    public void floyd(){
        int len = 0;
        //中间顶点的遍历
        for (int k = 0; k < dis.length; k++) {
            for (int i = 0; i < dis.length; i++) {
                //从i点出发
                for (int j = 0; j < dis.length; j++) {
                    //到j终点
                    len = dis[i][k]+dis[k][j]; //i==>k|k==>j
                    if (len<dis[i][j]){
                        System.out.println(k+" "+pre[k][j]);
                        dis[i][j] = len;
                        pre[i][j] = pre[k][j];
                        //因为上边的dis[i][k]+dis[k][j]是已经变化的,所以不再是k,而是pre[k][j]
                    }
                }
            }
        }
    }

10. 骑士周游问题

图的深度优先算法应用。
这里的问题:对回溯的这个过程,或者说代码的运行步骤,有些不太理解。Debug了解一下。

/**
 *
 * @param chessboard 棋盘
 * @param row 行
 * @param column 列
 * @param step 第几步 初始位置是第一步
 */
public static void travelChessBoard(int[][] chessboard,int row,int column,int step){
    chessboard[row][column] = step;
    visited[row*X+column] = true;//标记位置已经访问
    //获取当前位置可以走的集合
    ArrayList<Point> ps = next(new Point(column,row));
    //对ps进行排序,排序的规则就是对ps的所有Point对象的下一步位置的数目进行非递减排序
    sort(ps);

    while (!ps.isEmpty()){
        Point pNext = ps.remove(0);//取出一个可以走的位置
        //判断是还不是访问过
        if (!visited[pNext.y*X+ pNext.x]){
            travelChessBoard(chessboard,pNext.y,pNext.x,step+1);
        }
    }

    //判断step和该走的步数,吐过没有达到数量,就表示没有完成任务,将整个棋盘置为0
    //是有两种情况的,1是步数确实没有达到,2是step达到了,但是处于一个回溯的过程中
    if (step<X*Y&&!finished){
        chessboard[row][column]=0;
        visited[row*X+column]=false;
    }else {
        finished=true;
    }

}

public static ArrayList<Point> next(Point curPoint) {
    ArrayList<Point> ps = new ArrayList<>();

    Point p1 = new Point();

    if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0) {
        ps.add(new Point(p1));
    }
    if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0) {
        ps.add(new Point(p1));
    }

    if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
        ps.add(new Point(p1));
    }
    if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
        ps.add(new Point(p1));
    }

    if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
        ps.add(new Point(p1));
    }
    if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
        ps.add(new Point(p1));
    }

    if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
        ps.add(new Point(p1));
    }
    if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y +2) < Y) {
        ps.add(new Point(p1));
    }

    return ps;

}

//利用贪心算法,优化
//就是找到当前可以走的位置,看可以走的位置的下一步的位置数量,走下一步数量比较少的。减少回溯的可能
// 进行非递减排序【1,2,2,2,2,3,3,4】
public static void  sort(ArrayList<Point> ps){
    ps.sort(new Comparator<Point>() {
        @Override
        public int compare(Point o1, Point o2) {
            //获取o1的下一步的所有位置个数
            int count1 = next(o1).size();
            int count2 = next(o2).size();
            if (count1<count2){
                return -1;
            }else if (count1==count2){
                return 0;
            }else {
                return 1;
            }
        }
    });
}

你可能感兴趣的:(数据结构)