使用 Java 解决 LeetCode 问题时,必须熟练掌握的数据结构

159 阅读11分钟

以下是我对常见数据结构的详细分析,每种数据结构都包含其底层机制、性能分析、典型应用场景以及在算法问题中的高级使用技巧。我会尽量避免表面化的描述,并提供具体、可操作的内容。

1. 数组(Array)

详细描述

数组是一种线性数据结构,存储在连续的内存空间中,所有元素类型相同且大小固定。它的核心特性是通过索引实现 O(1) 时间复杂度的随机访问。

内部工作原理

  • 内存分配:数组在创建时分配一块连续的内存,元素存储在相邻的地址中。假设每个元素占用 s 个字节,数组起始地址为 base,第 i 个元素的地址为 base + i * s。

  • 索引访问:CPU 通过地址计算直接定位元素,无需遍历。

  • 动态数组:如 Java 的 ArrayList,底层是数组,当容量不足时会创建一个更大的数组并复制元素(时间复杂度 O(n))。

优缺点

  • 优点
    • 随机访问效率极高(O(1))。
    • 由于内存连续性,缓存命中率高,适合现代 CPU 的预取机制。
  • 缺点
    • 插入和删除效率低(O(n)),需要移动后续元素。

    • 大小固定(静态数组),动态数组扩容代价高。

    • 不适合频繁变动的场景。

适用场景

  • 快速访问:需要通过索引快速定位元素时,例如查找表。

  • 静态数据:数据量已知且不频繁修改时。

  • 基础结构:实现堆、哈希表等其他数据结构。

使用技巧

  • 双指针法:在有序或部分有序数组中,利用两个指针缩小搜索范围。例如,解决“两数之和”问题时,用左右指针夹逼。

  • 滑动窗口:处理子数组问题时,维护一个动态窗口,逐步调整边界。

  • 前缀和数组:预计算累加和,用于快速查询区间和。

示例问题:LeetCode 283 - Move Zeroes

问题:将数组中的所有 0 移动到末尾,保持非零元素相对顺序。


public void moveZeroes(int nums) {

    int nonZeroIdx = 0;

    // 第一步:将非零元素移到前面

    for (int i = 0; i < nums.length; i++) {

        if (nums[i] != 0) {

            nums[nonZeroIdx++] = nums[i];

        }

    }

    // 第二步:填充剩余部分为 0

    while (nonZeroIdx < nums.length) {

        nums[nonZeroIdx++] = 0;

    }

}

技巧分析

  • 使用双指针:nonZeroIdx 追踪非零元素的目标位置,i 遍历数组。

  • 空间复杂度 O(1),原地操作,避免额外数组。

2. 链表(Linked List)

详细描述

链表是由节点组成的数据结构,每个节点包含数据和指向下一节点的指针。相比数组,它在内存中是非连续的。

内部工作原理*

  • 节点结构:每个节点包含 value 和 next 指针(单链表),双链表还有 prev 指针。
  • 动态分配:节点在堆内存中单独分配,通过指针连接。
  • 操作
    • 插入/删除(已知位置):O(1),只需调整指针。

    • 访问:O(n),需从头遍历。

优缺点*

  • 优点
    • 动态大小,无需预分配空间。
    • 插入和删除高效(O(1))。
  • 缺点
    • 随机访问慢(O(n))。

    • 内存开销大(指针占用空间)。

    • 不缓存友好。

适用场景*

  • 动态数据:频繁插入和删除的场景。

  • 栈/队列实现:单链表适合栈,双链表适合队列。

  • 复杂结构:如环形链表、跳表。

使用技巧

  • 快慢指针:检测环、找中点、倒数第 k 个节点。

  • 哑节点(Dummy Node) :简化头节点操作,避免边界判断。

  • 反转链表:通过迭代或递归调整指针方向。

示例问题:LeetCode 141 - Linked List Cycle

问题:判断链表中是否存在环。


public boolean hasCycle(ListNode head) {

    ListNode slow = head, fast = head;

    while (fast != null && [fast.next](fast.next) != null) {

        slow = [slow.next](slow.next);        // 慢指针走一步

        fast = [fast.next.next](fast.next.next);   // 快指针走两步

        if (slow == fast) return true// 相遇说明有环

    }

    return false;

}

