面试题42.连续子数组的最大和
-
状态定义:dp[i] 代表以元素 nums[i] 为结尾的连续子数组的最大和
-
状态转移方程:若 dp[i - 1] <= 0,说明 dp[i-1] 对dp[i] 产生负贡献,即 dp[i-1] + nums[i] 还不如 nums[i] 本身大。
- 当 dp[i−1] > 0 时:执行 dp[i] = dp[i−1] + nums[i];
- 当 dp[i−1] ≤ 0 时:执行 dp[i] = nums[i];
-
初始状态:dp[0] = nums[0],即以 nums[0] 结尾的连续子数组最大和为 nums[0]。
-
返回值:返回 dp 数组中的最大值,代表全局最大值
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
if(len < 1) return 0;
int[] dp = new int[len];
dp[0] = nums[0];
int max = dp[0]; //记录dp数组中的最大值
for(int i = 1; i < len; i++) {
if(dp[i - 1] <= 0) dp[i] = nums[i];
else dp[i] = nums[i] + dp[i - 1];
max = Math.max(max, dp[i]);
}
return max;
}
}
- 时间复杂度 O(N) : 线性遍历数组 nums 即可获得结果,使用 O(N) 时间。
- 空间复杂度 O(N) : 使用原数组大小的额外空间
优化
- 由于 dp[i] 只与 dp[i−1] 和 nums[i] 有关系,因此可以将原数组 nums 用作 dp 列表,即直接在 nums 上修改即可。
- 由于省去 dp 列表使用的额外空间,因此空间复杂度从 O(N) 降至 O(1) 。
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
if(len < 1) return 0;
int res = nums[0];
for(int i = 1; i < len; i++) {
nums[i] = nums[i] + Math.max(nums[i - 1], 0);
res = Math.max(res, nums[i]);
}
return res;
}
}
- 若题目要求不能修改原有数组,考虑到在dp列表中,dp[i] 只和 dp[i-1] 有关,所以用两个参数存储循环过程中的 dp[i] 和 dp[i-1] 的值即可,空间复杂度也为O(1)。
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
if(len < 1) return 0;
int res = nums[0];
int pre = 0; //用于记录dp[i-1]的值,对于dp[0]而言,其前面的dp[-1]=0
int cur = nums[0]; //用于记录dp[i]的值
for(int num : nums) {
cur = num;
if(pre > 0) cur += pre;
if(cur > res) res = cur;
pre = cur;
}
return res;
}
}
———————————————————————————————————————
面试题43.1~n 整数中出现 1 的次数
假设 f(n) 函数,意思是1~n 这n个整数的十进制表示中1出现的次数,将 n 拆分为两部分,最高一位的数字high和其他位的数字last,分别判断情况后将结果相加。
例子一:如 n=1234,high=1,pow=1000,last=234
可以将数字范围分为两部分 1 ~ 999 和 1000 ~ 1234
- 1 ~ 999 这个范围 1 的个数是 f(pow - 1)
- 1000~1234这个范围1的个数需要分为两部分:
- 千分位是1的个数:千分位为1的这个数字的个数刚好就是234+1(last+1),注意,这儿只看千分位,不看其他位
- 其他位是1的个数:即是234中出现1的个数,为f(last)
所以全部加起来是f(pow-1) + last + 1 + f(last);
例子二:如3234,high=3,pow=1000,last=234
可以将数字范围分成几部分 1 ~ 999,1000 ~ 1999,2000 ~ 2999 和 3000 ~ 3234
- 1 ~ 999这个范围 1 的个数是 f(pow-1)
- 1000 ~ 1999这个范围 1 的个数需要分为两部分:
- 千分位是 1 的个数:千分位为1数字的个数刚好就是 pow,注意,这儿只看千分位,不看其他位
- 其他位是 1 的个数:即是999中出现 1 的个数,为 f(pow-1)
- 2000 ~ 2999这个范围 1 的个数是 f(pow-1)
- 3000 ~ 3234这个范围 1 的个数是 f(last)
所以全部加起来是pow + high*f(pow-1) + f(last);
class Solution {
public int countDigitOne(int n) {
return f(n);
}
//1~n 这n个整数的十进制表示中 1 出现的次数
private int f(int n) {
if(n <= 0) return;
String s = String.valueOf(n);
int high = s.charAt(0) - 0;
int pow = (int)Math.pow(10, s.length() - 1);
int last = n - high * pow;
if(high == 1) {
return f(pow - 1) + last + 1 + f(last);
} else {
return pow + high * f(pow - 1) + f(last);
}
}
}
———————————————————————————————————————
面试题44.数字序列中某一位的数字
0 占第0位
1~9 共 9x1 = 9 个数字,占位 9x1 = 9 位
10~99 共 9x10 = 90 个数字,占位 90x2 = 180 位
100~999 共 9x100 = 900 个数字,占位 900x3 = 2700 位
…
算法的逻辑就是:依次算一下占位数字,并不断地累加得到当前的总占位数,并判断和输入n的关系,总占位数小于n,说明第n位不在目前的范围內,继续累加;否则,说明在范围,然后找到相应数字返回。
举个例子:
假设输入n为14,我们想找到第14位:
-
(1) 此时设置当前位置为 0 的位置 temp=0
-
(2) 占 1 位的数字有 9 个:num=9, (1~9除了0,因为 temp 已设为 0 了)
-
(3) 占1位 base=1
-
(4) 当占1位的数字都走完了,目前一共占到了多少位: temp + num x base = 0 + 9 x 1 = 9,说明占1位的数字走完后,当前占到了第9位。(更新temp=temp + num x base = 9)
-
(5) 和输入的值比较下,9 < 14,说明我们想找的第 14 位不在当前占 1 位的数字中。
-
(6) 那就有可能在占 2 位的数字中,所以这一轮我们看看占2位的数字(10~99):
- 每个数字占位 base = base + 1 = 2
- 有多少个数字 num = num x 10 = 90
- 再回到第(4)步,算一下当占2位的数字也都走完了,目前一共占到了多少位:temp + num x base = 9 + 90 x 2 = 189,说明当占2位的数字走完后,当前占到了第189位
- 再回到第5步,发现 189 > 14,说明我们想找到的数字就在10~99之间
- 此时,循环终止…因为没必要再往下算占3位的情况了
-
(7) 我们知道第14位就在10~99之间的话就很好办了:
- 前一轮我们知道占1位的数字走完后,占到了第9位,那我们想找的第14位的值也就是9之后的第5位:14 - 9 = 5位
- 占两位的数字中(10~99),第一个起始数字是10,(10 = 10的1次方,也就是10(base-1))
- 由于10~99这个范围内的数字,都是占base=2位,所以 5/2 = 2,10 + 2 = 12,第14位就在数字12里;5%2 = 1,说明第14位就是数字12中的第一个位置值,如果把12当成字符串,那就是下标为0的值“12”.charAt(1-1) = 1
class Solution {
public int findNthDigit(int n) {
if(n < 10) return n;
long m = n; //将 n 转换为 long 类型,防止大数越界
long temp = 0; //表示当前占到第几位,从0开始
long base = 1; //表示当前所处的区间里,数字是几位的(1~9,base=1;10~99,base=2等等)
long num = 9; //表示现在区间内多少个数字
char res = '0';
//查找n处于哪个数字区间里
//num*base 表示这个区间里的数字有num个,每个数字的位数都是base,相乘就是区间里数字总位数
while(temp + num * base < m) {
temp += base * num;
base += 1;
num *= 10;
}
//m-temp 表示题目所给数字到这个区间首数字有多少位
//base表示这个区间每个数有多少位
//两者相除表示之间大概有多少个数
long a = (m - temp) / base;
long b = (m - temp) % base; //是否处在一个完整数的末尾
if(b != 0) {
long c = (long) (Math.pow(10, base - 1) + a);
res = String.valueOf(c).charAt((int)b - 1);
} else {
long c = (long) (Math.pow(10, base - 1) + a - 1);
res = String.valueOf(c).charAt((int)base - 1);
}
return res - '0';
}
}
———————————————————————————————————————
面试题45.把数组排成最小的数
设数组 nums 中任意两数字的字符串为 x 和 y,则规定排序判断规则为:
- 若拼接字符串 x+y > y+x ,则 x “大于” y ;
- 反之,若 x + y < y + x,则 x “小于” y ;
1、初始化:字符串列表 strs,保存各数字的字符串格式;
2、列表排序: 应用以上 “排序判断规则” ,对 strs 执行排序;
3、返回值: 拼接 strs 中的所有字符串,并返回。
class Solution {
public String minNumber(int[] nums) {
String[] strs = new String[nums.length];
for(int i = 0; i < nums.length; i++) {
str[i] = String.valueOf(nums[i]);
}
Arrays.sort(strs, (x, y).compareTo(y + x));
StringBuilder sb = new StringBuilser();
for(String s : strs) {
sb.append(s);
}
return sb.toString();
}
}