文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备免费赠送 :《尼恩技术圣经+高并发系列PDF》,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》面试必备 + 大厂必备 +涨薪必备 加尼恩免费领免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》面试必备 + 大厂必备 +涨薪必备 加尼恩免费领免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
(资料图片仅供参考)
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
在40岁老架构师尼恩的(50+)读者社群中,经常有小伙伴需要面试饿了么、 头条、美团、阿里、京东等大厂。有很多的小伙伴,完成了人生的逆袭,拿到了高端的offer。
最近一个6年经验的小伙伴,年薪拿到 60W, 非常牛掰。
下面是一个小伙伴成功拿到饿了么 高级 Java 的offer,其面试经历,还是两个字:
下面,从小伙的面试正题看看,收个饿了么Offer需要学点啥?
下面的这些面试题,对于面试其他的 高级java岗位,也很有参考意义。
这里也把题目以及参考答案,收入咱们的《尼恩Java面试宝典》 V71,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请到公众号 【技术自由圈】获取
数据库事务的隔离级别是指在并发访问数据库时,各个事务之间隔离程度的不同。常见的隔离级别有以下四种:
读未提交(Read Uncommitted):这是最低的隔离级别,一个事务可以读取另一个未提交事务的数据,可能会导致脏读、不可重复读和幻读问题。
适用于读多写少的场景,可以提高并发性能。但是,如果一个事务读取了未提交的数据,其他事务可能会受到影响,因此需要谨慎使用。
读已提交(Read Committed):这是一种较高的隔离级别,一个事务只能读取另一个已提交事务的数据,可以避免脏读问题,但是仍可能出现不可重复读和幻读问题。
适用于读多写少的场景,可以保证数据的一致性,但可能会降低并发性能。
可重复读(Repeatable Read):这是一种更高的隔离级别,一个事务在执行过程中,多次读取同一数据会得到相同结果,可以避免脏读和不可重复读问题,但是仍可能出现幻读问题。
适用于需要保证数据一致性的场景,如银行交易、订单处理等。但是,由于需要在事务执行期间锁定数据,可能会降低并发性能。
串行化(Serializable):最高的隔离级别,所有事务串行执行,可以避免脏读、不可重复读和幻读问题,但是对性能有较大影响。
适用于对数据一致性要求非常高的场景,如金融交易、医疗诊断等。但是,由于串行执行,可能会降低并发性能。
在实际开发中,根据具体的业务需求和性能要求,可以选择不同的隔离级别来平衡数据一致性和并发性能。
隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交 | 最低级别,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 |
读已提交 | 语句级 | 否 | 是 | 是 |
可重复读 | 事务级 | 否 | 否 | 是 |
串行化 | 最高级别,事务级 | 否 | 否 | 否 |
表中列出了四种常见的数据库事务隔离级别,以及它们对于脏读、不可重复读和幻读的处理情况。其中,脏读指的是一个事务读取到了另一个事务尚未提交的数据;不可重复读指的是一个事务多次读取同一数据,但是由于其他事务的修改,每次读取的结果都不同;幻读指的是一个事务多次读取同一范围的数据,但是由于其他事务的插入或删除,每次读取的结果都不同。
事务是指作为单个逻辑工作单元执行的一系列操作,要么全部执行,要么全部不执行。
事务具有四个关键特性,即ACID:
原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚,不允许只执行其中的一部分操作。
一致性(Consistency):事务执行前后,数据库的状态必须保持一致,即满足所有的约束条件。
隔离性(Isolation):事务之间是相互隔离的,一个事务的执行不应该影响其他事务的执行。每个事务都应该认为它是唯一在执行的事务,每个事务都应该感觉不到其他事务的存在。
持久性(Durability):事务一旦提交,对数据库中的数据修改就是永久性的,即使系统崩溃也不会丢失。
事务的实现需要数据库管理系统支持,通常通过日志记录和锁机制来实现。
日志记录:在事务执行过程中,数据库管理系统会将所有的操作记录在日志中,如果事务执行失败,可以通过日志进行回滚,保证数据的一致性。
锁机制:为了保证事务之间的隔离性,数据库管理系统会使用锁机制,对事务进行隔离。当一个事务对某个数据进行修改时,会对该数据进行加锁,其他事务需要等待该事务释放锁后才能对该数据进行修改。
Redis可以通过发布订阅(Pub/Sub)模式来实现消息的发布和订阅。
Redis是使用C实现的,可以通过分析Redis源码里的pubsub.c文件,了解发布和订阅机制的底层实现
Redis通过PUBLISH,SUBSCRIBE和PSUBSCRIBE等命令实现发布和订阅功能
通过SUBSCRIBE命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个频道,字典的值则是一个链表,链表中保存了所有订阅这个频道的客户端。SUBSCRIBE命令的关键,就是将客户端添加到给定频道的订阅链表中。
通过PUBLISH命令向订阅者发送消息,redis-server会使用给定频道作为键,在它维护的频道字典中查找记录了订阅这个频道的所有客户端的链表,将消息发布给所有订阅者
Pub和Sub从字面上理解就是发布(Publish)和订阅(Subscribe),在redis中,可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的信息,这一功能最明显的用法就是实时消息系统,比如普通的即时聊天,群聊等功能。
1. 创建订阅者集合
首先,需要在Redis中创建一个订阅者集合,用于存储所有订阅者的相关信息。可以使用Redis中的SET命令创建一个集合,其中键为订阅者的名字,值为该订阅者的ID。
2. 发布消息
然后,使用Redis的PUBLISH命令向指定的主题发布一条消息。主题是一个字符串,可以是任意名称,用于标识要发布的消息。可以使用Redis的JSON格式来表示消息内容,例如:
PUBLISH topic "Hello World"
3. 订阅消息
接下来,订阅者可以使用Redis的SUBSCRIBE命令订阅指定的主题。同样,主题也是一个字符串,可以是任意名称。订阅后,Redis会返回一个包含当前订阅者集合信息的响应。可以使用Redis的PSUBSCRIBE命令来订阅多个主题,例如:
PSUBSCRIBE "topic1", "topic2"
4. 处理消息
当有消息发布到指定的主题时,Redis会自动将消息发送给所有已订阅该主题的订阅者。订阅者可以使用Redis的LPUSH、RPUSH等命令来接收并处理消息,例如:
LPUSH "my-subscriber-channel" "{"message": "Hello World"}"
以上代码将消息发布到名为"my-subscriber-channel"的频道中,并传递了一个JSON格式的消息对象。其他订阅者可以使用相同的方式接收并处理该消息。
更多详细内容,请 参考 尼恩《Java高并发核心编程 卷1 加强版:NIO、Netty、Redis、ZooKeeper》,书里做了 详细的介绍,非常细致
Java中的程序计数器(Program Counter Register)是一块内存区域,用于存储当前线程正在执行的字节码指令地址。Java虚拟机之所以要在内存结构中设计自己的程序计数器,而不使用内核的程序计数器,主要有以下原因:
另外,Java中的程序计数器还有以下优点:
综上所述,Java使用自己的程序计数器是为了支持多线程并发执行,并且通过内存结构来进行管理,以提高程序的稳定性和可靠性。Java虚拟机需要在内存结构中设计自己的程序计数器,以实现跨平台性、线程私有性和快速访问。
分布式事务是指在分布式系统中,多个事务操作涉及到多个数据库或资源,需要保证这些事务操作要么全部成功,要么全部失败。2PC(Two-Phase Commit)是一种分布式事务协议,用于协调分布式事务的提交和回滚。其过程主要分为两个阶段:
在这个阶段,协调者(Coordinator)向所有参与者(Participant)发送“准备”请求,询问它们是否可以执行事务,并将其执行结果保存在日志中。参与者执行事务,并将执行结果反馈给协调者。如果所有参与者都可以执行事务,则协调者发送“提交”请求,否则发送“回滚”请求。
在这个阶段,如果协调者发送的是“提交”请求,则所有参与者执行事务,并将执行结果提交。如果协调者发送的是“回滚”请求,则所有参与者撤销事务,并将执行结果回滚。最后,协调者向所有参与者发送“完成”请求,表示事务已经完成。
在2PC的过程中,协调者是必须是强一致性的,即它需要对所有参与者的数据进行一致性检查,以确保所有参与者的数据都能正确地被提交或回滚。
2PC协议的优点是可以保证事务的原子性和一致性,即要么全部提交,要么全部回滚。
它也存在一些缺点,如:
因此,在实际应用中,需要根据具体业务场景选择合适的分布式事务方案,如TCC、Saga等。
Redis之所以能够高效地处理请求,主要是因为它采用了以下几种优化措施:
综合上述优化措施,使得Redis能够在单线程的情况下,处理大量的请求,并且保持高效的性能。
NIO(Non-blocking I/O)是Java提供的一种新的I/O模型,它支持非阻塞式的、基于事件驱动的I/O操作。相比于传统的阻塞式I/O模型,NIO能够更好地处理高并发的网络请求,提高系统的吞吐量和响应速度。
NIO 的实现主要依赖于两个类:Channel
和 Buffer
。
Channel
表示一个连接到某个端口的实体,它可以与另一个 Channel 或服务端通信;Buffer
则表示一种数据结构,用于存储读入的数据,并提供了一些方法来处理这些数据。NIO 通过Selector
(选择器)来实现事件驱动。它可以同时监听多个 Channel
的状态变化,并在有数据可读或可写时通知应用程序进行处理。Selector
会不断地轮询注册在其上的Channel
,当Channel
有数据可读或者可写时,Selector
会通知应用程序进行相应的处理。在NIO中,可以使用Channel
和Buffer
来进行数据的读写操作,而且可以使用单线程来处理多个Channel
的读写操作,从而避免了多线程之间的竞争和锁的开销。
Netty是一个基于NIO的客户端/服务器框架,它提供了高度可定制化的网络编程API,可以帮助开发者快速地构建高性能、高可靠性的网络应用程序。Netty的设计思路是基于“Reactor模式”,它采用了线程池、缓冲区池、内存池等技术来优化网络通信的性能,同时提供了丰富的编解码器和协议支持,使得开发者可以轻松地实现各种协议的数据交换。
Netty 主要的设计思想包括:
Netty的核心组件包括Channel
、EventLoop
、ChannelFuture
、ChannelHandler
等。
Channel
是Netty的核心概念,它代表了一个网络连接,可以进行数据的读写操作;EventLoop
是Netty的事件循环组件,它负责处理所有的I/O事件,并将事件分发给对应的Channel进行处理;ChannelFuture
是Netty的异步操作结果的封装类,可以用来获取异步操作的结果;ChannelHandler
是Netty的数据处理器,它负责对Channel中的数据进行编解码、处理和转发。总之,NIO和Netty的实现都是基于事件驱动的异步非阻塞模型,能够更好地处理高并发的网络请求,提高系统的吞吐量和响应速度。
更多详细内容,请 参考 尼恩《Java高并发核心编程 卷1 加强版:NIO、Netty、Redis、ZooKeeper》,书里做了 详细的介绍,非常细致
微服务架构的拆分和合并需要考虑多个因素,如业务复杂度、团队规模、技术栈、可维护性、性能等。
需要注意的是,微服务的拆分和合并需要谨慎考虑,应该根据具体情况进行决策。
在微服务架构中,我们可以使用消息队列或接口调用来实现不同微服务之间的通信。
需要注意的是,消息队列和接口调用各有优缺点,应该根据具体情况选择合适的通信方式。同时,在实际应用中,我们也可以将消息队列和接口调用结合起来使用,以实现更加灵活和高效的通信方式。
在分库分表中,为了避免不同的数据库中出现相同的ID,需要设计全局唯一的ID。一种常见的方案是使用雪花算法 (SnowFlake) 生成全局唯一ID。
Snowflake算法是Twitter开源的一个分布式ID生成算法,它可以保证在分布式环境下生成唯一的ID。Snowflake算法生成的ID是一个64位的整数,其中1位是符号位,41位是时间戳,10位是工作机器ID,12位是序列号。
Snowflake算法的ID生成规则如下:
第一位是符号位,始终为0,表示生成的是正整数
接下来的41位是时间戳,精确到毫秒级别,可以使用当前时间减去一个固定的起始时间,得到一个相对时间戳
接下来的10位是机器标识符,可以根据需要自行设计,比如可以使用IP地址、MAC地址、数据中心ID等信息来生成
最后的12位是序列号,可以使用计数器来实现,每次生成ID时自增,当序列号达到最大值时,可以等待下一毫秒再继续生成
使用Snowflake算法生成的ID具有以下优点:
全局唯一,可以在分布式系统中生成唯一的ID
时间戳有序,可以根据ID的时间戳来进行排序,方便数据库的查询和分析
高性能,生成ID的速度非常快,可以支持高并发的场景
易于实现,Snowflake算法的实现比较简单,可以使用Java等语言来实现
需要注意的是,在分库分表的场景下,如果使用Snowflake算法生成ID,需要保证每个分库分表的机器标识符不同,否则可能会导致生成重复的ID。可以考虑使用数据中心ID和机器ID来生成机器标识符,以保证每个分库分表的机器标识符不同。
以下是Java实现Snowflake算法生成全局唯一ID的示例代码:
public class SnowflakeIdGenerator { // 起始的时间戳 private final static long START_TIMESTAMP = 1480166465631L; // 每一部分占用的位数 private final static long SEQUENCE_BIT = 12; // 序列号占用的位数 private final static long MACHINE_BIT = 10; // 机器标识占用的位数 private final static long DATACENTER_BIT = 1; // 数据中心占用的位数 // 每一部分的最大值 private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); // 每一部分向左的位移 private final static long MACHINE_LEFT = SEQUENCE_BIT; private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; private final static long TIMESTAMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; private long datacenterId; // 数据中心 private long machineId; // 机器标识 private long sequence = 0L; // 序列号 private long lastTimestamp = -1L; // 上一次时间戳 public SnowflakeIdGenerator(long datacenterId, long machineId) { if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { throw new IllegalArgumentException("datacenterId can"t be greater than MAX_DATACENTER_NUM or less than 0"); } if (machineId > MAX_MACHINE_NUM || machineId < 0) { throw new IllegalArgumentException("machineId can"t be greater than MAX_MACHINE_NUM or less than 0"); } this.datacenterId = datacenterId; this.machineId = machineId; } public synchronized long nextId() { long timestamp = timeGen(); if (timestamp < lastTimestamp) { throw new RuntimeException("Clock moved backwards. Refusing to generate id"); } if (timestamp == lastTimestamp) { sequence = (sequence + 1) & MAX_SEQUENCE; if (sequence == 0L) { timestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = timestamp; return ((timestamp - START_TIMESTAMP) << TIMESTAMP_LEFT) | (datacenterId << DATACENTER_LEFT) | (machineId << MACHINE_LEFT) | sequence; } private long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } private long timeGen() { return System.currentTimeMillis(); }}
使用示例:
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);long id = idGenerator.nextId();System.out.println(id);
这里的datacenterId和machineId可以根据实际情况进行设定,比如可以使用Zookeeper来管理datacenterId和machineId的分配。
Snowflake算法是一种常用的分布式ID生成算法,但是在高并发场景下,可能会出现ID重复的问题,这会导致数据的错误和不一致。为了解决这个问题,百度在Snowflake算法的基础上进行了一些优化,使得生成的ID更加稳定和唯一。
百度对Snowflake算法的优化主要有以下几点:
1. 增加数据中心ID和机器ID的位数
原始的Snowflake算法中,数据中心ID和机器ID的位数分别为5位和5位,总共10位。百度将数据中心ID和机器ID的位数分别增加到了8位和8位,总共16位。这样可以支持更多的数据中心和机器,也可以减少ID重复的可能性。
2. 使用Zookeeper来管理数据中心ID和机器ID
在原始的Snowflake算法中,数据中心ID和机器ID是静态配置的,需要在每个应用程序中进行配置。这样会带来一些问题,比如在扩容或缩容时需要修改配置文件,容易出错,而且不够灵活。为了解决这个问题,百度使用Zookeeper来管理数据中心ID和机器ID。每个应用程序在启动时,都会向Zookeeper注册自己的ID,Zookeeper会分配一个唯一的ID给应用程序。这样可以避免手动配置的问题,也可以支持动态扩容和缩容。
3. 改进哈希函数
百度使用了MurmurHash3哈希函数来存储雪花序列。MurmurHash3哈希函数是一种高效的哈希函数,可以快速地将一组数字映射到一个固定的数组位置。
使用线程安全的哈希表:在生成全局唯一标识符时,需要在多个线程中同时使用哈希表来存储雪花序列。为了保证哈希表的线程安全性,百度使用了C++11的标准库中提供的线程安全的哈希表。
增加哈希表的大小:为了提高哈希表的效率,百度在实际应用中增加了哈希表的大小。当哈希表的大小达到一定程度时,就会自动扩容,以保证哈希表的性能和稳定性。
4. 时间戳精度
在雪花算法中,时间戳的精度为毫秒级别。为了进一步提高时间戳的精度,百度对雪花算法进行了优化,将时间戳的精度提高到了微秒级别。这样可以更好地支持分布式系统中的时间同步和时序控制。
5. 序列号范围:
在雪花算法中,序列号的范围是0到4095。为了支持更大的并发量和更高的性能,百度对雪花算法进行了优化,将序列号的范围扩展到了1到4096。这样可以更好地支持高并发场景下的数据写入和查询操作。
6. 机器标识码:
在雪花算法中,机器标识码用于表示当前机器的唯一标识符。为了避免机器标识码冲突,百度对雪花算法进行了优化,将机器标识码的范围从0到32位扩展到了128位。这样可以更好地支持多台机器之间的唯一标识符冲突问题。
7. 并发控制:
在雪花算法中,为了保证并发写入时的正确性,百度对雪花算法进行了优化,引入了写入锁和读锁等机制。这样可以更好地支持高并发场景下的写入操作,并且可以避免写入冲突和数据丢失的问题。
通过以上优化,百度实现了一个更加稳定和可靠的分布式ID生成算法,可以在高并发场景下生成唯一的ID,保证数据的正确性和一致性。
Redis可以通过以下几种方式进行单机热点数据的统计:
使用INFO
命令查看Redis实例的各种性能指标,如内存使用情况、连接数、执行命令数等。INFO
命令是Redis自带的一个命令,可以在任何Redis客户端中使用。
1)使用INFO
命令获取Redis服务器的统计信息。
2)解析统计信息,获取内存使用情况相关的数据。
3)根据内存使用情况,计算出每个key的内存占用情况。
4)对所有key的内存占用情况进行排序,获取前N个内存占用最大的key,即为热点数据。
使用MONITOR
命令实时监测Redis实例的性能指标,并将结果输出到标准输出流。MONITOR
命令可以设置监控周期和输出格式,非常灵活。
使用Redis集群中的CLUSTER INFO
命令查看集群中各个节点的性能指标,包括内存使用情况、连接数、执行命令数等。CLUSTER INFO
命令只能在Redis集群中使用。
在应用程序中集成Redis监控工具,如New Relic、Datadog等。这些工具可以帮助你实时监测Redis实例的性能指标,并提供详细的报告和警报功能。
在Redis集群中,当新加入一个节点时,需要将集群中的数据进行重新分片,以保证各个节点负载均衡。具体步骤如下:
确定新节点的插槽范围。在Redis集群中,数据被分成16384个插槽,每个插槽都有一个编号,从0到16383。新节点需要被分配一定范围的插槽,可以根据当前集群中的节点数量和插槽数量来计算。
将新节点加入集群。可以使用Redis的CLUSTER MEET
命令将新节点加入集群,例如:
CLUSTER MEET
CLUSTER ADDSLOTS
命令将一定范围的插槽分配给新节点,例如:CLUSTER ADDSLOTS 0 1 2 3 4 ... 100
其中,0 1 2 3 4 ... 100
表示要分配的插槽编号。
等待集群重新分片。当新节点加入集群并分配了插槽后,集群会自动进行重新分片,将相应的数据迁移到新节点上。这个过程需要一定的时间,可以使用CLUSTER INFO
命令来查看集群状态,直到集群状态为ok
。
重复上述步骤,直到所有节点都加入集群并分配了插槽。
需要注意的是,Redis集群具有自动平衡数据的功能,当某个节点的插槽数量过多或过少时,集群会自动将一些插槽迁移到其他节点上,以保持各个节点的负载均衡。因此,在进行节点的添加和删除时,可以让集群自动进行数据迁移,以减少手动操作的复杂性。
答案是:分别可以用分治法、堆排序、快速选择算法、BitMap算法,
下面是用java写出几种算法的代码
分治法的思路是将大问题分解为小问题,然后分别解决小问题,最后将小问题的解合并起来得到大问题的解。在找出100亿个整数中最大的100个数时,可以将整个数据集分成若干个小数据集,分别找出每个小数据集中最大的100个数,然后将这些最大的100个数合并起来,再找出其中最大的100个数即可。
Java代码实现如下:
import java.io.*;import java.util.*;public class Top100NumbersByDivideAndConquer { private static final int MAX_NUMBERS = 1000000000; // 最多处理10亿个数 private static final int MAX_NUMBERS_PER_FILE = 10000000; // 每个文件最多处理1千万个数 private static final int MAX_NUMBERS_PER_GROUP = 1000000; // 每个小数据集最多处理100万个数 private static final int MAX_GROUPS = MAX_NUMBERS / MAX_NUMBERS_PER_GROUP; // 最多分成10000个小数据集 private static final int MAX_TOP_NUMBERS = 100; // 找出最大的100个数 public static void main(String[] args) throws Exception { // 生成随机数文件 generateRandomNumbersFile("numbers.txt", MAX_NUMBERS); // 将随机数文件分成若干个小文件 List files = splitNumbersFile("numbers.txt", MAX_NUMBERS_PER_FILE); // 找出每个小文件中最大的100个数 List> topNumbersPerFile = new ArrayList<>(); for (String file : files) { List numbers = readNumbersFromFile(file); List topNumbers = findTopNumbersByHeapSort(numbers, MAX_TOP_NUMBERS); topNumbersPerFile.add(topNumbers); } // 将每个小文件中最大的100个数合并起来 List topNumbers = mergeTopNumbers(topNumbersPerFile, MAX_TOP_NUMBERS); // 输出最大的100个数 System.out.println("Top " + MAX_TOP_NUMBERS + " numbers:"); for (int i = 0; i < MAX_TOP_NUMBERS; i++) { System.out.println(topNumbers.get(i)); } } // 生成随机数文件 private static void generateRandomNumbersFile(String fileName, int count) throws Exception { Random random = new Random(); BufferedWriter writer = new BufferedWriter(new FileWriter(fileName)); for (int i = 0; i < count; i++) { writer.write(String.valueOf(random.nextInt())); writer.newLine(); } writer.close(); } // 将随机数文件分成若干个小文件 private static List splitNumbersFile(String fileName, int maxNumbersPerFile) throws Exception { List files = new ArrayList<>(); BufferedReader reader = new BufferedReader(new FileReader(fileName)); String line; int count = 0; int fileIndex = 0; BufferedWriter writer = new BufferedWriter(new FileWriter("numbers_" + fileIndex + ".txt")); while ((line = reader.readLine()) != null) { writer.write(line); writer.newLine(); count++; if (count >= maxNumbersPerFile) { writer.close(); files.add("numbers_" + fileIndex + ".txt"); fileIndex++; writer = new BufferedWriter(new FileWriter("numbers_" + fileIndex + ".txt")); count = 0; } } writer.close(); files.add("numbers_" + fileIndex + ".txt"); reader.close(); return files; } // 从文件中读取数字 private static List readNumbersFromFile(String fileName) throws Exception { List numbers = new ArrayList<>(); BufferedReader reader = new BufferedReader(new FileReader(fileName)); String line; while ((line = reader.readLine()) != null) { numbers.add(Integer.parseInt(line)); } reader.close(); return numbers; } // 使用堆排序算法找出最大的k个数 private static List findTopNumbersByHeapSort(List numbers, int k) { PriorityQueue heap = new PriorityQueue<>(k); for (int number : numbers) { if (heap.size() < k) { heap.offer(number); } else if (number > heap.peek()) { heap.poll(); heap.offer(number); } } List topNumbers = new ArrayList<>(heap); Collections.sort(topNumbers, Collections.reverseOrder()); return topNumbers; } // 合并每个小文件中最大的k个数 private static List mergeTopNumbers(List> topNumbersPerFile, int k) { PriorityQueue heap = new PriorityQueue<>(k); for (List topNumbers : topNumbersPerFile) { for (int number : topNumbers) { if (heap.size() < k) { heap.offer(number); } else if (number > heap.peek()) { heap.poll(); heap.offer(number); } } } List topNumbers = new ArrayList<>(heap); Collections.sort(topNumbers, Collections.reverseOrder()); return topNumbers; }}
堆排序算法的思路是使用一个小根堆来存储当前已经找到的最大的k个数,然后遍历剩余的数,如果比堆顶元素大,则将堆顶元素替换为该数,然后重新调整堆。
Java代码实现如下:
import java.io.*;import java.util.*;public class Top100NumbersByHeapSort { private static final int MAX_NUMBERS = 1000000000; // 最多处理10亿个数 private static final int MAX_TOP_NUMBERS = 100; // 找出最大的100个数 public static void main(String[] args) throws Exception { // 生成随机数文件 generateRandomNumbersFile("numbers.txt", MAX_NUMBERS); // 找出最大的100个数 List numbers = readNumbersFromFile("numbers.txt"); List topNumbers = findTopNumbersByHeapSort(numbers, MAX_TOP_NUMBERS); // 输出最大的100个数 System.out.println("Top " + MAX_TOP_NUMBERS + " numbers:"); for (int i = 0; i < MAX_TOP_NUMBERS; i++) { System.out.println(topNumbers.get(i)); } } // 生成随机数文件 private static void generateRandomNumbersFile(String fileName, int count) throws Exception { Random random = new Random(); BufferedWriter writer = new BufferedWriter(new FileWriter(fileName)); for (int i = 0; i < count; i++) { writer.write(String.valueOf(random.nextInt())); writer.newLine(); } writer.close(); } // 从文件中读取数字 private static List readNumbersFromFile(String fileName) throws Exception { List numbers = new ArrayList<>(); BufferedReader reader = new BufferedReader(new FileReader(fileName)); String line; while ((line = reader.readLine()) != null) { numbers.add(Integer.parseInt(line)); } reader.close(); return numbers; } // 使用堆排序算法找出最大的k个数 private static List findTopNumbersByHeapSort(List numbers, int k) { PriorityQueue heap = new PriorityQueue<>(k); for (int number : numbers) { if (heap.size() < k) { heap.offer(number); } else if (number > heap.peek()) { heap.poll(); heap.offer(number); } } List topNumbers = new ArrayList<>(heap); Collections.sort(topNumbers, Collections.reverseOrder()); return topNumbers; }}
快速选择算法的思路是使用快速排序的思路,将数据集分成两部分,然后只对包含最大的k个数的那一部分继续递归,直到找到最大的k个数。
Java代码实现如下:
import java.io.*;import java.util.*;public class Top100NumbersByQuickSelect { private static final int MAX_NUMBERS = 1000000000; // 最多处理10亿个数 private static final int MAX_TOP_NUMBERS = 100; // 找出最大的100个数 public static void main(String[] args) throws Exception { // 生成随机数文件 generateRandomNumbersFile("numbers.txt", MAX_NUMBERS); // 找出最大的100个数 List numbers = readNumbersFromFile("numbers.txt"); List topNumbers = findTopNumbersByQuickSelect(numbers, MAX_TOP_NUMBERS); // 输出最大的100个数 System.out.println("Top " + MAX_TOP_NUMBERS + " numbers:"); for (int i = 0; i < MAX_TOP_NUMBERS; i++) { System.out.println(topNumbers.get(i)); } } // 生成随机数文件 private static void generateRandomNumbersFile(String fileName, int count) throws Exception { Random random = new Random(); BufferedWriter writer = new BufferedWriter(new FileWriter(fileName)); for (int i = 0; i < count; i++) { writer.write(String.valueOf(random.nextInt())); writer.newLine(); } writer.close(); } // 从文件中读取数字 private static List readNumbersFromFile(String fileName) throws Exception { List numbers = new ArrayList<>(); BufferedReader reader = new BufferedReader(new FileReader(fileName)); String line; while ((line = reader.readLine()) != null) { numbers.add(Integer.parseInt(line)); } reader.close(); return numbers; } // 使用快速选择算法找出最大的k个数 private static List findTopNumbersByQuickSelect(List numbers, int k) { int left = 0; int right = numbers.size() - 1; while (left <= right) { int pivotIndex = partition(numbers, left, right); if (pivotIndex == k) { break; } else if (pivotIndex < k) { left = pivotIndex + 1; } else { right = pivotIndex - 1; } } List topNumbers = new ArrayList<>(numbers.subList(0, k)); Collections.sort(topNumbers, Collections.reverseOrder()); return topNumbers; } private static int partition(List numbers, int left, int right) { int pivotIndex = left; int pivotValue = numbers.get(pivotIndex); swap(numbers, pivotIndex, right); int storeIndex = left; for (int i = left; i < right; i++) { if (numbers.get(i) > pivotValue) { swap(numbers, i, storeIndex); storeIndex++; } } swap(numbers, storeIndex, right); return storeIndex; } private static void swap(List numbers, int i, int j) { int temp = numbers.get(i); numbers.set(i, numbers.get(j)); numbers.set(j, temp); }}
BitMap算法的思路是使用一个BitMap来记录每个数是否出现过,然后遍历BitMap,找出出现次数最多的k个数。
Java代码实现如下:
import java.io.*;import java.util.*;public class Top100NumbersByBitMap { private static final int MAX_NUMBERS = 1000000000; // 最多处理10亿个数 private static final int MAX_TOP_NUMBERS = 100; // 找出最大的100个数 public static void main(String[] args) throws Exception { // 生成随机数文件 generateRandomNumbersFile("numbers.txt", MAX_NUMBERS); // 找出最大的100个数 List numbers = readNumbersFromFile("numbers.txt"); List topNumbers = findTopNumbersByBitMap(numbers, MAX_TOP_NUMBERS); // 输出最大的100个数 System.out.println("Top " + MAX_TOP_NUMBERS + " numbers:"); for (int i = 0; i < MAX_TOP_NUMBERS; i++) { System.out.println(topNumbers.get(i)); } } // 生成随机数文件 private static void generateRandomNumbersFile(String fileName, int count) throws Exception { Random random = new Random(); BufferedWriter writer = new BufferedWriter(new FileWriter(fileName)); for (int i = 0; i < count; i++) { writer.write(String.valueOf(random.nextInt())); writer.newLine(); } writer.close(); } // 从文件中读取数字 private static List readNumbersFromFile(String fileName) throws Exception { List numbers = new ArrayList<>(); BufferedReader reader = new BufferedReader(new FileReader(fileName)); String line; while ((line = reader.readLine()) != null) { numbers.add(Integer.parseInt(line)); } reader.close(); return numbers; } // 使用BitMap算法找出最大的k个数 private static List findTopNumbersByBitMap(List numbers, int k) { int[] bitMap = new int[Integer.MAX_VALUE / 32 + 1]; for (int number : numbers) { int index = number / 32; int bit = number % 32; bitMap[index] |= (1 << bit); } List topNumbers = new ArrayList<>(); while (topNumbers.size() < k) { int maxCount = 0; int maxNumber = 0; for (int i = 0; i < bitMap.length; i++) { for (int j = 0; j < 32; j++) { if ((bitMap[i] & (1 << j)) != 0) { int number = i * 32 + j; int count = countNumberInList(numbers, number); if (count > maxCount) { maxCount = count; maxNumber = number; } } } } topNumbers.add(maxNumber); removeNumberFromList(numbers, maxNumber); } return topNumbers; } private static int countNumberInList(List numbers, int number) { int count = 0; for (int n : numbers) { if (n == number) { count++; } } return count; } private static void removeNumberFromList(List numbers, int number) { for (Iterator iterator = numbers.iterator(); iterator.hasNext();) { if (iterator.next() == number) { iterator.remove(); } } }}
以上四种算法都可以用来解决从100亿个整数的文件中找出其中最大的100个数的问题。
其中,分治法和BitMap算法适用于分布式环境下的数据处理,
而堆排序算法和快速选择算法则适用于单机环境下的数据处理。
在尼恩的(50+)读者社群中,很多、很多小伙伴需要进大厂、拿高薪。
尼恩团队,会持续结合一些大厂的面试真题,给大家梳理一下学习路径,看看大家需要学点啥?
前面用2篇文章,给大家介绍了大厂面试真题的知识要点:
《字节狂问1小时,小伙offer到手,太狠了!(字节面试真题)》
《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》
这些面试真题,都会收入到 史上最全、持续升级的 PDF电子书 《尼恩Java面试宝典》。
本文题目以及参考答案,收入咱们的 《尼恩Java面试宝典》V71版本,可以找尼恩获取
基本上,把尼恩的 《尼恩Java面试宝典》吃透,大厂offer很容易到滴。
另外,下一期的 大厂面经大家有啥需求,可以发消息给尼恩。
《吃透8图1模板,人人可以做架构》
《10Wqps评论中台,如何架构?B站是这么做的!!!》
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
《100亿级订单怎么调度,来一个大厂的极品方案》
《2个大厂 100亿级 超大流量 红包 架构方案》
… 更多架构文章,正在添加中
《响应式圣经:10W字,实现Spring响应式编程自由》
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
《Linux命令大全:2W多字,一次实现Linux自由》
《TCP协议详解 (史上最全)》
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
《Redis分布式锁(图解 - 秒懂 - 史上最全)》
《Zookeeper 分布式锁 - 图解 - 秒懂》
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《缓存之王:Caffeine 的使用(史上最全)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》
4000页《尼恩Java面试宝典 》 40个专题
关键词: