求一行有不规则间隔为0的数据的和时,如果数据中有如何对多条件不重复值求和该如何调整函数。

      • 1.8之后为什么要引入红黑树?
      • HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?
      • 当两个对象的 hashCode 相等时会怎么样?
      • 什么是哈希碰撞,如何解决哈希碰撞?
      • 如果两个键的 hashCode 相同,如何存储键值对?
        • 那我自定义给的容量不是2的N次方呢?
      • 3.负载因子(加载因子)
      • 5.树化阈值(转红黑树的边界值)
      • 6.取消数化阈值(树转链表)
      • 3.指定容量和负载因子
        • 2-3-4树和红黑树的关系
        • 红黑树对于插入节点的分析
        • 红黑树对于删除节点的分析
        • 扩容方法中JDK8对JDK7的优化
      • 4.不带泛型的map的entrySet的迭代器进行遍历

HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的

hash算法又叫摘要算法(Digest),是将给定数据转化为固定长度的不规则值的函数

  • 输出值的数据长度固定(同一hash函数下)
  • 相同的输入必定得到相同的输出值
  • 相似的数据得到的输出值的差距也许会有点大
  • 输入不同的数据有可能得到一致的输出值(哈希碰撞,也叫哈希冲突)

使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用

既然是hash算法,那么碰撞就不可能完全避免的

因为碰撞的概率关系到了算法的安全性,所以一个安全的哈希算法必须满足

  • 不能通过哈希值算出原值

根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全

jdk1.8之后:数组+链表+红黑树

大致过程大家应该都知道,简单讲就是hashcode结合数组长度,得到数组索引,空数组则放入,有元素则比对hash值,hash值相同则发生哈希碰撞,用equals比对key是否一致,一致则取到对应的值(put则覆盖),不一致就继续比对,到尾结点了就补链,链长度过长影响效率,默认链表长度大于为8且数组长度大于64时转红黑树

1.8之后为什么要引入红黑树?

哈希算法再好也无法避免哈希碰撞,当碰撞过多,接入链表时,前期数据少,效率还行

但是后期的不断碰撞,导致链表过长,读取需要遍历,时间消耗O(n)

而引入红黑树后,查找时间O(logn),提高了效率

//扩容条件2:size是否大于了边界值

第一个条件没啥好说的,空的就扩呗

第二个条件就需要多注意一下了

  1. size表示hashmap中k-v的实际数量,并不是数组的长度

如果老边界值为0则给初始值

链表长度阈值8,数组长度大小64

链表阈值至少大于2且至少为8,才能符合红黑树在收缩的时候能转为普通链表

数组长度至少为4倍的链表阈值以避免冲突

HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?

答:对于 key 的 hashCode 做 hash 操作,无符号右移 16 位然后做异或运算。还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移 16 位异或运算效率是最高的。

当两个对象的 hashCode 相等时会怎么样?

答:会产生哈希碰撞。若 key 值内容相同则替换旧的 value,不然连接到链表后面,链表长度超过阈值 8 就转换为红黑树存储。

什么是哈希碰撞,如何解决哈希碰撞?

答:只要两个元素的 key 计算的哈希码值相同就会发生哈希碰撞。jdk8 之前使用链表解决哈希碰撞。jdk8之后使用链表 + 红黑树解决哈希碰撞。

如果两个键的 hashCode 相同,如何存储键值对?

答:通过 equals 比较内容是否相同。相同:则新的 value 覆盖之前的 value。不相同:则将新的键值对添加到哈希表中。

这两个接口都是一个声明接口

序列化的含义、意义及使用场景

  • 序列化:将对象写入到IO流中
  • 反序列化:从IO流中恢复对象
  • 意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在

默认初始容量 - 必须是 2 的幂,它为什么必须是2的幂呢?

大家知道hashmap为了存取高效,要尽量减少哈希碰撞,即:让数据分配均匀,数组空间都占据,链表长度接近

