【密码学】一文读懂MurMurHash第一版
MurMurHash
我来填坑了,之前说来讲一下MurMurHash算法,然后本文来简单描述一下这个算法的主要过程。MurMurHash这个哈希算法在2011年3月1日第一版被提出来,第一版呢最终输出的是一个32bit的哈希值,第一版已经不推荐使用了,本文先来讲一下第一版的算法过程,由于我只找到了这个第一版算法的代码,没有找到对应的参考文献,所以本文的算法流程是我根据Google提供的代码来说的,有哪里说的不对的地方,也欢迎读者指正。
原始代码
Google原始的第一版的代码是用C写的,为了方便读者看到原汁原味的代码,这里贴一下(不是为了凑字数)。
//----------------------------------------------------------------------------- // MurmurHash, by Austin Appleby // Note - This code makes a few assumptions about how your machine behaves - // 1. We can read a 4-byte value from any address without crashing // 2. sizeof(int) == 4 // And it has a few limitations - // 1. It will not work incrementally. // 2. It will not produce the same results on little-endian and big-endian // machines. // Changing the seed value will totally change the output of the hash. // If you don't have a preference, use a seed of 0. unsigned int MurmurHash ( const void * key, int len, unsigned int seed ) { // 'm' and 'r' are mixing constants generated offline. // They're not really 'magic', they just happen to work well. const unsigned int m = 0xc6a4a793; const int r = 16; // Initialize the hash to a 'random' value unsigned int h = seed ^ (len * m); // Mix 4 bytes at a time into the hash const unsigned char * data = (const unsigned char *)key; while(len >= 4) { h += *(unsigned int *)data; h *= m; h ^= h >> r; data += 4; len -= 4; } // Handle the last few bytes of the input array switch(len) { case 3: h += data[2] << 16; case 2: h += data[1] << 8; case 1: h += data[0]; h *= m; h ^= h >> r; }; // Do a few final mixes of the hash to ensure the last few // bytes are well-incorporated. h *= m; h ^= h >> 10; h *= m; h ^= h >> 17; return h; }
可以发现,整个算法的代码还是比较少的,通过上面代码我们可以看出,这里输入是原始的字节流,输出是最终的哈希值是32bit的数字。
算法结构
根据代码里面的注释,里面说了m和r并不是魔数,只不过碰巧做得很好,所以里面也没有说为啥要取这两个数。
MurMurHash算法结构
这个算法整体来说并不算特别复杂,比起之前我们讲过的密码学安全的哈希函数,这个算法流程可真是太短了,最终生成的哈希值也只有32位,对于后面的版本实际上也有128位的,但是这个只有32位了,其他版本后面在讲,请叫我挖坑小王子。
根据上图简单描述一下算法的结构,三个初始值,然后初始化h, 之后就是针对每个消息的分块单独去处理,最后一个分块,如果剩余的不足32bit, 也就是4个字节,分别对于剩余的三个字节单独处理,最后输出得到最终的哈希值。
代码实现
虽然上面Google给出了c的代码,这里还是学习一下,我还是用rust来实现一下吧。
use std::convert::TryInto; pub struct MurmurHashV1 {} impl MurmurHashV1 { pub fn hash(key: &[u8], seed: u32) -> u32 { let m = 0xc6a4a793; let r = 16; let len = key.len(); let mut h = seed ^ (len.wrapping_mul(m) as u32); let remain = len % 4; let mut offset = len; if remain > 0 { offset = len - remain; } let data = key[..offset].chunks(4).map(|it| u32::from_le_bytes(it.try_into().unwrap())).collect::<Vec<u32>>(); for item in data { h = h.wrapping_add(item); h = h.wrapping_mul(m as u32); h ^= h >> r; } if remain >= 3 { h = h.wrapping_add((key[offset + 2] as u32).wrapping_shl(16)); } if remain >= 2 { h = h.wrapping_add((key[offset + 1] as u32).wrapping_shl(8)); } if remain >= 1 { h = h.wrapping_add(key[offset] as u32); h = h.wrapping_mul(m as u32); h ^= h >> r; } h = h.wrapping_mul(m as u32); h ^= h >> 10; h = h.wrapping_mul(m as u32); h ^= h >> 17; h } } #[cfg(test)] mod tests { use crate::MurmurHashV1; #[test] fn it_works() { let result = MurmurHashV1::hash("1234567".as_bytes(), 0); println!("hash(1234567) = {}", result); } }
因为最近go不是更新了支持泛型,感觉是时候了解一下go了,然后这个算法打算也用go来写一下(后面有可能,一些算法也会用go来实现一下,取决于我go的学习进度了,不保证一定有,rust版本后面的大概率都会有),虽然感觉这个我写起来一股c味,还是贴一下吧,原谅我这现学现卖的go的代码。
package main import ( "encoding/binary" "fmt" ) func murmurhash(key []byte, seed uint32) uint32 { m := uint32(0xc6a4a793) r := 16 keyLen := uint32(len(key)) var h = seed ^ (keyLen * m) offset := 0 e := binary.LittleEndian for keyLen >= 4 { h += e.Uint32(key[offset : offset+4]) h *= m h ^= h >> r offset += 4 keyLen -= 4 } switch keyLen { case 3: h += uint32(key[offset+2]) << 16 fallthrough case 2: h += uint32(key[offset+1]) << 8 fallthrough case 1: h += uint32(key[offset]) h *= m h ^= h >> r } h *= m h ^= h >> 10 h *= m h ^= h >> 17 return h } func main() { fmt.Println(murmurhash([]byte("1234567"), 0)) }