技巧分析

  • 快慢指针:快指针每次走两步,慢指针走一步,若有环必相遇。

  • 时间复杂度 O(n),空间复杂度 O(1)。

3. 栈(Stack)

详细描述

栈是一种后进先出(LIFO)的数据结构,仅允许在栈顶操作(push 和 pop)。

内部工作原理

  • 实现方式

    • 数组:固定大小,push/pop 在数组末尾。
    • 链表:动态大小,push/pop 在链表头。
  • 操作:push 和 pop 均为 O(1)。

优缺点

  • 优点
    • 实现简单,操作高效。
    • 适合逆序处理。
  • 缺点
    • 只能访问栈顶元素。

    • 数组实现时容量有限。

适用场景

  • 表达式求值:如中缀转后缀、计算结果。

  • 回溯算法:DFS、括号匹配。

  • 单调栈:解决“下一个更大元素”问题。

使用技巧

  • 单调栈:维护递增或递减的栈,处理最近更大/更小元素。

  • 辅助栈:如最小栈问题,用额外栈记录状态。

  • 递归替代:用栈模拟递归调用栈。

示例问题:LeetCode 155 - Min Stack

问题:设计一个支持 push、pop、top 和 getMin 的栈。


class MinStack {

    Stack<Integer> stack = new Stack<>();

    Stack<Integer> minStack = new Stack<>();

    public void push(int val) {

        stack.push(val);

        if (minStack.isEmpty() || val <= minStack.peek()) {

            minStack.push(val);  // 只压入小于等于当前最小值的元素

        }

    }

    public void pop() {

        if (stack.pop().equals(minStack.peek())) {

            minStack.pop();  // 若弹出的是最小值,同步更新

        }

    }

    public int top() {

        return stack.peek();

    }

    public int getMin() {

        return minStack.peek();

    }

}

技巧分析

  • 辅助栈记录最小值,确保 getMin 为 O(1)。

  • 同步更新,保持栈顶始终是最小值。

4. 队列(Queue)

详细描述

队列是一种先进先出(FIFO)的数据结构,支持在队尾入队(enqueue),队头出队(dequeue)。

内部工作原理

  • 实现方式

    • 链表:动态大小,入队 O(1),出队 O(1)。
    • 循环数组:固定大小,避免空间浪费。
  • 双端队列(Deque) :支持两端操作。

优缺点

  • 优点
    • 按顺序处理任务。
    • 动态扩展(链表实现)。
  • 缺点
    • 不支持随机访问。

    • 数组实现时需处理循环逻辑。

适用场景

  • BFS:层序遍历、图的最短路径。

  • 任务调度:如打印队列。

  • 滑动窗口:用双端队列维护最大/最小值。

使用技巧

  • 单调队列:在窗口问题中维护单调性。

  • 优先队列:结合堆实现,按优先级出队。

  • 循环队列:优化空间利用率。

示例问题:LeetCode 102 - Binary Tree Level Order Traversal

问题:二叉树层序遍历。


public List<List<Integer>> levelOrder(TreeNode root) {

    List<List<Integer>> result = new ArrayList<>();

    if (root == null) return result;

    Queue<TreeNode> queue = new LinkedList<>();

    queue.offer(root);

    while (!queue.isEmpty()) {

        int size = queue.size();  // 当前层节点数

        List<Integer> level = new ArrayList<>();

        for (int i = 0; i < size; 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);

        }

        result.add(level);

    }

    return result;

}

技巧分析

  • 使用队列实现 BFS。

  • 记录每层节点数,分层输出。

5. 哈希表(Hash Table)

详细描述

哈希表通过哈希函数将键映射到存储位置,实现高效的查找、插入和删除。

内部工作原理

  • 哈希函数:将键转换为数组索引,如 hash(key) % tableSize。

  • 冲突解决

    • 链地址法:每个桶存储链表。
    • 开放地址法:探测下一个空位。
  • 负载因子:元素数/桶数,过高时需扩容。

优缺点

  • 优点
    • 平均时间复杂度 O(1)。
    • 适合快速查找。
  • 缺点
    • 空间开销大。

    • 哈希冲突可能退化为 O(n)。

适用场景

  • 计数:统计元素出现次数。

  • 去重:快速检查重复元素。

  • 映射:键值对存储。

使用技巧

  • 滑动窗口+哈希:解决子串/子数组问题。

  • 分组:按键聚合数据。

  • 优化空间:用数组替代哈希表(当键范围有限时)。

