Swift下Dictionary背后的魔法

Swift中Dictionary的底层原理与哈希魔法


用最通俗的语言,描述最难懂的技术。

01

前情描述

此前介绍了Array,本篇将讲解Dictionary虽然两者的设计思路有相似之处,但Dictionary的核心在于Hash原理。

02

Dictionary是什么

DictionarySwift下存储键值的对象。

03

Dictionary内存查看

依然是在测试项目中运行测试代码:

var dict = ["1" : "Dog","2" : "Cat","3" : "fish"]
withUnsafePointer(to: &dict) {
    print($0)
}
print("end")

print("end")打上断点,执行:

可以看到Dictionary的内存地址0x00000001005469b0应该是在堆上,查看该地址后,发现和Array一样,是一个类结构HeapObject。除了能看到存储的字典元素个数3以外,其他的keyvalue同样无法找到,所以下面的内容就是看keyvalue存在哪里,以及Dictionary的底层原理。

04

Dictionary底层原理

4.1 Dictionary结构

直接看源码中对Dictionary的定义:

// File: Dictionary.swift, line: 388


@frozen
publicstruct Dictionary<Key: Hashable, Value> {
/// The element type of a dictionary: a tuple containing an individual
/// key-value pair.
public typealias Element = (key: Key, value: Value)

  @usableFromInline
  internal var _variant: _Variant

  @inlinable
  internal init(_native: __owned _NativeDictionary<Key, Value>) {
    _variant = _Variant(native: _native)
  }

#if _runtime(_ObjC)
  @inlinable
  internal init(_cocoa: __owned __CocoaDictionary) {
    _variant = _Variant(cocoa: _cocoa)
  }

// ...
}

可以看到源码中只有一个_variant属性,它是_Variant类型,继续看_Variant类型的属性,该属性在DictionaryExtension中定义:

// File: DictionaryVariant.swift, line: 33


extension Dictionary {
  @usableFromInline
  @frozen
  internal struct _Variant {
    @usableFromInline
    internal var object: _BridgeStorage<__RawDictionaryStorage>
      
  // ...
  }
}

_Variant也只有一个属性object,类型是_BridgeStorage的初始化就是一个强转赋值,这个和Array那一样的逻辑,所以得看传进来的是什么。

我们回过去看Dictionary的初始化方法,如果我们调用的是Swift原生的初始化方法,那么会执行init(_native: __owned _NativeDictionary<Key, Value>)方法,那么传给_Variant的就是_NativeDictionary,所以Dictionary就是_NativeDictionary。

接下来看_NativeDictionary的定义:

// File: NativeDictionary.swift, line: 15


@usableFromInline
@frozen
internal struct _NativeDictionary<Key: Hashable, Value> {
  @usableFromInline
  internal typealias Element = (key: Key, value: Value)

/// See this comments on __RawDictionaryStorage and its subclasses to
/// understand why we store an untyped storage here.
  @usableFromInline
  internal var _storage: __RawDictionaryStorage

/// Constructs an instance from the empty singleton.
  @inlinable
  internal init() {
    self._storage = __RawDictionaryStorage.empty
  }

/// Constructs a dictionary adopting the given storage.
  @inlinable
  internal init(_ storage: __owned __RawDictionaryStorage) {
    self._storage = storage
  }

  @inlinable
  internal init(capacity: Int) {
    if capacity == 0 {
      self._storage = __RawDictionaryStorage.empty
    } else {
      self._storage = _DictionaryStorage<Key, Value>.allocate(capacity: capacity)
    }
  }

// ...
}

_NativeDictionary同样有一个属性_storage,是__RawDictionaryStorage类型的,然后猜__RawDictionaryStorage是类类型的,找下__RawDictionaryStorage的定义:

// File: DictionaryStorage.swift, line: 22


