java 算法中常用的数据结构及其使用场景

64 阅读8分钟

快速结论(先看这一段)

  • 想要随机访问/数组式存储:ArrayList(默认选择)。
  • 想做队列/栈(高性能):ArrayDeque(优于 LinkedList 作队列/栈)。
  • 想做链式插入/删除(特别是中间频繁操作):LinkedList(但实际很少是最佳选择)。
  • 想做优先级操作(Top-K、Dijkstra):PriorityQueue(堆)。
  • 想做键值查找:HashMap(常用)、TreeMap(需排序)、LinkedHashMap(按插入/访问顺序,方便实现 LRU)。
  • 并发场景用 ConcurrentHashMapCopyOnWriteArrayList、阻塞队列等。

面试速查表(简短)

  • 随机访问快 → ArrayList
  • 栈 / 队列 → ArrayDeque(非并发)
  • 双端队列需并发 → LinkedBlockingDeque 或并发队列实现
  • 优先级 / Top-K → PriorityQueue(堆)
  • 无序键值 → HashMap(预设容量以减少扩容)
  • 保持插入/访问顺序 → LinkedHashMap(LRU)
  • 有序键 / 范围查询 → TreeMap(红黑树)
  • 并发 Map → ConcurrentHashMap(非阻塞弱一致迭代)
  • 读多写少的线程安全列表 → CopyOnWriteArrayList

ArrayList(动态数组)

是什么 / 实现

  • 基于动态数组(Object[] elementData)实现。默认初始容量 10(空构造返回空数组,在首次 add 时扩容到 10)。扩容策略:newCapacity = oldCapacity + (oldCapacity >> 1),即约 1.5x 摆动增长(摊销 O(1) 插入)。

常用操作复杂度

  • get(i):O(1)
  • add(e)(尾插,摊销):O(1)
  • add(index, e)remove(index):O(n)(需要移动元素)
  • containsindexOf:O(n)
  • 迭代(顺序访问):极快(连续内存,CPU 缓存友好)

重要内部细节 & 易错点

  • 扩容会 System.arraycopy,在大数组场景下可能触发 GC/复制开销。可以用 ensureCapacity 预分配减少扩容次数。
  • 使用自定义对象作为元素时,不要忘了 equals/hashCode 的实现会影响 contains 等操作。
  • 不是线程安全的;并发修改时迭代会抛 ConcurrentModificationException(fail-fast)。
  • trimToSize() 可以将内部数组裁剪到当前 size,节省内存(但可能影响后续扩容性能)。

典型场景 & 优点

  • 随机访问频繁、尾部插入多、内存/缓存效率重要 → ArrayList 最优。

示例

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
int v = list.get(1); // O(1)
list.add(1, 99);     // 在中间插入,O(n)

实战建议

  • 若知道元素个数,new ArrayList<>(expectedSize) 预设容量。
  • 需要线程安全时用 Collections.synchronizedList(简单)或 CopyOnWriteArrayList(读多写少)。
  • 面试问点:扩容因子、为什么 get 快(连续内存,缓存命中)。

LinkedList(双向链表,同时实现 List 和 Deque)

是什么 / 实现

  • 双向链表,节点保存 item, prev, next。实现了 ListDeque 接口,因此既可当链表也可当队列/栈使用。

常用操作复杂度

  • addFirst / addLastremoveFirst / removeLast:O(1)
  • get(index)add(index, e):O(n)(定位节点需要从头/尾遍历)
  • contains:O(n)

重要细节 & 易错点

  • 节点对象开销较大(每个节点有 3 引用),内存占用比 ArrayList 高。
  • Iteratorremove() 在链表中通常是 O(1)(因为 iterator 保存当前节点引用)。
  • 作为队列/双端队列功能完备,但通常 ArrayDeque 更快(内存更少、局部性更好)。
  • 不建议频繁按索引访问或在随机位置插入大量元素(除非需要通过迭代器在当前位置插入/删除)。

典型场景

  • 需要频繁在两端插入/删除,或需要在迭代过程中基于 iterator 做 remove/add 时使用。

示例

LinkedList<Integer> l = new LinkedList<>();
l.addFirst(1);
l.addLast(2);
int head = l.removeFirst();
for (Iterator<Integer> it = l.iterator(); it.hasNext();) {
    if (it.next() == 2) it.remove(); // 安全且 O(1)
}