而hashmap的算法就是取模hash%length,计算机直接取余的效率不如位运算

&:按位与运算,两个当且仅当都为1的时候结果才为1

扩容是非常消耗性能的,默认的容量不一定适用,所以需要创建的就指定一个适合的大小才是正确的

那我自定义给的容量不是2的N次方呢?

会给一个比自定义容量更大一些的2n的数,例如14时就给16,20时就给32

3.负载因子(加载因子)

上文已经讲得很清楚了,这里不再赘述

最大容量是230,左移30位

5.树化阈值(转红黑树的边界值)

当链表长度大于8时会转为红黑树

(其实也需要数组长度大于64,上文也提到了)

源码中的讲解是:到8转为红黑树 到6转为链表

红黑树的占用空间比链表要大很多,所以一开始还是链表会更好 ->空间和时间的权衡

为什么转化为红黑树的阈值8和转化为链表的阈值6不一样,是为了避免频繁来回转化

红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3, 而log(6)=2. 6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

6.取消数化阈值(树转链表)

到8转为红黑树 到6转为链表

该值必须大于 2 并且应该至少为 8 以符合树移除中关于在收缩时转换回普通 bin 的假设。

我们可以看见收缩转回普通bin,证明hashmap会从红黑树收缩为链表

用的最多的,值都是默认值,也许并不是最好的选择

因为扩容是非常消耗性能的,所以我们可以自定义容量

可以看到其实指定容量,就是把自定义的容量和默认的负载因子传给的下面的方法↓

3.指定容量和负载因子

指定的容量必须为2n,上文也提到了那我自定义给的容量不是2的N次方呢?的问题,这里不再赘述

一句话:为了得到更大的容量来减少扩容,提高效率

s/loadFactor的结果是小数,加1.0F与(int)ft相当于是对小数做一 个向上取整以尽可能的保证更大容量,更大的容量能够减少resize的调用次数

所以+1.0F是为了获取更大的容量

例如:原来集合的元素个数是6个,那么6/0.75是8, 是2的n次幂,那么新的数组大小就是8了。然后原来数组的数据就会存储到长度是8的新的数组中了,这样会导致在存储元素的时候,容量不够,还得继续扩容,那么性能降低了

而如果+1呢,数组长度直接变为16了,这样可以减少数组的扩容

  • 先通过hash值计算出key映射到哪个桶(数组索引)
  • 比对hash,如果桶上没有碰撞冲突,则直接插入
  • 如果出现碰撞冲突了,则需要处理冲突
    • 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据
    • 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树
  • 比对key,如果桶中存在重复的键,则为该键替换新值value
//之前提到了hash和数组长度进行 按位与,得到索引

有点意思的是,hashmap此处key为null时返回索引为0

并不是,而是随着第一次putVal进行了判空而创建的(调用了扩容方法)

上文提到了,这里再复制一遍

大家知道hashmap为了存取高效,要尽量减少哈希碰撞,即:让数据分配均匀,数组空间都占据,链表长度接近

而hashmap的算法就是取模hash%length,计算机直接取余的效率不如位运算

除非表太小,否则替换给定哈希索引处bin中的所有链接节点,在这种情况下改为调整大小

 //putVal方法中,循环链表,判断count>=链表阈值-1时,进行转化

在讲解转化方法之前,先讲一讲什么是2-3-4树和红黑树

这里推荐一个数据结构学习网站

2-3-4树是四阶的B树(Balance Tree),他属于一种多路查找树,它的结构有以下限制:
所有叶子节点都拥有相同的深度
节点只能是2-节点、3-节点、4-节点

  • 2-节点:包含1个元素的节点,有2个子节点
  • 3-节点:包含2个元素的节点,有3个子节点
  • 4-节点:包含3个元素的节点,有4个子节点

所有节点必须至少包含1个元素

元素始终保持排序顺序,整体上保持二叉查找树的性质,即父结点大于左子结点,小于右子结点

