| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 数据结构与算法 -> Java.ArrayList LinkedList HashMap JDK8版本 -> 正文阅读 |
|
[数据结构与算法]Java.ArrayList LinkedList HashMap JDK8版本 |
常规集合中用的最多的是ArrayList,LinkedList,HashMap,内部数据结构分别是:数组,列表,以及数组,列表,二叉树(红黑树)。分别实现:不排序(是否按照大小排序)有序(插入顺序)集合,排序无序集合, key/value存储。这三个也是面试中经常能遇到的,我就遇到过很多次,虽说能理解内部结构但对实现细节了解不到位,回答的可想而知。本文就一次插到低,从此对面试官说去你MA的。 下面我们从自己去实现这三个集合的角度去思考。如果如果实现就需要考虑如下问题:
基础知识数据结构.红黑树 Red Black Tree 自平衡二叉树 术语排序: 是否按照元素大小排序,插入数据后,这个列表是否完成了排序,按照自己提供的迭代器可以输出就算 有序:插入数据的先后顺序,怎么存储不管,是否可以通过某种方法,可以遍历出来就算 列表:一些数字或者对象 的集合 链表:数据结构中的类似锁链的一种集合数据存储结构 英文能力ensure 确保 capacity 容量 Internal 内部 explicit 明确的 overflow 溢出 Sequential 顺序式 Linked 链接,关联( 不是有序的意思) factor 因素 因子 ArrayList用什么样的数据结构存储集合呢-- 数组/链表,数组大小固定,链表到是可以无限链接下去,这里用数组吧,后面再实现一个链表的 数组是固定长度的,是否要初始化,初始化多少呢?-- (1.7前) 初始化10个元素,10怎么来的,总得初始化一个大小吧 -- (1.8后) 懒加载,第一次Add的时候初始化为10个 怎么存储不同类型不同形态的数据-- 实现方法,Object[]? ?泛型<T>? 通配符? -- 存储空间:Object[] elementData -- 操作:public boolean add(E e)? 无论初始化多少,也有不够用的时候,按照啥规则扩展容量?-- 添加一个或者多个元素,扩展至少保证最需要需求,除非超过Integer.MAX_VALUE(JVM限制的,部分VM会预留部分表头,所以一般别超过Integer.MAX_VALUE-8个元素) -- 扩容规则还是看代码吧
Add元素怎么放,删除的时候怎么删除-- 添加:元素从前往后放,size指向下一个位置的下标;可重复,可为null,列表中的下标顺序就是添加的顺序 -- 删除:中间删除元素,后边的元素挨个移动到前面,也会保持顺序 数据量少了要不要缩减空间-- remove方法中没有看到有空间缩小的逻辑 并发支持,怎么支持-- 多个线程操作就操作呗,不支持的是:遍历和 其他操作 并行,会触发ConcurrentModificationException异常,这就是所谓的:fast-faild
-- 线程不安全,咱就不要探究哪个方法支持,哪个方法不支持,咱们就应该避免这样的事发生。 比如:add 重新分配空间,导致空间变动,如果其他线程换在iterator遍历,遍历的是旧的。 -- 如果需要线程安全,三种方法: 1. 外边加锁,保持ArrayList的单线程操作; 2. ArrayList方法上synchronized,SynchronizedList list =?Collections.synchronizedList(new ArrayList()), 避免不了fast-faild问题。监控的是list对象 3.?synchronized太重,用ReentrantLock,类:CopyOnWriteArrayList,该类iterator不检查版本 总结:从上面总结出来的是否进行排序: 无序状态 是否添加顺序有序 : 数组从前到后 并发支持,怎么支持 : 要完全避免线程安全问题,并让结果可预期,就不要并发操作 并发操作不同怎办: 遍历和删除会触发fast-faild机制;其他 各种操作复杂程度
ListIterator更强大的遍历迭代器,可以双向移动。只是遍历双向的,因为底层还是数组,没法像连表一样扩展,所以用来做队列不是好的选择。 但如果要自己用两个下标start,end 来玩队列,也不是不可以,太复杂了。 LinkedList底层数据结构-- 看名称是想做一个排序的列表,既然是排序用链表好一点,并且是双向链表(没有表头) ?存储不够怎么扩展 -- 不存在这个问题 数据量少了要不要缩减空间-- 不存在这个问题 各种操作复杂程度-- 都是检索的复杂度O(n),找到就直接操作完事,双直针也省了 是否进行排序-- 明知道顾问,不排序
是否添加顺序有序-- add 的顺序就是 foreach的顺序,有序 并发支持,怎么支持-- 不支持,要不自己加锁synchronized,要不?Collections.synchronizedList 包起来 并发操作不同怎办 ?-- 也有fast-faild机制 怎么存储不同类型不同形态的数据-- 泛型Node<E>存储数据 HashMap为什么要有HashMap数组添加元素,扩容太费劲,删除需要移动数组。双向列表呢,搜索操作位置的时间是必须的。 需求:能不能有一种结构,能实现O(1)的,梦想可以有的,怎么实现呢 底层数据结构思路:按照地址访问是O(1), 如果node可以跟 地址有个映射就可以了。? 链表的节点地址完全不确定,排除;数组地址倒是固定,能用到地址也就是一个偏移量,总不能跟0x****这样的地址对应吧,out:node跟地址偏移量[0,n] 映射,这个映射算法在hashCode实现 如果一个node经过hash后可以唯一对于一个[0,n] 就很OK,但必然会有重复的情况。如果两个node的hash值都是1咋搞? 如果这个节点有值了,并且薪添加的node也落入这个节点,那就挂起来。结构有两种如下图,区别:数组是否存储数据,第二个能省空间。 ? 问题又来了,怎么平衡数组长度和总数量量呢,数组短,都成了直接访问链表。数组长? 那啥时候应该给数组扩容,扩多少呢?? 存储不够怎么扩展什么时候扩:size > (capacity * load factor)? (size? 现有数据量,capacity 容量-数组的长度,load factor 加载因子默认0.75) capacity 容量-数组的长度 - 会取2的次幂,跟后边的Hash算法相关,将方法:tableSizeFor 扩多少,怎么扩:见resize代码。不考虑边界,capacity 扩展2倍,即数组长度。复制节点: 1. 如果普通数据节点,则直接赋值到新数组中即可 2. 如果是链表或者红黑树节点,则按照 hash<=oldCap为边界 拆分两个节点结合,然后根据数量重新组织,拆分后的节点少于6 UNTREEIFY_THRESHOLD就组织为链表,超过则组织为树结构
新增节点逻辑从中体验下结构: 1. 数组槽位下标计算:(数组长度 - 1) & hash(key) 2. 怎么判断两个节点是一个Node: hash值一样,key是==,如果不是null 则equals方法 3. 链表元素超过8 TREEIFY_THRESHOLD个,就转为树
删除节点雷同新增节点中的查重逻辑 不同是:加入树删除的少于UNTREEIFY_THRESHOLD了,不会转为链表。 树转链表,只有resize 方法内用到。 并发支持hashMap就是简单的数组和链表,并没有控制并发的锁机制,所以是线程不安全的。如果并发操作:可能的动作是数组,链表,树并发操作的结果,天知道。 如果需要: 1. 外部代码控制锁 2. 或者使用HashTable,或者ConcurrentHashMap 其他特征数据量少了要不要缩减空间: 看情况 各种操作复杂程度: 看情况 是否进行排序:无序 是否添加顺序有序:无序 怎么存储不同类型不同形态的数据:泛型 什么用红黑树,不用二叉树,平衡二叉树之所以不用二叉树: 红黑树是一种平衡的二叉树,插入、删除、查找的最坏时间复杂度都为 O(logn),避免了二叉树最坏情况下的O(n)时间复杂度。 之所以不用平衡二叉树: 平衡二叉树是比红黑树更严格的平衡树,为了保持保持平衡,需要旋转的次数更多,也就是说平衡二叉树保持平衡的效率更低,所以平衡二叉树插入和删除的效率比红黑树要低。 红黑树怎么保持平衡的知道吗? 旋转:旋转分为两种,左旋和右旋 Hash函数怎么设计的,怎么保证每个槽位数量的离散性hashHashMap的哈希函数是先拿到 key 的hashcode,是一个32位的int类型的数值,然后让hashcode的高16位和低16位进行异或操作。 后边在hashCode基础上高低16位异或 ,有人称之为“扰动函数”,目的当然是为了更好的让hash值能均匀分布,为什么? -- 不清楚
为什么哈希/扰动函数能降hash碰撞因为 key.hashCode() 函数调用的是 key 键值类型自带的哈希函数,返回 int 型散列值。int 值范围为 -2147483648~2147483647,加起来大概 40 亿的映射空间。 只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。 假如 HashMap 数组的初始大小才 16,就需要用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。 源码中模运算就是把散列值和数组长度 - 1 做一个 “与&” 操作,位运算比取余 % 运算要快。
这样是要快捷一些,但是新的问题来了,就算散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。如果散列本身做得不好,分布上成等差数列的漏洞,如果正好让最后几个低位呈现规律性重复,那就更难搞了。 这时候 扰动函数的价值就体现出来了,看一下扰动函数的示意图: ?右移 16 位,正好是 32bit 的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。 为什么HashMap的容量是2的倍数呢?
源码解读参考HashMap源码解读—Java8版本_步尔斯特的博客-CSDN博客 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/26 8:33:55- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |