文章目录
- 引言
- 正文
- 面经整理一
- 1、讲一下java的多态,重载,重写的概念,区别
- 2、说一下Java的数组,链表的结构,优缺点
- 3、创建java线程的方式有哪些,具体说说
- 4、创建线程池呢、每个参数的意义
- 5、通过那几种方式保证线程安全
- 6、jvm加载
- 7、了解数据库事物?特性?
- 8、怎么提高sql查询性能 (只说了添加索引,分库分表)
- 9、String特性是什么
- 10、redis数据类型
- 11、redis持久化 答了(AOF,RDB)接着问了他们之间的区别
- 面经整理二
- 1、spring是怎么实现aop的
- 2、aop的原理
- 3、用户通过客户端或者浏览器发送请求,在springboot框架下是如何接收这个请求、解析并向下透传到control层的
- 4、mysql的底层结构
- 5、索引分为哪几个类型
- 6、以非聚簇索引作为一个查询条件,去获取到整条记录的中间过程是什么
- 7、建索引要满足哪些原则
- 8、hashmap扩容的过程
- 9、synchronized和可重入锁有哪些区别
- 10、线程安全的集合concurrenthashmap是如何保证线程安全的
- 11、java的内存分布
- 12、1.8的jvm默认的两个垃圾回收器是哪两个,区别是什么
- 13、有一个web服务的接口,刚上线的时候没有什么问题,突然一段时间后客户端调用该接口,该接口的反馈都是超时,如何定位这个服务的问题
- 正式面试
- 第一面
- A面
- B面
- ThreadLocal了解吗?如何防止内存泄漏?
- 如何对复杂对象进行深拷贝?
- SpringAOP的实现过程
- 笔试
- 总结
引言
- 今天要面试万得数据,上午在整理项目相关的问题,看相关的代码,现在整理一下面经,毕竟下午两点钟就要开始的!
正文
面经整理一
作者:网抑云八级选手
链接:https://www.nowcoder.com/feed/main/detail/a44af7a275754bc0a200fcc9778d64c0?sourceSSR=search
来源:牛客网
1、讲一下java的多态,重载,重写的概念,区别
多态
- 同一个接口或者基类可以指向不同的实现或子类对象,编写更加通用和灵活的代码
- 编译时多态(静态多态):通过方法重载实现。
- 运行时多态(动态多态):通过方法重写和接口实现。
重载
- 重载是指在同一个类中,可以有多个同名的方法,但它们的参数列表不同(参数的数量或类型不同)。重载是编译时多态的一种形式。
- 主要特点
- 方法名相同。
- 参数列表不同(参数的数量或类型不同)。
- 可以有不同的返回类型。
- 可以有不同的访问修饰符。
重写
- 重写是指子类提供了与父类中同名方法的实现。
- 重写是运行时多态的一种形式。通过重写,子类可以提供特定的实现,而不是使用父类的实现。
- 主要特点
- 方法名、参数列表、返回类型必须相同。
- 访问修饰符不能比父类的更严格(可以更宽松)。
- 子类方法不能比父类方法抛出更多或更广泛的异常。
区别
- 多态:同一个接口或者基类可以多种实现
- 重载:在同一个类中,同名方法有不同的参数列表,是编译时多态
- 重写:子类提供与父类同名方法的实现,运行时多态
2、说一下Java的数组,链表的结构,优缺点
数组
- 一种线性数据结构,用于存储相同类型元素的集合。
- 数组在内存中是连续分配的,所有元素的大小相同,可以通过索引快速访问。
- 优点
- 快速访问:由于数组元素在内存中是连续存储的,可以通过索引在O(1)时间复杂度内直接访问任意元素。
- 节省内存:没有额外的存储开销(如指针)。
- 缓存友好:由于数组元素连续存储,使用缓存时效率更高。
- 缺点
- 固定大小:数组的大小在创建时必须确定,之后不能改变。这可能导致内存浪费或不够用。
- 插入和删除效率低:在数组中插入或删除元素需要移动大量元素,时间复杂度为O(n)。
- 不适合频繁插入和删除操作:由于上述原因,数组不适合在中间频繁进行插入和删除操作。
链表
-
一种线性数据结构,由一系列节点(Node)组成,每个节点包含数据和指向下一个节点的引用(或指针)
-
根据节点之间的连接方式,链表可以分为单向链表(Singly Linked List)和双向链表(Doubly Linked List)
-
优点:
- 动态大小:链表可以根据需要动态增长或缩小,不需要预先分配固定大小。
- 插入和删除操作高效:在链表中插入或删除元素不需要移动其他元素,只需修改指针即可,时间复杂度为O(1)(在已知位置的情况下)。
- 适用于频繁插入和删除操作:尤其是在中间插入或删除时,链表比数组更高效。
-
缺点
- 访问速度慢:由于无法通过索引直接访问元素,必须从头节点开始逐个遍历,时间复杂度为O(n)。
- 额外的内存开销:每个节点除了存储数据,还需要存储指针,这会占用额外的内存。
- 缓存不友好:由于链表元素在内存中不是连续存储的,使用缓存时效率较低。
3、创建java线程的方式有哪些,具体说说
继承Thread类
- 结构
- 通过继承Thread类并重写run方法来创建线程。
- 步骤
- 创建一个继承自Thread类的子类。
- 在子类中重写run方法,将线程的执行逻辑写在run方法中。
- 创建该子类的实例,并调用start方法启动线程。
实现Runnable接口
-
结构
- 通过实现Runnable接口并将其传递给Thread类来创建线程。
-
步骤
- 创建一个实现Runnable接口的类。
- 在类中实现run方法,将线程的执行逻辑写在run方法中。
- 创建该类的实例,并将其作为参数传递给Thread对象。
- 调用Thread对象的start方法启动线程。
。。。
总结
- 继承Thread类:简单直接,但不支持多继承。
- 实现Runnable接口:推荐使用,符合Java单继承多实现的特性。
- 使用Callable和Future:适用于需要返回结果或抛出异常的任务。
- 使用ExecutorService框架:适用于需要高效管理线程池的场景。
4、创建线程池呢、每个参数的意义
5、通过那几种方式保证线程安全
6、jvm加载
7、了解数据库事物?特性?
8、怎么提高sql查询性能 (只说了添加索引,分库分表)
-
优化查询语句
- 避免SELECT *,尽量只查询需要的列
- 使用WHERE条件筛选数据,减少返回的数据量
- 避免在WHERE子句中使用函数,使用函数会导致无法使用索引,从而降低查询性能。
-
索引优化
- 创建合适的索引
- 使用覆盖索引
-
数据库分区
- 使用分区表:将大表按照某个字段(如日期、ID范围)进行分区,减少单个分区的数据量,提高查询性能。
-
数据库配置优化
- 调整数据库配置:根据实际需求调整数据库的内存分配、缓存大小、连接数等配置参数,提高整体性能。
9、String特性是什么
不可变性(Immutability)
- 在Java中,String对象是不可变的。一旦创建,字符串的值就不能更改。如果需要修改字符串,则会创建一个新的字符串对象。
字符串池(String Pool)
- Java使用字符串池来优化字符串的存储和管理。当你创建一个字符串字面量时,JVM会先检查字符串池中是否存在相同的字符串。如果存在,则返回对该字符串的引用;如果不存在,则在池中创建新的字符串。
字符串连接(Concatenation):
- Java支持使用+操作符进行字符串连接,也可以使用StringBuilder或StringBuffer来提高性能。StringBuilder和StringBuffer是可变的,并且提供了对字符串的高效修改操作。
10、redis数据类型
11、redis持久化 答了(AOF,RDB)接着问了他们之间的区别
面经整理二
作者:MO小天才
链接:https://www.nowcoder.com/feed/main/detail/363997511497459ea21b12aa686439d7?sourceSSR=search
来源:牛客网
1、spring是怎么实现aop的
-
一种用于分离关注点的技术,特别是在横切关注点(如日志记录、事务管理、权限控制等)方面。Spring AOP的实现主要基于代理模式。
-
切面(Aspect):
- 切面是横切关注点的模块化表现,是通知(advice)和切入点(pointcut)的结合。
-
通知(Advice):
- 通知是在切入点上执行的操作。常见的通知类型包括前置通知(before)、后置通知(after)、返回通知(after returning)、异常通知(after throwing)和环绕通知(around)。
-
切入点(Pointcut):
- 切入点定义了通知应当应用到哪些连接点(Join Points)。通常使用AspectJ表达式语言来定义切入点。
-
目标对象(Target Object):
- 目标对象是被通知的对象,即那些包含核心业务逻辑的方法被AOP框架拦截的对象。
-
代理(Proxy):
- 代理是AOP框架创建的对象,它被用来代替目标对象。代理对象负责拦截方法调用,并在适当的时间执行通知。
-
织入(Weaving):
- 织入是将切面应用到目标对象并创建代理对象的过程。Spring AOP在运行时通过动态代理或CGLIB代理进行织入。
2、aop的原理
- Spring AOP通过动态代理(JDK动态代理和CGLIB代理)实现了AOP的功能,使得我们可以在不修改业务代码的情况下将横切关注点(如日志记录、事务管理等)应用到目标对象的方法上。
3、用户通过客户端或者浏览器发送请求,在springboot框架下是如何接收这个请求、解析并向下透传到control层的
1、接收请求
- 当用户发送HTTP请求时,嵌入式服务器会受到请求
2、请求解析
- 将请求传递给Servlet容器进行解析,变成HttpServletRequest和HttpServletResponse对象
3、拦截请求
- DispatcherServlet,它是Spring MVC的前端控制器,负责处理所有进来的HTTP请求。
- 映射到所有路径(/),因此它会拦截所有请求。
4、映射控制器
- 会根据请求的URL和HTTP方法(GET、POST等),通过HandlerMapping找到相应的处理器
- HandlerMapping的工作是将请求映射到具体的控制器类和方法上。
5、调用控制器方法
- DispatcherServlet会使用HandlerAdapter调用控制器方法。
- 控制器方法通常使用@RequestMapping或其他映射注解(如@GetMapping、@PostMapping等)来指定URL路径和请求方法。
6、处理请求并返回响应对象
- 控制器方法处理请求,并返回一个响应对象,一般是调用service层的处理业务逻辑,并将结果返回
7、返回给客户端
- 响应返回给Servlet容器,Servlet容器将响应发送回客户端
4、mysql的底层结构
5、索引分为哪几个类型
6、以非聚簇索引作为一个查询条件,去获取到整条记录的中间过程是什么
1. 在非聚簇索引中查找键值
- 查找索引键:、
- 数据库引擎在非聚簇索引的 B+ 树中查找符合查询条件的索引键。
- 获取聚簇索引键:
- 找到匹配的索引键后,获取指向实际数据记录的指针或聚簇索引键值。
2. 在聚簇索引中查找完整记录
- 查找数据记录:
- 使用从非聚簇索引中获取的聚簇索引键值,在聚簇索引的 B+ 树中查找完整的数据记录。
- 返回完整记录:
- 找到对应的叶节点后,提取并返回完整的数据记录。
7、建索引要满足哪些原则
8、hashmap扩容的过程
触发条件
- 容量(capacity):HashMap桶(bucket)数组的当前大小。
- 负载因子(load factor):当HashMap的元素数量超过capacity * load factor时触发扩容。默认负载因子为0.75。
扩容过程
-
a. 计算新容量
- HashMap扩容时,新容量通常是旧容量的两倍。扩容后,新容量和新阈值会重新计算。
-
b. 创建新桶数组
- 创建一个大小为新容量的新的桶数组
-
c.重新分配元素
- 将旧桶数组中的所有元素重新计算哈希并分配到新桶数组中。由于容量的变化,元素的位置可能会发生变化。
- 链表节点重新分配:旧桶数组中存在链表时,需要对链表节点重新分配。根据节点的哈希值决定其在新桶数组中的位置。如果节点的哈希值与旧容量进行按位与运算结果为0,则其放置在新桶中的原位置,否则放置在原位置加上旧容量的位置。
-
更新HashMap的引用和阈值
- 将HashMap的桶数组引用更新为新的桶数组,并更新阈值。
9、synchronized和可重入锁有哪些区别
10、线程安全的集合concurrenthashmap是如何保证线程安全的
- 分段锁:
- 减少锁争用,提高并发性能。
- CAS 操作和自旋锁:
- 保证原子性和线程安全。
- 细粒度锁:
- 对每个桶进行锁定,允许更多并发操作。
- 树化和链化:
- 提高哈希冲突处理效率,每一个桶的链表长度超过阈值,转为红黑树,提高查找和出入效率
- 无锁读操作:
- 提高并发读性能。
- 高效扩容:
- 分批次进行扩容,减少性能影响。
11、java的内存分布
- 堆(Heap):存储所有对象实例和数组,分为年轻代和老年代。
- 栈(Stack):每个线程都有自己的栈,存储局部变量、方法调用等信息。
- 方法区(Method Area):存储类的元数据、静态变量、常量池等。在Java 8之后称为元空间(Metaspace)。
- 程序计数器(PC Register):指示当前线程执行的字节码位置。
- 本地方法栈(Native Method Stack):为虚拟机执行本地方法服务。
12、1.8的jvm默认的两个垃圾回收器是哪两个,区别是什么
-
1. Parallel GC(并行垃圾回收器)
- 特点
- 年轻代收集:使用多线程的复制算法(copying)。
- 老年代收集:使用多线程的标记-压缩算法(mark-compact)。
- 多线程:使用多个线程同时进行垃圾回收操作,适用于多核 CPU,提高垃圾回收效率。
- 吞吐量优先:Parallel GC 设计的目标是最大化吞吐量,最小化垃圾回收对应用程序的影响时间。
- 特点
-
2. G1 GC(Garbage-First Garbage Collector)
- 特点
- 分区:将整个堆内存划分为多个相等大小的独立区域(Region),每个区域可以充当 Eden、Survivor 或 Old 区。
- 混合收集:年轻代和老年代一起进行收集(mixed collection),在一次垃圾回收过程中,既回收年轻代,又回收部分老年代。
- 并发标记:G1 GC 使用并发标记阶段来标记存活对象,从而减少应用程序暂停时间。
- 停顿时间目标:允许用户指定期望的停顿时间,通过调优停顿时间目标来满足应用程序的低延迟要求。
- 特点
13、有一个web服务的接口,刚上线的时候没有什么问题,突然一段时间后客户端调用该接口,该接口的反馈都是超时,如何定位这个服务的问题
- 1. 检查服务器资源
- 2. 检查应用日志
- 3. 检查数据库性能
- 慢查询
- 连接池耗尽
- 锁争用
- 4、检查外部依赖是否正常
- 缓存服务器
- 消息队列
- 5、分析网络问题
- 网络延迟
- 防火墙和负载均衡器
- 6、重现问题
- 尝试在本地或者测试环境中,重现则个问题,以便更容易调试和分析
正式面试
第一面
A面
-
1、重点介绍一下个人项目
- 说的过于琐碎,没有必要,需要概括一下,重点说出你的要点,而不是很细致地完全讲出来,没有必要!
- 重点介绍Java相关的项目
-
2、浮点数了解吗?怎么实现的
- 浮点数的实现主要是基于科学计数法,有以下四部分构成,分别是
- 符号位:0正数,1负数
- 基数:使用二进制表示
- 指数:表示基数应该乘以多少次幂,可以是正数或者负数
- 尾数:有效位数
- 特殊
- 不能表示所有数字,涉及小数比较时,需要处理小数误差
- 正零和负零
- 无穷大:超出表示范围的数字
- NaN:未定义或者不确定地结果
- 浮点数的实现主要是基于科学计数法,有以下四部分构成,分别是
-
3、给你一个数组,数组里面全部是数字,然后再给你一个s表示窗口的大小,从最左侧滑动,然后滑动到最右侧,求取每一次移动的滑动窗口平均值,你计算一下滑动窗口的最小值?并计算一下的时间复杂度和空间复杂度
- 想不出更好的做法,只能使用模拟的方式实现,维护滑动窗口的sum,然后每一次计算一次平均值,比较平均值的最最优值。
- 时间复杂度是O(n),空间复杂度是O(1).
B面
-
1、简单的自我介绍
- 项目介绍太过繁琐,很多细节不需要,只需要说你做了什么项目,大概概括一下!项目就说做了什么,然后你有哪些工作,然后总结一下,没有必要面面俱到,什么都说。
-
项目上线了吗?
- 并没有上线,在等进一步审核。
-
系统的准确率是多少
- 分情况说,比较真实。
-
针对这种情况有什么其他的优化方案吗?
- 拓宽信息模态
- 更换信息收集场景
-
这个项目是基于大语言模型的,谁在维护和训练?
- 使用开源大语言模型的API直接调用的,并没有的自己训练一个大语言模型
-
说一下ThreadLocal的使用?怎么防止内存泄露
- ThreadLocal是的key是弱引用,但是他的value会强引用,通过调用对应的set等方法会自动回收不再使用value,但是线程不再被使用,不再调用对应方法就不会进行内存回收,导致内存泄漏
- 如何避免内存泄漏
- 先获取到 ThreadLocalMap 这个引用的,并且调用了它的 remove 方法,手动调用remove方法,防止内存泄漏
-
MySQL中聚簇索引和非聚簇索引有什么区别
-
聚簇索引
- 聚簇索引决定了磁盘上的存储顺序,同时聚簇索引的叶子节点包含了完整的数据行,查询聚簇索引无需额外查询数据
- 主键默认创建为聚簇索引,如果没有指定主键,会选择一个唯一非空索引作为聚簇缩影。
-
非聚簇索引
- 非聚簇索引是单独的索引结构,叶子节点不包含数据行,而是包含了指向数据行的指针
- 非聚簇索引和数据的存储顺序无关,非聚簇索引的叶子节存储的是数据行的位置或者主键位置,通常需要二次查找。
-
-
建表的时候,没有指定任何列作为他的主键,会创建的主键索引吗?
- 首先,检查表中是否存在唯一非空的索引,有就将其作为主键索引
- 然后,如果表中没有主键,没有唯一索引,自动生成一个6字节长的隐藏主键列,不可见无法访问
-
InnoDB引擎默认的隔离级别是什么?这个隔离级别会带来问题?做了哪些操作来尽量缓解这个问题?
- 可重复读,解决了脏读和不可重读,但是还是存在部分幻读
- 如何解决幻读的问题
- 提供了两种查询方式的,分别是快照读和当前读,都会产生幻读
- 针对快照读,使用MVCC来解决幻读问题,第一次查询生成一个readview,然后在当前事务中都使用相同的readview,保证结果相同。
- 针对当前读,依靠行级锁中的间隙锁来实现
- 但并不能完全避免,一般是没有加上对应的for update
-
SpringAOP的实现过程
- 用于在不修改代码的情况下,动态地将某些行为应用到特定的方法或者类上。主要是基于动态代理和字节码操作实现。
- 定义一个切面类,这个切面类中要指明增强的方式是@Before还是@AfterReturning等,同时指定对应地切点,然后定义具体的需要额外执行的逻辑和行为。
- 主要是定义通知类和配置类,具体编程实现如下
- 第二阶段会有一个算法题,HR会通知你的。
ThreadLocal了解吗?如何防止内存泄漏?
资料来源
ThreadLocal介绍
- 应用场景
- ThreadLocal用作保存每一个线程独享的对象,每一个线程都创建一个副本,这样每一个线程都可以修改自己所拥有的副本,不会影响其他线程的副本,确保线程安全
- 保存线程不安全的工具类,典型需要使用的类是SimpleDataFormat
- ThreadLocal 用作每一个线程内需要独立保存信息,以便供线程内其他方法更方便地获取该信息场景。线程内每个方法获取到的信息是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal获取,避免传参,类似线程内的全局变量。
- 保存业务的一些内容,比如说在拦截器中获取用户的信息,可以让不同方法直接使用,避免了传递参数麻烦
- ThreadLocal用作保存每一个线程独享的对象,每一个线程都创建一个副本,这样每一个线程都可以修改自己所拥有的副本,不会影响其他线程的副本,确保线程安全
ThreadLocal可以用来解决共享资源的多线程访问的问题吗?
- 不是,ThreadLocal 并不是用来解决共享资源问题的。虽然 ThreadLocal 确实可以用于解决多线程情况下的线程安全问题,但其资源并不是共享的,而是每个线程独享的。
ThreadLocal和Synchronized关系?
- ThreadLocal 是通过让每个线程独享自己的副本,避免了资源的竞争。
- synchronized 主要用于临界资源的分配,在同一时刻限制最多只有一个线程能访问该资源。
ThreadLocal、Thread和ThreadLocalMap的关系
- 一个Thread有一个ThreadLocalMap,而ThreadLocalMap的key就是一个一个ThreadLocal
防止内存泄漏
- 内存泄漏
- 当某一个对象不在有用时,占用的内存却不能被回收,是内存泄漏
- 引用分析
- ThreadLocal中的key是弱引用,如果不在引用了,垃圾回收机制能够成功收回
- ThreadLocal中value是强引用,如果某一个Thread长时间运行,并且某一个Thread不再被使用,那么这个value就会内存泄漏,没法被垃圾回收
- set、remove和rehash都会刷新key-value,如果key为null,会自动回收对应的value,但是如果不调用就没有办法刷新了
- 如何避免内存泄漏
- 先获取到 ThreadLocalMap 这个引用的,并且调用了它的 remove 方法,手动调用remove方法,防止内存泄漏
如何对复杂对象进行深拷贝?
- 实现 Cloneable 接口并重写 clone() 方法
- 通过实现 Cloneable 接口并重写 clone() 方法,可以对对象进行深拷贝
- 使用序列化进行深拷贝
- 通过将对象序列化为字节流然后反序列化,可以实现深拷贝。
- 递归实现深拷贝
- 通过自定义的递归方法,我们可以实现对复杂对象的深拷贝。此方法适用于对象嵌套结构较为复杂的情况。
SpringAOP的实现过程
-
一种用于分离关注点的技术,特别是在横切关注点(如日志记录、事务管理、权限控制等)方面。Spring AOP的实现主要基于代理模式。
-
切面(Aspect):
- 切面是横切关注点的模块化表现,是通知(advice)和切入点(pointcut)的结合。
-
通知(Advice):
- 通知是在切入点上执行的操作。常见的通知类型包括前置通知(before)、后置通知(after)、返回通知(after returning)、异常通知(after throwing)和环绕通知(around)。
-
切入点(Pointcut):
- 切入点定义了通知应当应用到哪些连接点(Join Points)。通常使用AspectJ表达式语言来定义切入点。
-
目标对象(Target Object):
- 目标对象是被通知的对象,即那些包含核心业务逻辑的方法被AOP框架拦截的对象。
-
代理(Proxy):
- 代理是AOP框架创建的对象,它被用来代替目标对象。代理对象负责拦截方法调用,并在适当的时间执行通知。
-
织入(Weaving):
- 织入是将切面应用到目标对象并创建代理对象的过程。Spring AOP在运行时通过动态代理或CGLIB代理进行织入。
-
Spring AOP通过动态代理(JDK动态代理和CGLIB代理)实现了AOP的功能,使得我们可以在不修改业务代码的情况下将横切关注点(如日志记录、事务管理等)应用到目标对象的方法上。
笔试
对于必胜的理解
- 对于序列a而言,其中的元素[a1,a2,a3,a4,a5]等,要是必赢的情况下,随便取出来一个数字ax,使得剩下元素的最大值amax和amin的累加和 大于等于 target,但是最大值不能大于等于target,只要能够是这种情况,那么先手肯定是必胜的。
- 如果是这种只能够报数三次的情况,肯定能够判定出来的。当前方选什么才能让后方必输
- 但是如果当前报数并不能立刻判定出能否获胜的话,就不好判定了!
A和B都会按照最优的方式报数
- 如果轮到A,只要A能够赢,或者说存在必赢的方案,一定会选择必赢的方案
- 如果轮到B,只要B能够赢,或者说存在必赢的方案,一定会选择必赢的方案
- 言外之意,就是A每一次做出决定之前,都要判定会不会给B带来必赢的局面,如果能够给B带来必赢的局面,就不选当前方案
能否通过动态规划实现
- 如果使用动态规划实现,必须能够使用集合的语言进行表达,但是这个题目不知道怎么用集合的语言表达。
- 我应该还是会使用回溯实现!
回溯条件
- 如果无论A选什么,B都存在必胜的条件,那么就返回false
- 如果无论A选了某个分支,B无论选了什么,那么就返回true
具体实现代码
import java.io.FilterOutputStream;
import java.util.*;
/**
* @author Long Guo
* @create 2024-08-10-14:32
*///TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
// 定义不同的状态表示当前是谁在报数
static boolean A = true;
static boolean B = false;
static List<Integer> list; // 能够报的数字的列表
static Map<Integer, Boolean> cached;
static boolean dfs(int target, boolean AorB, int cacheNum) {
/*
* 具体含义:当前做选择的游戏玩家是否存在胜利的情况,存在返回true,不存在返回false
* list表示剩余列表中所有能够选择的数字
* target表示目标数值
* AorB表示对应的角色,true表示为A先手,false表示B,后手
* 基本思路:
* 在所有搜索树中,总共有两种选择关系,分别是的由A到B和由B到A
* A选B:一旦所有B的情况中,有一种是B必胜的情况,那么就不能选,因为B一定会选必胜的情况,所以这里需要的将所有B可选的情况通过与&进行连接
* B选A:一旦所有的A的情况中,有一种的A必胜的情况,B就不会选,所以这里要选择全部是false的情况,也就是通过或进行连接,全部为false的情况
* 最终通过回溯,判定当前节点下是否存在A必胜的情况
*/
// 临界条件,必赢状况,这里需要特殊处理,如果是B的回合,那就是A必输的情况
int m = list.size();
if (list.get(m - 1) >= target) return true;
// 分情况遍历所有可能地选择
boolean flag;
for (int i = 0; i < m; i++) {
int curNum = cacheNum | (1 << list.get(i));
if (cached.containsKey(curNum)) flag = cached.get(curNum);
else {
int tempNum = list.get(i);
int tempTar = target - tempNum;
list.remove(i);
flag = dfs(tempTar, !AorB, curNum);
cached.put(curNum, flag);
list.add(i, tempNum);
}
if (!flag) return true;
}
return false;
}
static boolean isAlwaysWin(int N, int Target) {
for (int i = 0; i < N; i++) {
int cacheNum = 0 | (1 << i + 1);
int tempNum = list.get(i);
int tempTar = Target - tempNum;
list.remove(i);
if (!dfs(tempTar, B, cacheNum)) return true;
list.add(i, tempNum);
cached.put(cacheNum, false);
}
return false;
}
//
public static void main(String[] args) {
// 处理输入输出
// Scanner in = new Scanner(System.in);
// String nInput = in.nextLine();
// String targetInput = in.nextLine();
// int n = Integer.parseInt(nInput.replaceAll("[^0-9]]",""));
// int target = Integer.parseInt(targetInput.replaceAll("[^0-9]]",""));
int n = 5;
int target = 8;
// 生成对应的可报数的列表
list = new ArrayList<>();
cached = new HashMap<>();
for (int i = 1; i <= n; i++) list.add(i);
// 这两种是一个特殊情况,刚好是需要顺次报完所有的数字的,偶数的话后手必胜,奇数的话A必胜
if (n % 2 == 0 && target == (n + 1) * n / 2) System.out.println(false);
else if (n % 2 == 1 && target == (n + 1) * n / 2) System.out.println(true);
else
// 正常需要迭代判断的情况
System.out.println(isAlwaysWin(n, target));
//
// System.out.println(isAlwaysWinSingle(list,n,target));
}
}
总结
- 过不过再说哈,现在先写在这里!
- 第一面还行,直接让我过了,说过两天给我发一道算法题,然后让我好好做一下!继续在准备,晚上去运动一下,然后接着看项目!加油!
8/11 - 今天晚上完成了wind数据的笔试,差不多花了四五个小时,然后想清楚了,也写完了。感觉还行,还能继续面试!