而且结点有多个元素时,每个元素必须大于它左边的和它的左子树中元素

2-3-4树的查询操作像普通的二叉搜索树一-样,非常简单,但由于其结点元素数不确定,在一些编程语言中实现起来并不方便, 实现一般使用它的等同——红黑树

2-3-4树和红黑树的关系

  • 3节点 - 左倾/右倾

红黑树Red-Black-Tree [RBT] ,是一个自平衡二叉查找树,每个节点都遵守下面的原则

  • 每个节点要么是黑色,要么是红色

    • 一棵树当中没有子结点(即度为0)的结点称为叶子结点

  • 每个红色节点的两个子节点一定都是黑色

    • 图中红色节点没有子节点是因为默认隐藏掉了为null的黑色节点
  • 任意一个节点到每个叶子节点的路径都包含数量相等的黑节点

红黑树能自平衡靠的是什么?左旋、右旋、变色

左旋:子左给父右,父子交换

以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,

右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。

右旋:子右给父左,父子交换

以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,

左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。

节点颜色由黑边红或者由红变黑

红黑树对于插入节点的分析

对于一颗普通的二叉搜索树来说,就是左小右大

红黑树的插入也是这样的,只是插入完可能是需要做平衡处理(旋转、变色)

  • 2-3-4树2节点新增一个元素直接合并为3节点
    • 红黑树:新增一个红色节点这个红色节点会添加在黑色节点下:不需要调整
  • 2-3-4树3节点新增个元素 合并为一个4节点
    • 红黑树:就会有6种情况两种(左中右)的不需要调整
    • 根左左 根右右 旋转1次
    • 根左右 根右左 旋转2次
  • 2-3-4树4节点新增一个元素4节点中间元素升级为父节点新增元素和剩下节点合并
    • 红黑树:新增节点是红色+爷爷节点是黑色父 节点和叔叔节点为红色
    • 调整为爷爷节点变为红色,父亲和叔叔节点变为黑色,如果爷爷节点为root节点则调整为黑色

红黑树对于删除节点的分析

我们先看一颗普通的二叉树

前驱节点:对一棵二叉树进行中序遍历,遍历后的顺序,当前节点的前一个节点为该节点的前驱节点

后继节点:对一棵二叉树进行中序遍历,遍历后的顺序,当前节点的后一个节点为该节点的后继节点

即:1节点的前驱节点为:5,后继节点为:6

而删除操作就是有3种情况

  1. 删除叶子节点,直接删除
  2. 删除的节点有一个子节点,那么用子节点来替代
  3. 如果删除的节点右两个子节点,此时需要找到前驱节点或者后继节点替代可以转换为1、2的情况
 //传入数组tab和hash值用于确定下标
 //n是数组长度,index是下标,e是根据hash值从数组中取的节点
 //判断数组还有没有容量及是否大于64的数组阈值,没则扩容
 //桶中节点不为空则循环转换红黑树节点
 //hd链表头结点,tl尾结点
 //将普通节点转为树节点
 //链表处理变treeNode后放置回桶中
 //将红黑树节点链表进行平衡操作,才是最终变为红黑树
dir = -1; // 标识当前链表节点会放到当前树节点的左侧 * 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较 * 如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节点是相同Class的实例,那么通过comparable的方式再比较两者。 * 如果还是相等,最后再通过tieBreakOrder比较一次 * 如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左孩子,也可能是左孩子的右孩子 或者 更深层次的节点。 * 如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右孩子,也可能是右孩子的左孩子 或者 更深层次的节点。 * 如果当前树节点不是叶子节点,那么最终会以当前树节点的左孩子或者右孩子 为 起始节点 再从GOTO1 处开始 重新寻找自己(当前链表节点)的位置 * 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。 * 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。 // 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的 // 因为我们要基于树来做查找,所以就应该把 tab[N] 得到的对象一定根是节点对象,而目前只是链表的第一个节点对象,所以要做相应的处理。 //把红黑树的根节点设为 其所在的数组槽 的第一个元素 //首先明确:TreeNode既是一个红黑树结构,也是一个双链表结构 //这个方法里做的事情,就是保证树的根节点一定也要成为链表的首节点