实战建议

  • 大多数“队列/栈”场景优先考虑 ArrayDeque
  • 需要保存插入顺序并频繁从中间用 iterator 修改时,可以考虑 LinkedList

ArrayDeque(强烈推荐用于栈/队列)

是什么 / 实现

  • 基于循环数组(环形缓冲区)实现的双端队列。头尾索引移动而不是整体移动元素。不允许 null 元素null 用作返回值标识)。

常用操作复杂度

  • offer, poll, peek, push, pop:O(1) 摊销
  • 扩容(当满)会进行数组复制(通常以 2 倍扩容或类似策略),但很少发生

重要细节 & 易错点

  • 不支持 nulladd(null)NullPointerException)。
  • 相较于 LinkedList 更高效(更低内存、局部性更好)。
  • 作为栈请用 push/pop;不要用老旧的 StackStack 是同步的且性能差)。

典型场景

  • BFS 队列、DFS 非递归栈、需要双端操作的场景。

示例

ArrayDeque<Integer> dq = new ArrayDeque<>();
dq.offer(1); // 队尾入队
int x = dq.poll(); // 队头出队
dq.push(5); // 当栈使用
int top = dq.pop();

实战建议

  • 一般把 ArrayDeque 作为默认的队列/栈实现。
  • 非线程安全,若并发需要阻塞队列或并发队列(见后面)。

PriorityQueue(堆 —— 优先队列)

是什么 / 实现

  • 基于数组二叉堆实现(最小堆,默认按自然顺序)。可以传入 Comparator 指定顺序。底层为 Object[],插入/删除按堆性质上浮/下沉。

常用操作复杂度

  • offer / add:O(log n)
  • poll:O(log n)
  • peek:O(1)
  • remove(Object)contains(Object):O(n)(需线性查找)

重要细节 & 易错点

  • 迭代器遍历不是有序的(如果你需要按优先级访问,应不断 poll())。
  • 不允许 null
  • Comparator/元素的 Comparable 必须一致(否则会抛异常)。
  • PriorityQueue 不是线程安全的。

典型场景

  • Top-K(保留 K 个最大/最小元素)、Dijkstra、事件调度(按时间排序)、合并 K 个已排序流等。

示例(Top-K)

int k = 3;
PriorityQueue<Integer> pq = new PriorityQueue<>(); // 小顶堆
for (int v : nums) {
    pq.offer(v);
    if (pq.size() > k) pq.poll(); // 保留 k 个最大
}

面试/实战提示

  • 要实现大顶堆:new PriorityQueue<>(Comparator.reverseOrder())
  • 想在 O(n) 时间内取 top-k(k 小于 n)可使用小顶堆;对全部排序则 O(n log n)。

HashMap / HashSet(哈希表)

是什么 / 实现

  • HashMap 基于哈希表(Java 8 起为数组 + 链表/红黑树混合结构实现)。键通过 hashCode() 映射到桶(bucket),在桶内查找 equals()。当单个桶中链表长度超过阈值(默认 8),链表会转为红黑树以避免 O(n) 退化。
  • HashSet 底层使用 HashMap 的 key 存储值。

常用操作复杂度

  • get / put / remove:平均 O(1),最坏 O(log n)(链表转树后)或最坏 O(n)(早期极端哈希冲突)。
  • 迭代成本 O(n)。

重要细节 & 易错点

  • hashCode / equals:自定义类型作 key 时必须正确覆写 hashCode()equals(),否则查找失败。
  • 初始容量和 loadFactor(默认 0.75)决定何时扩容:threshold = capacity * loadFactor。扩容会重新分配数组并 rehash,代价大。可通过 new HashMap<>(expectedCapacity) 预设。
  • null key 是允许的(仅一个 null key),null value 也允许。
  • 并发修改(多线程)不是线程安全的;并发使用请用 ConcurrentHashMap(注意 ConcurrentHashMap 不允许 null key/value)。
  • Java 8 以后 putIfAbsent / computeIfAbsent / merge 等原子方法很适合并发逻辑(但本身不是线程安全,需要注意)。

常见 API

map.putIfAbsent(k, v);
map.computeIfAbsent(k, key -> new ArrayList<>());
map.merge(k, 1, Integer::sum);
int val = map.getOrDefault(k, 0);

示例(计数)

Map<Integer, Integer> freq = new HashMap<>();
for (int x : nums) {
    freq.put(x, freq.getOrDefault(x, 0) + 1);
}

