Rust 挑戰 - 動手實現 HashMap 2

Rust 挑戰 - 動手實現 HashMap 1

讓我們從上一篇文章結束的地方繼續。我們要實現 get_mut() 方法,這應該與 get() 方法一樣,但編譯器不會讓我們簡單地將不可變變量更新爲可變變量。

解決方案是通過迭代器循環遍歷 entry,而不是老式的索引計數。由於我們需要從給定的索引開始,循環遍歷整個數組,以 index-1 結束,這本身有點棘手,但可以使用 Iterator::split_at_mut() 方法完成。這樣,我們就可以最終實現 get_mut() 方法了。

代碼如下:

impl<Key: Eq + Hash, Val> HashMap<Key, Val> {
    ......

    pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut Val>
    where
        Key: Borrow<Q>,
        Q: Eq + Hash,
    {
        if self.len() == 0 {
            return None;
        }
        let idx: usize = self.get_index(key);
        for entry in self.iter_mut_starting_at(idx) {
            match entry {
                Entry::Vacant ={
                    return None;
                }
                Entry::Occupied { key: k, val } if (k as &Key).borrow() == key ={
                    return Some(val);
                }
                _ ={}
            }
        }
        panic!("fatal: unreachable");
    }

    fn iter_mut_starting_at(&mut self, idx: usize) -> impl Iterator<Item = &mut Entry<Key, Val>> {
        let (s1, s2) = self.xs.split_at_mut(idx);
        s2.iter_mut().chain(s1.iter_mut())
    }

    ......
}

現在只剩下兩個方法:insert() 和 remove()。現在是討論 Entry 枚舉 Tombstone 變量有什麼作用的時候了。

我們的哈希衝突解決方案是從索引開始,探測被佔用 Entry 的鏈,直到找到匹配的鍵,或者直到找到一個空的鍵,這標誌着哈希衝突鍵的結束。

如果我們在鏈的中間找到匹配的鍵,並通過將其標記爲 Vacant 來刪除它,那麼我們將鏈分成了兩部分。下次我們搜索相同的鏈時,我們將無法搜索鏈的後半部分,因爲我們將到達中間的空 Entry。

這就是爲什麼我們不能簡單地刪除 Entry 並將其標記爲 Vacant。Tombstone 變量讓我們知道那裏什麼都沒有,但我們仍然需要繼續探索。

有了它,我們就可以實現 remove() 方法——如果我們找到一個具有匹配鍵的 Entry,我們將其標記爲 Tomtsbone 並遞減計數器。否則,它是一個 no-op。這聽起來很簡單,但在 Rust 中實現起來有點棘手。讓我們首先實現一個從 Entry 獲取值的輔助方法。

enum Entry<Key, Val> {
    Vacant,
    Tombstone,
    Occupied { key: Key, val: Val },
}

use std::mem::swap;

impl<Key, Val> Entry<Key, Val> {
    fn take(&mut self) -> Option<Val> {
        match self {
            Self::Occupied { key, val } ={
                let mut occupied = Self::Tombstone;
                swap(self, &mut occupied);
                if let Self::Occupied { key, val } = occupied {
                    Some(val)
                } else {
                    panic!("fatal: unreachable");
                }
            }
            _ => None,
        }
    }
}

有了這個,我們就可以實現 remove() 方法了。

impl<Key: Eq + Hash, Val> HashMap<Key, Val> {
    ......

    pub fn remove<Q>(&mut self, key: &Q) -> Option<Val>
    where
        Key: Borrow<Q>,
        Q: Eq + Hash,
    {
        if self.len() == 0 {
            return None;
        }
        let idx = self.get_index(key);
        let mut result = None;
        for entry in self.iter_mut_starting_at(idx) {
            match entry {
                Entry::Occupied { key: k, .. } if (k as &Key).borrow() == key ={
                    result = entry.take();
                    break;
                }
                Entry::Vacant ={
                    result = None;
                    break;
                }
                _ ={}
            }
        }
        result.map(|val| {
            self.n_occupied -= 1;
            val
        })
    }

    ......
}

現在只剩下最後一個方法:insert()。此方法首先檢查負載因子並在必要時調整數組的大小。完成所有這些之後,它將把鍵、值對插入到表中。讓我們創建一個輔助方法 insert_helper() 來執行插入部分,不需要檢查負載因子以調整大小。我們另外分別定義計算負載因子和調整大小的方法。

impl<Key: Eq + Hash, Val> HashMap<Key, Val> {
    ......

    pub fn insert(&mut self, key: Key, val: Val) -> Option<Val> {
        if self.load_factor() >= 0.75 {
            self.resize();
        }

        self.insert_helper(key, val)
    }

    fn load_factor(&self) ->  f64 {
        todo!()
    }

    fn resize(&mut self) {
        todo!()
    }

    fn insert_helper(&mut self, key: Key, val: Val) -> Option<Val> {
        todo!()
    }

    ......
}

load_factor() 方法很簡單,但是存在一種極端情況—我們使用空數組初始化哈希表,因此我們需要顯式地處理這種情況。至於 resize() 方法,我們希望將數組的大小增加一倍,除非當前的大小太小。最簡單的實現是創建一個哈希表的新實例,簡單地插入當前哈希表中的每個元素,最後交換這兩個哈希表。

fn load_factor(&self) ->  f64 {
    if self.xs.is_empty() {
        1.0
    } else {
        1.0 - self.n_vacant as f64 / self.xs.len() as f64
    }
}

fn resize(&mut self) {
    let new_size = std::cmp::max(64, self.xs.len() * 2);
    let mut new_table = Self {
        xs: (0..new_size).map(|_| Entry::Vacant).collect(),
        n_occupied: 0,
        n_vacant: new_size,
    };
    for entry in std::mem::take(&mut self.xs) {
        if let Entry::Occupied { key, val } = entry {
            new_table.insert_helper(key, val);
        }
    }

    swap(self, &mut new_table);
}

好了,現在只剩下 insert_helper() 方法。從概念上講,這並不太難。我們探測 Entry 並查找匹配的鍵。如果找到,則覆蓋該值。如果沒有,插入到空 Entry,不要忘記相應地更新計數器。

impl<Key, Val> Entry<Key, Val> {
    ......

    fn replace(&mut self, mut x: Val) -> Option<Val> {
        match self {
            Self::Occupied { key, val } ={
                swap(&mut x, val);
                Some(x)
            }
            _ => None,
        }
    }
}
impl<Key: Eq + Hash, Val> HashMap<Key, Val> {
    ......
    fn insert_helper(&mut self, key: Key, val: Val) -> Option<Val> {
        let idx = self.get_index(&key);
        let mut result = None;
        for entry in self.iter_mut_starting_at(idx) {
            match entry {
                Entry::Occupied { key: k, .. } if (k as &Key).borrow() == &key ={
                    result = entry.replace(val);
                    break;
                }
                Entry::Vacant ={
                    *entry = Entry::Occupied { key, val };
                    break;
                }
                _ ={}
            }
        }
        if result.is_none() {
            self.n_occupied += 1;
            self.n_vacant -= 1;
        }
        result
    }
    ......
}

現在我們已經完成了 Rust 中 HashMap 的實現。

在下一篇文章中,我將編寫一些單元測試和基準測試函數來衡量我們的實現與標準庫 std::collections::HashMap 相比有多快或多慢。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/L1JaLOHuyMOguR3IHUohBA