《java编程语言 面经 面试题 研二期间整理的面试题 》

面试题1:为何linkList插入删除效率比arrayList高?

  arrayList底层是一个数组,底层的arraycopy方法,只要是插入一个元素,其后的元素就会向后移动一位,虽然arraycoy是一个本地方法,效率非常高,但频繁的插入,每次后面的元素都需要拷贝一遍,效率变低了,特别是在头位置插入元素时。LinkedList是一个双向的链表,它的插入只是修改相邻元素next和previous引用。

  在ArrayList的前面或中间插入数据时,你必须将其后的所有数据相应的后移,这样必然要花费较多时间,所以,当你的操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能; 而访问链表中的某个元素时,就必须从链表的一端开始沿着连接方向一个一个元素地去查找,直到找到所需的元素为止,所以,当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
  除了底层数组和链表数据结构本身插入数据特点外,arrayList的扩容过程也会影响插入效率
知识补充
ArrayList延伸:ArrayList是经常会被用到的,一般情况下,使用的时候会像这样进行声明:
List arrayList = new ArrayList();
  如果像上面这样使用默认的构造方法,初始容量被设置为10。当ArrayList中的元素超过10个以后,会重新分配内存空间,使数组的大小增长到16。可以通过调试看到动态增长的数量变化:10->16->25->38->58->88->…
也可以使用下面的方式进行声明:
List arrayList = new ArrayList(4);
  将ArrayList的默认容量设置为4。当ArrayList中的元素超过4个以后,会重新分配内存空间,使数组的大小增长到7。可以通过调试看到动态增长的数量变化:4->7->11->17->26->…

那么容量变化的规则是什么呢?公式:((旧容量 * 3) / 2) + 1

面试题2:hashMap存储机制、扩容如何实现?

  首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
  当我们想插入一个键值对时执行put方法,首先会根据key的值获得该键值对应该存放在哈希表中哪个位置,然后判断该数组这个位置上有没有相同的key的键值对,如果有则替换value,如果没有就插入该位置,并且将Entry中的next执行上一个本来存在这个位置的Entry。

// 存储时:
int hash = key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值
int index = hash % Entry[].length;

  当要执行get方法时,也是先通过key的哈希值通过获得这个值对应哈希表中的索引,然后遍历链表,得到需要的键值对。key的哈希值确定键值对数组index:hashcode % table.length取模。
补充:哈希表介绍
  首先由于,数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。哈希表综合了两种数据结构,是一种链表数组。
        在这里插入图片描述
       在这里插入图片描述
  从上图我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
  扩容实现原理:当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能

面试题3:hash冲突有那些解决办法?(没记住)

当关键字集很大时,可能会有不同的键的键值对会映射到哈希表的同一个地址上。当发生冲突时,解决办法有:
1 开放定址法,这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi
2. 再哈希法,这种方法是同时构造多个不同的哈希函数: Hi=RH1(key) i=1,2,…,k,当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。
3. 链地址法,这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。hashmap解决冲突 链地址法
4.建立公共溢出区,将哈希表分成基本表和溢出表两部分,凡是与基本表冲突的数据都放到溢出表中。

  重新构造哈希函数,也是一种方法:
1、直接定址法,哈希函数为关键字的线性函数,H(key) = key, 此法仅适合于:地址集合的大小 = 关键字集合的大小
2、平方取中法, 以关键字的平方值的中间几位作为存储地址。求“关键字的平方值” 的目的是“扩大差别” ,同时平方值的中间各位又能受到整个关键字中各位的影响。 此法适于:关键字中的每一位都有某些数字重复出现频度很高的现象
3、折叠法,将关键字分割成若干部分,然后取它们的叠加和为哈希地址。两种叠加处理的方法:移位叠加:将分 割后的几部分低位对齐相加;间界叠加:从一端沿分割界来回折叠,然后对齐相加。此法适于:关键字的数字位数特别多
4、随机数法, 设定哈希函数为:H(key) = Random(key)其中,Random 为伪随机函数,此法适于:对长度不等的关键字构造哈希函数
5、除留余数法,设定哈希函数为:H(key) = key MOD p ( p≤m ),其中, m为表长,p 为不大于 m 的素数,或是不含 20 以下的质因子

面试题4:ArrayList、Vector、HashMap、HashSet的默认初始容量、加载因子、扩容增量

  List 元素是有序的、可重复:ArrayList、Vector默认初始容量为10  Set(集) 元素无序的、不可重复:

  1. Vector:线程安全,但速度慢,底层数据结构是数组结构,加载因子为1:即当 元素个数 超过 容量长度 时,进行扩容。扩容增量:原容量的 1倍, 如 Vector的容量为10,一次扩容后是容量为20
  2. ArrayList:线程不安全,查询速度快,底层数据结构是数组结构,扩容增量:原容量的 0.5倍+1。如 ArrayList的容量为10,一次扩容后是容量为16
  3. HashSet:线程不安全,存取速度快,底层实现是一个HashMap(保存数据),实现Set接口,默认初始容量为16(为何是16,见下方对HashMap的描述),加载因子为0.75:即当
    元素个数 超过 容量长度的0.75倍 时,进行扩容, 扩容增量:原容量的 1 倍,如
    HashSet的容量为16,一次扩容后是容量为32。
  4. HashMap:默认初始容量为16,(为何是16:16是2^4,可以提高查询效率,另外,32=16<<1 -->至于详细的原因可另行分析,或分析源代码),加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容,扩容增量:原容量的 1 倍,如 HashSet的容量为16,一次扩容后是容量为32

