JAVA-数据结构与算法-二分查找算法、汉诺塔(分治算法)和背包问题(动态规划)

598 阅读6分钟

写在前面

二分查找算法(非递归)

  • 只适用于从有序队列中进行查找
  • 核心是修改mid的取值
/**
 * @param arr 待查找的数组
 * @param target 目标数
 * @return 返回对应下标,-1为不存在
 */
public static int binarySearch(int[] arr, int target) {
    int left = 0;
    int right = arr.length - 1;
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] > target) {
            right = mid - 1;
            mid = (left + right) / 2;
        } else {
            left = mid + 1;
            mid = (left + right) / 2;
        }
    }
    return -1;
}

汉诺塔(分治算法)

  • 分治算法,把一个复杂的问题分成两个或更多的子问题,再把子问题分成更小的小问题,再将所有小问题的解合并,就是原题的解 image.png
  • a,b,c->A塔,B塔,C塔;num为数量
  • 3个盘举例
abc为形参
例如如果要移动3个盘,上面两个盘的目标就是移动到B塔,进入第二层,这个递归中abc->ACB
第一步,进入第三层,此时abc->ABC,第一个盘A->C
第二步,跳出第三层,此时abc->ACB,第二个盘A->B
第三步,进入第三层,此时abc->CAB,第一个盘C->B
第四步,跳出第三层,跳出第二层,回到第一层,此时abc->ABC,第三个盘A->C
第五步,进入第二层,B的两个盘的目标是移动到C塔,进入递归后abc->BAC
第六步,进入第三层,此时abc->BCA,第一个盘B->A
第七步,跳出第三层,回到第二层,此时abc->BAC,第二个盘B->C
第八步,进入第三层,此时abc->ABC,第一个盘A->C,完成
第九步,跳出三二一层
public static void hanoitower(int num, char a, char b, char c) {
    //只有一个
    if (num == 1) {
        System.out.println("第1个盘从 " + a + "->" + c);
    } else {
        //如果有n>=2的情况,看作两个盘 1最下面一个盘 2上面的所有盘
        //把上面的所有盘A->B,移动的过程使用C,对于这个目标来说,B为目标塔,C为中间塔
        hanoitower(num - 1, a, c, b);
        //把最下面的盘A->C
        System.out.println("第" + num + "个盘从 " + a + "->" + c);
        //把B塔所有盘从B->C,移动过程使用A,对于这个目标来说,C塔为目标,A塔为中间塔
        hanoitower(num -1 , b, a, c);
    }
}

小结

塔的转换,举例如果有四个盘,最上面的三个盘为一组最终目标是B塔,但是此时对于最上面的两个盘的阶段目标却是C塔,因为只有将最上面的两个盘移动到了C塔,才能保证第三个盘到B塔,那么也就意味着前两个盘要先从A->C,再从C->B;相对应的,如果上面两个盘目标为C,那么第一个盘的目标为B,这也就说明第一步递归时,要交换bc的次序hanoitower(num - 1, a, c, b);进入最后一层递归,完成第一个盘的移动后,就要移动第二个盘A->C,并且还要再将第一个盘B->C,完成第二个盘的目标,此时hanoitower(num -1 , b, a, c),也就完成了四个盘的例子中,最上面两个盘的第一阶段任务A->C

根据上面一段话,可以总结,汉诺塔的分解,就是对于不同盘的数量,每个盘有不同的任务,针对不同的盘,对ABC塔有不同的顺序,就比如说,四个盘中的最上面两个盘的移动,第一阶段中A->C时,顺序为ABC,B是中间塔,让第一个盘先移动到B;但是第二个阶段,当顶部三个盘全部到达B后,顶部两个盘需要从B->A,顺序为BCA,第一个盘需要先到C,第二个盘才能到A;但是,对于三个盘中最上面两个盘的移动,上面两个盘,第一阶段为A->B,顺序为ACB,C是中间塔,需要让第一个盘先移动到C;第二阶段B->C,对于B上的两个盘来说,顺序为BAC,A为中间塔,第一个盘需要先从A,第二个盘才能到C

