一、力扣
1、鸡蛋掉落
class Solution {
public int superEggDrop(int k, int n) {
// 处理特殊情况:只有1层楼时,只需1次尝试
if (n == 1) {
return 1;
}
// f[i][j] 表示使用i次尝试和j个鸡蛋能确定的最大楼层数
int[][] f = new int[n + 1][k + 1];
// 初始化:当只有1层楼时,无论有多少鸡蛋,都只需1次尝试
for (int i = 1; i <= k; ++i) {
f[1][i] = 1;
}
int ans = -1;
// 遍历尝试次数,从2次开始逐步增加
for (int i = 2; i <= n; ++i) {
// 对于每个尝试次数i,遍历所有可能的鸡蛋数量j
for (int j = 1; j <= k; ++j) {
// 状态转移方程:当前状态的楼层数由两种情况组成:
// 1. 鸡蛋破碎:剩下i-1次尝试和j-1个鸡蛋,可覆盖f[i-1][j-1]层
// 2. 鸡蛋未破碎:剩下i-1次尝试和j个鸡蛋,可覆盖f[i-1][j]层
// 总楼层数为这两种情况之和再加当前尝试的楼层(+1)
f[i][j] = 1 + f[i - 1][j - 1] + f[i - 1][j];
}
// 当当前尝试次数i下k个鸡蛋能覆盖的楼层数超过等于n时,记录答案并终止循环
if (f[i][k] >= n) {
ans = i;
break;
}
}
return ans;
}
}
2、有效三角形的个数
class Solution {
public int triangleNumber(int[] nums) {
int res = 0;
Arrays.sort(nums);
int n = nums.length;
for (int i = 2; i < n; i++) {
int left = 0, right = i - 1;
while (left < right) {
if (nums[left] + nums[right] > nums[i]) {
res += right - left;
right--;
} else {
left++;
}
}
}
return res;
}
}
3、打乱数组
class Solution {
int[] nums;
Random random = new Random();
public Solution(int[] nums) {
this.nums = nums;
}
public int[] reset() {
return nums;
}
public int[] shuffle() {
int[] ans = nums.clone();
int n = ans.length;
for (int i = 0; i < n; i++) {
swap(ans, i, i + random.nextInt(n - i));
}
return ans;
}
public void swap(int[] ans, int x, int y) {
int temp = ans[x];
ans[x] = ans[y];
ans[y] = temp;
}
}
二、面试
1、String s = "A"
和 String s = new String("A")
的区别是什么?前者存储在哪个区域?
区别:
String s = "A"
使用字面量直接赋值,JVM 会先检查字符串常量池中是否存在值为"A"
的对象。若存在,则直接复用其引用;若不存在,则在常量池中创建新对象。String s = new String("A")
通过new
关键字显式创建对象,无论常量池是否存在"A"
,都会在堆内存中生成新的String
对象。
存储区域:
String s = "A"
的对象存储在字符串常量池(JDK 7+ 后位于堆内存的 Metaspace 区域)。String s = new String("A")
的对象存储在堆内存中,且会额外在常量池中生成"A"
的字面量对象(除非已存在)。
2、深拷贝和浅拷贝的区别是什么?
浅拷贝:仅复制对象本身,不复制其引用的其他对象。新旧对象的引用字段指向同一内存地址,修改其中一个会影响另一个。
深拷贝:递归复制对象及其引用的所有子对象,新旧对象完全独立,互不影响。
示例:
// 浅拷贝
class Person { String name; }
Person a = new Person("Alice");
Person b = a; // b 和 a 共享 name 引用
// 深拷贝
Person c = new Person(a.name); // c 的 name 是独立的新字符串
3、Java中有 copyOf
函数吗?它是浅拷贝还是深拷贝?
存在性:Java 提供了 Arrays.copyOf()
和 System.arraycopy()
方法用于数组复制。
拷贝类型:均为浅拷贝。
- 对于基本类型数组(如
int[]
),复制的是值; - 对于引用类型数组(如
Object[]
),仅复制引用,不复制实际对象。
示例:
Integer[] arr = {1, 2};
Integer[] copy = Arrays.copyOf(arr, arr.length);
arr[0] = 100; // copy[0] 仍为 1,但若修改原数组元素的对象属性,会影响拷贝
4、ArrayList
和 LinkedList
各自的优缺点是什么?插入操作的实现原理?
ArrayList
:
- 优点:基于动态数组,支持 O(1) 时间的随机访问(通过索引)。
- 缺点:插入/删除需移动后续元素(平均 O(n) 时间),扩容时可能触发数组复制。
- 插入原理:在末尾插入直接追加(O(1));在中间插入需将插入点后的元素后移(System.arraycopy)。
LinkedList
:
- 优点:基于双向链表,插入/删除只需调整指针(O(1) 时间,前提是已知位置)。
- 缺点:随机访问需遍历链表(O(n) 时间),额外内存开销(存储前后节点引用)。
- 插入原理:修改相邻节点的
next
和prev
指针,无需移动数据。
5、HashMap
和 Hashtable
的区别是什么?
特性 | HashMap | Hashtable |
---|---|---|
线程安全 | 非线程安全 | 线程安全(方法同步) |
null 值 | 允许一个 null 键和多个 null 值 | 不允许 null 键或值 |
性能 | 更高(无同步开销) | 较低(同步导致竞争) |
迭代器 | Fail-Fast(ConcurrentModificationException ) | Fail-Safe(弱一致性) |
继承关系 | 实现 Map 接口 | 继承 Dictionary 类(过时) |
6、处理哈希冲突的方法有哪些?
-
开放地址法:
- 线性探测:冲突时顺序查找下一个空槽。
- 二次探测:按二次方步长(如 1², 2²)探测,减少聚集。
- 双重哈希:使用第二个哈希函数计算探测步长。
-
链地址法:
每个哈希桶存储链表或红黑树(如 Java 8+ 的HashMap
),冲突元素追加到链表尾部。 -
再哈希:
当负载因子过高时,分配更大的哈希表,并重新计算所有元素的哈希值。 -
公共溢出区:
单独维护一个溢出区存储冲突元素,主表仅存储无冲突的条目。
应用场景:
- 链地址法适合高冲突场景(如
HashMap
); - 开放地址法适合内存紧凑且冲突较少的场景。
7、抽奖是并发处理还是串行?如何应对高并发场景(如并发量从10增至1万)?
8、Redis集群是有状态还是无状态?如何保证高可用?
Redis集群是有状态的,因为每个节点负责存储特定分片的数据,并维护其状态(如键值、主从关系等)。为保证高可用,Redis集群采用主从复制+自动故障转移机制:每个主节点(Master)挂载一个或多个从节点(Slave),主节点处理写请求并同步数据到从节点;当主节点故障时,集群通过Gossip协议自动选举从节点晋升为新主节点,并更新路由表,确保服务持续可用。同时,集群支持动态扩缩容和数据自动重新分片(Resharding),进一步保障容错性和扩展性。