算法面试刷题课–竞赛命题人带你刷70+中高级题型|算法面试专题课(Java版)[完结无密]

6 阅读4分钟

很多同学在面试算法题时,看到题目没思路;或者写出来了,代码边界条件一塌糊涂。高级算法题通常不是“凭空发明”出来的,而是由基础模板演化而来的。 学习地址:pan.baidu.com/s/1WwerIZ_elz_FyPKqXAiZCA?pwd=waug

我们将从学习方法和实战代码两个维度,通过一道经典的困难题(LRU缓存机制)来拆解。

一、 学习方法的“三板斧”

不要一上来就死磕难题,建立知识体系是关键:

  1. 由点及面,分类刷题

    • 不要随机刷题!比如本周只刷“二叉树”,下周只刷“动态规划”。将同一类题型的套路内化成肌肉记忆。
  2. 重视数据结构的选择

    • 遇到题目先问自己:什么数据结构最合适?链表适合增删,数组适合查找,Hash表适合O(1)查找,堆适合TopK问题。
  3. 代码的鲁棒性

    • Bug-free coding 是进阶的关键。写代码前先想好 corner case(空指针、边界值、负数)。

二、 实战演练:搞定经典困难题——LRU 缓存机制

这是 LeetCode 第 146 题(困难),也是字节跳动、腾讯等大厂的高频面试题。它考察的是哈希表双向链表的完美结合。

题目描述

设计并实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作:获取数据 get 和写入数据 put

  • get(key):如果密钥存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
  • put(key, value):如果密钥不存在,则写入数据。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据。

代码实现(Python 解析版)

为了满足 O(1) 的时间复杂度,我们需要:

  • Hash Map:快速定位节点。
  • Double Linked List:快速进行节点的插入和删除,并维护访问顺序。

python

复制

class DLinkedNode:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = {}  # map key to node
        # 使用伪头部和伪尾部节点,避免边界检查(这是一个重要的编程技巧)
        self.head = DLinkedNode()
        self.tail = DLinkedNode()
        self.head.next = self.tail
        self.tail.prev = self.head

    def _add_node(self, node):
        """将节点添加到头部(表示最近被使用)"""
        node.prev = self.head
        node.next = self.head.next
        
        self.head.next.prev = node
        self.head.next = node

    def _remove_node(self, node):
        """移除指定节点"""
        prev = node.prev
        new_next = node.next
        prev.next = new_next
        new_next.prev = prev

    def _move_to_head(self, node):
        """将节点移动到头部"""
        self._remove_node(node)
        self._add_node(node)

    def _pop_tail(self):
        """移除并返回尾部节点(表示最久未使用)"""
        res = self.tail.prev
        self._remove_node(res)
        return res

    def get(self, key: int) -> int:
        node = self.cache.get(key, None)
        if not node:
            return -1
        
        # 关键一步:被访问了,移动到头部
        self._move_to_head(node)
        return node.value

    def put(self, key: int, value: int) -> None:
        node = self.cache.get(key)
        
        if not node:
            # 如果 key 不存在,创建新节点
            new_node = DLinkedNode(key, value)
            self.cache[key] = new_node
            self._add_node(new_node)
            
            # 如果超出容量,删除尾部节点
            if len(self.cache) > self.capacity:
                tail = self._pop_tail()
                del self.cache[tail.key]
        else:
            # 如果 key 存在,更新 value 并移动到头部
            node.value = value
            self._move_to_head(node)

# 测试代码
if __name__ == "__main__":
    cache = LRUCache(2) # 容量为 2
    
    cache.put(1, 1)
    cache.put(2, 2)
    print(cache.get(1)) # 返回 1
    cache.put(3, 3)     # 该操作会使得密钥 2 作废
    print(cache.get(2)) # 返回 -1 (未找到)
    cache.put(4, 4)     # 该操作会使得密钥 1 作废
    print(cache.get(1)) # 返回 -1 (未找到)
    print(cache.get(3)) # 返回 3
    print(cache.get(4)) # 返回 4

三、 代码解析与进阶技巧

这道题之所以是困难题,不是因为你没见过,而是因为链表操作细节繁琐。以下是几个进阶编程技巧:

  1. 伪头节点
    在代码中,我定义了 self.head 和 self.tail 作为虚拟节点。这样在插入和删除时,永远不需要判断 node.next 是否为 None,这极大地减少了 if-else 判断,降低了 Bug 率。
  2. 封装辅助函数
    不要把所有逻辑都塞在 get 和 put 里。将 remove_nodeadd_node 等操作封装起来,逻辑会像搭积木一样清晰,面试官也能一眼看懂你的思路。
  3. Hash Map 的价值
    如果没有 Hash Map,删除链表某个节点需要 O(N) 去找它。有了 self.cache[key] = node 的映射,查找就是 O(1),这决定了算法的档次。

总结

从中初级到高级的跨越,在于数据结构的组合应用能力。LRU 只是一个开始,后续还可以尝试 LFU(更复杂的频率统计)以及跳表、Tire 树等高级结构。

记住:先画图,理清指针关系;再动手,注意边界条件。  照着这个节奏,你会发现算法面试其实并不可怕。