示例问题:LeetCode 3 - Longest Substring Without Repeating Characters

问题:最长无重复字符子串。


public int lengthOfLongestSubstring(String s) {

    HashMap<Character, Integer> map = new HashMap<>();

    int left = 0, max = 0;

    for (int right = 0; right < s.length(); right++) {

        char c = s.charAt(right);

        if (map.containsKey(c)) {

            left = Math.max(left, map.get(c) + 1);  // 跳到重复字符后一位

        }

        map.put(c, right);

        max = Math.max(max, right - left + 1);

    }

    return max;

}

技巧分析

  • 滑动窗口+哈希表,记录字符最后出现位置。

  • 动态调整左边界,避免重复。

6. 树(Tree)

详细描述

树是一种层次结构,包含节点和边,常见类型包括二叉树和二叉搜索树(BST)。

内部工作原理

  • 节点:包含值和指向子节点的指针。

  • BST:左子树 < 根 < 右子树,查找复杂度 O(h)(h 为树高)。

  • 平衡树:如 AVL、红黑树,保持 O(log n) 操作。

优缺点

  • 优点
    • 表示层次关系。
    • BST 支持快速查找。
  • 缺点
    • 维护复杂(平衡树)。

    • 退化时性能下降。

适用场景

  • 搜索:有序数据查找。

  • 递归问题:分治法。

  • 层次遍历:BFS。

使用技巧

  • 递归遍历:前序、中序、后序。

  • Morris 遍历:O(1) 空间中序遍历。

  • 范围检查:验证 BST。

示例问题:LeetCode 98 - Validate Binary Search Tree

问题:验证二叉搜索树。


public boolean isValidBST(TreeNode root) {

    return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);

}

private boolean isValidBST(TreeNode node, long min, long max) {

    if (node == null) return true;

    if (node.val <= min || node.val >= max) return false;

    return isValidBST(node.left, min, node.val) && isValidBST(node.right, node.val, max);

}

技巧分析

  • 递归检查每个节点值是否在范围内。

  • 用 long 避免整数溢出。

7. 堆(Heap)

详细描述

堆是一种完全二叉树,满足堆性质(如大顶堆:父节点 >= 子节点)。

内部工作原理

  • 数组实现:索引 i 的左子节点为 2i+1,右子节点为 2i+2。

  • 维护:插入上浮(O(log n)),删除下沉(O(log n))。

优缺点

  • 优点
    • 获取极值 O(1)。
    • 插入/删除 O(log n)。
  • 缺点
    • 不支持随机访问。

适用场景

  • 优先级队列:任务调度。

  • Top K:高频元素。

  • 中位数:双堆维护。

使用技巧

  • 小顶堆/大顶堆:选择合适类型。

  • 堆排序:构建堆后逐步调整。

  • 动态维护:实时更新堆。

示例问题:LeetCode 347 - Top K Frequent Elements

问题:前 K 个高频元素。


public int topKFrequent(int nums, int k) {

    HashMap<Integer, Integer> freq = new HashMap<>();

    for (int num : nums) freq.put(num, freq.getOrDefault(num, 0) + 1);

    PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) → freq.get(a) - freq.get(b));

    for (int num : freq.keySet()) {

        pq.offer(num);

        if (pq.size() > k) pq.poll();

    }

    int result = new int[k];

    for (int i = k - 1; i >= 0; i--) {

        result[i] = pq.poll();

    }

    return result;

}

技巧分析

  • 小顶堆维护 K 个元素。

  • 哈希表统计频率。

8. 图(Graph)

详细描述

图由节点和边组成,可分为有向图和无向图。

内部工作原理

  • 表示

    • 邻接表:List 或 Map 存储邻居。
    • 邻接矩阵:二维数组表示连接。
  • 遍历:DFS(递归/栈)、BFS(队列)。

优缺点

  • 优点
    • 表示复杂关系。
    • 支持多种算法。
  • 缺点
    • 实现复杂。

    • 空间/时间开销大。

适用场景

  • 路径问题:最短路径。

  • 连通性:检测连通分量。

  • 拓扑排序:依赖关系。

使用技巧

  • DFS:检测环、路径搜索。

  • BFS:最短路径。

  • 哈希表:记录访问状态。

示例问题:LeetCode 133 - Clone Graph