/// An instance of this class has all `Dictionary` data tail-allocated.
/// Enough bytes are allocated to hold the bitmap for marking valid entries,
/// keys, and values. The data layout starts with the bitmap, followed by the
/// keys, followed by the values.
// NOTE: older runtimes called this class _RawDictionaryStorage. The two
// must coexist without a conflicting ObjC class name, so it was
// renamed. The old name must not be used in the new runtime.
@_fixed_layout
@usableFromInline
@_objc_non_lazy_realization
internal class __RawDictionaryStorage: __SwiftNativeNSDictionary {
// NOTE: The precise layout of this type is relied on in the runtime to
// provide a statically allocated empty singleton.  See
// stdlib/public/stubs/GlobalObjects.cpp for details.

/// The current number of occupied entries in this dictionary.
  @usableFromInline
  @nonobjc
  internal final var _count: Int

/// The maximum number of elements that can be inserted into this set without
/// exceeding the hash table's maximum load factor.
  @usableFromInline
  @nonobjc
  internal final var _capacity: Int

/// The scale of this dictionary. The number of buckets is 2 raised to the
/// power of `scale`.
  @usableFromInline
  @nonobjc
  internal final var _scale: Int8

/// The scale corresponding to the highest `reserveCapacity(_:)` call so far,
/// or 0 if there were none. This may be used later to allow removals to
/// resize storage.
///
/// FIXME: <rdar://problem/18114559> Shrink storage on deletion
  @usableFromInline
  @nonobjc
  internal final var _reservedScale: Int8

// Currently unused, set to zero.
  @nonobjc
  internal final var _extra: Int16

/// A mutation count, enabling stricter index validation.
  @usableFromInline
  @nonobjc
  internal final var _age: Int32

/// The hash seed used to hash elements in this dictionary instance.
  @usableFromInline
  internal final var _seed: Int

/// A raw pointer to the start of the tail-allocated hash buffer holding keys.
  @usableFromInline
  @nonobjc
  internal final var _rawKeys: UnsafeMutableRawPointer

/// A raw pointer to the start of the tail-allocated hash buffer holding
/// values.
  @usableFromInline
  @nonobjc
  internal final var _rawValues: UnsafeMutableRawPointer
    
// ...
    
}

__RawDictionaryStorage父类没有属性,所以整个__RawDictionaryStorage的属性就这些了。这样的话Dictionary的属性就大概清楚了。

4.2 rawKeys和rawValues

我们很快就可以定位到keysvalues,但是它们定义的都是指针,那是不是拿到指针指向的地址,就能拿到我们想要的值,继续使用lldb命令打印的地址更大一些看内存,同时再增加一组keyvalue:

打印的范围增大之后,确实发现了31,32,33,34表示的就是字符串1,2,3,4,但是问题来了,并没有紧挨着靠着起始位置,顺序也是乱的。要解释这个就得说文章开头所说的Hash原理了,直接参考延伸参考文档,或者自己搜索即可。

Swift源码中,Dictionary的底层实现依赖于__RawDictionaryStorage。它不仅存储了keyvalue,还维护了哈希表的元数据,如容量、已用槽位、冲突计数等。通过桥接存储(_BridgeStorage),Swift可以高效地在原生和Cocoa字典间切换。

4.3 Dictionary的初始化函数

我们可以通过生成SIL文件查看Dictionary的初始化函数,具体生成方法参考Array 文章:

%39 = function_ref @Swift.Dictionary.init(dictionaryLiteral: (A, B)...) -> [A : B] : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@owned Array<(τ_0_0, τ_0_1)>, @thin Dictionary<τ_0_0, τ_0_1>.Type) -> @owned Dictionary<τ_0_0, τ_0_1> // user: %40

所以我们在源码中运行代码,加上断点调试:

// File: Dictionary.swift, line: 819


  @inlinable
  @_effects(readonly)
  @_semantics("optimize.sil.specialize.generic.size.never")
public init(dictionaryLiteral elements: (Key, Value)...) {
    // 生成一个_NativeDictionary对象
    let native = _NativeDictionary<Key, Value>(capacity: elements.count)
      // 遍历所有键值对
    for (key, value) in elements {
      // 在native对象中调用find方法,寻找桶(bucket),found是bool值
      let (bucket, found) = native.find(key)
        // 初始化开始,如果found是yes,说明有重复元素,编译器会报错,报错信息如下字符串
      _precondition(!found, "Dictionary literal contains duplicate keys")
        // 把键值对插入桶中
      native._insert(at: bucket, key: key, value: value)
    }
    // 自己的初始化方法
    self.init(_native: native)
  }
}

我们看下Bucket结构:

// File: HashTable.swift, line: 123

internal struct Bucket {
  @usableFromInline
  internal var offset: Int
}

