hashmap的扩容因子是0.75 缘由 参考:HashMap默认加载因子为何选择0.75?(阿里)html
阿里的人问 数组的时间复杂度是多少,链表的是多少,hashmap的时间复杂度是多少。。。。。java
后来才知道,时间复杂度是要区分 增删改查的。。。。主要看查询的时间复杂度;git
一、数组 查询的时间复杂度 O(n)面试
二、链表 查询的时间复杂度 O(n)算法
三、hashmap 查询的时间复杂度 O(1)编程
数组 查询的时间复杂度 O(n)数组
建议看一下下面的博客:安全
hashSet,hashtable,hashMap 都是基于散列函数, 时间复杂度 O(1) 可是若是太差的话是O(n)数据结构
TreeSet==>O(log(n))==> 基于树的搜索,只须要搜索一半便可编程语言
O⑴的缘由是离散后,下标对应关键字
hash就是散列,甚至再散列。可是我一直对hash表的时间复杂度有个疑问。一个须要存储的字符串,经过hash函数散列到一个相对较短的索引,使得存取速度加快。但为何存取的时间复杂度能达到常量级O(1)呢?? 查找时搜索索引不须要费时间吗?为何不是O(n)呢? n是hash表的长度,
若是对Hashtable的构造有很深的理解的话,就知道了,Hashtable 实际上是综合了数组和链表的优势,当Hashtable对数值进行搜索的时候,首先用该数值与Hashtable的长度作了取模的操做,获得的数字直接做为hashtable中entry数组的index,由于hashtable是由entry数组组成的,所以,能够直接定位到指定的位置,不须要搜索,固然,这里还有个问题,每一个entry实际上是链表,若是entry有不少值的话,仍是须要挨个遍历的,所以能够这样讲Hashtable的时间复杂度最好是O(1)可是最差是 O(n) 最差的时候也就是hashtable中全部的值的hash值都同样,都分配在一个entry里面,固然这个几率跟中1亿彩票的几率相差不大。
若是还不理解能够参考我写的专门的博客:
关于HashMap的:HashMap的实现原理--链表散列
关于Hashtable的:Hashtable数据存储结构-遍历规则,Hash类型的复杂度为啥都是O(1)-源码分析
在看起来就是对Entry链表的循环的时间复杂度影响最大,链表查找的时间复杂度为O(n),与链表长度有关。咱们要保证那个链表长度为1,才能够说时间复杂度能知足O(1)。但这么说来只有那个hash算法尽可能减小冲突,才能使链表长度尽量短,理想状态为1。所以能够得出结论:HashMap的查找时间复杂度只有在最理想的状况下才会为O(1),最差是O(n),而要保证这个理想状态不是咱们开发者控制的。
======================================================================开始=======================================================================================
Data Structure | 新增 | 查询/Find | 删除/Delete | GetByIndex |
数组 Array (T[]) | O(n) | O(n) | O(n) | O(1) |
链表 Linked list (LinkedList<T>) | O(1) | O(n) | O(n) | O(n) |
Resizable array list (List<T>) | O(1) | O(n) | O(n) | O(1) |
Stack (Stack<T>) | O(1) | - | O(1) | - |
Queue (Queue<T>) | O(1) | - | O(1) | - |
Hash table (Dictionary<K,T>) | O(1) | O(1) | O(1) | - |
Tree-based dictionary(SortedDictionary<K,T>) | O(log n) | O(log n) | O(log n) | - |
Hash table based set (HashSet<T>) | O(1) | O(1) | O(1) | - |
Tree based set (SortedSet<T>) | O(log n) | O(log n) | O(log n) | - |
如何选择数据结构
Array (T[])
Linked list (LinkedList<T>)
Resizable array list (List<T>)
Stack (Stack<T>)
Queue (Queue<T>)
Hash table (Dictionary<K,T>)
Tree-based dictionary (SortedDictionary<K,T>)
Hash table based set (HashSet<T>)
Tree based set (SortedSet<T>)
在计算机程序设计中,数组(Array)是最简单的并且应用最普遍的数据结构之一。在任何编程语言中,数组都有一些共性:
对于数组的常规操做包括:
在 C# 中,能够经过以下的方式声明数组变量。
int allocationSize = 10; bool[] booleanArray = new bool[allocationSize]; FileInfo[] fileInfoArray = new FileInfo[allocationSize];
上面的代码将在 CLR 托管堆中分配一块连续的内存空间,用以容纳数量为 allocationSize ,类型为 arrayType 的数组元素。若是 arrayType 为值类型,则将会有 allocationSize 个未封箱(unboxed)的 arrayType 值被建立。若是 arrayType 为引用类型,则将会有 allocationSize 个 arrayType 类型的引用被建立。
若是咱们为 FileInfo[] 数组中的一些位置赋上值,则引用关系为下图所示。
.NET 中的数组都支持对元素的直接读写操做。语法以下:
// 读数组元素 bool b = booleanArray[7]; // 写数组元素 booleanArray[0] = false;
访问一个数组元素的时间复杂度为 O(1),所以对数组的访问时间是恒定的。也就是说,与数组中包含的元素数量没有直接关系,访问一个元素的时间是相同的。
因为数组是固定长度的,而且数组中只能存储同一种类型或类型的衍生类型。这在使用中会受到一些限制。.NET 提供了一种数据结构 ArrayList 来解决这些问题。
ArrayList countDown = new ArrayList(); countDown.Add(3); countDown.Add(2); countDown.Add(1); countDown.Add("blast off!"); countDown.Add(new ArrayList());
ArrayList 是长度可变的数组,而且它能够存储不一样类型的元素。
但这些灵活性是以牺牲性能为代价的。在上面 Array 的描述中,咱们知道 Array 在存储值类型时是采用未装箱(unboxed)的方式。因为 ArrayList 的 Add 方法接受 object 类型的参数,致使若是添加值类型的值会发生装箱(boxing)操做。这在频繁读写 ArrayList 时会产生额外的开销,致使性能降低。
当 .NET 中引入泛型功能后,上面 ArrayList 所带来的性能代价可使用泛型来消除。.NET 提供了新的数组类型 List<T>。
泛型容许开发人员在建立数据结构时推迟数据类型的选择,直到使用时才肯定选择哪一种类型。泛型(Generics)的主要优势包括:
List<T> 等同于同质的一维数组(Homogeneous self-redimensioning array)。它像 Array 同样能够快速的读取元素,还能够保持长度可变的灵活性。
// 建立 int 类型列表 List<int> myFavoriteIntegers = new List<int>(); // 建立 string 类型列表 List<string> friendsNames = new List<string>();
List<T> 内部一样使用 Array 来实现,但它隐藏了这些实现的复杂性。当建立 List<T> 时无需指定初始长度,当添加元素到 List<T> 中时,也无需关心数组大小的调整(resize)问题。
List<int> powersOf2 = new List<int>(); powersOf2.Add(1); powersOf2.Add(2); powersOf2[1] = 10; int sum = powersOf2[1] + powersOf2[2];
List<T> 的渐进运行时(Asymptotic Running Time)复杂度与 Array 是相同的。
在链表(Linked List)中,每个元素都指向下一个元素,以此来造成了一个链(chain)。
向链表中插入一个新的节点的渐进时间取决于链表是不是有序的。若是链表不须要保持顺序,则插入操做就是常量时间O(1),能够在链表的头部或尾部添加新的节点。而若是须要保持链表的顺序结构,则须要查找到新节点被插入的位置,这使得须要从链表的头部 head 开始逐个遍历,结果就是操做变成了O(n)。下图展现了插入节点的示例。
链表与数组的不一样之处在于,数组的中的内容在内存中时连续排列的,能够经过下标来访问,而链表中内容的顺序则是由各对象的指针所决定,这就决定了其内容的排列不必定是连续的,因此不能经过下标来访问。若是须要更快速的查找操做,使用数组多是更好的选择。
使用链表的最主要的优点就是,向链表中插入或删除节点无需调整结构的容量。而相反,对于数组来讲容量始终是固定的,若是须要存放更多的数据,则须要调整数组的容量,这就会发生新建数组、数据拷贝等一系列复杂且影响效率的操做。即便是 List<T> 类,虽然其隐藏了容量调整的复杂性,但仍然难逃性能损耗的惩罚。
链表的另外一个优势就是特别适合以排序的顺序动态的添加新元素。若是要在数组的中间的某个位置添加新元素,不只要移动全部其他的元素,甚至还有可能须要从新调整容量。
因此总结来讲,数组适合数据的数量是有上限的状况,而链表适合元素数量不固定的状况。
在 .NET 中已经内置了 LinkedList<T> 类,该类实现了双向链表(doubly-linked list)功能,也就是节点同时持有其左右节点的引用。而对于删除操做,若是使用 Remove(T),则运算复杂度为 O(n),其中 n 为链表的长度。而若是使用 Remove(LinkedListNode<T>), 则运算复杂度为 O(1)。
当咱们须要使用先进先出顺序(FIFO)的数据结构时,.NET 为咱们提供了 Queue<T>。Queue<T> 类提供了 Enqueue 和 Dequeue 方法来实现对 Queue<T> 的存取。
Queue<T> 内部创建了一个存放 T 对象的环形数组,并经过 head 和 tail 变量来指向该数组的头和尾。
默认状况下,Queue<T> 的初始化容量是 32,也能够经过构造函数指定容量。
Enqueue 方法会判断 Queue<T> 中是否有足够容量存放新元素。若是有,则直接添加元素,并使索引 tail 递增。在这里的 tail 使用求模操做以保证 tail 不会超过数组长度。若是容量不够,则 Queue<T> 根据特定的增加因子扩充数组容量。
默认状况下,增加因子(growth factor)的值为 2.0,因此内部数组的长度会增长一倍。也能够经过构造函数中指定增加因子。Queue<T> 的容量也能够经过 TrimExcess 方法来减小。
Dequeue 方法根据 head 索引返回当前元素,以后将 head 索引指向 null,再递增 head 的值。
当须要使用后进先出顺序(LIFO)的数据结构时,.NET 为咱们提供了 Stack<T>。Stack<T> 类提供了 Push 和 Pop 方法来实现对 Stack<T> 的存取。
Stack<T> 中存储的元素能够经过一个垂直的集合来形象的表示。当新的元素压入栈中(Push)时,新元素被放到全部其余元素的顶端。当须要弹出栈(Pop)时,元素则被从顶端移除。
Stack<T> 的默认容量是 10。和 Queue<T> 相似,Stack<T> 的初始容量也能够在构造函数中指定。Stack<T> 的容量能够根据实际的使用自动的扩展,而且能够经过 TrimExcess 方法来减小容量。
若是 Stack<T> 中元素的数量 Count 小于其容量,则 Push 操做的复杂度为 O(1)。若是容量须要被扩展,则 Push 操做的复杂度变为 O(n)。Pop 操做的复杂度始终为 O(1)。
如今咱们要使用员工的社保号做为惟一标识进行存储。社保号的格式为 DDD-DD-DDDD(D 的范围为数字 0-9)。
若是使用 Array 存储员工信息,要查询社保号为 111-22-3333 的员工,则将会尝试遍历数组的全部选择,即执行复杂度为 O(n) 的查询操做。好一些的办法是将社保号排序,以使查询复杂度下降到 O(log(n))。但理想状况下,咱们更但愿查询复杂度为 O(1)。
一种方案是创建一个大数组,范围从 000-00-0000 到 999-99-9999 。
这种方案的缺点是浪费空间。若是咱们仅须要存储 1000 个员工的信息,那么仅利用了 0.0001% 的空间。
第二种方案就是用哈希函数(Hash Function)压缩序列。
咱们选择使用社保号的后四位做为索引,以减小区间的跨度。这样范围将从 0000 到 9999。
在数学上,将这种从 9 位数转换为 4 位数的方式称为哈希转换(Hashing)。能够将一个数组的索引空间(indexers space)压缩至相应的哈希表(Hash Table)。
在上面的例子中,哈希函数的输入为 9 位数的社保号,输出结果为后 4 位。
H(x) = last four digits of x
上图中也说明在哈希函数计算中常见的一种行为:哈希冲突(Hash Collisions)。即有可能两个社保号的后 4 位均为 0000。
当要添加新元素到 Hashtable 中时,哈希冲突是致使操做被破坏的一个因素。若是没有冲突发生,则元素被成功插入。若是发生了冲突,则须要判断冲突的缘由。所以,哈希冲突提升了操做的代价,Hashtable 的设计目标就是要尽量减低冲突的发生。
避免哈希冲突的一个方法就是选择合适的哈希函数。哈希函数中的冲突发生的概率与数据的分布有关。例如,若是社保号的后 4 位是随即分布的,则使用后 4 位数字比较合适。但若是后 4 位是以员工的出生年份来分配的,则显然出生年份不是均匀分布的,则选择后 4 位会形成大量的冲突。
咱们将选择合适的哈希函数的方法称为冲突避免机制(Collision Avoidance)。
在处理冲突时,有不少策略能够实施,这些策略称为冲突解决机制(Collision Resolution)。其中一种方法就是将要插入的元素放到另一个块空间中,由于相同的哈希位置已经被占用。
例如,最简单的一种实现就是线性挖掘(Linear Probing),步骤以下:
如今若是咱们要将五个员工的信息插入到哈希表中:
Alice (333-33-1234) Bob (444-44-1234) Cal (555-55-1237) Danny (000-00-1235) Edward (111-00-1235)
则插入后的哈希表可能以下:
元素的插入过程:
Alice 的社保号被哈希为 1234,所以存放在位置 1234。 Bob 的社保号被哈希为 1234,但因为位置 1234 处已经存放 Alice 的信息,则检查下一个位置 1235,1235 为空,则 Bob 的信息就被放到 1235。 Cal 的社保号被哈希为 1237,1237 位置为空,因此 Cal 就放到 1237 处。 Danny 的社保号被哈希为 1235,1235 已被占用,则检查 1236 位置是否为空,1236 为空,因此 Danny 就被放到 1236。 Edward 的社保号被哈希为 1235,1235 已被占用,检查1236,也被占用,再检查1237,直到检查到 1238时,该位置为空,因而 Edward 被放到了1238 位置。
线性挖掘(Linear Probing)方式虽然简单,但并非解决冲突的最好的策略,由于它会致使同类哈希的汇集。这致使搜索哈希表时,冲突依然存在。例如上面例子中的哈希表,若是咱们要访问 Edward 的信息,由于 Edward 的社保号 111-00-1235 哈希为 1235,然而咱们在 1235 位置找到的是 Bob,因此再搜索 1236,找到的倒是 Danny,以此类推直到找到 Edward。
一种改进的方式为二次挖掘(Quadratic Probing),即每次检查位置空间的步长为平方倍数。也就是说,若是位置 s 被占用,则首先检查 s + 12 处,而后检查s – 12,s + 22,s – 22,s + 32 依此类推,而不是象线性挖掘那样以 s + 1,s + 2 … 方式增加。尽管如此,二次挖掘一样也会致使同类哈希汇集问题。
.NET 中的 Hashtable 的实现,要求添加元素时不只要提供元素(Item),还要为该元素提供一个键(Key)。例如,Key 为员工社保号,Item 为员工信息对象。能够经过 Key 做为索引来查找 Item。
Hashtable employees = new Hashtable(); // Add some values to the Hashtable, indexed by a string key employees.Add("111-22-3333", "Scott"); employees.Add("222-33-4444", "Sam"); employees.Add("333-44-55555", "Jisun"); // Access a particular key if (employees.ContainsKey("111-22-3333")) { string empName = (string)employees["111-22-3333"]; Console.WriteLine("Employee 111-22-3333's name is: " + empName); } else Console.WriteLine("Employee 111-22-3333 is not in the hash table...");
Hashtable 类中的哈希函数比前面介绍的社保号的实现要更为复杂。哈希函数必须返回一个序数(Ordinal Value)。对于社保号的例子,经过截取后四位就能够实现。但实际上 Hashtable 类能够接受任意类型的值做为 Key,这都要归功于 GetHashCode 方法,一个定义在 System.Object 中的方法。GetHashCode 的默认实现将返回一个惟一的整数,而且保证在对象的生命周期内保持不变。
Hashtable 类中的哈希函数定义以下:
H(key) = [GetHash(key) + 1 + (((GetHash(key) >> 5) + 1) % (hashsize – 1))] % hashsize
这里的 GetHash(key) 默认是调用 key 的 GetHashCode 方法以获取返回的哈希值。hashsize 指的是哈希表的长度。由于要进行求模,因此最后的结果 H(key) 的范围在 0 至 hashsize – 1 之间。
当在哈希表中添加或获取一个元素时,会发生哈希冲突。前面咱们简单地介绍了两种冲突解决策略:
线性挖掘(Linear Probing)
二次挖掘(Quadratic Probing)
在 Hashtable 类中则使用的是一种彻底不一样的技术,称为二度哈希(rehashing)(有些资料中也将其称为双精度哈希(double hashing))。
二度哈希的工做原理以下:
有一个包含一组哈希函数 H1…Hn 的集合。当须要从哈希表中添加或获取元素时,首先使用哈希函数 H1。若是致使冲突,则尝试使用 H2,以此类推,直到 Hn。全部的哈希函数都与 H1 十分类似,不一样的是它们选用的乘法因子(multiplicative factor)。
一般,哈希函数 Hk 的定义以下:
Hk(key) = [GetHash(key) + k * (1 + (((GetHash(key) >> 5) + 1) % (hashsize – 1)))] % hashsize
当使用二度哈希时,重要的是在执行了 hashsize 次挖掘后,哈希表中的每个位置都有且只有一次被访问到。也就是说,对于给定的 key,对哈希表中的同一位置不会同时使用 Hi 和 Hj。在 Hashtable 类中使用二度哈希公式,其始终保持 (1 + (((GetHash(key) >> 5) + 1) % (hashsize – 1)) 与 hashsize 互为素数(两数互为素数表示二者没有共同的质因子)。
二度哈希较前面介绍的线性挖掘(Linear Probing)和二次挖掘(Quadratic Probing)提供了更好的避免冲突的策略。
Hashtable 类中包含一个私有成员变量 loadFactor,loadFactor 指定了哈希表中元素数量与位置(slot)数量之间的最大比例。例如:若是 loadFactor 等于 0.5,则说明哈希表中只有一半的空间存放了元素值,其他一半都为空。
哈希表的构造函数容许用户指定 loadFactor 值,定义范围为 0.1 到 1.0。然而,无论你提供的值是多少,范围都不会超过 72%。即便你传递的值为 1.0,Hashtable 类的 loadFactor 值仍是 0.72。微软认为loadFactor 的最佳值为 0.72,这平衡了速度与空间。所以虽然默认的 loadFactor 为 1.0,但系统内部却自动地将其改变为 0.72。因此,建议你使用缺省值1.0(但实际
// Add some employees employeeData.Add(455110189) = new Employee("Scott Mitchell"); employeeData.Add(455110191) = new Employee("Jisun Lee"); // See if employee with SSN 123-45-6789 works here if (employeeData.ContainsKey(123456789))
上是 0.72)。
向 Hashtable 中添加新元素时,须要检查以保证元素与空间大小的比例不会超过最大比例。若是超过了,哈希表空间将被扩充。步骤以下:
由此看出,对哈希表的扩充将是以性能损耗为代价。所以,咱们应该预先估计哈希表中最有可能容纳的元素数量,在初始化哈希表时给予合适的值进行构造,以免没必要要的扩充。
Hashtable 类是一个类型松耦合的数据结构,开发人员能够指定任意的类型做为 Key 或 Item。当 .NET 引入泛型支持后,类型安全的 Dictionary<K,T> 类出现。Dictionary<K,T> 使用强类型来限制 Key 和 Item,当建立 Dictionary<K,T> 实例时,必须指定 Key 和 Item 的类型。
Dictionary<keyType, valueType> variableName = new Dictionary<keyType, valueType>();
若是继续使用上面描述的社保号和员工的示例,咱们能够建立一个 Dictionary<K,T> 的实例:
Dictionary<int, Employee> employeeData = new Dictionary<int, Employee>();
这样咱们就能够添加和删除员工信息了。
Dictionary<K,T> 与 Hashtable 的不一样之处还不止一处。除了支持强类型外,Dictionary<K,T> 还采用了不一样的冲突解决策略(Collision Resolution Strategy),这种新的技术称为链技术(chaining)。
前面使用的挖掘技术(probing),若是发生冲突,则将尝试列表中的下一个位置。若是使用二度哈希(rehashing),则将致使全部的哈希被从新计算。而新的链技术(chaining)将采用额外的数据结构来处理冲突。Dictionary<K,T> 中的每一个位置(slot)都映射到了一个数组。当冲突发生时,冲突的元素将被添加到桶(bucket)列表中。
下面的示意图中描述了 Dictionary<K,T> 中的每一个桶(bucket)都包含了一个链表以存储相同哈希的元素。
上图中,该 Dictionary 包含了 8 个桶,也就是自顶向下的黄色背景的位置。必定数量的 Employee 对象已经被添加至 Dictionary 中。若是一个新的 Employee 要被添加至 Dictionary 中,将会被添加至其 Key 的哈希所对应的桶中。若是在相同位置已经有一个 Employee 存在了,则将会将新元素添加到列表的前面。
向 Dictionary 中添加元素的操做涉及到哈希计算和链表操做,但其仍为常量,复杂度为 O(1)。
对 Dictionary 进行查询和删除操做时,其平均时间取决于 Dictionary 中元素的数量和桶(bucket)的数量。具体的说就是运行时间为 O(n/m),这里 n 为元素的总数量,m 是桶的数量。但 Dictionary 几乎老是被实现为 n = m,也就是说,元素的总数毫不会超过桶的总数。因此 O(n/m) 也变成了常量 O(1)。