扩容机制在上文讲述了,这里不再赘述

//获取扩容之前的元素列表 //获取之前的列表长度 //如果原来的容量已经大于了最大容量,则把threshold设置成int的最大值 //如果走下面这个else,则说明是第一次put元素,也是第一次进行扩展

 
 //到此为止,如果是第一次扩容的时候,也就是原来没有元素,下面的代码不会运行
 //如果原来有元素,则将原来的元素,进行放到新扩容的数组里面
 //循环原来的列表,将旧数组放置新数组中
 //如果列表里面只有一个元素,直接将e的key的hash与新容量重新计算下标
 
 //如果是红黑树,则进行红黑树的复制
 这一块是处理原来的HashMap冲突的,因为容量扩容了
 原来通过 & 运算出来的hash冲突的key,与新的容量 & 可能位置会发生改变
 所以需要重新计算下标,通过与oldCap 进行 & 运算 分为低位和高位,因为和oldCap &运算只有两种结果,一种是0 另一种是oldCap
 将结果=0的设置成低位,将等于oldCap的保存到高位,
 低位的hash值肯定小于oldCap,所以下标还是在原来的位置
 高位的hash一定大于oldCap,在二进制最左至少会多一个1(有可能还有前面还有其他二进制数字,但运算结果都是一样的)计算正好是原来位置加上新的容量
 //获取当前节点的下一个元素
 
 //如果不是第一个元素,就把他加到后面
 //将原来下标指向低位的链表
 //下标 j + oldCap 计算出来的新下标正好是原来位置索引加上新的容量

扩容方法中JDK8对JDK7的优化

jdk7中,所有元素按照索引算法全部重新计算效率比较低

可以看到在1.7中,借助transfer()方法(jdk1.8中已移除),在扩容的时候会重新计算threshold,数组长度并进行rehash,这样的效率是偏低的

    //判断是否有超出扩容的最大值,如果达到最大值则不进行扩容操作     // transfer()方法把原数组中的值放到新数组中     //设置hashmap扩容后为新的数组引用     //设置hashmap扩容新的阈值         //通过key值的hash值和新数组的大小算出在当前数组中的存放位置

jdk8中,在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就行

正是因为这样巧妙的rehash方式,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认

为是随机的,在resize的过程中保证了rehash之后每个桶上的节点数一定小于等于原来桶上的节点数,保证了

rehash之后不会出现更严重的hash冲突,均匀的把之前的冲突的节点分散到新的桶中了。

//扩容后的位置=原位置 //扩容后的位置 = 原位置 + 旧容量

从上面的源码中可以分析出扩容分为三种情况:

第二种是如果oldCap大于0,也就是扩容过的话,每次table的容量和threshold都会扩围原来的两倍

和put一样,为用户提供一个方法,实际上调用的removeNode

传递key的hash值选中位置,传递key以选中具体元素进行删除

判断删除的节点存不存在,存在则删除并返回节点的值value,反之返回null