直接理解为var offset: Int,相当于Array中的下标。

找到初始化方法后,我们就需要明确目标,找到核心HashTable在哪,同样跟Array类似,过掉断点你会在_DictionaryStorage申请堆空间的时候,看到如下代码:

// File: DictionaryStorage.swift, line: 437


static internal func allocate(
  scale: Int8,
  age: Int32?,
  seed: Int?
) -> _DictionaryStorage {
// The entry count must be representable by an Int value; hence the scale's
// peculiar upper bound.
  _internalInvariant(scale >= 0 && scale < Int.bitWidth - 1)

  let bucketCount = (1 as Int) &<< scale
  let wordCount = _UnsafeBitset.wordCount(forCapacity: bucketCount)
  let storage = Builtin.allocWithTailElems_3(
    _DictionaryStorage<Key, Value>.self,
    wordCount._builtinWordValue, _HashTable.Word.self,
    bucketCount._builtinWordValue, Key.self,
    bucketCount._builtinWordValue, Value.self)

  let metadataAddr = Builtin.projectTailElems(storage, _HashTable.Word.self)
  let keysAddr = Builtin.getTailAddr_Word(
    metadataAddr, wordCount._builtinWordValue, _HashTable.Word.self,
    Key.self)
  let valuesAddr = Builtin.getTailAddr_Word(
    keysAddr, bucketCount._builtinWordValue, Key.self,
    Value.self)
  storage._count = 0
  storage._capacity = _HashTable.capacity(forScale: scale)
  storage._scale = scale
  storage._reservedScale = 0
  storage._extra = 0

if let age = age {
    storage._age = age
  } else {
    // The default mutation count is simply a scrambled version of the storage
    // address.
    storage._age = Int32(
      truncatingIfNeeded: ObjectIdentifier(storage).hashValue)
  }

  storage._seed = seed ?? _HashTable.hashSeed(for: storage, scale: scale)
  storage._rawKeys = UnsafeMutableRawPointer(keysAddr)
  storage._rawValues = UnsafeMutableRawPointer(valuesAddr)

// Initialize hash table metadata.
  storage._hashTable.clear()
return storage
}
}

很好,我们又一次看到了allocWithTailElems_,这个函数的意思是除了给当前对象本身开辟堆空间,也会为尾部跟着的元素开辟新的空间,所以它们是连着的,这个等你读完HashTable可以进行验证。_DictionaryStorage,_HashTable.WordKey,Value,在内存上紧挨的。

我们也看到了桶(Bucket)的个数,也就是HashTable的模:

let bucketCount = (1 as Int) &<< scale
storage._scale = scale

bucketCount是由scale位移获得,而scale也赋值给了_DictionaryStoragescale,所以我们在前面_DictionaryStorage内存结构中获取scale后,通过同样的运算就可以获得bucketCount。

继续看HashTable。

4.4 HashTable

// File: HashTable.swift, line: 19

@usableFromInline
@frozen
internal struct _HashTable {
@usableFromInline
internal typealias Word = _UnsafeBitset.Word

@usableFromInline
internal var words: UnsafeMutablePointer<Word>

@usableFromInline
internal let bucketMask: Int

@inlinable
@inline(__always)
internal init(words: UnsafeMutablePointer<Word>, bucketCount: Int) {
  _internalInvariant(bucketCount > 0 && bucketCount & (bucketCount - 1) == 0,
    "bucketCount must be a power of two")
  self.words = words
// The bucket count is a power of two, so subtracting 1 will never overflow
// and get us a nice mask.
  self.bucketMask = bucketCount &- 1
}

//...
}

结构很简单,两个属性,words存放的就是就是HashTable桶的指针,指向的内容以UInt展示,从上面的源码可以看到这个指针指向的地方紧紧跟着_DictionaryStorage内容,我们看_DictionaryStorage如何推出_HashTable的:

// File: DictionaryStorage.swift, line: 87

  @inlinable
  @nonobjc
  internal final var _bucketCount: Int {
    @inline(__always) get { return1 &<< _scale }
  }

  @inlinable
  @nonobjc
  internal final var _metadata: UnsafeMutablePointer<_HashTable.Word> {
    @inline(__always) get {
      let address = Builtin.projectTailElems(self, _HashTable.Word.self)
      return UnsafeMutablePointer(address)
    }
  }

