攒青豆代码创作 | 青训营 X 码上掘金主题创作活动

52 阅读6分钟

当青训营遇上码上掘金,作为一个后端营员,我本次活动选择的是“主题4:攒青豆”来进行代码创作,并通过本文来记录一下相关灵感来源、发现问题和解决问题的过程。

一:题目介绍

image.png
现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)

以下为上图例子的解析: 
输入:height = [5,0,2,1,4,0,1,0,3] 
输出:17 
解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 个单位的青豆。

(题目让我想起了俄罗斯方块......)

二:思路确定

从题目可以看出,该题可以用数组或集合的方法来解决,因为输入的数组长度不固定,所以要么用集合解决,要么找出一种方法来确定数组的长度。在这里我选择用数组来做。

思路:1: 一开始被题目给的例子迷惑了,以为只需要找到整个数组中最大值的前三个,然后根据这三个数在原数组的位置来计算其之间小于这三个数的各自的差值,然后相加即可。然而稍作分析即可发现,题目的例子只是三个最大数恰好在整个数组的两边和中间,所以可以这么做,但是只要稍微一变,两边的数成为较小值,那么这种做法就会忽略掉两边的青豆,很明显是错误的。

思路2: 再进一步观察分析发现,每一个数字其最大所能添加的青豆数,是和这个数字左右两边的最大值有关的,比如题目中height = [5,0,2,1,4,0,1,0,3]的例子:
索引1位置的0,其能添加的青豆数等于4-0=4;
索引2位置的2,其能添加的青豆数等于4-2=2;
索引3位置的1,其能添加的青豆数等于4-1=3;
索引5位置的0,其能添加的青豆数等于3-0=3;

......
同样的,这个规律对于本身就是限制青豆“高度”的较大值也是满足的,比如:
索引0位置的5,其能添加的青豆数等于5-5=0;
索引4位置的4,其能添加的青豆数等于4-4=0;

......
再进一步思考就可以发现,每一位的数字其能添加的青豆数,和三个数有关,分别为:其左边的最大值、其本身、其右边的最大值,然后这三者中排序取中间那个值,再减去其本身,就是其最大能添加的青豆数,比如:
索引1位置的0,其左边数组的最大值为5,右边数组的最大值为4,其本身为0,则中间值为4,其能添加的青豆数等于4-0=4;
索引2位置的2,其左边数组的最大值为5,右边数组的最大值为4,其本身为0,则中间值为4,其能添加的青豆数等于4-2=2;
索引3位置的1,其左边数组的最大值为5,右边数组的最大值为4,其本身为0,则中间值为4,其能添加的青豆数等于4-1=3;
索引5位置的0,其左边数组的最大值为5,右边数组的最大值为3,其本身为0,则中间值为3,其能添加的青豆数等于3-0=3;
索引4位置的4,其左边数组的最大值为5,右边数组的最大值为3,其本身为4,则中间值为4,其能添加的青豆数等于4-4=0;

......
到此,即可总结出解法,只需要遍历该数组,然后每次遍历都取其左边最大值、右边最大值和其本身进行比较,取中间值,然后减去其本身,就是每一位可以添加的青豆数,相加的和就是最终结果。

三:初步代码

总结以上思路,进行初步代码创作,需要注意的点如下:
①由于输入是height = [5,0,2,1,4,0,1,0,3]的形式,所以不能直接scan进去,需要进行转换然后赋值给数组
②关于数组长度的确定,稍作观察即可发现,数组长度和输入字符串用“, ”进行split以后的字符串数组长度一致

import java.util.*;

