面試必談的雜湊,.Net 程式設計師溫故而知新

頭號碼甲發表於2019-08-01

 引言:

作為資深老鳥,有事沒事,出去面試;找準差距、定位價值。

面試必談雜湊,

Q1:什麼是雜湊?

Q2:雜湊為什麼快?

Q3:你是怎麼理解雜湊演算法利用空間換取時間的?

Q4:你是怎麼解決雜湊衝突的?

Q5:你有實際用寫過雜湊演算法嗎?

1. 知識儲備

  雜湊(也叫雜湊)是一種查詢演算法(可用於插入),雜湊演算法希望能做到不經過任何比較(發生衝突,還是需要少許比較),通過一次存取就能得到查詢的資料。

因此雜湊的關鍵在key和資料元素的儲存位置之間建立一個確定的對應關係,每個key在雜湊表中都有唯一的地址相對應(形成有限、連續的地址空間),查詢時根據對應關係經過一步計算得到key在雜湊表的位置。

在數學上, 原Key叫做原像,由對映函式h(key)對映的儲存位置叫做像;在IT領域,以上儲存位置叫雜湊地址(雜湊地址),這個對映過程叫做雜湊/雜湊。

故我們可以預見:

  ① 不同的key值,由雜湊函式h(x) 作用後可能對映到同一個雜湊地址, 這就是雜湊衝突,衝突發生的概率取決於 定義的雜湊函式

  ② 由雜湊表作用後的雜湊地址需要空間儲存,這一系列連續相鄰的地址空間叫雜湊表、 雜湊表。 

  

  處理雜湊衝突可分為兩大類:

  (1)開雜湊法發生衝突的元素儲存於陣列空間之外。可以把“開”字理解為需要另外“開闢”空間儲存發生衝突的元素, 又稱【鏈地址法】

  (2)閉雜湊法發生衝突的元素儲存於陣列空間之內。可以把“閉”字理解為所有元素,不管是否有衝突,都“關閉”於陣列之中,閉雜湊法又稱【開放定址法】,意指陣列空間對所有元素,不管是否衝突都是開放的

   雜湊表是用陣列實現的一片連續的地址空間,兩種衝突解決方案的區別在於發生衝突的元素是儲存在這片陣列的空間之外還是空間之內

2. 看圖說話

----------------------------以下是開雜湊法(鏈地址法)解決衝突的示意圖---------------------------
 

  從圖上看實現【雜湊】過程分兩部分:

① 雜湊函式

  收斂函式,不可避免會衝突,需要思考設定一個均衡的雜湊函式,使雜湊地址儘可能均勻地分佈在雜湊地址空間

②  構造雜湊表 + 衝突連結串列

  裝填因子loadfactor :所謂裝填因子是指雜湊表中已存入的記錄數n與雜湊地址空間大小m的比值,即 α=n / m ,α越小,衝突發生的可能性就越小;α越大(最大可取1),衝突發生的可能性就越大。

  另一方面,α越小,儲存窨的利用率就越低;反之,儲存窨的利用率就越高。為了既兼顧減少衝突的發生,又兼顧提高儲存空間的利用率,通常把α控制在0.6~0.9的範圍之內

 

雜湊在.Net中的應用

  Object基類中有GetHashCode方法,HashCode是一個數字值,用於在【基於雜湊特性的集合】中插入和查詢某物件;GetHashCode方法為需要快速檢查物件相等性的演算法提供此雜湊程式碼;

  Do not test for equality of hash codes to determine whether two objects are equal. (Unequal objects can have identical hash codes.) To test for equality, call the ReferenceEquals or Equals method.  (重要的話要讀3遍)

單純判斷【邏輯相等】時,本無所謂重寫 GetHashCode方法:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
using System.Collections.Generic;
using System.Linq;
using System.Collections;

namespace Test
{
    public class Persion
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public override bool Equals(object other1)  // 邏輯相等
        {
            return Name == (other1 as Persion)?.Name;
        }

        public override int GetHashCode()
        {
            return Name.GetHashCode();
        }
    }
    public class Program
    {
        static void Main(string[] args)
        {
            var p1 = new Persion {  Name="HJ" , Age=22};

            var p2 = new Persion { Name = "HJ", Age = 21 };

            var referenceEqual = (p1 == p2);    
            var logicEqual = (p1.Equals(p2));

            Console.WriteLine($"“==”操作符:引用相等(兩變數指向一個例項),始終為:{referenceEqual}");
            Console.WriteLine($"“Equal”方法:邏輯相等, {logicEqual}");
            Console.Read();
        }
    }
}
----------------------------------------------------------------
output:
“==”操作符:引用相等(兩變數指向一個例項),始終為:False
“Equal”方法: 邏輯相等, True

但是若需要利用HashCode快速 查詢 /插入某元素, 則一定要重寫GetHashCode方法。