// The _HashTable struct contains pointers into tail-allocated storage, so
// this is unsafe and needs `_fixLifetime` calls in the caller.
  @inlinable
  @nonobjc
  internal final var _hashTable: _HashTable {
    @inline(__always) get {
      return _HashTable(words: _metadata, bucketCount: _bucketCount)
    }
  }
}

这个指针来自Builtin.projectTailElems,同样在前面的Array 中阅读过,然后得到就是_DictionaryStorage的尾部指针。

那么HashTable 如何用UInt来表示桶bucket的呢?其实和Bitmap一样,用UInt的每个Bit位表示一个桶,当Bit位等于0的时候,说明是空桶,如果是1,那么表示这个位置存储了元素。

这就是words的基本内容,之后来看看另外的属性bucketMask,我们看到初始化赋值的时候,有个表达式 bucketCount &- 1,这个有什么作用呢?

前面我们说过了,如果key用哈希函数得到了一个很大的哈希值,可以用取余的方式缩小哈希函数的输出域,在哈希表中,可以模上哈希表的大小,这样新得到的哈希值就可以均匀分布到哈希表上。但是在计算机模运算是消耗最大的,Apple工程师肯定也知道这点,所以有了现在的优化结果。

举个例子,在十进制中,除数是10000,如何得到123456的余数呢?一种方法就是老老实实计算,的到答案3456,另外的一种就是直接取数字的后四位,所以那那种性能更好,不言而喻。在十进制中,只要除数是10的倍数,都可以使用这个方法。

同理在二进制中,是要除数是2的倍数,就可以用上述方法求得余数。比如同样是10000,不过这个数是二进制表示的,相当于十进制的16,是2的倍数。那么如何取110011010的余数呢?除了算以后,我们还可以使用上面的方法,取最后的四位数1010,就是110011010的余数。

方法知道了如何利用计算机表达呢?我们可以直接用1100110101111做与运算,就能取到后四位,而且位运算的效率很高。而1111和除数10000只相差1,看到这明白了么?简言之,在计算机中,如果一个数是2的倍数,作为除数的话,那么求任何数的余数,只要将该除数减1,然后和要求的数做与运算就能获得余数。

回到我们开始的bucketMask中,bucketMask等于bucketCount &- 1,只要bucketCount满足是2的倍数,那么bucketMask就是当作是给取余的标记使用,任何大的哈希值与上bucketMask就能映射到哈希表上。而bucketCount等于1 &<< _scale,恰好是2 的倍数。同样,我们也知道了_scale的作用,就是获取哈希表大小的,哈希表的规模也一定是2的倍数。

4.5 哈希表的规模

看了上文得知,哈希表的规模bucketCount1 &<< _scale这样获得的,那么scale是如何得到呢?直接上源码:

// File: HashTable.swift, line: 68

internal static func scale(forCapacity capacity: Int) -> Int8 {
    let capacity = Swift.max(capacity, 1)
    // Calculate the minimum number of entries we need to allocate to satisfy
    // the maximum load factor. `capacity + 1` below ensures that we always
    // leave at least one hole.
    let minimumEntries = Swift.max(
      Int((Double(capacity) / maxLoadFactor).rounded(.up)),
      capacity + 1)
    // The actual number of entries we need to allocate is the lowest power of
    // two greater than or equal to the minimum entry count. Calculate its
    // exponent.
    let exponent = (Swift.max(minimumEntries, 2) - 1)._binaryLogarithm() + 1
    _internalInvariant(exponent >= 0 && exponent < Int.bitWidth)
    // The scale is the exponent corresponding to the bucket count.
    let scale = Int8(truncatingIfNeeded: exponent)
    _internalInvariant(self.capacity(forScale: scale) >= capacity)
    return scale
  }

scale得到就是简单的数学运算,在前面字典的初始化方法中,capacity传进来的是字典的词条个数。后面扩容的话,可能传进来的是字典的capacity。

4.6 Word字段的个数

开始的开辟空间有个allocWithTailElems_3方法没说:

let storage = Builtin.allocWithTailElems_3(
  _DictionaryStorage<Key, Value>.self,
  wordCount._builtinWordValue, _HashTable.Word.self,
  bucketCount._builtinWordValue, Key.self,
  bucketCount._builtinWordValue, Value.self)

