面试必谈的哈希,.Net 程序员温故而知新
引言:
作为资深老鸟,有事没事,出去面试;找准差距、定位价值。
面试必谈哈希,
Q1:什么是哈希?
Q2:哈希为什么快?
Q3:你是怎么理解哈希算法利用空间换取时间的?
Q4:你是怎么解决哈希冲突的?
Q5:你有实际用写过哈希算法吗?
1. 知识储备
哈希(也叫散列)是一种查找算法(可用于插入),哈希算法希望能做到不经过任何比较(发生冲突,还是需要少许比较),通过一次存取就能得到查找的数据。
因此哈希的关键在key和数据元素的存储位置之间建立一个确定的对应关系,每个key在哈希表中都有唯一的一个地址相对应(形成有限连续的地址空间),查找时可根据这个对应关系经过一步计算得到key在散列表的位置
在数学上, 原Key叫做原像,由哈希函数h(key)映射的存储位置叫做像,
在IT领域,以上存储位置叫哈希地址(散列地址),这个映射过程叫做哈希。
故我们可以初步预见:
① 不同的key值,由哈希函数h(x) 作用后可能映射到同一个哈希地址, 这就是哈希冲突,冲突发生的概率取决于 定义的哈希函数
② 由哈希表作用后的哈希地址 需要空间存储,这一系列连续相邻的地址空间叫哈希表、 散列表。
处理哈希冲突可分为两大类:
开散列法(又称为链地址法)和闭散列法(又称为开放地址法)。
哈希表是用数组实现的一片连续的地址空间,两种冲突解决技术的区别在于发生冲突的元素是存储在这片数组的空间之外还是空间之内:
(1)开散列法发生冲突的元素存储于数组空间之外。可以把“开”字理解为需要另外“开辟”空间存储发生冲突的元素, 又称链地址法
(2)闭散列法发生冲突的元素存储于数组空间之内。可以把“闭”字理解为所有元素,不管是否有冲突,都“关闭”于数组之中,闭散列法又称开放定址法,意指数组空间对所有元素,不管是否冲突都是开放的
2. 看图说话
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大提问。
- 面试.net资深程序员的前5分钟
- .NET程序员面试的题一部 (转)
- 面试.NET资深程序员的前5分钟
- 面试稳了!集齐几千名程序员精选的 100 道前端面试题!
- JAVA程序员面试32问,你能回答多少题
- 程序员的.NET时代——李洪根(本文首发于《CSDN开发高手》2003年第12期)
- 程序员面试题目总结--链表(6)【单链表排序】
- .NET面试中的一些常见问题与答案
- 程序员有趣的面试智力题
- .Net 加密 哈希
- 面试感悟----一名3年工作经验的程序员应该具备的技能
- Redis之Hash___redis中哈希(Hash)在.net中的运用
- 北京易车 招聘.net网站开发高级程序员 6-8k
- java程序员面试32问
- Java程序员面试中的多线程问题
- 一位程序员的外国梦-国外面试经验谈
- 如何在面试中发现优秀程序员
- 计算机经典书籍电子书合集(适合计算机学生学习以及程序员笔试、面试)
- 面试感悟----一名3年工作经验的程序员应该具备的技能
- 程序员的.NET时代(入门篇)