一、力扣
1、排序数组

java
class Solution {
public int[] sortArray(int[] nums) {
quicksort(nums,0,nums.length-1);
return nums;
}
public void quicksort(int[] nums,int left,int right){
if(left>=right) return;
Random random=new Random();
int part=left+random.nextInt(right-left+1);
swap(nums,left,part);
int i=left+1,j=right;
int target=nums[left];
while(true){
while(i<=j&&nums[i]<target) i++;
while(i<=j&&nums[j]>target) j--;
if(i>=j) break;
swap(nums,i,j);
i++;
j--;
}
swap(nums,left,j);
quicksort(nums,left,j-1);
quicksort(nums,j+1,right);
}
public void swap(int[] nums,int left,int right){
int temp=nums[left];
nums[left]=nums[right];
nums[right]=temp;
}
}
2、二叉树的序列化与反序列化

java
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
String temp=sdfs(root);
System.out.println(temp);
return temp;
}
public String sdfs(TreeNode root){
if(root==null){
return "None";
}
return root.val+","+sdfs(root.left)+","+sdfs(root.right);
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
String[] ans=data.split(",");
ArrayDeque<String> queue=new ArrayDeque<>(Arrays.asList(ans));
return rdfs(queue);
}
public TreeNode rdfs(ArrayDeque<String> queue){
String temp=queue.poll();
if("None".equals(temp)){
return null;
}
TreeNode res=new TreeNode(Integer.parseInt(temp));
res.left=rdfs(queue);
res.right=rdfs(queue);
return res;
}
}
3、基本计算器

java
class Solution {
public int calculate(String s) {
char[] ans=s.toCharArray();
int point=0;
int n=ans.length;
ArrayDeque<Integer> stack=new ArrayDeque<>();
stack.push(1);
int aos=1;
int res=0;
while(point<n){
char temp=ans[point];
if(temp==' '){
point++;
} else if(temp=='+'){
aos=stack.peek();
point++;
}else if(temp=='-'){
aos=-stack.peek();
point++;
}else if(temp=='('){
stack.push(aos);
point++;
}else if(temp==')'){
stack.pop();
point++;
}else{
int pre=0;
while(point<n&&Character.isDigit(ans[point])){
pre=pre*10+ans[point]-'0';
point++;
}
res+=pre*aos;
}
}
return res;
}
}
4、整数拆分


java
class Solution {
/**
* 计算将正整数n拆分为至少两个正整数之和时的最大乘积
* @param n 输入的正整数
* @return 最大乘积
*/
public int integerBreak(int n) {
// dp[i]表示将整数i拆分为至少两个正整数之和时的最大乘积
int[] dp = new int[n + 1];
dp[2] = 1; // 初始条件:2只能拆分为1+1,乘积为1
// 从3开始递推,因为2的情况已处理
for (int i = 3; i <= n; i++) {
// 尝试所有可能的拆分方式:将i拆分为j和i-j两部分
for (int j = i - 1; j >= 1; j--) {
// 状态转移方程:
// 比较两种情况:
// 1. 不继续拆分i-j,直接相乘:j * (i-j)
// 2. 继续拆分i-j,取i-j的最优解:j * dp[i-j]
// 取两者的较大值,并与当前dp[i]比较更新
dp[i] = Math.max(dp[i], Math.max(dp[i - j] * j, (i - j) * j));
}
}
return dp[n];
}
}
5、不同的二叉搜索树


java
class Solution {
public int numTrees(int n) {
//初始化 dp 数组
int[] dp = new int[n + 1];
//初始化0个节点和1个节点的情况
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
//对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加
//一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-j
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
}
6、验证IP地址

java
class Solution {
public String validIPAddress(String queryIP) {
if (queryIP.indexOf('.') >= 0) {
return isIpV4(queryIP) ? "IPv4" : "Neither";
} else {
return isIpV6(queryIP) ? "IPv6" : "Neither";
}
}
public boolean isIpV4(String queryIP) {
//加-1是防止出现空字符串无法计数 比如192.168.1.1. 后边多了一个点,不加-1会被忽略后边的空串
String[] split = queryIP.split("\\.", -1);
//个数不是4个
if (split.length != 4) {
return false;
}
for (String s : split) {
//每个长度不在 1-3之间
if (s.length() > 3 || s.length() == 0) {
return false;
}
//有前导0 并且长度不为1
if (s.charAt(0) == '0' && s.length() != 1) {
return false;
}
//计算数字
int ans = 0;
for (int j = 0; j < s.length(); j++) {
char c = s.charAt(j);
//不是数字
if (!Character.isDigit(c)) {
return false;
}
ans = ans * 10 + (c - '0');
}
//数字超过255
if (ans > 255) {
return false;
}
}
return true;
}
public boolean isIpV6(String queryIP) {
String[] split = queryIP.split(":", -1);
//数量不是8个
if (split.length != 8) {
return false;
}
for (String s : split) {
//每个串 长度不在1-4之间
if (s.length() > 4 || s.length() == 0) {
return false;
}
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
//不是数字并且字母不在 a-f之间
if (!Character.isDigit(c) && !(Character.toLowerCase(c) >= 'a') || !(Character.toLowerCase(c) <= 'f')) {
return false;
}
}
}
return true;
}
}
7、分割等和子集

