Java/Python 核心知识点面试题(含答案):语法+集合+JVM+设计模式+算法
一、语法基础(Java/Python 通用 + 各自重点)
(一)通用语法
- Java 和 Python 的核心区别是什么?
-
类型:Java 静态类型(编译时检查),Python 动态类型(运行时检查);
-
运行方式:Java 编译为字节码(.class),JVM 解释执行;Python 源码编译为字节码(.pyc),解释器逐行执行;
-
性能:Java 性能更高(静态类型 + JIT 优化),Python 稍弱(GIL 影响并发);
-
应用场景:Java 适用于后端、大数据、安卓;Python 适用于 AI、脚本、数据分析。
- 面向对象的三大特性?举例说明。
-
封装:隐藏对象内部细节,通过方法暴露接口(如 Java 的
private字段 +getter/setter); -
继承:子类复用父类属性和方法(如
class Dog extends Animal,Dog 继承 Animal 的eat()方法); -
多态:同一方法不同实现(如 Animal 的
makeSound(),Dog 实现为 "汪汪",Cat 实现为 "喵喵")。
- 重载(Overload)和重写(Override)的区别?Python 支持重载吗?
-
重载:同一类中,方法名相同、参数列表(类型 / 个数 / 顺序)不同,编译时绑定;
-
重写:子类覆盖父类同名方法,参数列表一致,运行时绑定;
-
Python 不支持传统重载(后定义的方法会覆盖前一个),可通过默认参数 /
*args模拟。
- 接口和抽象类的区别?Java 8 中接口的新特性?
-
抽象类:可含抽象方法 + 普通方法,有构造器,只能单继承;
-
接口:Java 8 前仅抽象方法 + 常量,无构造器,支持多实现;
-
Java 8 接口新特性:支持默认方法(
default修饰,有实现)、静态方法(static修饰)。
- 什么是泛型?Java 和 Python 泛型的区别?
-
泛型:定义时不指定具体类型,使用时指定,实现代码复用(如 Java
ListList[int]); -
Java 泛型:编译时擦除(本质是 Object 强制转换),仅编译期类型检查;
-
Python 泛型(Type Hint):仅语法提示,运行时不生效(如
def func(x: int) -> str,x 仍可传字符串)。
(二)Java 语法重点
- 基本数据类型和包装类的区别?自动装箱 / 拆箱问题?
-
基本类型:8 种(
int/char等),栈存储,无方法;包装类:对应引用类型(Integer/Character),堆存储,有方法; -
自动装箱:基本类型 → 包装类(如
int → Integer);拆箱:包装类 → 基本类型(如Integer → int); -
问题:
Integer a=127, b=127相等(缓存 [-128,127]),a=128, b=128不相等(新对象);包装类为null时拆箱会抛 NPE。
- final、finally、finalize 的区别?
-
final:修饰类(不可继承)、方法(不可重写)、变量(不可修改);
-
finally:try-catch 后必执行(除非 JVM 退出),用于释放资源;
-
finalize:Object 类方法,GC 回收对象前调用,不可靠(不保证执行),已废弃。
- static 关键字的作用?
-
静态变量:类共享,不属于实例,随类加载初始化;
-
静态方法:无
this指针,不能访问非静态成员,可直接通过类名调用; -
静态代码块:类加载时执行一次,用于初始化静态资源;
-
静态内部类:不依赖外部类实例,不能访问外部类非静态成员。
- 异常体系与 try-catch-finally 执行顺序?
-
体系:
Throwable是父类,子类为Error(系统错误,不可处理)和Exception(可处理); -
checked exception:编译时检查(如
IOException),必须捕获或抛出;unchecked exception:运行时异常(如NullPointerException),无需强制处理; -
执行顺序:try → 异常时执行 catch → 无论是否异常,finally 必执行(return 前执行,finally 中 return 会覆盖 try/catch 的 return)。
- Lambda 表达式与函数式接口?
-
Lambda 语法:
(参数) -> 表达式/代码块(如(a,b) -> a+b),简化匿名内部类; -
函数式接口:仅含一个抽象方法的接口(如
Runnable、Comparator),可通过@FunctionalInterface注解校验。
- Stream API 的优缺点?
-
常用操作:过滤(
filter)、映射(map)、聚合(reduce)、排序(sorted); -
优点:代码简洁、支持链式调用、并行流(
parallelStream)利用多核; -
缺点:并行流需注意线程安全(如非线程安全集合的
forEach),调试困难。
(三)Python 语法重点
- 解释器工作原理与 GIL?
-
原理:Python 是解释型语言,源码 → 字节码(.pyc)→ 解释器(CPython)执行;
-
GIL(全局解释器锁):CPython 特有,同一时刻仅一个线程执行 Python 字节码,多核下并发变串行,IO 密集型(如网络请求)影响小,CPU 密集型(如计算)影响大。
- 装饰器、生成器、迭代器的原理?
-
装饰器:函数嵌套,包装目标函数,增强功能(如日志、计时),用
@decorator语法;带参数装饰器需三层嵌套(如def decorator(param): def wrapper(func): ... return wrapper); -
生成器:含
yield关键字的函数,执行到yield暂停,返回迭代器,节省内存(如def gen(): yield 1; yield 2); -
迭代器:实现
__iter__()和__next__()方法的对象,可通过next()逐个获取元素(如list的迭代器)。
- 深拷贝与浅拷贝的区别?
-
浅拷贝:复制对象引用,子对象共享(如
copy.copy(list),列表元素为对象时,仅复制对象地址); -
深拷贝:递归复制对象及子对象,完全独立(如
copy.deepcopy(list)); -
注意:不可变对象(
tuple/str)的浅拷贝等于本身。
- 闭包的定义与条件?
-
定义:内层函数引用外层函数的变量,外层函数返回内层函数;
-
条件:内层函数嵌套外层函数、内层函数引用外层函数变量、外层函数返回内层函数;
-
问题:外层变量若为可变类型(如
list),多次调用可能共享状态。
- Python 的垃圾回收机制?
-
核心:引用计数为主(对象被引用一次计数 + 1,引用消失 - 1,计数为 0 回收);
-
辅助机制:分代回收(新生代 / 老年代,新生代回收频繁)、标记清除(解决循环引用,如
a=b, b=a)。
- 字典(dict)的底层实现?Python 3.7+ 为何有序?
-
底层:哈希表(数组 + 链表),键通过哈希函数映射到数组索引,查找时间复杂度 O (1);
-
有序原因:Python 3.7+ 用双向链表记录插入顺序,数组存储链表节点指针,遍历按链表顺序。
__init__、__new__、__del__的区别?
-
__new__:创建对象时调用,返回对象实例(构造方法),可用于单例模式; -
__init__:初始化对象时调用,接收参数给实例赋值(初始化方法); -
__del__:对象被 GC 回收时调用(析构方法),用于释放资源,不可靠。
二、集合框架(Java/Python 对应对比)
(一)Java 集合框架
- 集合框架结构与 List/Set/Map 区别?
-
结构:两大体系 ------
Collection(存储单元素,如 List/Set)和Map(存储键值对,如 HashMap); -
区别:List(有序、可重复,如 ArrayList)、Set(无序、不可重复,如 HashSet)、Map(键唯一、值可重复,如 HashMap)。
- ArrayList 和 LinkedList 的区别?
| 特性 | ArrayList(动态数组) | LinkedList(双向链表) |
|---|---|---|
| 查找效率 | O (1)(随机访问) | O (n)(遍历查找) |
| 插入删除效率 | O (n)(需移动元素) | O (1)(仅改指针,已知位置) |
| 内存占用 | 连续空间,浪费少 | 额外存储指针,浪费多 |
| 适用场景 | 高频查询、少量插入删除 | 高频插入删除、少量查询 |
- HashMap 的底层原理(JDK 1.7 vs 1.8)?
-
JDK 1.7:数组 + 链表,哈希冲突用链地址法(链表头插);
-
JDK 1.8:数组 + 链表 + 红黑树(链表长度 > 8 且数组容量≥64 时转红黑树,化为链表);
-
扩容机制:初始容量 16,负载因子 0.75,扩容阈值 = 容量 × 负载因子,扩容时容量翻倍(重新哈希);
-
线程不安全:多线程扩容时可能出现链表循环;ConcurrentHashMap 线程安全:JDK 1.7 用分段锁,JDK 1.8 用 CAS+Synchronized(锁数组节点)。
- HashSet 的底层实现与去重原理?
-
底层:基于 HashMap,值存储在 HashMap 的 key 中(value 为固定空对象
PRESENT); -
去重:依赖 HashMap 的 key 唯一性(key 的
hashCode()和equals()方法),若两对象hashCode相等且equals为 true,则视为同一 key。
- TreeMap/TreeSet 的底层实现与排序?
-
底层:红黑树(平衡二叉查找树),保证有序;
-
排序:自然排序(实现
Comparable接口,重写compareTo())或定制排序(传入Comparator接口实现类)。
- Vector 和 ArrayList 的区别?
-
线程安全:Vector 线程安全(方法加
synchronized),ArrayList 线程不安全; -
扩容机制:Vector 初始容量 10,默认扩容翻倍;ArrayList 初始容量 10,默认扩容 1.5 倍。
- 迭代器的 fail-fast 和 fail-safe 区别?
-
fail-fast(快速失败):如 ArrayList 的迭代器,遍历中修改集合(增删)会抛
ConcurrentModificationException(通过modCount校验); -
fail-safe(安全失败):如 CopyOnWriteArrayList 的迭代器,遍历前复制集合快照,修改原集合不影响快照,无异常,但可能读取旧数据。
(二)Python 集合框架
- list/tuple/set/dict 的核心区别?
| 特性 | list(列表) | tuple(元组) | set(集合) | dict(字典) |
|---|---|---|---|---|
| 可变性 | 可变 | 不可变 | 可变 | 可变 |
| 有序性 | 有序(3.7+) | 有序 | 无序 | 有序(3.7+) |
| 查找效率 | O(n) | O(n) | O(1) | O(1) |
| 适用场景 | 有序存储 | 不可变数据 | 去重 / 交集 | 键值对查询 |
- list 的底层与 append/extend 区别?
-
底层:动态数组,预分配容量,满了自动扩容(默认扩容 1.125 倍左右);
-
append:添加单个元素(如
list.append(1));extend:添加可迭代对象的所有元素(如list.extend([2,3]))。
- dict 查找 O (1) 的原因与哈希冲突解决?
-
O (1) 原因:键通过哈希函数映射到数组索引,直接访问索引;
-
哈希冲突:多个键映射到同一索引,用开放寻址法(线性探测 / 二次探测)解决(CPython 实现)。
- set 的底层与去重原理?
-
底层:哈希表,存储唯一元素,无键值对;
-
去重:元素必须可哈希(不可变类型,如
int/str/tuple),通过哈希值和__eq__方法判断唯一性; -
交集 / 并集:
a & b(交集)、a | b(并集),基于哈希表快速计算。
- tuple 比 list 高效的原因?tuple 真的不可变吗?
-
高效原因:不可变,内存分配更紧凑,无需预留扩容空间;
-
不可变限制:仅顶层元素不可变,若元素为可变类型(如
list),则元素内部可修改(如t = (1, [2]); t[1].append(3)有效)。
- OrderedDict 和普通 dict 的区别?
-
Python 3.7+ 普通 dict 已有序,OrderedDict 额外特性:
-
支持
move_to_end()移动元素到首尾; -
支持按插入顺序或访问顺序排序;
-
相等性判断考虑顺序(普通 dict 不考虑,如
{1:2,3:4} == {3:4,1:2}为 True)。
-
(三)通用对比问题
- Java ArrayList vs Python list?
-
底层:均为动态数组;
-
性能:Java 效率更高(静态类型 + 数组存储基本类型),Python list 存储引用,效率稍低;
-
功能:Python list 支持动态添加不同类型元素(如
[1, "a"]),Java ArrayList 仅支持单一类型(泛型限制)。
- Java HashMap vs Python dict?
-
哈希冲突:HashMap 用链地址法,dict 用开放寻址法;
-
扩容:HashMap 扩容翻倍,dict 扩容为原容量的 2~4 倍;
-
有序性:HashMap 无序(Java 8 后链表有序但遍历无序),Python 3.7+ dict 有序。
- 集合选择场景?
-
有序存储 + 高频查询:Java ArrayList / Python list;
-
无序去重 + 高频查找:Java HashSet / Python set;
-
键值对查询:Java HashMap / Python dict;
-
有序键值对 + 排序:Java TreeMap / Python OrderedDict(或普通 dict + 排序)。
三、JVM 内存模型与 GC 机制(Java 重点)
- JVM 运行时数据区结构?
-
程序计数器:存储当前线程执行指令地址,线程私有,无 OOM;
-
虚拟机栈:线程私有,存储栈帧(局部变量表、操作数栈等),栈深度过大抛
StackOverflowError,扩容失败抛 OOM; -
本地方法栈:类似虚拟机栈,为 native 方法服务;
-
方法区:存储类元信息、常量、静态变量,JDK 8 后为元空间(本地内存),JDK 7 及以前为永久代(堆内存),满了抛 OOM;
-
堆:线程共享,存储对象实例,JVM 最大内存区域,满了抛 OOM。
- 堆内存划分与 GC 策略?
-
划分:年轻代(Eden 区 + Survivor 区 S0/S1,比例 8:1:1)、老年代;
-
策略:年轻代用复制算法(Eden 区满触发 Minor GC,存活对象复制到 S0/S1,多次存活后进入老年代);老年代用标记 - 整理算法(空间碎片少),Full GC 触发时回收老年代 + 年轻代。
- OOM 常见场景与解决方案?
-
堆溢出(
OutOfMemoryError: Java heap space):对象过多且无法回收,解决方案:增大堆内存(-Xms/-Xmx)、排查内存泄漏(如静态集合持有对象); -
栈溢出(
StackOverflowError):递归过深或栈帧过大,解决方案:增大栈容量(-Xss)、优化递归(改为迭代); -
方法区 / 元空间溢出:类加载过多(如频繁动态生成类),解决方案:增大元空间(
-XX:MetaspaceSize)、减少无用类加载。
- 对象可回收判断方式?
-
引用计数法:简单但无法解决循环引用(如
a=b, b=a); -
可达性分析:以 GC Roots(如线程栈局部变量、静态变量)为起点,遍历对象引用链,不可达对象标记为可回收。
- 四大引用类型区别与应用?
| 类型 | 特点 | 应用场景 |
|---|---|---|
| 强引用 | 不回收,OOM 也不释放 | 普通对象引用(Object o=new Object()) |
| 软引用 | 内存不足时回收 | 缓存(如 SoftReference 包装缓存对象) |
| 弱引用 | GC 时立即回收 | 临时数据(如 WeakHashMap 键) |
| 虚引用 | 仅用于跟踪 GC,无实际引用 | 堆外内存回收(如 PhantomReference) |
- GC 算法原理与优缺点?
-
标记 - 清除:标记可回收对象,直接清除,优点:简单,缺点:产生内存碎片;
-
标记 - 复制:将内存分为两块,存活对象复制到另一块,优点:无碎片,缺点:浪费一半内存(年轻代适用);
-
标记 - 整理:标记后将存活对象移到一端,清除剩余部分,优点:无碎片,缺点:移动对象开销大(老年代适用);
-
分代收集:结合以上算法,年轻代用复制,老年代用标记 - 整理,平衡效率与碎片。
- 主流 GC 收集器特点?
-
SerialGC:单线程收集,简单高效,适用于单核心、小内存场景;
-
ParallelGC:多线程收集,注重吞吐量(GC 时间 / 总时间占比),适用于后台任务;
-
CMS:并发标记清除,低延迟(GC 时用户线程不暂停),流程:初始标记(STW)→ 并发标记 → 重新标记(STW)→ 并发清除;缺点:产生碎片、占用 CPU 资源;
-
G1:分区回收(将堆分为多个 Region),兼顾吞吐量与延迟,支持预测性 GC,流程:初始标记 → 并发标记 → 最终标记 → 筛选回收(STW,回收价值最高的 Region);
-
ZGC/Shenandoah:超低延迟(暂停时间 < 10ms),适用于大内存、低延迟场景。
- JVM 调优核心指标与参数?
-
核心指标:吞吐量(优先)、延迟(优先);
-
常用参数:
-
堆大小:
-Xms2G(初始堆)、-Xmx2G(最大堆),建议两者相等避免频繁扩容; -
新生代比例:
-XX:NewRatio=2(老年代:新生代 = 2:1); -
收集器选择:
-XX:+UseG1GC(启用 G1)、-XX:+UseConcMarkSweepGC(启用 CMS); -
GC 日志:
-XX:+PrintGCDetails -Xloggc:gc.log(输出 GC 日志)。
-
- 类加载机制与生命周期?
-
生命周期:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载;
-
加载:通过类全限定名获取字节码,生成
Class对象; -
准备:为静态变量分配内存并设置默认值(如
static int a=1此时为 0,初始化时赋值 1); -
初始化:执行静态代码块和静态变量赋值,触发条件:new 对象、调用静态方法 / 变量、反射、子类初始化。
- 双亲委派模型原理与作用?如何打破?
-
原理:类加载器(Bootstrap → Extension → Application)加载类时,先委托父加载器加载,父加载器无法加载再自己加载;
-
作用:防止类重复加载、保护核心类(如
java.lang.String不能被自定义类替换); -
打破方式:重写
ClassLoader的loadClass()方法(如 Tomcat 类加载器,为每个 Web 应用单独加载类)。
四、常用设计模式
- 设计模式六大原则?
-
单一职责:一个类只负责一个功能(如 UserService 只处理用户相关逻辑);
-
开闭原则:对扩展开放,对修改关闭(如用接口扩展功能,而非修改原有代码);
-
里氏替换:子类可替换父类,不改变原有逻辑(如子类不能重写父类的
final方法); -
依赖倒置:依赖抽象(接口 / 抽象类),不依赖具体实现(如依赖
Logger接口,而非FileLogger实现类); -
接口隔离:接口拆分细小,避免实现不需要的方法(如将大接口拆分为多个小接口);
-
迪米特法则:最少知道原则,一个对象只与直接关联对象通信(如减少类之间的依赖)。
- 单例模式的实现方式?
-
饿汉式:类加载时初始化实例,线程安全,缺点:提前占用内存(
private static final Singleton INSTANCE = new Singleton()); -
懒汉式:延迟初始化,线程不安全(需加
synchronized优化,效率低); -
双重检查锁:线程安全且高效(
volatile修饰实例,防止指令重排:private static volatile Singleton INSTANCE;); -
静态内部类:线程安全,延迟加载(内部类加载时初始化实例);
-
枚举:最简单,线程安全,防止反射破坏(
enum Singleton { INSTANCE; })。
- 工厂模式的区别?
-
简单工厂:一个工厂类创建所有对象,缺点:违反开闭原则(新增产品需修改工厂);
-
工厂方法:每个产品对应一个工厂类,实现工厂接口,符合开闭原则(新增产品只需新增工厂);
-
抽象工厂:创建一系列相关产品(产品族),如创建 "手机 + 电脑" 套装,缺点:扩展产品族需修改抽象工厂接口。
- 代理模式的实现与区别?
-
静态代理:手动编写代理类,实现目标接口,缺点:重复代码多(每个目标类需对应代理类);
-
动态代理:运行时生成代理类,JDK 动态代理(基于接口,需目标类实现接口)、CGLIB 动态代理(基于子类,无需实现接口,需依赖 CGLIB 库);
-
与装饰器模式区别:代理模式侧重 "控制访问"(如权限校验),装饰器模式侧重 "增强功能"(如添加日志)。
- 装饰器模式的应用?
-
原理:包装目标对象,动态添加功能,不改变原类结构(如 Java 的
BufferedReader装饰Reader,添加缓冲功能); -
Python 示例:用装饰器为函数添加计时功能(
@timeit)。
- 观察者模式的原理?
-
定义:观察者订阅被观察者,被观察者状态变化时通知所有观察者(如订阅公众号,公众号更新推送给订阅者);
-
Java 实现:
Observable类(被观察者)、Observer接口(观察者),Observable调用notifyObservers()通知观察者。
- 策略模式的应用?
-
原理:定义算法族,封装成独立类,可动态切换(如排序算法族:快速排序 / 归并排序,根据场景切换);
-
优化多条件判断:如
if (type == 1) 快速排序; else if (type == 2) 归并排序;改为策略模式,新增算法无需修改判断逻辑。
- 适配器模式的区别?
-
类适配器:继承目标类和适配者类,缺点:单继承限制;
-
对象适配器:持有适配者对象,组合方式,更灵活(推荐);
-
应用:将旧接口适配新接口(如将
LegacyService适配NewService接口)。
- 建造者模式与工厂模式的区别?
-
建造者模式:侧重复杂对象的分步构建(如
StringBuilder分步 append 构建字符串),关注 "构建过程"; -
工厂模式:侧重对象的创建,不关注构建过程,直接返回成品。
- 项目中常用的设计模式?
-
单例模式:配置类(如
Config.getInstance())、工具类(如Logger); -
工厂模式:对象创建(如
UserFactory.createUser()); -
代理模式:权限校验、日志记录(如 Spring AOP 基于动态代理);
-
装饰器模式:功能增强(如 Spring 的
BeanWrapper); -
策略模式:动态切换算法(如支付方式选择:微信支付 / 支付宝支付)。
五、数据结构与算法
(一)基础数据结构
- 数组和链表的区别?
-
存储:数组连续内存,链表非连续(节点 + 指针);
-
查找:数组 O (1)(随机访问),链表 O (n);
-
插入删除:数组 O (n)(移动元素),链表 O (1)(改指针);
-
适用场景:数组适合高频查询,链表适合高频插入删除。
- 栈和队列的应用?
-
栈:先进后出(FILO),应用:递归调用栈、表达式求值、括号匹配;
-
队列:先进先出(FIFO),应用:任务调度、消息队列、BFS 遍历;
-
实现:栈可用数组 / 链表,队列可用数组(循环队列)/ 链表。
- 哈希表的原理?
-
核心:哈希函数(将键映射到索引)+ 哈希冲突解决(链地址法 / 开放寻址法);
-
哈希函数设计原则:均匀分布(减少冲突)、计算高效;
-
应用:缓存(HashMap)、去重(HashSet)。
- 二叉查找树(BST)的特点?
-
特点:左子树所有节点值 节点,右子树所有节点值 > 根节点;
-
操作:查找 / 插入 / 删除 O (logn)(平衡时),最坏 O (n)(退化为链表);
-
优化:红黑树 / AVL 树(平衡 BST)。
- 红黑树的核心特性?
-
特性:节点非红即黑、根节点黑、叶子节点(NIL)黑、红节点的子节点黑、任意节点到叶子节点的黑路径长度相等;
-
优势:平衡二叉树,查找 / 插入 / 删除 O (logn),旋转次数少(比 AVL 树高效),适合作为 TreeMap/TreeSet 底层。
- 堆的原理与应用?
-
原理:完全二叉树,大顶堆(父节点 ≥ 子节点)、小顶堆(父节点 ≤ 子节点);
-
堆排序步骤:构建堆 → 交换堆顶与堆尾 → 调整堆 → 重复;
-
应用:优先队列(如 Java 的
PriorityQueue)、Top K 问题(用小顶堆找最大 K 个元素)。
- 图的存储与遍历?
-
存储:邻接矩阵(二维数组,适合稠密图)、邻接表(链表数组,适合稀疏图);
-
遍历:DFS(深度优先,递归 / 栈实现,适合路径查找)、BFS(广度优先,队列实现,适合最短路径查找)。
(二)核心算法
- 排序算法对比?
| 算法 | 时间复杂度(平均) | 空间复杂度 | 稳定性 | 特点 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(1) | 稳定 | 简单,适合小规模数据 |
| 选择排序 | O(n²) | O(1) | 不稳定 | 交换次数少 |
| 插入排序 | O(n²) | O(1) | 稳定 | 适合基本有序数据 |
| 快速排序 | O(nlogn) | O(logn) | 不稳定 | 高效,实际应用广泛 |
| 归并排序 | O(nlogn) | O(n) | 稳定 | 适合大规模数据、外部排序 |
| 堆排序 | O(nlogn) | O(1) | 不稳定 | 无需额外空间 |
-
快速排序优化:基准值选中间元素 / 三数取中,避免最坏情况(O (n²));
-
实际应用:Java
Arrays.sort()对基本类型用双轴快排,对对象用归并排序(保证稳定性)。
- 二分查找的实现与注意事项?
-
原理:有序数组中,每次比较中间元素,缩小查找范围;
-
迭代实现:
left=0, right=n-1; while(left { mid=(left+right)/2; if(target==nums[mid]) return mid; else if(target[mid]) right=mid-1; else left=mid+1; }; -
注意:避免
mid溢出(用mid=left+(right-left)/2替代(left+right)/2);重复元素时需调整边界(如找第一个等于 target 的元素)。
- 动态规划(DP)的核心思想?
-
核心:将大问题拆分为小问题,存储小问题答案(避免重复计算),定义状态转移方程;
-
步骤:定义状态 → 确定转移方程 → 初始化 → 计算结果;
-
示例:爬楼梯(状态
dp[i]为第 i 级台阶的走法数,转移方程dp[i] = dp[i-1] + dp[i-2])。
- 贪心算法与动态规划的区别?
-
贪心:局部最优 → 全局最优(如活动选择问题,选结束时间最早的),适用场景有限(需满足贪心选择性质);
-
动态规划:全局最优包含局部最优,需存储中间状态(如最长递增子序列),适用更复杂场景。
- 回溯算法的应用?
-
核心:试探性搜索,不满足条件则回溯(如走迷宫);
-
应用:全排列、组合总和、N 皇后问题、子集问题;
-
实现:递归 + 剪枝(减少无效搜索,如 N 皇后剪枝同行 / 同列 / 对角线的位置)。
- 分治算法的核心思想?
-
核心:分(拆分为子问题)→ 治(解决子问题)→ 合(合并子问题结果);
-
应用:归并排序、快速排序、大数乘法、二分查找。
(三)实战编程题(高频手撕)
-
反转链表(单链表)
// Java 实现
public ListNode reverseList(ListNode head) {
ListNode prev = null, curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
# Python 实现
def reverse_list(head):
prev, curr = None, head
while curr:
next_node = curr.next
curr.next = prev
prev, curr = curr, next_node
return prev
-
两数之和(数组)
public int[] twoSum(int[] nums, int target) {
Map Integer> map = new HashMap<>();
for (int i = 0; i ++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[]{map.get(complement), i};
}
map.put(nums[i], i);
}
return new int[]{};
}
-
二分查找(有序数组)
def binary_search(nums, target):
left, right = 0, len(nums)-1
while left = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] left = mid + 1
else:
right = mid - 1
return -1
-
二叉树层序遍历(BFS)
public List<List(TreeNode root) {
List<List res = new ArrayList
if (root == null) return res;
Queue queue = new LinkedList
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List new ArrayList for (int i = 0; i i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
res.add(level);
}
return res;
}
-
全排列(回溯)
def permute(nums):
res = []
def backtrack(path, used):
if len(path) == len(nums):
res.append(path.copy())
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
path.append(nums[i])
backtrack(path, used)
path.pop()
used[i] = False
backtrack([], [False]*len(nums))
return res
-
LRU 缓存实现(哈希表 + 双向链表)
class LRUCache {
class Node {
int key, val;
Node prev, next;
Node(int k, int v) { key = k; val = v; }
}
private Map map;
private Node head, tail;
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
public int get(int key) {
if (!map.containsKey(key)) return -1;
Node node = map.get(key);
remove(node);
addToHead(node);
return node.val;
}
public void put(int key, int value) {
if (map.containsKey(key)) {
remove(map.get(key));
}
Node node = new Node(key, value);
map.put(key, node);
addToHead(node);
if (map.size() > capacity) {
Node tailNode = tail.prev;
remove(tailNode);
map.remove(tailNode.key);
}
}
private void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void addToHead(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
}