* 方法为final,不可被覆写,子类可以通过实现afterNodeRemoval方法来增加自己的处理逻辑(解析中有描述) * @return 返回被删除的节点对象,如果没有删除任何节点则返回null * 如果 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p(该节点为 树的根节点 或 链表的首节点)不为空 * 需要从该节点p向下遍历,找到那个和key匹配的节点对象 // 如果当前节点的键和key相等,那么当前节点就是要删除的节点,赋值给node * 到这一步说明首节点没有匹配上,那么检查下是否有next节点 * 如果没有next节点,就说明该节点所在位置上没有发生hash碰撞, 就一个节点并且还没匹配上,也就没得删了,最终也就返回null了 * 如果存在next节点,就说明该数组位置上发生了hash碰撞,此时可能存在一个链表,也可能是一颗红黑树 // 如果当前节点是TreeNode类型,说明已经是一个红黑树,那么调用getTreeNode方法从树结构中查找满足条件的节点 // 如果不是树节点,那么就是一个链表,只需要从头到尾逐个节点比对即可 // 如果e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量,调出循环 // 走到这里,说明e也没有匹配上 p = e; // 把当前节点p指向e,这一步是让p存储的永远下一次循环里e的父节点,如果下一次e匹配上了,那么p就是node的父节点 } while ((e = e.next) != null); // 如果e存在下一个节点,那么继续去匹配下一个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点 * 如果node不为空,说明根据key匹配到了要删除的节点 * 如果不需要对比value值 或者 需要对比value值但是value值也相等 * 那么就可以删除该node节点了 tab[index] = node.next; // 由于删除的是首节点,那么直接将节点数组对应位置指向到第二个节点即可 else // 如果node节点不是首节点,此时p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了

和put与remove一样,提供一个对外的方法,实际上是调用的getNode方法

如果有值返回值,无值返回null

也就是比对key是否一致,一致则返回

否则就先判断是不是红黑树,是则用getTreeNode获取并返回

不是红黑树,则循环链表并和之前一样比对key并判断

4.不带泛型的map的entrySet的迭代器进行遍历

和第3种方法差不多,区别只有不带泛型,取值需要强转

}

如果EXCEL电子表格的数据比较复杂,所使用的行列比较多,那么,如何通过公式获取每一行或每一列中,有数据的单元格的最后一个单元格呢?

  比如第一行,有效数据的最后一个单元格是B2,而第五行,有效数据的最后一个单元格是F5。

  再比如,A列的有效数据的最后一个单元格是A5,而C列,有效数据的最后一个单元格是C2。

  诸如此类,在不规则数据的电子表格中,我们如何通过公式获取行列中的有效数据的最后一个单元格的值呢?

  借助9E+307这个数,再加上LOOKUP函数,即可处理此类问题。

  A、获取第一行有效数据最后一个单元格

  由于我们知道,最大使用行到F1,所以,上述的查找范围是从A1:F1;当然,您也可以修改得再大点,比如,A1:Z1,结果同样是正确的。

  B、获取第二行有效数据最后一个单元格

  获取行的公式,大体类似,下面就不再介绍了。

  C、获取第一列有效数据最后一个单元格

  C、获取第四列有效数据最后一个单元格

  获取其它列有效数据最后一个单元格公式都是类似的,不再介绍。

  为什么我们要借助9E+307这个数呢?到底有何用意?

  9E+307用来表示EXCEL中最大的表中不可能出现的数,因而返回的是第一行列中最末一个单元格的数据。

  这是使用科学记数法表示的9*10^307,是接近Excel允许键入的最大数值9.99E+307的一个数。

  由于9E+307是个很大的数字,一般不会出现,其实只是一个代表性的数字。

  1、作为一个大的数值,9E+307常被用来作为查找、数值比较等,比如常用=LOOKUP(9E+307,B:B)查找B列最后一个数值;

  2、严格来说,9E+307的使用是不规范的,比如如果A列刚好有数值9.9E+307,上面这个公式就不正确了,但实际的数据很少出现这种情况。此外,在公式中使用9E+307时,可能会被Excel自动更正为E9+307,而导致计算错误。

  比如说,公式:=LOOKUP(9E+307,A:A),查找的是A列有效数据最后一个单元格

  那么,由于从上图的表中,我们都看到了,A5单元格的123,比公式中的100还要大,所以,该公式=LOOKUP(100,A:A)返回的结果是3,而不是123,所以,尽管公式未提示错误,但实际上并非我们所需要的正确结果。所以,在使用的过程中,常常需要9E+307作为对比,用它来当作一个很少出错误或者不会出错误的参照对象。

}

我要回帖

更多关于 如何对多条件不重复值求和 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信