环形链表
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
思路:
第一阶段:快慢指针先找相遇点
第二阶段:头指针和相遇节点指针一起移动,直到移到相同点,这个相同点就是环入口
推导:
一、先定义好三个"物理距离"
假设链表长这样(E 是环入口,M 是快慢指针相遇点):
text
head ───a───> E ───b───> M
↑ │
└─────c─────┘
-
a:从头节点head走到环入口E的距离。 -
b:从环入口E走到相遇点M的距离(沿环走的方向)。 -
c:从相遇点M继续走到环入口E的距离(沿环走的方向)。 -
环的总长 =
b + c。
二、快慢指针各走了多远?
假设相遇时:
-
慢指针走了:
a + b -
快指针走了:
a + b + n(b + c),其中n ≥ 1是快指针在环里多转的圈数。
因为快指针速度是慢指针的 2 倍,所以在相同时间内,快指针走的路程是慢指针的 2 倍:
a+b+n(b+c)⏟快指针=2⋅(a+b)⏟慢指针快指针a+b+n(b+c)=2⋅慢指针(a+b)
化简:
a+b+n(b+c)=2a+2ba+b+n(b+c)=2a+2bn(b+c)=a+bn(b+c)=a+ba=(n−1)(b+c)+ca=(n−1)(b+c)+c
三、关键:如何"读"这个等式?
把等式右边拆开看:
a=(n−1)(b+c)+c⏟
这句话的物理含义是:
从头节点走
a步到环入口,等价于------从相遇点M出发,先绕环转n-1整圈,再走c步,同样也会到达环入口E。
也就是说,a 和 c 这两段路,终点是同一个地方(环入口 E),只是起点不同:
-
从
head出发走a步 → 到达E -
从
M出发走c步(绕若干圈后)→ 到达E
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head==null)return null;
ListNode slow=head;
ListNode fast=head;
while(fast!=null&&fast.next!=null){
slow=slow.next;
fast=fast.next.next;
if(slow==fast){
ListNode meet=slow;
ListNode h1=head;
while(h1!=meet){
h1=h1.next;
meet=meet.next;
}
return meet;
}
}
return null;
}
}
二叉树的右视图
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
思路:BFS,开一个结果数组和队列,队列存储每一行的所有节点,对于每次遍历,当遍历到当前行最后一个节点时,将当前节点加入res当中
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List rightSideView(TreeNode root) {
Listres=new ArrayList<>();
Queue queue=new LinkedList<>();
if(root==null)return res;
queue.offer(root);
while(!queue.isEmpty()){
int n=queue.size();
for(int i=0;i<n;i++){
TreeNode node=queue.poll();
if(i==n-1){
res.add(node.val);
}
if(node.left!=null){
queue.offer(node.left);
}
if(node.right!=null){
queue.offer(node.right);
}
}
}
return res;
}
}
pdd面经
2.JAVA面向对象的三个特征。
封装
把数据(属性)和操作数据的方法(行为)捆绑在类里,对外只暴露必要接口,隐藏内部实现细节。好处是降低耦合、保护数据完整性、便于修改实现而不影响调用方。
继承
子类复用父类的属性和方法,表达「is-a」关系,实现代码复用和多态的基础。注意:Java 是单继承,但可通过接口实现多继承语义
多态 同一接口/父类引用指向不同子类对象,运行时表现出不同行为(动态绑定)。前提是继承 + 方法重写 + 父类引用指向子类对象。
多态 = 声明类型统一(父类/接口),实际类型各异(子类对象),方法调用在运行时按实际类型动态分派,从而用同一段调用代码产生不同行为。
1. 「同一接口/父类引用」
Animal a1 和 Animal a2 的变量类型 是 Animal。编译器在编译期只认 Animal 这个类型,它只知道「有个 speak() 方法」,不知道也不关心具体是狗还是猫。
2. 「指向不同子类对象」
new Dog() 和 new Cat() 在堆上创建了不同的对象 ,a1、a2 分别持有它们的引用。也就是说:引用的静态类型相同,但指向的运行时对象不同。
3. 「运行时表现出不同行为」+「动态绑定」
关键问题来了:a1.speak() 编译时,编译器看到的是 Animal.speak(),它怎么知道要调用 Dog 的实现?
答案是方法调用的绑定发生在运行时,而不是编译时:
-
编译期:只检查
Animal有没有speak(),有就通过(编译通过)。 -
运行期:JVM 看
a1实际指向的对象 是Dog,于是去Dog的方法表里找speak()的实现来执行。
这个「运行时根据实际对象类型决定调用哪个方法」的机制,就叫动态绑定(动态分派)。JVM 底层通常通过**虚方法表(vtable)**实现:每个类有一张方法表,子类重写的方法会覆盖父类对应槽位,调用时按实际对象的方法表取入口。
4. 「前提是继承/实现 + 方法重写 + 父类引用指向子类对象」
这三个是缺一不可的条件:
| 条件 | 作用 | 缺了会怎样 |
|---|---|---|
| 继承/实现 | 建立「is-a」关系,子类才能被父类引用指向 | Animal a = new Dog() 编译报错,类型不兼容 |
| 方法重写 | 子类提供自己的实现 | 不重写就继承父类实现,调用结果都一样,没有「不同行为」 |
| 父类引用指向子类对象 | 让「声明类型」和「实际类型」分离 | 若写 Dog d = new Dog(),编译期就绑定了 Dog,没有动态分派可言 |
三者合起来,才制造出「同一个调用,不同对象,不同结果」的效果。
多态的实际价值(为什么设计它)
没有多态,你只能写:
java
if (animal instanceof Dog) ((Dog) animal).speak();
else if (animal instanceof Cat) ((Cat) animal).speak();
每加一种动物就要改这段 if-else,违反开闭原则。有了多态,调用方只写 animal.speak(),新增子类无需改动调用方代码------这就是「面向接口编程」
3.反射是什么?优缺点是什么?
反射是程序在运行时 动态获取类的信息(类名、方法、字段、构造器、注解等)并操作对象的能力。核心入口是 Class 对象,常用 API:Class.forName()、getDeclaredMethods()、newInstance()、setAccessible(true) 等。Spring 的 IOC/DI、MyBatis 映射、动态代理都大量依赖反射。
优点
-
运行时动态性:框架可以在不知道具体类的情况下装配、调用。
-
解耦:配置驱动,扩展性强。
-
能突破访问限制(
setAccessible),实现通用逻辑。
缺点
-
性能开销 :方法调用比直接调用慢(有安全检查、无法内联优化),可通过缓存
Method对象、MethodHandle缓解。 -
安全性:可绕过访问控制,破坏封装;
-
可读性/调试难:错误在运行时才暴露,编译期无法检查。
4.介绍一下数据库中的索引。
索引是帮助数据库高效检索数据的数据结构,本质是「排好序的、指向数据行的辅助结构」,用空间换时间。
常见类型
-
按结构:B+ 树索引(MySQL InnoDB 默认)、哈希索引、全文索引、R-Tree(空间)。
-
按逻辑:主键索引(聚簇)、唯一索引、普通索引、联合索引、前缀索引。
-
按物理:聚簇索引(数据即索引,InnoDB 主键)、二级索引(叶子存主键,需回表)。
关键概念
-
最左前缀原则 :联合索引
(a,b,c)能命中a、a,b、a,b,c,不能跳过a直接用b。 -
回表 :二级索引查到主键后再回聚簇索引取整行;覆盖索引可避免回表。
-
索引失效 :对索引列做函数运算、隐式类型转换、
like '%x'前导模糊、or连接非索引列等。
代价:占空间、增删改要维护索引、降低写性能。
我的思考 :索引不是越多越好,要结合查询模式设计。联合索引的顺序要按「等值列在前、范围列在后」排;写多读少的表要克制建索引;用
explain看type、key、rows验证是否命中。
5.B+如何实现范围查询?
-
所有数据都在叶子节点,非叶子节点只存索引键,起导航作用------同样磁盘页能存更多键,树更矮,IO 更少。
-
叶子节点之间用双向链表连接,且按键有序。
6.volatile和synchronized的区别
7.介绍一下JAVA里面的集合。
8.hashmap扩容
9.concurrenthashmap安全性
10.CAS和加锁
11.什么是悲观锁?什么是乐观锁?
12.jvm介绍