bucketCount我们已经知道了,那么wordCount是多少呢?

_HashTable.Word上面也提过,就是UInt,用UInt bit位当作桶bucket。而UInt是8个字节大小,也就是64bit位,一个UInt最多可以当作64个桶,所以存在一个规模比64大的哈希表,1个_HashTable.word肯定记录不了,需要wordCount_HashTable.Word,那么wordCount怎么计算?

wordCountscale一样,也是数学计算,不过我直接翻译成了Swift的代码:

mutating func getWordCount() -> Int {
      let bucketCount = (1 as Int) &<< scale
      let kElement = bucketCount &+ UInt.bitWidth &- 1
      let element = UInt(bitPattern: kElement)
      let capacity = UInt(bitPattern: UInt.bitWidth)
      return Int(bitPattern: element / capacity)
  }

4.7 Dictionary底层的线性探测(Linear Probing)

我们先来看下Dictionary查找key在哪个桶的核心方法,走断点就行:

// File: DictionaryStorage.swift, line: 216


  @_alwaysEmitIntoClient
  @inline(never)
  internal final func find<Key: Hashable>(_ key: Key, hashValue: Int) -> (bucket: _HashTable.Bucket, found: Bool) {
    // 获取hashTable对象
      let hashTable = _hashTable
        // 获取key的hash值在hashTable中对应的桶,就是下标
      var bucket = hashTable.idealBucket(forHashValue: hashValue)
        // 遍历,判断条件是这个桶是否存在值
      while hashTable._isOccupied(bucket) {
        // 判断当前桶存放的key与要找的key是否一致
        if uncheckedKey(at: bucket) == key {
          // 找到对应的key,返回true,并且返回key所在的桶的位置
          return (bucket, true)
        }
        // 线性探测,获取下一个目标桶的位置
        bucket = hashTable.bucket(wrappedAfter: bucket)
      }
     // 未找到key,返回false,并且返回这个key应该放入桶的位置
      return (bucket, false)
  }
}

我们在上述代码中也看到了,如果发生了哈希碰撞,会调用hashTable.bucket(wrappedAfter: bucket),来寻下个可以存放的桶,看下这个的实现:

// File: HastTable.swift, line: 336


  /// The next bucket after `bucket`, with wraparound at the end of the table.
  @inlinable
  @inline(__always)
  internal func bucket(wrappedAfter bucket: Bucket) -> Bucket {
    // The bucket is less than bucketCount, which is power of two less than
    // Int.max. Therefore adding 1 does not overflow.
    return Bucket(offset: (bucket.offset &+ 1) & bucketMask)
  }
}

可以很清楚看到,获取新的Bucket并没有开辟新的空间,只是简单做了加1的操作,很明显的是开放定址法,而且是线性探测序列:

4.8 写时复制

这个跟数组的原理一致,也调用引用计数的分析:

// File: DictionaryVariant.swift, line: 369


  @inlinable
  internal mutating func setValue(_ value: __owned Value, forKey key: Key) {
#if _runtime(_ObjC)
    if !isNative {
      // Make sure we have space for an extra element.
      let cocoa = asCocoa
      self = .init(native: _NativeDictionary<Key, Value>(
        cocoa,
        capacity: cocoa.count + 1))
    }
#endif
    // 写时复制的引用判断
    let isUnique = self.isUniquelyReferenced()
    asNative.setValue(value, forKey: key, isUnique: isUnique)
  }

SwiftDictionary虽然是struct类型,但底层采用引用语义。当有多个变量引用同一个底层存储时,只有在写操作发生时才会真正复制内存(Copy-on-Write)。这样既保证了值类型的语义,又提升了性能。例如:

var dict1 = ["a": 1]
var dict2 = dict1
dict2["b"] = 2 // 此时dict2才会复制底层存储,dict1不受影响

05

延伸

5.1 冲突解决策略

SwiftDictionary采用的是开放寻址法(Open Addressing)来解决哈希冲突,而不是链表法(Separate Chaining)。在开放寻址法中,所有的数据都存储在同一块连续的内存空间(数组)中。当发生哈希冲突时(即两个不同的key计算出的哈希值映射到同一个位置),Dictionary 会按照一定的规则在数组中寻找下一个可用的槽位(slot)。

