前言 网上搜了很多关于这道面试题的解法,大部分都是错的... 这里给出正确的解法(自己自测了多个边界值,确定算法没有问题,如果有朋友发现遗漏的地方,欢迎指正!如果有更好的解法,也欢迎告诉我~)
题目
先看题目:给定一个数n,如23121;给定一组数字A如{2,4,9}求由A中元素组成的、小于n的最大数,如小于23121的最大数为22999
思路
步骤1:先求出小于n的最大上限 maxBelow:(求最大上限的目的是为了确定最终结果的长度)
- 例如n=23121,数组 {2,4,9},则最大上限为 23120;
- 例如n=23121,数组{3,4,9},则最大上限为 9999,因为数组最小为3,比n的最高位数字2还大,所以小于n的最高位必然不能取2;因为最高位取不了2,自然得到的maxBelow总长度也会比n小
- 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;
}
}