? 我們看一個實錘:

  在使用 LINQ.Union()方法計算A,B兩集合的並集 :A∪B, 自然會想到使用邏輯相等比較器Comparer 定義A,B中元素邏輯相等:

  public static IEnumerable<TSource> Union<TSource>(this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

public static IEnumerable<TSource> Union<TSource>(this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
    if (first == null) throw Error.ArgumentNull("first");
    if (second == null) throw Error.ArgumentNull("second");
        return UnionIterator<TSource>(first, second, comparer);
}

static IEnumerable<TSource> UnionIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
      Set<TSource> set = new Set<TSource>(comparer);
      foreach (TSource element in first)
          if (set.Add(element)) yield return element;      //  Set 便是Union方法內部構造的雜湊表
      foreach (TSource element in second)
           if (set.Add(element)) yield return element;
}
Union方法入口

  若單純比較元素邏輯相等,重寫Equal方法就可以了,為什麼這裡要強調必須重寫GetHashCode方法?

  觀察Union原始碼中求A,B並集的實現,內部會構造雜湊表Set 快速查詢和插入並集元素。

  故我們需要給元素編寫合適的雜湊函式, 請關注下方程式碼區的 internal int InternalGetHashCode(TElement value) 函式

 

 

高潮來了,不是總說沒處理過雜湊衝突嗎? 結【知識儲備】圍觀鏈地址法處理雜湊衝突
internal class Set<TElement>
    {
        int[] buckets;            //  連續相鄰的地址空間,盛放不同衝突連結串列的容器,俗稱雜湊桶
        Slot[] slots;             //  用於解決衝突的連結串列
        int count;
        int freeList;
        IEqualityComparer<TElement> comparer;

        public Set() : this(null) { }

        public Set(IEqualityComparer<TElement> comparer) {
            if (comparer == null) comparer = EqualityComparer<TElement>.Default;
            this.comparer = comparer;
            buckets = new int[7];   // 初始雜湊桶和衝突連結串列長度 都是7
            slots = new Slot[7];
            freeList = -1;
        }

        // If value is not in set, add it and return true; otherwise return false
        public bool Add(TElement value) {
            return !Find(value, true);
        }

        // Check whether value is in set
        public bool Contains(TElement value) {
            return Find(value, false);
        }

        // If value is in set, remove it and return true; otherwise return false
        public bool Remove(TElement value) {
            int hashCode = InternalGetHashCode(value);
            int bucket = hashCode % buckets.Length;
            int last = -1;
            for (int i = buckets[bucket] - 1; i >= 0; last = i, i = slots[i].next) {
                if (slots[i].hashCode == hashCode && comparer.Equals(slots[i].value, value)) {
                    if (last < 0) {
                        buckets[bucket] = slots[i].next + 1;
                    }
                    else {
                        slots[last].next = slots[i].next;
                    }
                    slots[i].hashCode = -1;
                    slots[i].value = default(TElement);
                    slots[i].next = freeList;
                    freeList = i;
                    return true;
                }
            }
            return false;
        }

        bool Find(TElement value, bool add) {
            int hashCode = InternalGetHashCode(value);
            for (int i = buckets[hashCode % buckets.Length] - 1; i >= 0; i = slots[i].next) {
                if (slots[i].hashCode == hashCode && comparer.Equals(slots[i].value, value)) return true;
            }
            if (add) {
                int index;
                if (freeList >= 0) {
                    index = freeList;
                    freeList = slots[index].next;
                }
                else {
                    if (count == slots.Length) Resize();
                    index = count;
                    count++;
                }
                int bucket = hashCode % buckets.Length;
                slots[index].hashCode = hashCode;
                slots[index].value = value;
                slots[index].next = buckets[bucket] - 1;
                buckets[bucket] = index + 1;
            }
            return false;
        }

        void Resize() {
            int newSize = checked(count * 2 + 1);    // 嘗試擴容
            int[] newBuckets = new int[newSize];
            Slot[] newSlots = new Slot[newSize];
            Array.Copy(slots, 0, newSlots, 0, count);
            for (int i = 0; i < count; i++) {
                int bucket = newSlots[i].hashCode % newSize;
                newSlots[i].next = newBuckets[bucket] - 1;
                newBuckets[bucket] = i + 1;
            }
            buckets = newBuckets;
            slots = newSlots;
        }

        internal int InternalGetHashCode(TElement value)
        {
            //Microsoft DevDivBugs 171937. work around comparer implementations that throw when passed null
            return (value == null) ? 0 : comparer.GetHashCode(value) & 0x7FFFFFFF;
        }

        internal struct Slot
        {
            internal int hashCode;
            internal TElement value;
            internal int next;
        }
    }

因此有最佳實踐: 為兩物件重寫Equal 方法返回true時, 請最好重寫 GetHashCode方法,為這兩物件返回相同的hashcode。

話雖如此,寫一個合適、均衡的雜湊函式還是比較考驗演算法的。

在一般場景中,經驗會幫助你編寫雜湊函式, 比如以上Person類中,字串型別Name的HashCode總是相等的。

That‘all   看完了通篇文章的同棧猿,應該就可以回答文章引言 5大提問。

作者:JulianHuang

感謝您的認真閱讀,如有問題請下方留言大膽斧正,共同進步;覺得有用,請下方或加關注。

本文歡迎轉載,但請保留此段宣告,且在文章頁面明顯位置註明本文的作者及原文連結。

相關文章