常见的寻址方式有:

  • 线性探测(Linear Probing):每次冲突时,依次检查下一个位置(+1),直到找到空槽。例如,如果槽位 i 被占用,就尝试 i+1、i+2、i+3,直到找到空位。优点是实现简单,缺点是容易出现“堆积”(clustering)现象,导致查找效率下降;

  • 二次探测(Quadratic Probing):每次冲突时,按照二次方的步长查找下一个槽位。例如,i+1²、i+2²、i+3²……这种方式可以减少堆积问题,提高查找效率;

  • 双重哈希(Double Hashing):使用第二个哈希函数决定步长,进一步减少冲突。

SwiftDictionary实现中,主要采用了二次探测的方式(quadratic probing),以兼顾性能和空间利用率。

插入流程举例:

  1. 计算key的哈希值,定位到初始槽位;

  2. 如果该槽位为空,直接插入;

  3. 如果已被占用,则按照二次探测规则查找下一个可用槽位,直到找到空位为止。

查找流程举例:

  1. 计算key的哈希值,定位到初始槽位;

  2. 如果该槽位的key与目标key相等,查找成功;

  3. 如果不相等,则继续按照二次探测规则查找下一个槽位,直到找到目标key或遇到空槽(查找失败)。

这种设计使得SwiftDictionary在绝大多数情况下都能保持 O(1) 的查找和插入效率。

5.2 性能优化与扩容机制

Dictionary 的性能与其负载因子Load Factor)密切相关。负载因子指的是已用槽位数量与总槽位数量的比例。例如,一个容量为 16 的哈希表,存储了 8 个元素,则负载因子为 0.5。

  • 当负载因子过高时,哈希冲突会变多,查找效率下降;

  • 当负载因子过低时,内存空间浪费。

SwiftDictionary会在负载因子超过一定阈值(通常为 0.75 左右)时自动扩容。扩容过程包括:

  1. 分配更大的内存空间(通常为原来的 2 倍);

  2. 重新计算所有 key 的哈希值,并将其插入到新的槽位中(rehash);

  3. 更新相关的元数据。

Swift 如何避免频繁扩容导致的性能抖动?

  • Swift在每次扩容时,都会将容量增加到足够大,避免短时间内多次扩容;

  • 扩容和rehash操作是一次性完成的,保证插入和查找的平均时间复杂度仍为 O(1);

  • 在实现上,Swift还会根据实际元素数量和内存使用情况,动态调整扩容策略,尽量减少扩容带来的性能波动。

小结: 合理的负载因子和自动扩容机制,使得SwiftDictionary能够在保证高性能的同时,节省内存并应对不同规模的数据存储需求。

5.3 哈希算法通俗理解和实现

    • 顺序查表法

    • 假设现在有1000个人的档案资料需要存放进档案柜子(即哈希桶/slot)里。要求是能够快速查询到某人档案是否已经存档,如果已经存档则能快速调出档案。如果是你,你会怎么做?最普通的做法就是把每个人的档案依次放到柜子里,然后柜子外面贴上人名,需要查询某个人的档案的时候就根据这个人的姓名来确定是否已经存档。但是1000个人最坏的情况下我们查找一个人的姓名就要对比1000次!并且人越多,最大查询的次数也就越多,专业的说这种方法的时间复杂的就是O(n),意思就是人数增加n倍,那么查询的最大次数也就会增加n倍!这种方法,人数少的时候还好,人数越多查询起来就越费劲!那么有什么更好的解决方法吗?答案就是散列表算法,即哈希表算法。

    • 哈希表算法

    • 假设每个人的姓名笔划数都是不重复的,那么我们通过一个函数把要存档的人姓名笔划数转换到1000以内,然后把这个人的资料就放在转换后的数字指定的柜子里,这个函数就叫做哈希函数,按照这种方式存放的这1000个柜子就叫哈希表(散列表),人名笔画数就是哈希表的元素,转换后的数就是人名笔划数的哈希值(也就是柜子的序号)。当要查询某个人是否已经存档的时候,我们就通过哈希函数把他的姓名笔划数转化成哈希值,如果哈希值在1000以内,那么恭喜你这个人已经存档,可以到哈希值指定的柜子里去调出他的档案,否则这个人就是黑户,没有存档!这就是哈希表算法了,是不是很方便,只要通过一次计算得出哈希值就可以查询到结果了,专业的说法就是这种算法的时间复杂是O(1),即无论有多少人存档,都可以通过一次计算得出查询结果!

      当然上面的只是很理想的情况,人名的笔划数是不可能不重复的,转换而来的哈希值也不会是唯一的。那么怎么办呢?如果两个人算出的哈希值是一样的,难道把他们都放到一个柜子里面?如果1000个人得出的哈希值都是一样的呢?下面有几种方法可以解决这种冲突。

    • 开放寻址法

    • 这种方法的做法是,如果计算得出的哈希值对应的柜子里面已经放了别人的档案,那么对不起,你得再次通过哈希算法把这个哈希值再次变换,直到找到一个空的柜子为止!查询的时候也一样,首先到第一次计算得出的哈希值对应的柜子里面看看是不是你要找的档案,如果不是继续把这个哈希值通过哈希函数变换,直到找到你要的档案,如果找了几次都没找到而且哈希值对应的柜子里面是空的,那么对不起,查无此人!

    • 链表法

    • 这种方法的做法是,如果计算得出的哈希值对应的柜子里面已经放了别人的档案,那也不管了,懒得再找其他柜子了,就跟他的档案放在一起!当然是按顺序来存放。这样下次来找的时候一个哈希值对应的柜子里面可能有很多人的档案,最差的情况可能1000个人的档案都在一个柜子里面!那么时间复杂度又是O(n)了,跟普通的做法也没啥区别了。在算法实现的时候,每个数组元素存放的不是内容而是链表头,如果哈希值唯一,那么链表大小为1,否则链表大小为重复的哈希值个数。

