面试在他们的办公室里进行,双方面对面。轮次安排如下所示:
- 笔试评估
- 第一次技术面试
- 第二次技术面试
- HR 面试环节
- 求出数字N的阶乘,打印其前10位并验证结果。
- 在给定的链表中找到中间元素。
- 将给定的字符串转换为整数。s = “$123,840/y”
- 分析以下代码,找出可能存在的问题。
void 转账(Amount from, Amount to, Amount amount) {
synchronized (从) {
synchronized (到) {
从减少(金额);
到增加(金额);
}
}
}
第一轮问题
- 请介绍你自己以及你的个人简介。
- 如果你需要在代码中为不同地区运行调度器,并且每个地区执行不同的业务规则,而不使用 if-else 语句,你将如何实现这一点?答案 — 使用策略模式
- 你的任务是从一组类中找出它们所属的父类,你将如何做到这一点?答案 — 使用反射
- Kafka 相比于 Rabbit MQ、IBMMQ 等具有哪些优势?
- 如果输入是 Region,并且它可能包含多个子字段,如国家、州、县、镇、村等,每个这些字段可能属于一些分组,你将如何在数据库中设计这些内容?答案 — 将表进行规范化
- 阐述评估中的解决方案。答案 — 如下
- 解释一下你在设计中使用的设计模式和 SOLID 原则。
- 解释一下多线程中的锁、通知和等待。
- 解释一下死锁的情况。
- 解释 Oauth2 授权与认证。
- 详细说明如何优化阶乘计算,以便无需任何迭代或转换为字符串即可得到答案。
- 详细说明如何利用内置函数优化字符串转换问题。
- 详细说明如何实现同步,以避免提到的潜在问题。
- 如果我们要找出一个数的阶乘并打印出它的前10位,我们可以通过递归计算出那个数,然后将其转换为字符串形式,并从中提取所需的子字符串部分。
import java.math.BigInteger;
public class FactorialFirst10Digits {
public static void main(String[] args) {
int num = 100; // 此数字可根据需要更改
BigInteger factorial = calculateFactorial(num);
// 将结果转换为字符串并获取前10位数字
String factStr = factorial.toString();
String first10Digits = factStr.substring(0, Math.min(10, factStr.length()));
System.out.println(num + " 的阶乘为: " + factorial);
System.out.println("前10位: " + first10Digits);
}
private static BigInteger calculateFactorial(int n) {
BigInteger fact = BigInteger.ONE;
for (int i = 2; i <= n; i++) {
fact = fact.multiply(BigInteger.valueOf(i));
}
return fact;
}
}
更好的解决办法:
import java.math.BigInteger;
public class FactorialBigInteger {
public static BigInteger factorial(BigInteger n) {
if (n.equals(BigInteger.ZERO) || n.equals(BigInteger.ONE)) {
return BigInteger.ONE; // 基本情况:
}
return n.multiply(factorial(n.subtract(BigInteger.ONE))); // 递归步骤:
}
public static void main(String[] args) {
BigInteger num = new BigInteger("50");
BigInteger fact = factorial(num);
String factStr = fact.toString();
// 将结果转换为字符串并获取前10位数字
String first10Digits = factStr.substring(0, Math.min(10, factStr.length()));
// 输出结果为:
System.out.println("50的阶乘为: " + factStr);
System.out.println("前10位为: " + first10Digits);
}
}
预期优化方案:
对数求和法:
我们不直接计算 n!,而是对从 i=1 到 n 的每个 i 求 log10(i),然后将这些值相加,得到 log10(n!)。
小数部分:
分离对数的小数部分fff有助于我们捕捉有效数字。
首位数字的缩放:
通过计算 10^(f+L-1),这样就把数字调整为使得 n! 的前 L 位成为整数部分。
例如:
- 当 n=5 时,该方法的结果是 120(正好是 5!)。
- 当 n=100, 该方法的结果是 9332620170,这就是 100! 的前 10 位。
当 n=100 时,100! 非常大(有158位数字),但我们仍然可以不计算它的全部就能提取出它的前10位数字。
- 计算对数之和:
S = lg(100!) 大约是 157.97。
- (具体的数值其实不重要,也就是我们需要得到小数点后的部分。)
把小数点后面的部分分开:
比如说,
S≈157.97.
那么:
- 取157.97的整数部分就是取整后的结果157。
小数部分:f≈157.97-157=0.97。
算一下最前面的数字:
- 当L=10时,我们计算:
- 值=10^(f+L−1)=10^(0.97+10−1)=10^(0.97+9)=10^9.97。
- 10^9.97大约是9.33×10^9(在实际情况中,这个精确的值是依赖于计算出的对数)。当我们把它转换成
long
类型的时候,我们取其整数部分: - ⌊10^9.97⌋≈9332620170。
- 这个数代表了100!的前10位数字。(实际上,我们知道100!的前10位数字是9332620170。)
public class FactorialLeadingDigits {
public static void main(String[] args) {
int n = 100; // 例如,计算100的阶乘
int leadingDigitsCount = 10; // 我们想要前10位数字(即小数点前),
double logSum = 0.0;
// 计算从1到n的对数之和
for (int i = 1; i <= n; i++) {
logSum += Math.log10(i);
}
// 提取logSum的小数部分
double fractionalPart = logSum - Math.floor(logSum);
// 计算前导数字:
// 这样在转换为long时,
// 我们就能得到所需的前导数字,即
double leadingDigits = Math.pow(10, fractionalPart + leadingDigitsCount - 1);
// 转换为long会截断小数部分,从而得到前10位数字,如
System.out.println((long) leadingDigits);
}
}
2. 方法如下:双指针技术
- 慢指针(
slow
) 每次移动 一格。 - 快指针(
fast
) 每次移动 两格。 - 当
fast
到达末尾时,slow
就会在列表的中间。
class LinkedList {
Node head; // 链表的头
// 静态内部类 Node
static class Node {
int data;
Node next;
Node(int data) {
这个.data = data;
这个.next = null;
}
}
// 找到链表中间节点的方法
public Node findMiddle() {
if (head == null) return null; // 特殊情况
3. 我犯了个错误,手动排除了不同的字符,而不是应该用“\D”来排除非数字。
正确的应该是:
public class 数字提取器 {
public static void main(String[] args) {
String str = "abc123xyz"; // 一个包含数字的示例字符串
int num = 提取和转换(str);
System.out.println("提取到的数字: " + num);
}
public static int 提取和转换(String str) {
// 移除所有非数字字符
String numericStr = str.replaceAll("\\D", "");
// 如果没有找到数字,则
if (numericStr.isEmpty()) {
return 0; // 如果没有找到数字,则返回默认值0(表示没有找到数字)
}
// 转换为整数
return Integer.parseInt(numericStr);
}
}
4. 死锁的可能性:
- 问题: 如果两个线程同时尝试在相反方向上在同一账户之间转账,可能会导致死锁。
- 参考: 避免使用嵌套锁是防止死锁的一种常见做法。
-
- 总是按照一致的顺序获取锁,以防止死锁。例如,定义一个全局顺序(比如基于账户ID),并始终先锁定ID较小的账户。
-
- 只对修改共享数据的临界区代码进行同步。这样可以缩短持有锁的时间,并尽量减少这种竞争。
转账(Amount from, Amount to, Amount amount) {
金额 first = from.getId() < to.getId() ? from : to;
金额 second = from.getId() < to.getId() ? to : from;
同步 (first) {
同步 (second) {
from.借记(amount);
to.贷记(amount);
}
}
}
// 此函数的作用是将金额从一个账户转移到另一个账户。首先比较两个账户的ID,将较小的ID对应账户赋值给first,较大的ID对应账户赋值给second。然后使用同步机制确保在转账过程中两个账户的操作是同步的,从而避免并发问题。首先对from账户执行借记操作,然后对to账户执行贷记操作,完成转账过程。
定义一个函数 转账操作(金额 from, 金额 to, 金额 amount) {
synchronized (from) {
from.扣款(amount);
}
synchronized (to) {
to.存入(amount);
}
}