字节面试题 求小于n的最大数

4,371 阅读7分钟

前言 网上搜了很多关于这道面试题的解法,大部分都是错的... 这里给出正确的解法(自己自测了多个边界值,确定算法没有问题,如果有朋友发现遗漏的地方,欢迎指正!如果有更好的解法,也欢迎告诉我~)

题目

先看题目:给定一个数n,如23121;给定一组数字A如{2,4,9}求由A中元素组成的、小于n的最大数,如小于23121的最大数为22999

思路

步骤1:先求出小于n的最大上限 maxBelow:(求最大上限的目的是为了确定最终结果的长度)

  1. 例如n=23121,数组 {2,4,9},则最大上限为 23120;
  2. 例如n=23121,数组{3,4,9},则最大上限为 9999,因为数组最小为3,比n的最高位数字2还大,所以小于n的最高位必然不能取2;因为最高位取不了2,自然得到的maxBelow总长度也会比n小
  3. n = 21121, 数组nums = {2,4,9},最大上限值 maxBelow = 9999(n的第一位是2,存在于数组中,但是第二位是1,数组中最小值均比1大,所以最大上限的第一位不能取2,所以位数应该比n小,所以取值为9999)

对应的代码:

/**
 * 获取小于n的最大边界值(没法直接确定最大值是多少,只能取边界值)
 *
 * @param nums 数组,存放的是待拼接的数字集合
 * @param n    需要找到一个小于n的最大值
 */
static int getMaxBelow(int[] nums, int n) {
    // 把n转成字符串,方便后面操作
    String s = String.valueOf(n);
    if (check(s, nums[0])) {
        // 长度可以和n一样
        return n - 1;
    } else {
        // 如果位数小1的话,那就直接都取nums数组中最大的那个元素拼接就行,只要位数-1
        return (int) Math.pow(10, s.length() - 1) - 1;
    }
}


    static boolean check(String str, int minValue) {
        char c = str.charAt(0);
        if (minValue < c - '0') {
            return true;
        } else if (minValue == c - '0') {
            // 该位置相同,则递归往下查找
            return check(str.substring(1), minValue);
        } else {
            return false;
        }
    }

步骤2:根据上限值,从高位到低位逐个判断,该位能取到的最大值是多少,然后拼接到最终的结果

还是举个例子,n = 2411, 数组nums = {2, 4, 6, 8},则最大上限值 maxBelow = 2410;对 2410 的每一位进行遍历:

  • 首先是2,因为数组存在2,且n的第二位是4大于数组的最小值2,所以第一位可以取2;
  • 接下来是4,因为数组中存在4,但是n的第三位是1小于数组的最小值2,所以第二位不能取4,而应该比4小,先对4-1=3,然后按照二分查找(数组有序可以进行二分),从数组中查找第一个小于等于3的值,这里获取到的值为2,这样第二位的值就是2了;注意这里第二位的值没法取到4,所以往下的数直接取数组的最大值即可(也就是这里的8),不需要再进行后续的比较计算了,即得到的数为 2288

对应的代码:

/**
     * 获取小于N的最大数字
     * n=2413, nums=[2,4,6,8] => maxBelow=2412
     *
     * @param nums
     * @param n
     * @return
     */
    static String getMaxLessNum(int[] nums, int n) {
        Arrays.sort(nums);
        int maxBelow = getMaxBelow(nums, n); // 主要是确定能取到的位数

        String str = String.valueOf(maxBelow);
        StringBuilder sb = new StringBuilder();

        // 用于标识选中的数是否小于str当前位置的元素,
        // 如果是,那么后续都往里追加nums数组的最大值元素即可
        // 否则,需要判断当前位置的数组应该选择哪一个
        boolean flag = false;
        for (int i = 0; i < str.length(); i++) {
            // 循环遍历并进行组装
            char c = str.charAt(i);
            if (flag) {
                sb.append(nums[nums.length - 1]);
            } else {
                int index = getIndex(nums, str, i);
                sb.append(nums[index]);
                if (nums[index] < c - '0') {
                    flag = true;
                }
            }
        }
        return sb.toString();
    }


/**
     * 查找当前应该要用数组中哪个数字的索引
     * 比如 2411   {2,4,6,8}
     * 能不能选4还得看下一位的1,因为4 后面的 1 比nums中的数值都小,所以组装数字的时候不能选4
     *
     * @param nums
     * @param maxBelow
     * @param i
     * @return
     */
    static int getIndex(int[] nums, String maxBelow, int i) {
        int cur = maxBelow.charAt(i) - '0';
        if (i < maxBelow.length() - 1) {
            int next = maxBelow.charAt(i + 1) - '0';
            if (next < nums[0]) { // 如果下一位的数字比数组中最小值还小,说明当前位置cur必然不能取当前值,需要-1
                cur -= 1;
            }
        }
        
        // 查找小于等于cur的下标
        // 1) 先找第一个大于等于cur的索引
        // todo 使用二分查找而不是map,这样即使拿不到,还能取到比target小的索引
        int index = binarySearch(nums, cur);
        if (index >= nums.length) {
            index--;
        }
        // 2) 如果找到的元素比cur大,就往前挪一格
        if (nums[index] != cur && nums[index] > cur) {
            index--;
        }
        return index;
    }
    
    
    /**
     * 二分查找
     * @param nums
     * @param target
     * @return
     */
    static int binarySearch(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return left;
    }