面试题5、并发的HashMap为什么会引起死循环?

  多线程会导致HashMap的Entry链表形成环形数据结构。
参考 http://blog.csdn.net/zhuqiuhui/article/details/51849692

面试题6、stack arraylist区别

  stack 由VECTOR实现 先进后出 线程安全 删除元素只能从栈顶出栈删除 。Arraylist 线程不安全 可以删除任意节点的数据

面试题7、TreeMap实现原理(难)

  红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长
首先需要知道红黑树的基本性质:5条

  1. 每个节点都只能是红色或者黑色
  2. 根节点是黑色
  3. 每个叶节点(NIL节点,空节点)是黑色的。
  4. 也就是说在一条路径上不能出现相邻的两个红色结点。
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

对于新节点的插入有如下三个关键地方:

  1. 插入新节点总是红色节点
  2. 如果插入节点的父节点是黑色, 能维持性质
  3. 如果插入节点的父节点是红色, 破坏了性质. 故插入算法就是通过重新着色或旋转, 来维持性质

  TreeMap put()方法实现分析:分为两个步骤,第一:构建排序二叉树,第二:平衡二叉树。平衡二叉树过程包括三大基本操作:左旋、右旋、着色。构建排序二叉树比较简单,只需要将插入结点New不断从根结点比较,根据二叉搜索树的左子树<根结点<右子树结点,来最终决定插入位置。平衡二叉树包括:左旋、右旋、着色。
  左旋、右旋、着色操作需要对红黑二叉树进行当前的状况根据插入的新节点进行判断,通常会有1、插入的节点就是根结点,那么此时直接着色即可。2、插入的结点父节点P是黑色,那么也直接着色即可。3、若父节点P为红色,叔父节点存在并且也为红色,那么把插入结点N的父节点的父节点G着色成红色,父节点P和叔结点U着色成黑色。4、若父节点P为红色,叔父节点U为黑色或者缺少,且新增节点N为P节点的右孩子,那么要进行左旋操作,就是将新增节点(N)当做其父节点(P),将其父节点P当做新增节点(N)的左子节点。即:G.left —> N ,N.left —> P。5、父节点P为红色,叔父节点U为黑色或者缺少,P.right —> G、G.parent —> P,

当P为红色,Uncle为黑色,插入的N为左子树结点,要进行右旋。此时有P.right = G; G.parent = P;
在这里插入图片描述
在这里插入图片描述
当插入的结点N是右子树结点,父节点N为红色,Uncle为黑色,
在这里插入图片描述
此时要插入的N由右子树,改成插入成左子树。执行G.left = N; N.left = P;,然后此时将原先的P视为要插入的新结点N,原先的N视为P,进行当插入结点为左子树,即上面一种情形的操作。
在这里插入图片描述
当父节点和Uncle结点是红色,只需要把P和Uncle涂黑即可。
在这里插入图片描述
  TreeMap delete()方法这里的删除节点并不是直接删除,而是通过走了“弯路”通过一种捷径来删除的:找到被删除的节点D的子节点C,用C来替代D,不是直接删除D,因为D被C替代了,直接删除C即可。 红黑树删除节点同样会分成几种情况,这里是按照待删除节点不要考虑颜色冲突,而是根据有几个儿子的情况来进行分类:

  1. 没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
  2. 只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
  3. 有两个儿子。这时候就可以把被删除元素的右支的最小节点(被删除元素右支的最左边的节点)和被删除元素互换,我们把被删除元素右支的最左边的节点称之为后继节点(后继元素),然后在根据情况1或者情况2进行操作。如图:
    在这里插入图片描述
    在这里插入图片描述

面试题8、 ConcurrentHashMap实现原理

  ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。从ConcurrentHashMap代码中可以看出,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的Segment,根据key.hashCode()来决定把key放到哪个Segment中,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中.
  通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。ConcurrentHashMap get读取数据时不上锁 更新是上锁(分段锁) 读取更新可同时发生 hashtable就不能 。
ConcurrentHashMap把整个Map分为16个Segment(每个 Segment 对象守护每个散列映射表的若干个桶,每个桶是由若干个 HashEntry 对象链接起来的链表。),可以提供相同的线程安全,同时16个写线程并发执行(写线程才需要锁定,而读线程几乎不受限制).
        在这里插入图片描述

<

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值