bigywater.tech
Rust · 60

BTreeMap 与 HashMap 对比

选 BTreeMap 还是 HashMap,取决于要不要「有序」。这篇不讲抽象的 O(log n) 与 O(1),而是把有序遍历、范围查询、键的 trait 要求、插入与查找耗时、范围查询耗时和分配形态都真实跑了出来。

3.9×
100 万个随机键插入:BTreeMap 比 HashMap 慢的倍数
2158×
范围查询:HashMap 全表过滤比 BTreeMap::range 慢的倍数
167 对 10
存 1000 个键值对的 alloc 次数(BTreeMap 对 HashMap)

有序遍历与范围

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    遍历: [10, 20, 30, 40, 50, 60, 70, 80, 90]
    range(30..=60): [30, 40, 50, 60]
    range(..30): [10, 20]   range(75..): [80, 90]
    first_key_value Some((10, 20))  last_key_value Some((90, 180))
    pop_first Some((10, 20))  pop_last Some((90, 180))  剩 7 个
    小于等于 45 的最大键: Some(40)

HashMap 没有 range

实测 · 本机执行输出
  源码:
      1 | use std::collections::HashMap;
      2 | fn main() { let m: HashMap<i32, i32> = HashMap::new(); let _ = m.range(1..5); }
  编译:失败
    error[E0599]: no method named `range` found for struct `HashMap<K, V, S, A>` in the current scope
     --> <实验目录>/src/u60b.rs:2:66
      |
    2 | fn main() { let m: HashMap<i32, i32> = HashMap::new(); let _ = m.range(1..5); }
      |                                                                  ^^^^^ method not found in `HashMap<i32, i32>`
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0599`.

HashMap 的哈希与扩容见HashMap 的哈希与扩容。

键的要求

实测 · 本机执行输出
  源码:
      1 | use std::collections::{BTreeMap, HashMap};
      2 | #[derive(PartialEq, Eq, Hash, Debug)] struct P(i32);
      3 | fn main() {
      4 |     let mut h = HashMap::new(); h.insert(P(1), "hash ok");
      5 |     let mut b = BTreeMap::new(); b.insert(P(1), "btree");
      6 |     println!("{:?} {:?}", h, b);
      7 | }
  编译:失败
    error[E0277]: the trait bound `P: Ord` is not satisfied
        --> <实验目录>/src/u60c.rs:5:36
         |
       5 |     let mut b = BTreeMap::new(); b.insert(P(1), "btree");
         |                                    ^^^^^^ the trait `Ord` is not implemented for `P`
         |
    note: required by a bound in `BTreeMap::<K, V, A>::insert`
        --> <rust 标准库源码>/library/alloc/src/collections/btree/map.rs:1047:12
         |
    1045 |     pub fn insert(&mut self, key: K, value: V) -> Option<V>
         |            ------ required by a bound in this associated function
    1046 |     where
    1047 |         K: Ord,
         |            ^^^ required by this bound in `BTreeMap::<K, V, A>::insert`
    help: consider annotating `P` with `#[derive(Ord)]`
         |
       2 | #[derive(PartialEq, Eq, Hash, Debug)] #[derive(Ord)]
         |                                       ++++++++++++++
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0277`.
  编译:成功
  运行(退出码 0):
    自定义 Ord(反序)的遍历: [Rev(3), Rev(2), Rev(1)]

插入与查找的速度

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    n=    1000: 插入 HashMap     0.06 ms / BTreeMap     0.10 ms   查找 HashMap     0.01 ms / BTreeMap     0.07 ms
    n=  100000: 插入 HashMap     2.58 ms / BTreeMap     9.76 ms   查找 HashMap     1.03 ms / BTreeMap     7.78 ms
    n= 1000000: 插入 HashMap    30.71 ms / BTreeMap   119.13 ms   查找 HashMap    26.05 ms / BTreeMap   128.90 ms

耗时是本机 release 模式的实测(取 3 次最小),键为 xorshift 生成的随机 u64;键类型、值大小或机器不同,倍数会变。

范围查询

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    BTreeMap::range 找到 143 个,用时 1.209µs
    HashMap 全表过滤 找到 143 个,用时 2.609166ms

分配形态

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    BTreeMap<u64,u64> 1000 个:alloc 167 次,最大单次 544 字节
    HashMap<u64,u64>  1000 个:alloc 10 次,最大单次 34824 字节

分步演示

BTreeMap 对 HashMap
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。计时 release + black_box
    • 没有验证:字符串键下的对比、顺序插入(升序键)时的 BTreeMap 速度、并发场景、BTreeMap 节点的具体容量(只看到单次最大分配大小)、其它哈希器(需要下载 crate)
    京ICP备2026067502号-1京公网安备11010502063487号