完整代码


/**
 * @Description 小于n的最大数字
 * @Author Firenut
 * @Date 2023-08-20 18:17
 */
public class T2_1 {
    // 示例 1:A={1, 2, 9, 4},n=2533,返回 2499。
    // 示例 2:A={1, 2, 5, 4},n=2543,返回 2542。
    // 示例 3:A={1, 2, 5, 4},n=2541,返回 2525。
    // 示例 4:A={1, 2, 9, 4},n=2111,返回 1999。
    // 示例 5:A={5, 9},n=5555,返回 999。

 
    public static void main(String[] args) {
//        case 1
//        int[] nums = new int[]{2, 4, 6, 8};
//        int n = 2411;

//        case 2
//        int[] nums = new int[]{9, 8, 7, 6};
//        int n = 2411;

//        case 3
//        int[] nums = new int[]{8, 7, 6};
//        int n = 2411;

//        case 4
//        int[] nums = new int[]{4, 5, 6};
//        int n = 23;

        int[] nums = new int[]{2, 4, 6, 8};
        int n = 2411;

        System.out.println(getMaxLessNum(nums, n)); //2525
    }

    /**
     * 获取小于N的最大数字
     * n=2413, nums=[2,4,6,8] => maxBelow=2412
     *
     * @param nums
     * @param n
     * @return
     */
    static String getMaxLessNum(int[] nums, int n) {
        Arrays.sort(nums);
        int maxBelow = getMaxBelow(nums, n); // 主要是确定能取到的位数

        String str = String.valueOf(maxBelow);
        StringBuilder sb = new StringBuilder();

        // 用于标识选中的数是否小于str当前位置的元素,
        // 如果是,那么后续都往里追加nums数组的最大值元素即可
        // 否则,需要判断当前位置的数组应该选择哪一个
        boolean flag = false;
        for (int i = 0; i < str.length(); i++) {
            // 循环遍历并进行组装
            char c = str.charAt(i);
            if (flag) {
                sb.append(nums[nums.length - 1]);
            } else {
                int index = getIndex(nums, str, i);
                sb.append(nums[index]);
                if (nums[index] < c - '0') {
                    flag = true;
                }
            }
        }
        return sb.toString();
    }

    /**
     * 查找当前应该要用数组中哪个数字的索引
     * 比如 2411   {2,4,6,8}
     * 能不能选4还得看下一位的1,因为4 后面的 1 比nums中的数值都小,所以组装数字的时候不能选4
     *
     * @param nums
     * @param maxBelow
     * @param i
     * @return
     */
    static int getIndex(int[] nums, String maxBelow, int i) {
        int cur = maxBelow.charAt(i) - '0';
        if (i < maxBelow.length() - 1) {
            int next = maxBelow.charAt(i + 1) - '0';
            if (next < nums[0]) { // 如果下一位的数字比数组中最小值还小,说明当前位置cur必然不能取当前值,需要-1
                cur -= 1;
            }
        }
        // 查找小于等于cur的下标
        // 数组中没有找到,说明数组中最大数还是比当前cur要小
        if (index >= nums.length) {
            index--;
        }

        // 如果找到的元素比cur大,就往前挪
        if (nums[index] != cur && nums[index] > cur) {
            index--;
        }
        return index;
    }


    /**
     * 获取小于n的最大边界值(没法直接确定最大值是多少,只能取边界值)
     *
     * @param nums 数组,存放的是待拼接的数字集合
     * @param n    需要找到一个小于n的最大值
     */
    static int getMaxBelow(int[] nums, int n) {
        // 把n转成字符串,方便后面操作
        String s = String.valueOf(n);
        if (check(s, nums[0])) {
            // 长度可以和n一样
            return n - 1;
        } else {
            // 如果位数小1的话,那就直接都取nums数组中最大的那个元素拼接就行,只要位数-1
            return (int) Math.pow(10, s.length() - 1) - 1;
        }
    }


    /**
     * 需要检查以当前数组中最小的数作为最高位,能否取到与数字n相同位数的数值(比如 n=2413 nums=[2,4,6,8],则比n小的最大数字可以是4位数)
     * 检查以minValue开头的数字能否拼接成与str相同长度的字符串
     * 这里入参用String是为了方便逐个数字进行匹配,因为后续可能还有递归调用
     * 如果在方法体里面转成str,会比较麻烦
     *
     * @param str
     * @param minValue
     * @return
     */
    static boolean check(String str, int minValue) {
        char c = str.charAt(0);
        if (minValue < c - '0') {
            return true;
        } else if (minValue == c - '0') {
            // 该位置相同,则递归往下查找
            return check(str.substring(1), minValue);
        } else {
            return false;
        }
    }


    /**
     * 二分查找
     * @param nums
     * @param target
     * @return
     */
    static int binarySearch(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return left;
    }

}