烦人的周一老想摸鱼看B站,刷视频的同时可以看看我们组织内部分享的哔哩哔哩一面面试题,我已经把所有的问题和答案都整理好了,希望对大家有帮助:
RabbitMQ延迟队列知道吗?什么是死信交换机?
MQ消息堆积问题
MQ乱序问题
布隆过滤器防止雪崩,布隆过滤器原理和实现
直播购物遇到的超卖问题如何防止?
同时有多个商品秒杀的业务场景?
手撕:链表每k个反转
面经详解
RabbitMQ延迟队列知道吗?
-
正确答案:RabbitMQ本身并不直接支持延迟队列(Delay Queue)功能,但可以通过一些机制模拟实现延迟队列的效果。常见的实现方式包括使用TTL(Time To Live)和死信队列(Dead Letter Exchange, DLX)结合的方式。
-
解答思路:
- 首先理解延迟队列的概念:消息在发送后并不会立即被消费者消费,而是等待一段时间之后才被投递。
- RabbitMQ原生不支持延迟队列,所以需要借助其他机制实现。
- 常见做法是利用消息的TTL属性设置过期时间,并将过期的消息转发到另一个交换机(DLX),从而实现延迟效果。
- 可以进一步扩展为每个延迟等级创建独立的队列和绑定关系,实现多级延迟。
-
深度知识讲解:
1. TTL(Time To Live)
- 每条消息可以设置一个生存时间(TTL),当消息在队列中等待的时间超过这个值时,就会被认为“过期”。
- 同样地,也可以为整个队列设置TTL,这样所有进入该队列的消息都会继承相同的TTL值。
2. 死信队列(DLX,Dead Letter Exchange)
- 当消息因为某种原因无法被正常消费(例如过期、被拒绝、队列达到最大长度等),可以配置将其转发到一个特定的交换机,称为死信交换机。
- 死信交换机会根据路由规则将这些“死亡”的消息投递到对应的死信队列中。
3. 实现延迟队列的关键步骤
- 创建一个普通队列,并为其设置TTL。
- 配置该队列的死信交换机(DLX)和死信路由键。
- 所有发往该普通队列的消息会在TTL时间过后自动成为死信,并被转发到死信队列中,从而实现延迟效果。
4. 应用场景
- 订单超时未支付自动取消
- 定时任务调度
- 消息重试机制(如失败后延迟重试)
5. 注意事项
- 消息的延迟精度依赖于RabbitMQ的内部定时器机制,不能做到毫秒级别的精确控制。
- 如果有多个不同延迟时间的需求,可以为每种延迟时间配置不同的队列组合。
- 使用插件(如rabbitmq_delayed_message_exchange)可以直接支持延迟交换机,但需要额外安装插件。
什么是死信交换机?
-
正确答案:死信交换机(Dead Letter Exchange,简称DLX)是消息中间件中用于处理无法被正常消费的消息的一种机制。当一条消息在队列中多次投递失败后,可以将其转发到另一个专门的交换机,这个交换机就是死信交换机。
-
解答思路:
- 理解什么是“死信”消息:即无法被消费者成功处理的消息。
- 消息队列系统如RabbitMQ提供了内置机制,在满足某些条件时自动将消息路由到指定的死信交换机。
- 死信交换机本身是一个普通的交换机,它与一个或多个死信队列绑定,用于后续分析、重试或记录日志等操作。
- 配置流程包括设置x-dead-letter-exchange和x-dead-letter-routing-key参数。
-
深度知识讲解:
核心原理与配置参数
在 RabbitMQ 中,可以通过以下两个参数为队列设置死信机制:
- x-dead-letter-exchange:指定死信交换机名称。
- x-dead-letter-routing-key:指定消息进入死信交换机时使用的路由键,默认使用原消息的路由键。
触发死信的条件
以下几种情况会触发消息进入死信交换机:
- 消息被拒绝(basic.reject 或 basic.nack)且不重新入队(requeue=false)。
- 消息过期(TTL 设置超时)。
- 队列达到最大长度限制,导致消息被丢弃。
应用场景
- 错误消息隔离:防止错误消息不断重试影响系统稳定性。
- 日志记录与监控:集中处理异常消息,便于排查问题。
- 延迟重试机制:将死信消息重新投递到原始队列进行重试。
性能与可靠性
使用 DLX 可以提高系统的容错能力,但也需要注意:
- 不要让死信队列也变成“死循环”,应有相应的清理或人工干预机制。
- 监控死信队列中的消息数量和内容,有助于发现潜在的问题。
MQ消息堆积问题
Kafka 天然支持海量消息堆积,因为其底层采用分区 + 日志文件结构,所有消息顺序写入磁盘,读取效率高。但若消费者落后太多,仍需考虑消费性能优化。
- RocketMQ: 支持大量堆积,每个主题可以有多个队列(MessageQueue),支持并行消费,同时具备定时消息、事务消息等功能。
3. 消息堆积的监控手段
- 查看MQ控制台的堆积数指标。
- 通过API获取topic或queue的堆积情况。
- 设置监控告警,如Prometheus + Grafana。
4. 解决方案详解
-
提高消费者处理能力:
- 优化业务逻辑(避免同步调用外部服务)。
- 使用线程池/协程进行并发处理。
-
横向扩展消费者:
- 启动多个消费者实例,利用MQ的负载均衡机制分配队列。
- 注意是否支持广播模式或集群消费模式。
-
批量消费:
-
消费者一次性拉取多条消息进行处理,降低网络IO开销。
-
示例(以RocketMQ为例):
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for (MessageExt msg : msgs) { // 处理消息 } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }
-
-
死信队列(DLQ) :
- 将多次失败的消息转移到死信队列中,防止影响正常流程。
- 可用于后续人工干预或分析。
-
削峰填谷:
- 在高峰期限制生产者的发送速率。
- 使用限流算法如令牌桶、漏桶算法。
-
异步落盘与压缩:
- 对大数据量的消息进行压缩传输。
- 异步持久化数据,避免阻塞主线程。
5. 底层数据结构与实现原理
-
RabbitMQ内部结构:
- 使用Erlang进程模型,每个队列对应一个独立进程。
- 消息堆积时,内存不足会触发paging操作,将部分消息写入磁盘。
-
Kafka日志分段(LogSegment) :
- 所有消息按offset顺序追加到日志文件中。
- 分为多个segment文件,便于查找和清理过期数据。
- offset -> 文件位置映射通过索引文件实现(稀疏索引)。
-
RocketMQ的消息存储机制:
- CommitLog:所有消息顺序写入一个大文件。
- ConsumeQueue:每个Topic/QueueId维护一个偏移量索引文件。
- IndexFile:可选的索引文件,支持按key查询消息。
6. 实际案例
-
电商秒杀系统:
-
生产端每秒产生上万订单消息,而消费端处理一条需要调用库存、积分等多个服务。
-
解决方案:
- 消费端使用线程池并发处理。
- 消息体尽量轻量,只传关键ID,由消费端异步查询详细信息。
- 监控堆积情况,自动扩容消费者。
-
总结
消息堆积问题不仅考验你对MQ原理的理解,也涉及分布式系统设计、性能调优、运维监控等多个方面。掌握这些内容,有助于你在面试中展现出扎实的技术功底和实战经验。
MQ乱序问题
-
正确答案:MQ(消息队列)乱序问题指的是在使用消息队列进行异步通信时,消费者接收到的消息顺序与生产者发送的顺序不一致。这种问题通常发生在高并发、分布式环境下,由于多个分区(partition)、多线程消费、网络延迟等原因造成。
-
解答思路:
-
首先明确消息队列的基本工作流程:生产者发送消息 → 消息被写入MQ → 消费者从MQ拉取消息并处理。
-
然后分析可能导致乱序的因素:
- 多个分区(如Kafka中不同Partition)
- 多线程消费(一个消费者组下多个线程同时消费)
- 网络传输不稳定或重试机制
- 异步刷盘导致写入时间差
-
最后提出解决方案:
- 单分区单线程消费保证顺序性
- 引入本地排序逻辑或序列号判断
- 使用支持有序性的MQ中间件(如RocketMQ的顺序消息)
-
-
深度知识讲解:
一、MQ基本原理
消息队列是典型的发布-订阅模型,其核心组件包括:
- 生产者(Producer):负责发送消息
- Broker:负责接收、存储和转发消息
- 消费者(Consumer):负责拉取并处理消息
根据是否支持消息顺序性,可以将MQ分为两类:
- 支持严格顺序性:如RocketMQ顺序消息
- 不支持顺序性:如Kafka普通消息、RabbitMQ等
二、导致乱序的原因及底层实现分析
1. 分区(Partition)机制
如Kafka中,一个Topic可以有多个Partition。如果生产者没有按照Key进行分区控制,不同的消息可能被分配到不同的Partition中,而每个Partition独立消费,就会出现乱序。
底层数据结构: Kafka中每个Partition是一个有序的追加日志文件(append-only log),内部由多个Segment组成,每个Segment包含索引文件和数据文件。
Partition = [Segment0, Segment1, ..., SegmentN] Segment = {index file + data file}2. 多线程消费
在Kafka或RocketMQ中,如果开启多线程消费,不同线程可能处理不同Offset的消息,从而导致乱序。
底层调度机制: Kafka消费者组中的线程通过协调器(Group Coordinator)分配Partition,每个线程独立拉取消息。
3. 网络重传与ACK机制
如果消费者在处理完消息后未及时提交Offset,Broker会重新投递该消息,造成重复消费和乱序。
ACK机制类型:
- at most once
- at least once
- exactly once
三、解决方法及其底层实现
1. 单分区单线程消费
这是最直接的方式,适用于对顺序要求极高的场景,如订单状态变更、支付流水等。
缺点:牺牲了并发性和吞吐量。
2. 引入序列号排序
可以在每条消息中添加序列号字段,在消费端维护一个滑动窗口,按序处理。
布隆过滤器防止雪崩
-
正确答案:布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否属于集合。它不能完全防止缓存雪崩,但可以与其他技术结合使用来缓解由大量并发请求引发的系统崩溃问题。
-
解答思路:
- 首先理解什么是缓存雪崩:当缓存中大量键同时过期或失效时,所有请求会直接打到数据库上,可能造成数据库压力骤增甚至宕机。
- 布隆过滤器本身不处理缓存过期问题,但它可以作为前置过滤层,快速判断某个键是否存在,从而减少无效查询对后端系统的冲击。
- 在缓存雪崩场景下,布隆过滤器的作用是避免缓存穿透攻击(即查询不存在的数据),不至于让所有请求都穿过缓存去查数据库。
- 结合其他策略如缓存永不过期、过期时间随机化、本地缓存等,可以有效降低雪崩风险。
-
深度知识讲解:
布隆过滤器原理
布隆过滤器的核心是一个长度为 m 的 bit 数组和 k 个独立的哈希函数。每个哈希函数将输入元素映射到数组的一个位置。插入元素时,所有对应的位设为1;查询元素时,若任何一个对应位为0,则该元素肯定不在集合中;若全为1,则可能在集合中(可能存在误判)。
- 优点:内存占用小,适合大数据量场景。
- 缺点:存在假阳性(False Positive),不支持删除操作(除非引入计数布隆过滤器)。
缓存雪崩与布隆过滤器的关系
布隆过滤器主要解决的是缓存穿透问题,而缓存雪崩指的是大量缓存同时失效。虽然布隆过滤器不能直接防止缓存雪崩,但在高并发场景中,它可以作为第一道防线,防止恶意查询不存在的数据造成的数据库冲击。
抗雪崩策略组合使用建议:
- 设置不同的过期时间:给缓存设置一个基础过期时间 + 小范围随机值,避免同时失效。
- 本地缓存+远程缓存双层架构:如使用本地Guava Cache + Redis。
- 热点数据自动降级机制:在Redis宕机时返回默认值或降级页面。
- 使用布隆过滤器拦截非法访问:对于非热点但被频繁扫描的非法key,进行过滤。
数据结构实现原理解析
布隆过滤器的数据结构
- Bit数组:长度为m,初始全为0。
- 哈希函数集合:k个不同的哈希函数,输出范围是[0, m-1]。
插入操作步骤
- 输入元素x;
- 使用k个哈希函数计算出k个索引位置;
- 把这k个位置上的bit置为1。
查询操作步骤
-
输入元素x;
-
计算k个哈希结果;
-
检查每个位置是否都为1;
- 若任意一位为0 → 元素一定不存在;
- 所有位都为1 → 元素可能存在于集合中。
假阳性率公式
设插入n个元素,则假阳性概率约为:
p ≈ (1 - e^(-kn/m))^k调整k、m、n可以获得期望的误判率。
-
伪代码示例:
class BloomFilter:
def __init__(self, size, hash_seeds):
self.m = size
self.bits = [0] * size
self.hash_functions = [lambda x, seed=s: hash(x + str(seed)) % self.m for s in hash_seeds]
def add(self, item):
for hf in self.hash_functions:
pos = hf(item)
self.bits[pos] = 1
def contains(self, item):
for hf in self.hash_functions:
pos = hf(item)
if self.bits[pos] == 0:
return False
return True
-
扩展知识点:
- 可以使用Redis模块实现高性能布隆过滤器插件
RedisBloom。 - 布隆过滤器可应用于URL黑名单过滤、网页爬虫去重、垃圾邮件识别等大数据场景。
- 如果需要支持删除操作,可以使用计数布隆过滤器(Counting Bloom Filter) ,将bit数组改为计数数组。
- 如果要提高准确率,可以采用分层布隆过滤器或者动态布隆过滤器。
- 可以使用Redis模块实现高性能布隆过滤器插件
总结:布隆过滤器不能单独防止缓存雪崩,但它是构建缓存安全体系的重要组件之一,配合其他策略可有效提升系统的稳定性与抗压能力。
布隆过滤器原理和实现
-
正确答案:布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它返回的结果是“可能存在”或“一定不存在”,不能准确确定元素是否存在,但可以高效地排除大量不在集合中的元素。
-
解答思路: 布隆过滤器的核心思想是使用一个位数组(bit array)和多个哈希函数来标记元素的存在状态。具体流程如下:
- 初始化一个大小为 m 的位数组,所有位初始为0。
- 使用 k 个不同的哈希函数,每个哈希函数将输入映射到位数组的一个位置。
- 当插入一个元素时,通过这 k 个哈希函数计算出 k 个位置,并将这些位置的值设为1。
- 查询一个元素时,同样计算出 k 个位置,如果其中任何一个位置的值为0,则该元素肯定不存在;若全为1,则该元素可能存在(存在误判)。
注意:布隆过滤器不支持删除操作(除非使用变种如计数布隆过滤器),因为无法确定某个位置是否只被当前元素影响。
-
深度知识讲解:
核心原理与特性
-
位数组:布隆过滤器本质上是一个 bit 级别的数组,用来记录哪些位置被哪些元素“占用”。
-
多哈希函数:多个独立的哈希函数用于减少碰撞的可能性,提高准确性。
-
误判率(False Positive Rate) : 误判率与以下因素有关:
- m:位数组长度
- n:插入的元素数量
- k:哈希函数的数量
公式为:
P ≈ (1 - e^(-kn/m))^k可以根据期望的误判率反推最优的 k 和 m。
时间复杂度
- 插入和查询的时间复杂度都是 O(k),与元素数量无关,效率非常高。
- 空间复杂度是 O(m),通常远小于普通哈希表存储实际元素的空间。
应用场景
- 缓存系统中防止缓存穿透(如 Redis)
- 网络爬虫去重
- 数据库优化(如 HBase、Cassandra 中用于快速判断 SSTable 是否包含某行键)
变种
- Counting Bloom Filter:允许删除操作,将位数组改为计数器数组。
- Scalable Bloom Filter:动态扩容,适用于元素数量未知的情况。
- Spectral Bloom Filter:用于估计频率信息。
-
-
伪代码实现:
class BloomFilter: def __init__(self, size, hash_num): self.size = size # 位数组大小 self.hash_num = hash_num # 哈希函数数量 self.bit_array = [0] * size # 初始化位数组 def _hashes(self, item): # 返回 k 个不同的哈希值,模上 size 得到位索引 hashes = [] for i in range(self.hash_num): hash_val = hash(str(item) + str(i)) % self.size hashes.append(hash_val) return hashes def add(self, item): for pos in self._hashes(item): self.bit_array[pos] = 1 def check(self, item): for pos in self._hashes(item): if self.bit_array[pos] == 0: return False # 一定不存在 return True # 可能存在注意:在实际工程中,哈希函数应选择更均匀分布的算法(如 MurmurHash、SHA-1 等),而不是 Python 内置的 hash 函数。
-
扩展知识点:
为什么布隆过滤器不能精确判断元素存在?
因为多个不同元素经过哈希后可能会映射到相同的位组合,导致某些不存在的元素也被认为“可能存在”。
如何设计最优的哈希函数数量?
最优的哈希函数数量 k 满足:
k = (m / n) * ln(2)其中 m 是位数组大小,n 是预计插入的元素数量。
实际工程应用举例(Redis 布隆过滤器模块):
Redis 官方提供
RedisBloom模块,提供了布隆过滤器的命令接口,例如:BF.ADD bloom_key item BF.EXISTS bloom_key item这些命令背后就是基于布隆过滤器的数据结构实现的,常用于防止数据库穿透攻击。
与哈希表的对比:
特性 布隆过滤器 哈希表 存储内容 不存储实际元素 存储 key-value 映射 空间效率 高 较低 支持删除 否(除非变种) 是 查询结果 概率性(可能误判) 精确 总结来说,布隆过滤器适合对内存敏感、允许少量误判、不需要删除的场景。
直播购物遇到的超卖问题如何防止?
-
正确答案:直播购物中出现的超卖问题是指商品库存不足,但多个用户同时下单成功,导致实际发货数量超过库存。防止超卖的核心方法包括使用数据库事务、锁机制(如悲观锁和乐观锁)、Redis分布式锁、库存预扣机制等。
-
解答思路:
-
理解超卖场景:在高并发环境下,多个请求几乎同时读取库存并进行减库存操作,可能导致最终库存为负。
-
分析解决方案:
- 使用数据库事务保证一致性。
- 引入锁机制控制并发访问。
- 利用Redis原子操作实现分布式环境下的库存管理。
- 使用消息队列削峰填谷,避免瞬间高并发冲击数据库。
-
结合业务场景选择合适的技术方案。
-
-
深度知识讲解:
一、数据库事务 + 行级锁(悲观锁)
在MySQL中可以使用SELECT ... FOR UPDATE加行级锁来确保同一时间只有一个线程能修改库存。
实现原理:
- 患者锁思想,在查询时就锁定该行数据,其他事务必须等待当前事务提交后才能继续。
- 数据库事务隔离级别需要设置为可重复读或以上,防止脏读和不可重复读。
START TRANSACTION;
SELECT inventory FROM products WHERE product_id = 1001 FOR UPDATE;
IF inventory > 0 THEN
UPDATE products SET inventory = inventory - 1 WHERE product_id = 1001;
COMMIT;
ELSE
ROLLBACK;
二、乐观锁(适用于低冲突场景)
通过版本号或CAS(Compare and Swap)机制来控制更新。
实现原理:
- 不加锁,先读取库存和版本号。
- 更新时检查版本号是否一致,如果一致才允许更新,并将版本号+1。
- 如果不一致说明有其他线程已更新,拒绝本次操作。
UPDATE products SET inventory = inventory - 1, version = version + 1
WHERE product_id = 1001 AND version = expected_version;
三、Redis 原子操作(适用于分布式系统)
Redis 提供了 DECR 这样的原子命令,可以用于库存扣减。
实现原理:
- Redis 是单线程处理命令,天然支持原子性。
- 使用
DECR命令对库存键进行递减操作,返回值小于0表示库存不足。
示例代码(Python伪代码):
import redis
r = redis.Redis()
stock_key = "product:1001:stock"
current_stock = r.decr(stock_key)
if current_stock < 0:
# 回滚 Redis 操作(可能还需要回滚数据库)
r.incr(stock_key)
raise Exception("库存不足")
else:
# 同步写入数据库(可异步处理)
db.update_inventory(1001, current_stock)
四、库存预扣 + 消息队列(复杂业务场景)
- 用户下单时先预扣库存,生成订单,进入消息队列。
- 消费者逐个处理订单,确认支付后正式扣减库存。
- 如果未支付,则释放预扣库存。
五、分布式锁(Redis + Lua 脚本)
在分布式系统中,使用Redis + Lua脚本来实现一个安全的分布式锁,确保同一时刻只有一个节点能执行关键代码。
示例Lua脚本(扣减库存):
-- KEYS[1] = stock_key
-- ARGV[1] = product_id
local stock = redis.call('GET', KEYS[1])
if stock and tonumber(stock) > 0 then
redis.call('DECR', KEYS[1])
return 1 -- 成功
else
return 0 -- 库存不足
end
调用方式:
script = redis.register_script(lua_script)
result = script(keys=[stock_key], args=[])
if result == 0:
print("库存不足")
总结:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 数据库事务 + 悲观锁 | 单体架构,低并发 | 实现简单,一致性强 | 并发性能差,容易死锁 |
| 乐观锁 | 冲突较少场景 | 无锁,性能好 | 高冲突下失败率高 |
| Redis原子操作 | 高并发、分布式系统 | 高性能、天然原子性 | 需要与数据库同步 |
| 预扣库存 + MQ | 复杂业务流程 | 流程清晰、可扩展性强 | 实现复杂、延迟增加 |
| Redis + Lua 分布式锁 | 分布式系统 | 安全可靠 | 需要掌握Lua和Redis高级特性 |
根据系统的架构复杂度、并发量、技术栈选型等因素综合选择合适的防超卖策略。
同时有多个商品秒杀的业务场景?
-
正确答案:在多个商品秒杀的业务场景中,系统需要处理高并发请求、防止超卖、保证库存一致性、实现限流与防刷等核心问题。关键技术包括分布式锁、数据库事务、缓存机制(如Redis)、队列削峰填谷、限流算法(如令牌桶、漏桶)等。
-
解答思路: 面对多个商品同时参与秒杀的业务场景,首先要明确这个场景的核心挑战是“高并发 + 数据一致性”。具体来说,当数万甚至数十万用户在同一时刻访问同一个或多个秒杀商品页面并尝试下单时,系统必须能够:
- 快速响应请求;
- 避免商品超卖(即卖出数量超过库存);
- 控制请求频率,防止服务器崩溃;
- 实现公平性,防止恶意刷单;
- 提供良好的用户体验(如排队机制、失败重试等);
解决这些问题的关键在于架构设计和底层技术选型。
-
深度知识讲解:
1. 高并发处理
秒杀场景下,瞬间请求量极高,传统的同步阻塞式处理方式无法支撑。需要使用异步非阻塞架构,比如Nginx+Lua前端过滤、后端使用高性能语言(Go/Java)配合线程池处理任务。
2. 库存一致性与防止超卖
- 数据库层面:使用乐观锁(CAS更新库存)或悲观锁(SELECT FOR UPDATE)来控制并发修改。
- 缓存层面:将库存预加载到Redis中,利用其原子操作(如DECR)进行库存扣减,减少数据库压力。
- 分布式锁:使用Redis RedLock算法或ZooKeeper实现跨服务节点的互斥访问,确保同一时间只有一个请求能修改库存。
3. 缓存击穿与穿透
- 使用布隆过滤器防止非法请求穿透到数据库;
- 设置热点数据永不过期或后台异步更新;
- Redis集群部署,避免单点故障。
4. 限流与削峰填谷
- 前端限流:通过Nginx限流模块(limit_req_zone)控制QPS;
- 后端限流:使用Guava RateLimiter或Sentinel进行服务级限流;
- 异步队列:将下单请求放入消息队列(如Kafka/RabbitMQ),后端消费队列逐个处理,缓解瞬时压力。
5. 分布式事务与幂等性
- 下单操作可能涉及订单创建、支付、库存变更等多个服务,需引入TCC(Try-Confirm-Cancel)模式或Saga事务模型;
- 每个请求携带唯一ID,服务端根据ID判断是否重复提交,保证接口幂等。
6. 排队机制
可以前端展示排队人数,后端维护一个等待队列,按顺序放行用户进入秒杀流程,提升用户体验和系统稳定性。
7. 安全防护
- 验证码机制防止机器人刷单;
- 黑名单限制IP或设备;
- 请求签名验证防止篡改参数。
手撕:链表每k个反转
-
正确答案:题目要求将链表每k个节点一组进行反转,不足k的部分保持原样。例如,输入链表为1->2->3->4->5,k=2,则输出应为2->1->4->3->5。
-
解答思路:
- 首先遍历链表,判断当前是否有足够的k个节点来进行反转。
- 若有k个节点,则使用头插法或者三指针的方式进行反转。
- 反转完成后,记录该段的头节点和尾节点,并连接到前一段的末尾。
- 继续处理下一段,直到整个链表处理完毕。
- 注意要维护好各段之间的连接关系。
-
深度知识讲解: 这道题考察的是对链表操作的理解,尤其是链表反转、节点插入与删除等基础操作。此外还涉及递归或迭代的控制逻辑。
核心知识点包括:
- 链表结构:每个节点包含一个数据域和一个指针域,指向下一个节点。
- 指针操作:在链表中移动、断开、重连节点时必须谨慎处理指针,避免内存泄漏或环路。
- 分组控制:需要统计当前处理的节点数,达到k后立即停止并进入下一组处理。
- 边界处理:如链表长度小于k、正好是k的整数倍等情况。
时间复杂度分析:O(n),其中n为链表长度,每个节点最多被访问两次(一次遍历,一次反转)。 空间复杂度分析:O(1),只使用了常数级额外空间。
-
代码示例:
struct ListNode {
int val;
struct ListNode *next;
};
// 反转从 head 开始的 k 个节点,返回新的头节点
struct ListNode* reverseKGroup(struct ListNode* head, int k) {
// 先计算链表总长度
int length = 0;
struct ListNode *cur = head;
while (cur) {
length++;
cur = cur->next;
}
// 创建虚拟头节点,方便统一操作
struct ListNode dummy;
dummy.next = head;
struct ListNode *prev = &dummy;
// 分组处理
while (length >= k) {
struct ListNode *groupNext = prev->next; // 当前组的第一个节点
struct ListNode *current = groupNext;
struct ListNode *next = NULL;
for (int i = 0; i < k - 1; i++) {
next = current->next;
current->next = next->next;
next->next = groupNext;
groupNext = next;
}
prev->next = groupNext;
prev = current; // 更新 prev 到当前组的最后一个节点
length -= k;
}
return dummy.next;
}
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reverseKGroup(head: ListNode, k: int) -> ListNode:
# 计算剩余链表长度
count = 0
curr = head
while curr and count < k:
curr = curr.next
count += 1
# 如果不足k个,直接返回head
if count < k:
return head
# 反转前k个节点
prev, curr = None, head
for _ in range(k):
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
# 递归处理后续链表,并将当前段的头连接到后续段的头
head.next = reverseKGroup(curr, k)
return prev
早日上岸!
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。