博客记录-day155-力扣+面试

一、力扣

1、排序数组

912. 排序数组

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、二叉树的序列化与反序列化

297. 二叉树的序列化与反序列化

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、基本计算器

224. 基本计算器

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、整数拆分

343. 整数拆分

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、不同的二叉搜索树

96. 不同的二叉搜索树

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地址

468. 验证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、分割等和子集

416. 分割等和子集

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. 为什么有了进程还要线程?

引入线程的主要原因:

  1. 资源利用效率:线程共享进程资源,创建和销毁线程的开销远小于进程
  2. 并发性能:多线程可以充分利用多核处理器,提高程序执行效率
  3. 响应速度:线程切换比进程切换快,提高系统响应速度
  4. 通信效率:线程间通信更简单高效,共享内存无需额外IPC机制
  5. 编程模型:线程模型使并发编程更直观,便于开发复杂应用

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. 引入虚拟线程这一层的目的是什么?

引入虚拟线程的主要目的:

  1. 提高吞吐量:支持更多并发连接,特别适合IO密集型应用
  2. 简化编程模型:使用同步代码风格编写异步效果的程序,避免回调地狱
  3. 资源利用效率:减少线程资源消耗,提高系统整体效率
  4. 适应现代应用需求:支持微服务架构下的高并发场景
  5. 提高响应性:在IO等待时自动切换,减少阻塞时间
  6. 降低开发复杂度:相比反应式编程更直观易懂

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. 磁盘在内存申请中起到什么作用?

磁盘在内存申请中的作用:

  1. 提供交换空间:作为虚拟内存的扩展,存储暂时不用的内存页面
  2. 支持内存映射:通过mmap等机制将文件内容映射到内存地址空间
  3. 持久化存储:保存程序运行产生的数据,在需要时加载到内存
  4. 缓解内存压力:通过页面置换算法,将不常用数据暂存到磁盘
  5. 支持大型数据处理:允许处理超过物理内存大小的数据集

13. HDD磁盘为何以块(block)为单位读写?为何不以单个比特或字节为单位?

HDD磁盘以块为单位读写的原因:

  1. 物理结构限制:磁头一次定位就能读取一个扇区的数据
  2. 寻址开销优化:减少寻道和旋转延迟的影响
  3. 错误检测与纠正:块级别更容易实现ECC校验
  4. 传输效率:批量传输数据比逐位/逐字节传输效率高
  5. 控制器设计简化:简化了磁盘控制器的设计复杂度
  6. 缓存管理:便于系统进行缓存管理和预读优化

14. 内存缓存对齐的作用是什么?不对齐会导致什么问题?

内存缓存对齐的作用:

  1. 提高访问效率:CPU一次可以完整读取一个缓存行的数据
  2. 减少内存访问次数:避免一个数据跨越多个缓存行
  3. 避免伪共享:防止多线程程序中的缓存行争用
  4. 硬件要求:某些处理器架构要求特定类型数据必须对齐

不对齐可能导致的问题:

  • 性能下降:需要多次内存访问获取一个数据
  • 原子操作失效:某些架构下非对齐数据无法保证原子性
  • 硬件异常:某些处理器对非对齐访问会产生异常
  • 缓存效率降低:增加缓存未命中率
  • 伪共享问题:导致多线程程序性能下降

15. HDD的机械结构如何影响其读写方式?

HDD的机械结构对读写方式的影响:

  1. 寻道时间:磁头移动到指定磁道需要时间,影响随机访问性能
  2. 旋转延迟:等待目标扇区旋转到磁头下方的时间
  3. 顺序访问优势:连续读写可以减少寻道和旋转延迟
  4. 块读写:基于扇区的物理结构决定了以块为单位读写
  5. 读写调度算法:如电梯算法,优化磁头移动路径
  6. 预读策略:基于空间局部性原理,提前读取相邻数据
  7. 写入缓存:聚合小写入操作,减少实际磁头移动次数

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递归解析过程:

  1. 客户端查询:用户的设备向本地配置的DNS服务器发送查询请求
  2. 本地DNS服务器处理
    • 检查自身缓存,如有记录则直接返回
    • 如无缓存记录,则开始递归查询过程
  3. 递归查询过程
    • 本地DNS服务器向根域名服务器查询
    • 根域名服务器返回顶级域名(TLD)服务器地址
    • 本地DNS服务器向TLD服务器查询
    • TLD服务器返回权威域名服务器地址
    • 本地DNS服务器向权威域名服务器查询
    • 权威服务器返回最终IP地址
  4. 返回结果:本地DNS服务器将结果返回给客户端并缓存

普通用户的DNS查询通常不会直接访问根域名服务器,而是由本地DNS服务器代为查询。本地DNS服务器会缓存之前的查询结果,减少对根域名服务器的访问。

2. 根域名服务器是否会被大量请求"炸掉"?实际配置中如何处理DNS解析?

根域名服务器不会被"炸掉"的原因和实际DNS解析配置:

防止根域名服务器过载的机制

  1. 多层缓存:客户端、本地DNS服务器、ISP DNS服务器都有缓存
  2. 分布式部署:全球有13组根域名服务器,每组有多个镜像站点(共数百个节点)
  3. 任播技术(Anycast):相同IP地址在不同地理位置有多个服务器实例
  4. TTL机制:DNS记录有生存时间,减少重复查询
  5. 预取技术:某些DNS服务器会主动刷新即将过期的热门记录

实际DNS解析配置

  1. 本地DNS缓存:操作系统和浏览器维护DNS缓存
  2. 递归DNS服务器:ISP提供的DNS服务器处理大部分查询
  3. 转发器配置:许多本地DNS服务器配置为转发器模式,将查询转发给上级DNS
  4. 智能DNS:根据用户位置返回最近的服务器IP
  5. DNSSEC:提供DNS安全认证
  6. DNS负载均衡:通过轮询或权重分配流量
  7. CDN集成:与内容分发网络集成,优化访问速度

在实际应用中,绝大多数DNS查询在本地或ISP级别就能得到解答,只有极少数查询需要访问根域名服务器。

相关推荐
PPIO派欧云1 小时前
PPIO X OWL:一键开启任务自动化的高效革命
运维·人工智能·自动化·github·api·教程·ppio派欧云
一纸忘忧2 小时前
成立一周年!开源的本土化中文文档知识库
前端·javascript·github
吾日三省吾码4 小时前
GitHub Copilot (Gen-AI) 很有用,但不是很好
人工智能·github·copilot
Gladiator5756 小时前
博客记录-day154-面试
github
徐小夕9 小时前
写了一款3D可视化编辑器模版,开源!
前端·javascript·github
uhakadotcom9 小时前
持续写作的“农耕思维”:如何像农民一样播种,收获稳定成长与收入
后端·面试·github
服部9 小时前
如何查看指定作者在所有分支的提交记录
前端·git·github
小华同学ai10 小时前
2.1k star! 抓紧冲,DeepChat:连接AI与个人世界的智能助手的开源项目
人工智能·ai·开源·github·工具
互联网搬砖老肖12 小时前
运维打铁:域名详解及常见问题解决
运维·github