数组

数组

定义

数组,是有序的元素序列,用于存储多个相同类型的数据的集合,数组在程序设计中,为了处理方便,把具有相同类型的若干元素按无序的形式组织起来的一种形式。这些无序排列的同类数据元素的集合称为数组

连续的同一类型的数据,使用连续的内存空间存储
数组,即连续的同一类型的数据,使用连续的内存空间存储
值内存地址 = 数组开始地址 + 下标 * 类型占用内存长度
下标通常用来定位内存的空间地址,与内存的值无关

特性

数组的时间复杂度
查找(修改):

  1. 按下标查找(修改):通过公式直接映射到内存地址,其时间复杂度为 O(1)
  2. 按数值查找(修改):将查找目标与数组中的每个值进行比对,复杂度与数组的长度对应,其时间复杂度为 O(n)
    假设数组是有序的 就可以根据值映射到下标快速访

插入:数组的元素插入 ,需要将插入的元素后的元素全部依次向后移动,其时间复杂度为O(n)
删除:同插入类型,数组的元素删除,需要将删除的元素后的元素全部依次向前移动,其时间复杂度为O(n)
如果插入、删除元素为数组的最后一位,复杂度为O(1)(最优)

为什么一般使用数组要指定长度

不指明尺寸大小就无法开辟内存,无论是在堆上还是在栈上,为了实现常数时间的随机访问,数组元素都必须线性的分布在一段内存中
如果想实现动态数组的话,就意味着编译器必须要插入额外的代码来实现动态的重分配内存和转移元素,这会给程序带来额外的性能损耗

在Java中声明ArrayList,在不指定长度的情况下,默认分配长度为10的数组,假设其内存中存储地址为100-139,在数组满元素的情况下,当插入第11个元素时,数组将动态的将长度扩展为15(默认扩容1.5倍)

  • 情况1:若内存空间可以容纳扩展后的数组大小 ,仅扩展内存空间,为其分配内存地址为100-159
  • 情况2:当内存空间不足以容纳扩展后的数组大小时,将重新为扩展后的数组分配新的内存空间,如:200-259,并将110-139中的元素插入到200-259中,然后再由Java GC自动回收100-139所占用的内存空间

常见问题

下标越界
空指针

散列表(Hash哈希表)

原理及构造

散列表(Hash Table,也叫哈希表),是根据关键码(Key value)而直接进行访问的数据结构。即通过把关键码映射到表中的一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。散列表是一种面向查询的数据结构,其查询复杂度为O(1)
在这里插入图片描述
将数组的下标映射出来,与关键码一一对应,就是散列查找的原理
将关键码Key称为散列函数,将存放具体数据的数组称为散列表

特性

  • 散列函数的构造遵循非负整数,值相等则键相等,值不等则键不等的原则

即:

  1. 散列函数计算得到的散列值是一个非负整数
  2. 如果 key1 == key2,那 hash(key1) == hash(key2)
  3. 如果 key1 != key2,那 hash(key1) != hash(key2)
  • 散列冲突(散列碰撞)

散列冲突指 key1 != key2 的情况下,通过函数处理 hash(key1) == hash(key2),这时被称之为散列冲突。因为散列值是非负整数,总量是有限的,而现实世界中要处理的键值是无限的,将无限的数据映射到有限的集合,肯定避免不了冲突


散列表查询、增加、删除的时间复杂度取决于散列冲突;

  1. 在不考虑散列冲突的情况下,查询、增加、删除的时间复杂度均为O(1);
  2. 存在散列冲突时,最坏的情况可能退化为顺序查找,即时间复杂度为O(n
    散列碰撞为散列表的致使缺陷,需要谨慎设计使用

如何解决散列冲突

1. 开放寻址法

开放寻址法的核心思想是,如果出现了散列冲突,就重新探测一个空闲的位置

当关键字key的散列地址 p = H(key) 出现冲突时,产生另一个散列地址p1, 如果p1仍有冲突,则再以 p 为基础,产生另一个散列地址 p2,依次类推,直到出现一个不冲突的散列地址 pi, 将相应元素存入其中

开放寻址法的通用散列函数公式为:Hi = (H(key) + di) % m      i = 1, 2, 3, …, n
其中 H(key) 为哈希函数,m 为表长,di称为增量序列,根据增量序列的取值方式不同,相应的散列方式也不同,主要方式有如下几种

Linear Probing(线性探测法)
di = 1, 2, 3, …, m - 1
冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表
Quadratic probing(二次探测法)
di = 12, -12, 22, -22, …, k2, -k2    (k <= m/2)
冲突发生时,在表的左右进行跳跃式探测,比较灵活
Double hashing(双重散列)
同时构造多个不同的散列函数:
Hi = RHi(key)     i = 1, 2, …, k
当散列地址 Hi = RHi(key) 发生冲突时,再计算 Hi = RH2(key)…,直到冲突不再产生,这种方法不易产生聚集,但增加了计算时间

假设已知散列表长度m = 11,散列函数为:H(key) = key % 11
则H(47) = 3, H(26) = 4, H(60) = 5,假设下一关键字为69,则 H(69) = 3,与47冲突


采用线性探测法处理冲突,下一散列地址为 H1 = (3 + 1) % 11 = 4,仍有冲突,再寻找下一散列地址 H2 = (3 + 2) % 11 = 5,仍有冲突,继续寻找下一散列地址 H3 = (3 +3) % 11 = 6,此时无冲突,将69填入5号位置
采用二次探测法处理冲突,下一散列地址为 H1 = (3 + 12) % 11 = 4,仍有冲突,再寻找下一散列地址 H2 = (3 - 12) % 11 = 2,此时无冲突,将69填入2号位置

不管如何开放寻址,始终会用完内存空间,而且空间越少探测的复杂越高,若要尽量保证散列表的空闲位置,采用装载因子来表示空位有多少。开放寻址法适合装载因子可控,或较低的情况下使用。散列表的装载因子公式如下:

散列装载因子 = 已插入的数据个数 / 散列表长度

2.链表法

链表法的核心思想是,将所有的散列地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在散列表的第 i 个单元中,因而查找、插入和删除主要在同义词链中进行,保证其插入和删除数据的时间复杂度为O(1),但堆叠越多,查询的复杂度越高。
链表法适用于经常进行插入和删除的情况

常见应用

HashMap是基于散列表的Map接口实现,底层依赖于数组,默认长度为16,装载因子为0.75
用链表法扩容(JDK1.8后增加了红黑树,链表长度超过8则转树)
每次扩容增加的量为2的倍数(JAVA中散列函数转化为二进制后,以2的倍数为值能大量减少散列碰撞,更充分的利用空间)

初始化为指定长度时,默认为2的倍数,例如初始化长度为10,则为10,若初始化长度为7,则初始化后长度为8,获取容量的方法如下:

static final int tableSizeFor(int cap) {
	  int n = cap - 1;
   n |= n >>> 1;
   n |= n >>> 2;
   n |= n >>> 4;
   n |= n >>> 8;
   n |= n >>> 16;
   return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

HashMap是如何处理散列碰撞的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值