以上的具体步骤,其实并不能把这两个递归过程讲清楚,但是可以肯定的是,对于不同数量的盘,和不同层的盘来说,目标不同,塔也不同,通过传递形参的方式调整不同盘的对应的顺序

将一堆盘分成两块,第一块为上面的所有盘,第二块为最下面的一个盘,第一块作为形参来说,任务就是a->bb->c,而最后一块的任务就是a->c,那么通过不断的递归,对于不同的盘abc自然会对应不同的塔

背包问题(动态规划)

  • 动态规划,将待求解的问题分解成若干个问题,先求子问题,下一个子阶段的解是建立在上一个子阶段的解的基础上;用填表的方式,逐步推进
int[] w = {1,4,3}; //重量
int[] val = {1500, 3000, 2000}; //物品
int m = 4; //背包重量
int n = val.length; //物品的数两
//二维数组,用填表推导
int[][] v = new int[n+1][m+1];
//记录商品的情况
int[][] path = new int[n+1][m+1];

//v[0][j] = 0 v[i][0] = 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[i].length; j++) {
        if (w[i-1] > j) { //w的下标为从0开始,而填表的部分是从1开始
            //如果物品重量大于当前背包重量,表明这个物品放不进这个重量的背包,直接获取上面的价值
            v[i][j] = v[i-1][j];
        } else { //如果添加的部分重量 w[i-1] <= j 表示可以填多个
            //val下标从0开始,填表的部分从1开始
            //上一个物品的同重量的价值 vs 新物品的价值+剩下背包重量的上一个物品的价值之和
            //        v[i-1][j]        val[i-1] + v[i-1][j-w[i-1]]
            //举例说明,如果此时 物品重量为3 背包重量为4
            //那么就有两种可能性,第一种上一轮为背包重量为4的价值
            //第二种,物品重量为3的价值+背包重量为1的价值之和,重量为1指的是上一轮的这个重量的价值
            //因为,每次计算这个位置,都会进行比较,就能明确这个位置是最大的价值
            //如果是新物品的价值+剩下背包重量的上一个物品的价值之和,那就是新的放法,需要标记
            //v[i][j] = Math.max(v[i-1][j], val[i-1] + v[i-1][j-w[i-1]]);
            //为了记录背包情况,使用if-else处理
             if (v[i-1][j] > (val[i-1] + v[i-1][j-w[i-1]])) {
                v[i][j] = v[i-1][j];
             } else {
                 //此时说明这个重量最大价值进行更新,且是新放的方式,不是直接获取相同重量的上一个放法
                v[i][j] = val[i-1] + v[i-1][j-w[i-1]];
                path[i][j] = 1;
             }


        }
    }
}
for (int i = 0; i < v.length; i++) {
    for (int j = 0; j < v[i].length; j++) {
        System.out.print(v[i][j] + " ");
    }
    System.out.println();
}
for (int i = 0; i < path.length; i++) {
    for (int j = 0; j < path[i].length; j++) {
        System.out.print(path[i][j] + " ");
    }
    System.out.println();
}
int i = path.length - 1;
int j = path[0].length - 1;
//倒叙遍历
while (i > 0 && j > 0) {
    if (path[i][j] == 1) {
        //先遍历最大重量时,获取最大价值的物品放法
        //例如,这时背包重量为4,放物品3+1时,更新了最大价值
        //倒叙扫描,发现了path[3][4]==1,那么说明物品3被放进了背包
        //但是此时还剩下背包重量1,那么就要再遍历背包重量1
        //找到背包重量1时,发现path[1][1],发现背包重量为1是,物品1被放进了背包,且是更新了背包重量为1的放法
        System.out.println(i);
        j -= w[i-1];
        //由于这个物品最新的价值放法已经找到,因此可以排除这一行,直接在上两行找
    }
    i --;
}