public class main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        //读取输入为字符串并用“,”分割为字符串数组
        String[] str1 = sc.nextLine().split(",");
        //创建数组存放对应数据
        int[] arr = new int[str1.length];
        //遍历输入的字符串数组,将其赋值给数组
        for (int i = 0; i < str1.length; i++) {
            if (i == 0) {
                //字符串集合中0索引的数据在字符串的10索引字符位置
                arr[i] = str1[i].charAt(10) - '0';
            } else {
                //字符串集合中其他索引的数据在字符串的0索引字符位置
                arr[i] = str1[i].charAt(0) - '0';
            }
        }
        //count表示最终结果
        int count = 0;
        int leftMax;
        int rightMax;
        //创建新数组来进行排序
        int[] newArr = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            newArr[i] = arr[i];
        }
        //创建新数组来存放需要比较的三个值
        int[] compareArr = new int[3];
        for (int i = 1; i < arr.length - 1; i++) {
            //每次循环对i左边,i右边分别排序,并取得最大值
            Arrays.sort(newArr,0,i);
            leftMax = newArr[i - 1];
            Arrays.sort(newArr,i+1,arr.length);
            rightMax = newArr[arr.length-1];
            //将左边最大值、右边最大值和i对应值本身存进数组并进行排序
            compareArr[0] = leftMax;
            compareArr[1] = rightMax;
            compareArr[2] = arr[i];
            Arrays.sort(compareArr);
            //此时,排序后的三个数中,中间的数就是要找的数,减去i对应的值,再相加即可
            count = count + (compareArr[1] - arr[i]);
            //每次循环,最后需要把排序数组还原
            for (int j = 0; j < arr.length; j++) {
                newArr[j] = arr[j];
            }
        }
        System.out.println(count);
    }
}

经过测试,以上代码运行结果正确。

四:问题提出和解决

很明显,初步代码是存在很多效率上的问题的,比如每次循环中都要反复求左右两边的最大值,每次循环最后还要还原用来排序的数组,其时间复杂度超出n²,需要进行性能优化。

关于性能优化,主要从以下几点进行考虑:
①考虑到Java语言特点,可以将scanner替换为bufferreader

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//读取输入为字符串并用“,”分割为字符串数组
String[] str1 = br.readLine().split(",");

②考虑到Java语言特点,可以将string的charat替换为toCharArray

//遍历输入的字符串数组,将其赋值给数组
for (int i = 0; i < str1.length; i++) {
    if (i == 0) {
        //字符串集合中0索引的数据在字符串的10索引字符位置
        arr[i] = str1[i].toCharArray()[10] - '0';
    } else {
        //字符串集合中其他索引的数据在字符串的0索引字符位置
        arr[i] = str1[i].toCharArray()[0] - '0';
    }
}

③关于求左右两边最大值的问题,其实并不需要新建数组专门来存放排序后的数组,只需要循环一开始确定左右最大值后,在每次循环中都更新一下即可,这样可以一定程度节省排序时间和最后还原数组的时间

//左边最大值默认为索引0,后面每移动一位,更新即可
int leftMax = arr[0];
//右边最大值需要进行排序得出
int rightMax=0;
for (int i = 1; i < arr.length; i++) {
    if(arr[i]>rightMax) rightMax = arr[i];
}
//mid表示三个数中的中间大小的值
int mid = 0;
for (int i = 1; i < arr.length - 1; i++) {
    //将左边最大值、右边最大值和i对应值本身进行比较取中间值
    if (leftMax > rightMax) {
        if (leftMax < arr[i]) {
            mid = leftMax;
        } else {
            if (rightMax > arr[i]) {
                mid = rightMax;
            } else {
                mid = arr[i];
            }
        }
    } else {
        if (leftMax > arr[i]) {
            mid = leftMax;
        } else {
            if (rightMax > arr[i]) {
                mid = arr[i];
            } else {
                mid = rightMax;
            }
        }
    }
    //此时,排序后的三个数中,中间的数就是要找的数,减去i对应的值,再相加即可
    count = count + (mid - arr[i]);
    //每次循环判断需不需要更新左右两边的最大值
    if (arr[i] > leftMax) leftMax = arr[i];
    //如果右边最大值需要更新,那么还是需要重新比较确定最大值,但此时可以直接从i+1开始比较,节省时间
    if (arr[i] == rightMax) {
        rightMax=0;
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] > rightMax) rightMax = arr[j];
        }
    }
}

五:最终代码展示

码上掘金平台链接:主题4攒青豆代码实现 - 码上掘金 (juejin.cn)

六:可以改进的地方

目前来看,对于得出右边数组的最大值来进行比较的做法还是较为耗时的。左边数组的最大值可以通过每移一位比较一次即可,但是右边数组的最大值是需要考虑当前位数字的,当需要更新最大值时,还是需要遍历+比较,只不过相对之前的方案遍历量减小了而已,这方面是可以改进的,我还需要多多学习。

七:我的收获

总结来说,我的收获主要有以下几点:
①锻炼了解题思维,提高了观察并总结规律的能力
②锻炼了代码思维,书写代码更为规范
③对提高代码效率有了进一步实践,发现并一定程度解决了问题