问题:克隆无向图。


public Node cloneGraph(Node node) {

    if (node == null) return null;

    HashMap<Node, Node> map = new HashMap<>();

    return clone(node, map);

}

private Node clone(Node node, HashMap<Node, Node> map) {

    if (map.containsKey(node)) return map.get(node);

    Node copy = new Node(node.val);

    map.put(node, copy);

    for (Node neighbor : node.neighbors) {

        copy.neighbors.add(clone(neighbor, map));

    }

    return copy;

}

技巧分析

  • DFS 递归克隆。

  • 哈希表避免重复。

9. 并查集(Union-Find)

详细描述

并查集管理不相交集合,支持合并(union)和查找(find)。

内部工作原理

  • 数组:parent[i] 表示 i 的父节点。
  • 优化
    • 路径压缩:find 时将节点指向根。

    • 按秩合并:小树并入大树。

优缺点

  • 优点
    • 接近 O(1) 的操作。
    • 高效连通性查询。
  • 缺点
    • 不支持动态删除。

适用场景

  • 连通分量:图的连通性。

  • 最小生成树:Kruskal 算法。

  • 等价关系:分组问题。

使用技巧

  • 路径压缩:提高查找效率。

  • 初始化:每个元素自成集合。

  • 计数:跟踪连通分量数。

示例问题:LeetCode 547 - Number of Provinces

问题:计算朋友圈数量。

public int findCircleNum(int isConnected) {

    int n = isConnected.length;

    int parent = new int[n];

    int count = n;

    for (int i = 0; i < n; i++) parent[i] = i;

    for (int i = 0; i < n; i++) {

        for (int j = i + 1; j < n; j++) {

            if (isConnected[i] == 1) {

                int rootI = find(parent, i);

                int rootJ = find(parent, j);

                if (rootI != rootJ) {

                    parent[rootI] = rootJ;

                    count--;

                }

            }

        }

    }

    return count;

}

private int find(int parent, int x) {

    if (parent[x] != x) parent[x] = find(parent, parent[x]);

    return parent[x];

}

技巧分析

  • 并查集合并连通城市。

  • 路径压缩优化查找。

10. Trie(前缀树)

详细描述

Trie 是一种树形结构,专门用于处理字符串集合。

内部工作原理

  • 节点:包含字符和子节点指针。

  • 路径:从根到节点表示字符串。

  • 标记:叶子节点标记字符串结束。

优缺点

  • 优点
    • 前缀查询快(O(m),m 为字符串长度)。
    • 适合字典查找。
  • 缺点
    • 空间开销大。

适用场景

  • 前缀匹配:自动补全。

  • 字符串查询:拼写检查。

  • 路由表:IP 匹配。

使用技巧

  • 数组/哈希:子节点用数组(固定字符集)或哈希(动态字符集)。

  • 压缩 Trie:合并单子节点,节省空间。

  • 多模式匹配:Aho-Corasick 算法。

示例问题:LeetCode 208 - Implement Trie

问题:实现 Trie。

class TrieNode {

    TrieNode children = new TrieNode[26];

    boolean isEnd;

}

class Trie {

    TrieNode root;

    public Trie() {

        root = new TrieNode();

    }

    public void insert(String word) {

        TrieNode node = root;

        for (char c : word.toCharArray()) {

            int idx = c - 'a';

            if (node.children[idx] == null) node.children[idx] = new TrieNode();

            node = node.children[idx];

        }

        node.isEnd = true;

    }

    public boolean search(String word) {

        TrieNode node = root;

        for (char c : word.toCharArray()) {

            int idx = c - 'a';

            if (node.children[idx] == null) return false;

            node = node.children[idx];

        }

        return node.isEnd;

    }

    public boolean startsWith(String prefix) {

        TrieNode node = root;

        for (char c : prefix.toCharArray()) {

            int idx = c - 'a';

            if (node.children[idx] == null) return false;

            node = node.children[idx];

        }

        return true;

    }

}

技巧分析

  • 数组存储子节点,快速定位。

  • 分离插入和查询逻辑。

总结

以上是对数组、链表、栈、队列、哈希表、树、堆、图、并查集和 Trie 的详细描述,包括其底层原理、优缺点、适用场景和使用技巧。每种数据结构都配有具体示例和代码,展示了其在算法问题中的高级应用。