实战建议

  • 预估元素数量并用 new HashMap<>(capacity) 初始化来减少扩容。
  • 面试常问:负载因子、扩容成本、null 键行为、hashCode/equals 的要求、为什么 HashMap.get 是 O(1)。

LinkedHashMap(保持插入或访问顺序)

是什么 / 实现

  • LinkedHashMapHashMap 基础上维护一条双向链表来记录条目的插入顺序(或最近访问顺序,当 accessOrder=true 时)。
  • 典型用途:实现 LRU 缓存(通过重写 removeEldestEntry)。

常见用法(LRU)

class LRUCache<K,V> extends LinkedHashMap<K,V> {
    private final int capacity;
    LRUCache(int cap) {
        super(cap, 0.75f, true); // accessOrder=true
        this.capacity = cap;
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return size() > capacity;
    }
}

重要细节

  • accessOrder=true 时每次 get 会把 entry 移到链表尾(最近使用)。
  • 插入顺序和访问顺序可选。
  • 仍然继承自 HashMap 的复杂度特性(平均 O(1))。

实战建议

  • 需要有序迭代(插入/访问顺序)或实现简单缓存用 LinkedHashMap 非常方便。

TreeMap / TreeSet(基于红黑树的有序结构)

是什么 / 实现

  • 基于红黑树(自平衡二叉搜索树)实现,键按自然顺序或提供的 Comparator 排序。TreeMap 实现 NavigableMap,提供诸如 floorKey, ceilingKey, subMap 等范围查询方法。

常用操作复杂度

  • get, put, remove:O(log n)

重要细节 & 易错点

  • 不能使用 null 键(自然顺序比较器会 NPE);若使用自定义 comparator,需要保证比较器一致且稳定。
  • 适用于需要按顺序检索或范围查询的场景(比如:按时间窗口查找、排行榜区间查询)。

示例

TreeMap<Integer, String> tm = new TreeMap<>();
tm.put(5, "a");
Map.Entry<Integer, String> e = tm.floorEntry(4); // <= 4 的最大键
SortedMap<Integer, String> sub = tm.subMap(1, true, 6, false);

实战建议

  • 当你需要排序的键或需要前驱/后继查找时用 TreeMap
  • 面试常问:红黑树的性质、为什么操作是 O(log n)。

并发/线程安全集合(常见选择)

ConcurrentHashMap

  • Java 8 以后实现基于 CAS + 链表/树 + synchronized(在部分情况下)来保证并发安全。提供高并发读写性能。不允许 null key/value
  • 迭代器是弱一致(weakly consistent):可见一部分后续修改,但不会抛 ConcurrentModificationException

CopyOnWriteArrayList

  • 对写操作做“写时复制”,适合读多写少场景(如事件监听器集合)。迭代器是快照,读不会被写影响。写操作代价昂贵(复制整个数组)。

阻塞队列

  • ArrayBlockingQueue(有界、基于数组,适合生产者消费者)
  • LinkedBlockingQueue(可选界限,节点基于链表)
  • SynchronousQueue(无缓冲区,直接交接)
  • DelayQueuePriorityBlockingQueue 等特殊队列

非阻塞队列

  • ConcurrentLinkedQueue:基于无锁算法的线程安全队列(高并发下优选)。

实战建议

  • 并发 Map 首选 ConcurrentHashMap;并发队列选 LinkedBlockingQueueConcurrentLinkedQueue 依据需求(阻塞 vs 非阻塞)。

其他零碎但重要的点(面试常问)

  • Stack / VectorStack 继承 Vector(同步,老旧),建议用 ArrayDeque 代替 Stack
  • Collections.unmodifiableXXX:返回不可变视图(非深拷贝),底层集合变更会影响视图。
  • Arrays vs CollectionsArrays.asList 返回固定大小的视图(不支持 add/remove),经常踩坑。
  • Iterator 的 fail-fast 机制:大多数集合在结构性修改时检测并抛 ConcurrentModificationException,并发集合或 CopyOnWriteArrayList 不抛。
  • 内存/性能:链表节点对象开销大(多指针),数组结构更节省、缓存友好。大数据量下考虑 primitive 专用集合(fastutil/Trove)提升性能。
  • null 在不同集合中的处理HashMap/ArrayList/LinkedList 可含 null;ArrayDeque/PriorityQueue/ConcurrentHashMap 不允许 null。