课程表
java
package hot100;
import java.util.*;
public class lc207 {
/*课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,
其中 prerequisites[i] = [ai, bi] ,
表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。*/
//图,List【】 出度, 入度,对于入度==0,bfs
public boolean canFinish(int numCourses, int[][] prerequisites) {
//构建图
//注意写法!!是数组
List<Integer>[] graph = new ArrayList[numCourses];
for(int i = 0; i < numCourses; i++){
graph[i] = new ArrayList<>();
}
//出度
//入度
int[] degree = new int[numCourses];
for(int[] p : prerequisites){
int pre = p[1];
int next = p[0];
degree[next]++;
graph[pre].add(next);
}
//bfs
Deque<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < numCourses; i++) {
if (degree[i] == 0) {
queue.offer(i); // ✅ 入队的是课程编号
}
}
int count = 0;
while(!queue.isEmpty()){
int temp = queue.poll();
count++;
for(int chudu : graph[temp]){
degree[chudu]--;
if(degree[chudu] == 0){
queue.offer(chudu);
}
}
}
return count == numCourses;
}
public static void main(String[] args) {
lc207 solution = new lc207();
// 测试用例 1: 可以完成,无环
int numCourses1 = 2;
int[][] pre1 = {{1, 0}}; // 先修 0 -> 1
System.out.println("测试1: " + solution.canFinish(numCourses1, pre1)); // true
// 测试用例 2: 有环,无法完成
int numCourses2 = 2;
int[][] pre2 = {{1, 0}, {0, 1}}; // 0<->1 环
System.out.println("测试2: " + solution.canFinish(numCourses2, pre2)); // false
// 测试用例 3: 多门课程,无环
int numCourses3 = 4;
int[][] pre3 = {{1, 0}, {2, 1}, {3, 2}}; // 0->1->2->3
System.out.println("测试3: " + solution.canFinish(numCourses3, pre3)); // true
// 测试用例 4: 没有先修课程
int numCourses4 = 3;
int[][] pre4 = {};
System.out.println("测试4: " + solution.canFinish(numCourses4, pre4)); // true
}
}
208. 实现 Trie (前缀树)
java
package hot100;
public class Trie {
/*208. 实现 Trie (前缀树)
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,
返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix)
如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;
否则,返回 false 。*/
//26叉树
//26叉树
public class Node{
//主要是看儿子,儿子是那个应该就是信息
Node[] son = new Node[26];
boolean end = false;
}
//唯一的树
private final Node root = new Node();
public Trie() {
}
public void insert(String word) {
//toCharArray()
Node cur = root;
for(char c : word.toCharArray()){
int index = c - 'a';
if( cur.son[index] == null){
cur.son[index] = new Node();
}
//接下去要知道index才能准确找到儿子分支
cur = cur.son[index];
}
cur.end = true;//代表结束
}
public boolean search(String word) {
return find(word)== 1 ? true : false;
}
public boolean startsWith(String prefix) {
return find(prefix) != 0 ? true : false;
}
public int find(String word){
Node cur = root;
for(char c : word.toCharArray()){
int index = c - 'a';
if(cur.son[index] == null){
return 0;
}else{
cur = cur.son[index];
}
}
return cur.end ? 1 : 2;
}
// ========== main 测试方法 ==========
public static void main(String[] args) {
Trie trie = new Trie();
// 插入单词
trie.insert("apple");
trie.insert("app");
trie.insert("appl");
trie.insert("banana");
// 测试 search
System.out.println(trie.search("apple")); // true
System.out.println(trie.search("app")); // true
System.out.println(trie.search("appl")); // true
System.out.println(trie.search("appli")); // false
System.out.println(trie.search("banana")); // true
System.out.println(trie.search("banan")); // false
// 测试 startsWith
System.out.println(trie.startsWith("app")); // true
System.out.println(trie.startsWith("appl")); // true
System.out.println(trie.startsWith("ban")); // true
System.out.println(trie.startsWith("bana")); // true
System.out.println(trie.startsWith("cat")); // false
}
}
全排列
java
package hot100;
import java.util.*;
public class lc46 {
/*全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。
你可以 按任意顺序 返回答案。*/
List<List<Integer>> anss = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
dfs(nums, new ArrayList<>(),new boolean[nums.length]);
return anss;
}
public void dfs(int[] nums, List<Integer> ans, boolean[] used) {
if (ans.size() == nums.length) {
anss.add(new ArrayList<>(ans));
return;
}
for (int i = 0; i < nums.length; i++) {
if (!used[i]) {
used[i] = true;
ans.add(nums[i]);
dfs(nums, ans, used);
ans.remove(ans.size() - 1);
used[i] = false;
}
}
}
public static void main(String[] args) {
int[] nums = new int[]{1,2,3};
lc46 solution = new lc46();
System.out.println(solution.permute(nums));
}
}
子集
java
package hot100;
import java.util.*;
public class lc78 {
/* 子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。*/
List<List<Integer>> anss = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
dfs(nums, new ArrayList<>(), 0);
return anss;
}
public void dfs(int[] nums, List<Integer> ans, int index){
//空集也算
if(index >= 0 && index <= nums.length){
anss.add(new ArrayList<>(ans));
}
if(index > nums.length){
return;
}
for(int i = index; i < nums.length; i++){
ans.add(nums[i]);
//这里是i!!!
dfs(nums, ans, i+1);
ans.remove(ans.size() -1);
}
}
public static void main(String[] args) {
int[] nums = new int[]{1,2,3};
lc78 solution = new lc78();
System.out.println(solution.subsets(nums));
}
}
电话号码的字母组合
java
package hot100;
import java.util.*;
public class lc17 {
/*17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母*/
private static final String[] MAPPING = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz" // 9
};
List<String> anss = new ArrayList<>();
public List<String> letterCombinations(String digits) {
//全排列的变形
dfs(digits, new char[digits.length()], 0);
return anss;
}
public void dfs(String digits,char[] path, int index){
if(index == digits.length()){
//不能anss.add(path.toString);要加一个新的!!!
anss.add(new String(path));
return;
}
//是减0
String temp = MAPPING[digits.charAt(index) - '0'];
for(int i = 0; i < temp.length(); i++){
path[index] = temp.charAt(i);
dfs(digits, path,index+1);
}
}
public static void main(String[] args) {
String s = "23";
lc17 solution = new lc17();
System.out.println(solution.letterCombinations(s));
}
}
随机知识
操作系统是后端开发的基石,面试中侧重考察你对资源管理的理解,尤其是进程线程、内存管理和I/O模型。这些知识能帮你更好地解释JVM原理和多线程问题。
一、操作系统核心概念速览
1. 进程与线程
-
进程:资源分配的最小单位,有独立的内存地址空间。
-
线程:CPU调度的最小单位,同一进程的线程共享进程资源(堆、全局变量、文件句柄),有独立的栈和寄存器。
-
协程:用户态的轻量级线程,由程序自己调度,切换无需陷入内核。
2. 内存管理
-
虚拟内存:为每个进程提供连续、独立的地址空间,通过页表映射到物理内存。
-
分页:将虚拟地址划分为固定大小的页,物理内存划分为帧,以页为单位管理,解决外部碎片。
-
分段:按逻辑单元划分(代码段、数据段),有逻辑意义但可能产生外部碎片。
-
页表与TLB:TLB是页表的缓存,加速地址转换。
3. 文件系统
-
inode:存储文件元信息(大小、权限、数据块指针),文件名是给用户看的别名。
-
硬链接/软链接:硬链接指向同一个inode,删除原文件不影响;软链接(符号链接)存目标路径,类似快捷方式。
4. I/O管理
-
设备无关性:用户程序通过系统调用使用统一接口访问设备。
-
I/O模型:阻塞I/O、非阻塞I/O、I/O多路复用、信号驱动I/O、异步I/O。
二、高频面试题 + 答案(15题精选)
🧵 进程线程篇
1. 进程和线程的区别?
-
资源:进程拥有独立的内存和资源,线程共享进程资源,仅独享栈和寄存器。
-
开销:进程创建/销毁、切换开销大;线程开销小。
-
通信:进程间需IPC(管道、队列),线程直接读写共享内存(但需同步)。
-
健壮性:一个进程崩溃不影响其他进程;一个线程崩溃可能导致整个进程崩溃。
-
Java映射:JVM是单进程多线程,Java线程与OS内核线程一一对应(HotSpot)。
2. 进程间通信(IPC)有哪些方式?
-
管道/FIFO:半双工,用于父子进程或无亲缘关系进程(命名管道)。
-
消息队列:消息的链表,异步,适合非大量数据。
-
共享内存:最快,直接映射同一块物理内存,需信号量同步。
-
信号量/信号:信号量用于同步互斥;信号是短消息,通知事件。
-
Socket:可用于不同主机进程通信。
-
Java关联 :NIO的
FileChannel.map()就是利用内存映射文件(共享内存)。
3. 线程同步的方式有哪些?
-
互斥锁 :
synchronized、Lock,保证同一时间只有一个线程访问临界区。 -
信号量:控制同时访问资源的线程数量。
-
条件变量 :
Condition,实现等待-唤醒机制。 -
原子操作 :
AtomicInteger,无锁但只能保证单个操作的原子性。 -
Java中 :还有
CountDownLatch、CyclicBarrier、Semaphore等工具类。
🎯 死锁篇
4. 死锁产生的四个必要条件?如何预防?
-
互斥:资源独占。
-
持有并等待:持有资源的同时等待新资源。
-
不可剥夺:已获得资源不能被强行抢走。
-
循环等待:形成资源的环形等待链。
-
预防:破坏任意一个条件。例如一次性申请所有资源(破坏"持有并等待")、资源排序强制按序申请(破坏"循环等待")。
5. 银行家算法是什么?
- 一种死锁避免算法,核心思想:判断系统分配资源后是否仍处于安全状态。若存在一个安全序列,则分配;否则拒绝分配,进程必须等待。适用于资源静态分配的系统中。
🧠 内存管理篇
6. 什么是虚拟内存?有什么好处?
-
将物理内存和进程的逻辑地址空间分离,让进程感觉自己拥有连续的、独占的大内存。
-
好处:隔离进程、可运行比物理内存大的程序(借助磁盘)、简化内存管理、共享内存和写时拷贝。
7. 分页和分段的区别?
-
分页:页大小固定,按物理角度分割,消除外部碎片,有内部碎片,地址一维。
-
分段:段大小可变,按逻辑单元(如函数、数组)分割,有外部碎片,地址二维(段号+偏移)。
-
现代OS:通常段页式,Linux主要用分页,用平坦模式。
8. 页面置换算法有哪些?
-
OPT(最佳置换):换出未来最远使用的页,无法实现,仅作比较标准。
-
FIFO:先进先出,有Belady异常。
-
LRU(最近最久未使用):根据历史访问记录,换出最久没访问的页。实现可用链表或栈,近似实现用Clock算法。
-
Clock(时钟算法):循环检查引用位,如果为0则换出,否则清0并向后扫描,是NRU的改进。
9. 什么是写时复制(Copy-on-Write)?
- 多个进程共享同一块物理内存,只要没有写操作,就一直共享;当某个进程尝试写时,才复制一份私有副本。典型应用:
fork()后父子进程共享内存,exec时才复制;Linux内存管理、Redis的BGSAVE也用到了COW。
📂 文件系统
10. 硬链接和软链接的区别?
-
硬链接:共享同一个inode,文件只要有硬链接存在就不会被删除。不能跨文件系统,不能链接目录。
-
软链接:存放目标路径,删除原文件则失效,可以跨文件系统,可以链接目录。
⚡ I/O与高性能
11. 什么是I/O多路复用?select/poll/epoll的区别?
-
核心思想:单个线程同时监听多个文件描述符,一旦有事件就通知处理,避免阻塞在每个描述符上。
-
select:遍历所有FD,有最大文件描述符限制(一般1024),轮询O(n)。
-
poll:链表存储,无最大限制,但仍需轮询O(n)。
-
epoll :使用红黑树+事件驱动,当FD就绪时通过回调主动通知,O(1),支持ET/LT模式。ET边缘触发 :只有状态变化时才通知,需一次读完数据;LT水平触发:未读完会不断通知。
12. 什么是零拷贝?Java中如何实现?
-
传统数据发送需要4次拷贝(用户态↔内核态)和4次上下文切换。零拷贝减少CPU拷贝次数。
-
sendfile + DMA scatter/gather:数据只拷贝两次到Socket缓冲区,内核态完成传输。
-
mmap:将文件映射到内存,减少内核到用户态的拷贝,适合小文件。
-
Java实现 :
FileChannel.transferTo()底层就是调用sendfile;MappedByteBuffer底层是mmap。RocketMQ、Kafka都大量使用零拷贝提高吞吐。
🔧 调度与其他
13. 进程调度算法有哪些?
-
FCFS:先来先服务,非抢占,对短作业不友好。
-
SJF:最短作业优先,平均等待时间最小,但长作业可能饥饿。
-
优先级调度:按优先级,抢占或非抢占。
-
时间片轮转:公平,每个进程固定时间片,响应快,适合交互。
-
多级反馈队列:多个队列不同优先级和时间片,新进程进最高优先队列,用不完时间片降级,兼顾短作业和交互。
14. 用户态和内核态的区别?为什么要区分?
-
用户态受限,不能执行特权指令(如I/O、内存管理);内核态拥有所有权限。
-
区分目的:保护系统安全,防止用户程序破坏系统。系统调用是用户态进入内核态的唯一入口。
-
切换方式:系统调用、中断、异常。切换涉及栈切换和保存寄存器,有一定开销。
15. Linux中常见的锁或同步机制?
-
互斥锁(mutex):内核态互斥锁,睡眠等待。
-
自旋锁(spinlock):忙等待,不休眠,适合临界区极小的情况。
-
读写锁(rwlock):多读单写,提高并发。
-
RCU(Read-Copy Update):读者无锁,写者先拷贝再更新,适合读多写少。
-
信号量(semaphore):计数的睡眠锁,控制访问数量。
碎碎念:后续会更新每天学习的八股和算法 题,开始准备秋招的第72天。努力连续更新100天!以后每天就按,秋招项目【java +agent】,科研,必做项目,算法,八股,锻炼身体来总结。
总结:所有努力都是有用的,现在刷题都很得心应手还,虽然思路还是可能会忘记。
1.hot100 【acm】 57/100 ,2到3h,快速把hot100过一遍【9/20】
2.秋招项目,【java 项目】,继续
【agent 项目 】,继续
3.科研。确定方向就搞就可以了
4.实习;
6.背八股,无
7.锻炼身体,无
要点:坚持