java
class Solution {
public boolean canPartition(int[] nums) {
int sum=0;
for(int x:nums){
sum+=x;
}
int n=nums.length;
if(sum%2==1) return false;
int target=sum/2;
boolean[][] dp=new boolean[n+1][target+1];
dp[0][0]=true;
for(int i=0;i<n;i++){
for(int j=0;j<=target;j++){
dp[i+1][j]=dp[i][j]||(j>=nums[i]&&dp[i][j-nums[i]]);
}
}
return dp[n][sum/2];
}
}
二、面试
1、操作系统相关问题
1. 操作系统的进程跟线程有什么区别?
进程和线程的主要区别:
-
定义:
- 进程是操作系统分配资源的基本单位,拥有独立的内存空间
- 线程是CPU调度的基本单位,共享所属进程的内存空间
-
资源占用:
- 进程拥有独立的地址空间、内存、数据堆等资源
- 线程共享进程的地址空间和资源,但有独立的栈空间和寄存器
-
通信方式:
- 进程间通信需要特殊的IPC机制(管道、消息队列、共享内存等)
- 线程间通信更简单,可直接访问共享变量
-
切换开销:
- 进程切换开销大,需要切换页表、刷新TLB等
- 线程切换开销小,只需保存和恢复少量寄存器状态
-
安全性:
- 进程间相互独立,一个进程崩溃通常不影响其他进程
- 线程间共享内存,一个线程崩溃可能导致整个进程崩溃
2. 为什么有了进程还要线程?
引入线程的主要原因:
- 资源利用效率:线程共享进程资源,创建和销毁线程的开销远小于进程
- 并发性能:多线程可以充分利用多核处理器,提高程序执行效率
- 响应速度:线程切换比进程切换快,提高系统响应速度
- 通信效率:线程间通信更简单高效,共享内存无需额外IPC机制
- 编程模型:线程模型使并发编程更直观,便于开发复杂应用
3. 线程切换时需要涉及哪些部分?进程切换相比线程切换少了哪些操作?
线程切换涉及的部分:
- 保存和恢复线程上下文(寄存器状态、程序计数器等)
- 切换栈指针
- 更新线程控制块(TCB)信息
进程切换比线程切换多的操作:
- 切换页表(地址空间切换)
- 刷新TLB(地址转换缓冲)
- 切换内核态上下文
- 可能涉及更多特权级别切换
- 可能需要刷新缓存
4. 是否存在比线程更轻量级的调度单位?
是的,存在比线程更轻量级的调度单位:
- 协程(Coroutine):用户态的轻量级线程,由应用程序自己调度
- 纤程(Fiber):某些系统提供的轻量级线程
- 绿色线程(Green Thread):在用户空间实现的线程
- 任务(Task):某些系统中的轻量级执行单元
- Go语言中的Goroutine:Go运行时管理的轻量级线程
这些单位的共同特点是在用户空间调度,避免了内核态切换的开销,通常采用协作式调度而非抢占式调度。
5. 虚拟线程(协程)与Java线程的区别是什么?
虚拟线程(Java 19+引入)与传统Java线程的区别:
-
实现方式:
- 传统Java线程是1:1映射到操作系统线程
- 虚拟线程是M:N模型,多个虚拟线程映射到少量平台线程
-
资源消耗:
- 传统线程每个约消耗1MB栈内存,数量受限于系统资源
- 虚拟线程极其轻量,可创建数百万个,每个只占几KB内存
-
调度机制:
- 传统线程由操作系统调度
- 虚拟线程由JVM调度,在阻塞操作时会自动切换
-
适用场景:
- 传统线程适合计算密集型任务
- 虚拟线程特别适合IO密集型任务
6. 虚拟线程如何避免并发问题?与普通线程的并发控制有何不同?
虚拟线程的并发控制与普通线程基本相同:
- 两者都需要使用相同的同步机制(synchronized、Lock、Atomic变量等)
- 两者都面临相同的并发问题(竞态条件、死锁等)
- 两者都可以使用相同的并发工具类(ConcurrentHashMap等)
主要区别:
- 虚拟线程在阻塞操作时会自动让出底层平台线程,提高资源利用率
- 使用虚拟线程时应避免长时间持有锁,否则可能影响调度效率
- 虚拟线程更适合使用结构化并发(Structured Concurrency)模式
7. 引入虚拟线程这一层的目的是什么?
引入虚拟线程的主要目的:
- 提高吞吐量:支持更多并发连接,特别适合IO密集型应用
- 简化编程模型:使用同步代码风格编写异步效果的程序,避免回调地狱
- 资源利用效率:减少线程资源消耗,提高系统整体效率
- 适应现代应用需求:支持微服务架构下的高并发场景
- 提高响应性:在IO等待时自动切换,减少阻塞时间
- 降低开发复杂度:相比反应式编程更直观易懂
8. 若物理内存为8G,JVM最大堆内存能否设置为100G?磁盘空间是否影响JVM堆大小?
- JVM最大堆内存理论上可以设置为100G,但实际运行会受限于可用的虚拟内存
- 在Windows/Linux系统中,虚拟内存由物理内存+交换空间(页面文件/swap分区)组成
- 如果交换空间足够大,可以设置超过物理内存的堆大小,但性能会严重下降
- 磁盘空间本身不直接影响JVM堆大小,但影响可配置的交换空间大小
- 实际应用中,不建议将JVM堆设置超过物理内存,会导致频繁页面交换,性能极差
9. LINUX系统中虚拟内存上限由什么决定?进程可用内存大小由哪些因素决定?
Linux系统中虚拟内存上限由以下因素决定:
- 硬件架构(32位/64位)的地址空间限制
- 内核参数配置(如vm.max_map_count)
- 物理内存大小+交换空间大小
进程可用内存大小由以下因素决定:
- 系统总可用内存(物理内存+交换空间)
- 系统对单个进程的资源限制(ulimit设置)
- 内核参数(如overcommit策略)
- 进程自身的内存管理策略
- 其他进程的内存占用情况
10. 三个JVM进程各占6G内存,总物理内存8G,能否同时启动?
理论上可以启动,但会有严重性能问题:
- 总共需要18G内存,而物理内存只有8G
- 系统会使用交换空间(swap)来弥补不足的内存
- 大量数据会被交换到磁盘,导致频繁的页面调度
- 系统会变得极其缓慢,可能出现严重的"内存颠簸"(thrashing)
- 在极端情况下,如果交换空间不足,系统OOM killer可能会终止某些进程
11. 个人电脑内存(如16G/32G)是否能运行所有应用?若总和超过内存限制会怎样?
个人电脑内存无法运行"所有"应用,当应用内存总和超过限制时:
- 系统会使用虚拟内存机制,将不常用的内存页面交换到磁盘
- 频繁访问被交换出去的数据会导致系统响应变慢
- 如果交换空间也耗尽,新的内存分配请求会失败
- Windows系统可能会弹出"内存不足"警告
- Linux系统的OOM killer会选择终止某些进程以释放内存
- 应用可能崩溃或被强制关闭
12. 磁盘在内存申请中起到什么作用?
磁盘在内存申请中的作用:
- 提供交换空间:作为虚拟内存的扩展,存储暂时不用的内存页面
- 支持内存映射:通过mmap等机制将文件内容映射到内存地址空间
- 持久化存储:保存程序运行产生的数据,在需要时加载到内存
- 缓解内存压力:通过页面置换算法,将不常用数据暂存到磁盘
- 支持大型数据处理:允许处理超过物理内存大小的数据集
13. HDD磁盘为何以块(block)为单位读写?为何不以单个比特或字节为单位?
HDD磁盘以块为单位读写的原因:
- 物理结构限制:磁头一次定位就能读取一个扇区的数据
- 寻址开销优化:减少寻道和旋转延迟的影响
- 错误检测与纠正:块级别更容易实现ECC校验
- 传输效率:批量传输数据比逐位/逐字节传输效率高
- 控制器设计简化:简化了磁盘控制器的设计复杂度
- 缓存管理:便于系统进行缓存管理和预读优化
14. 内存缓存对齐的作用是什么?不对齐会导致什么问题?
内存缓存对齐的作用:
- 提高访问效率:CPU一次可以完整读取一个缓存行的数据
- 减少内存访问次数:避免一个数据跨越多个缓存行
- 避免伪共享:防止多线程程序中的缓存行争用
- 硬件要求:某些处理器架构要求特定类型数据必须对齐
不对齐可能导致的问题:
- 性能下降:需要多次内存访问获取一个数据
- 原子操作失效:某些架构下非对齐数据无法保证原子性
- 硬件异常:某些处理器对非对齐访问会产生异常
- 缓存效率降低:增加缓存未命中率
- 伪共享问题:导致多线程程序性能下降
15. HDD的机械结构如何影响其读写方式?
HDD的机械结构对读写方式的影响:
- 寻道时间:磁头移动到指定磁道需要时间,影响随机访问性能
- 旋转延迟:等待目标扇区旋转到磁头下方的时间
- 顺序访问优势:连续读写可以减少寻道和旋转延迟
- 块读写:基于扇区的物理结构决定了以块为单位读写
- 读写调度算法:如电梯算法,优化磁头移动路径
- 预读策略:基于空间局部性原理,提前读取相邻数据
- 写入缓存:聚合小写入操作,减少实际磁头移动次数
2、Java相关问题
1. Java中Check Exception和Unchecked Exception的区别是什么?
Checked Exception和Unchecked Exception的区别:
Checked Exception:
- 必须在代码中显式处理(try-catch或throws声明)
- 编译时检查,不处理会导致编译错误
- 代表可预见、可恢复的异常情况
- 继承自Exception但不是RuntimeException
- 例如:IOException, SQLException, ClassNotFoundException
Unchecked Exception:
- 不强制在代码中显式处理
- 编译时不检查,运行时才会发现
- 通常代表程序错误或不可恢复的情况
- 包括RuntimeException及其子类和Error及其子类
- 例如:NullPointerException, ArrayIndexOutOfBoundsException, OutOfMemoryError
2. Interface和Abstract Class的区别是什么?何时使用接口?何时使用抽象类?
Interface和Abstract Class的区别:
特性 | 接口 | 抽象类 |
---|---|---|
方法实现 | 只能有默认方法、静态方法和私有方法 | 可以有具体实现的方法 |
变量 | 只能有常量(public static final) | 可以有各种类型的变量 |
继承 | 一个类可以实现多个接口 | 一个类只能继承一个抽象类 |
构造器 | 不能有构造器 | 可以有构造器 |
访问修饰符 | 方法默认为public | 可以使用各种访问修饰符 |
设计目的 | 定义行为契约 | 提供基础实现和共享代码 |
何时使用接口:
- 需要定义一组行为规范而不关心实现细节
- 需要支持多重继承
- 不同类之间有共同的行为但没有继承关系
- 需要实现"能力"的概念(如Comparable, Serializable)
- 设计面向未来的扩展性
何时使用抽象类:
- 需要在相关类之间共享代码和状态
- 需要非public的成员
- 需要定义实例变量或构造器
- 类之间有明确的继承关系和共同的基础功能
- 需要控制子类必须实现的方法和可选实现的方法
3、网络/DNS相关问题
1. DNS递归解析的过程是怎样的?是否会直接访问根域名服务器?
DNS递归解析过程:
- 客户端查询:用户的设备向本地配置的DNS服务器发送查询请求
- 本地DNS服务器处理 :
- 检查自身缓存,如有记录则直接返回
- 如无缓存记录,则开始递归查询过程
- 递归查询过程 :
- 本地DNS服务器向根域名服务器查询
- 根域名服务器返回顶级域名(TLD)服务器地址
- 本地DNS服务器向TLD服务器查询
- TLD服务器返回权威域名服务器地址
- 本地DNS服务器向权威域名服务器查询
- 权威服务器返回最终IP地址
- 返回结果:本地DNS服务器将结果返回给客户端并缓存
普通用户的DNS查询通常不会直接访问根域名服务器,而是由本地DNS服务器代为查询。本地DNS服务器会缓存之前的查询结果,减少对根域名服务器的访问。
2. 根域名服务器是否会被大量请求"炸掉"?实际配置中如何处理DNS解析?
根域名服务器不会被"炸掉"的原因和实际DNS解析配置:
防止根域名服务器过载的机制:
- 多层缓存:客户端、本地DNS服务器、ISP DNS服务器都有缓存
- 分布式部署:全球有13组根域名服务器,每组有多个镜像站点(共数百个节点)
- 任播技术(Anycast):相同IP地址在不同地理位置有多个服务器实例
- TTL机制:DNS记录有生存时间,减少重复查询
- 预取技术:某些DNS服务器会主动刷新即将过期的热门记录
实际DNS解析配置:
- 本地DNS缓存:操作系统和浏览器维护DNS缓存
- 递归DNS服务器:ISP提供的DNS服务器处理大部分查询
- 转发器配置:许多本地DNS服务器配置为转发器模式,将查询转发给上级DNS
- 智能DNS:根据用户位置返回最近的服务器IP
- DNSSEC:提供DNS安全认证
- DNS负载均衡:通过轮询或权重分配流量
- CDN集成:与内容分发网络集成,优化访问速度
在实际应用中,绝大多数DNS查询在本地或ISP级别就能得到解答,只有极少数查询需要访问根域名服务器。