5.4 哈希表算法计算机中的理解

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

散列函数

散列函数是一种函数(像y=f(x)),经典的散列函数有以下特性:

  • 输入域是无穷大的;

  • 输出域是有穷尽的;

  • 输入相同,得到的输出也相同;

  • 因为输入域是远远大于输出域的,那么一定会出现不同的输出,却得到相同的输出,这个叫哈希碰撞;

  • 满射性:结果尽可能充分覆盖整个输出域;

  • 最重要的一点:离散性,如果你的样本数量足够大,那么所有的结果在输出域上几乎是均匀分布的。

常用的散列函数有:直接定址法、求余法、数字分析法、平方取中法、折叠法、随机数法等,这些方法简单的讲两个:

  • 求余法: Hash(key)=key%MM通常是散列表规模,M尽可能用素数,因为如果,key都是10的倍数,而M是10,那岂不是都在0上了。。。

  • 平方取中法: 首先算出key2,截取中间若干数位作为地址,比如hash(123)=512,因为1232=15129,取中间三位。那为什么要倾向于保留居中的数位呢,这正是为了使得构成原关键码的各个数位,能够对最终的散列地址有尽可能接近的影响。

但这些方法就不多说了,因为这些都是数学上的方法,了解一下即可。我们编程语言会帮我们预置一些哈希函数,比如:MD5SHA1SHA256等。

但这些函数的输出域都比较大,比如MD5,它的大小是32位的16进制数,非常大,而我们的哈希表规模会比较小,和你的数据量有关,那怎么办呢?

这里有个推论:因为哈希函数Hash(key)具有满射性和离散性,那么Hash(key)%M也具有满射性和离散性,所以我们只要取模就行了,其实Hash(key)%M本身也是一个哈希函数哈 = =。

哈希函数在计算行业里有很多应用,除了我们现在在了解哈希表外,比如我们经常听见的数字签名,服务器为了负载均衡而做的一致性哈希设计,搜索服务用的布隆过滤器等,都是用的哈希函数的特性。

06

结束语

本篇文章已经把Dictionary底层大概原理说了,除了key是如何做哈希的,因为源码中这个是私有属性,这个私有属性用到了我们结构体里面的属性seed,请各位小伙伴自行查看即可,你可以在这里简单理解为MD5就好,不会影响整体的阅读逻辑。

07

参考文档

  • 哈希表算法通俗理解和实现:

    https://blog.youkuaiyun.com/u013752202/article/details/51104156

  • 开放定址法:

    https://www.cnblogs.com/hongshijie/p/9419387.html

  • 哈希表(散列表)原理详解:

    https://blog.youkuaiyun.com/duan19920101/article/details/51579136/



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值