IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 集合概要小结 -> 正文阅读

[数据结构与算法]集合概要小结

一.简介

集合的主要作用是存储对象的容器,本质是用于存储对象的数据结构。

二.类型

集合类存放于java.util包中,主要set、list、map。

  • Collection是集合list set queue的最基本接口
  • Iterator:迭代器,可以通过迭代器遍历集合中的数据
  • Map:映射表的基础接口
    关系如下图:
    在这里插入图片描述
    在这里插入图片描述

2.1 List
一共三个实现类:ArrayList、Vector、LinkedList

ArryList
查询较快,增删较慢。内部通过数组实现,允许对元素进行快速随机访问。数组的每个元素之间不能有间隔,当数组大小不能满足时需要增加存储能力,就要将已有数组的数据复制到新的存储空间中。

Vector
也是通过数组实现,支持同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性。

LinkList
用链表结构存储数据,增删较快,查询较慢,可以当作堆栈、队列和双向队列使用

2.2 Set
无序,值不重复,如果想要让两个不同的对象视为相等的,必须覆盖Object的hashCode方法和equals方法

HashSet
哈希表存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不 同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的 hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。

TreeSet

  • TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增 加一个对象都会进行排序,将对象插入的二叉树指定的位置;
  • Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用;
  • 在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序;
  • 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

LinkHashSet

LinkedHashSet底层使用 LinkedHashMap 来保存所有元素,它继承于 HashSet

2.3 Map

在这里插入图片描述

HashMap
HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以使用ConcurrentHashMap。
java7和java8的区别
根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

HashTable
遗留类,建议不使用,不需要线程安全的场合可以使用HashMap,需要线程安全的场合可以用ConcurrentHashMap

TreeMap
TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序.在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。

LinkHashMap
有序

DEFAULT_INITIAL_CAPACITY =16:默认初始容量
MAXIMUM_CAPACITY :最大容量1 << 30
DEFAULT_LOAD_FACTOR =0.75f:负载因子
TREEIFY_THRESHOLD =8:阈值 如果链表长度超过阀值就把链表转成红黑树
UNTREEIFY_THRESHOLD =6:链表长度低于6,就把红黑树转回链表
MIN_TREEIFY_CAPACITY =64:开始转换为树结构的值

三. 扩容机制
如果这个桶中bin的数量小于 TREEIFY_THRESHOLD 当然不会转化成树形结构存储;
如果这个桶中bin的数量大于了 TREEIFY_THRESHOLD ,但是capacity小于 MIN_TREEIFY_CAPACITY 则
依然使用链表结构进行存储,此时会对HashMap进行扩容;
如果capacity大于了 MIN_TREEIFY_CAPACITY ,则会进行树化。

HashMap
在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发环境下不能使用HashMap。
jdk1.7采用头插法
HashMap的线程不安全主要是发生在扩容函数中,即根源在transfer函数中

void transfer(Entry[] newTable, boolean rehash) { 
    int newCapacity = newTable.length; 
    for (Entry<K,V> e : table) { 
        while(null != e) { 
            Entry<K,V> next = e.next; 
            if (rehash) { 
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity); 
            e.next = newTable[i]; 
            newTable[i] = e; 
            e = next; 
        } 
    } 
}

这段代码是 HashMap 的扩容操作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。理解了头插法后再继续往下看是如何造成死循环以及数据丢失的。
扩容造成死循环和数据丢失的分析过程
假设有两个线程A、B同时对下面这个HashMap进行扩容操作
在这里插入图片描述
正常扩容后的结果是下面这样子的:
在这里插入图片描述
但是当线程A执行到上面 transfer 函数的第11行代码时,线程A被挂起。即如下图中位置所示:
newTable[i] = e; //线程在此处挂起
此时线程A中:e=3、next=7、e.next=null
在这里插入图片描述
当线程A的时间片耗尽后,CPU开始执行线程B,并在线程B中成功的完成了数据迁移
在这里插入图片描述
重点来了,根据Java内存模式可知,线程B执行完数据迁移后,此时主内存中 newTable 和 table 都是最新的,也就是说:7.next=3、3.next=null。
随后线程A获得CPU时间片继续执行 newTable[i] = e ,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:
在这里插入图片描述
接着继续执行下一轮循环,此时e=7,从主内存中读取e.next时发现主内存中7.next=3,于是乎next=3,并将7采用头插法的方式放入新数组中,并继续执行完此轮循环,结果如下:
在这里插入图片描述
执行下一次循环可以发现,next=e.next=null,所以此轮循环将会是最后一轮循环。接下来当执行完e.next=newTable[i]即3.next=7后,3和7之间就相互连接了,当执行完newTable[i]=e后,3被头插法重新插入到链表中,执行结果如下图所示:
在这里插入图片描述
上面说了此时e.next=null即next=null,当执行完e=null后,将不会进行下一轮循环。到此线程A、B的扩容操作完成,很明显当线程A执行完后, HashMap 中出现了环形结构,当在以后对该 HashMap 进行操作时会出现死循环。并且从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。
JDK1.8不安全分析(尾插法)
根据上面JDK1.7出现的问题,在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到 transfer 函数,因为JDK1.8直接在 resize 函数中完成了数据迁移。另外说一句,JDK1.8在进行元素插入时使用的是尾插法。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { 
    Node<K,V>[] tab; Node<K,V> p; 
    int n, i; 
    if ((tab = table) == null || (n = tab.length) == 0) 
        n = (tab = resize()).length; 
    if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素 
        tab[i] = newNode(hash, key, value, null); 
    else {
        Node<K,V> e; K k; 
          if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 
              e = p; 
          else if (p instanceof TreeNode) 
              e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 
          else {
              for (int binCount = 0; ; ++binCount) { 
                  if ((e = p.next) == null) { 
                      p.next = newNode(hash, key, value, null); 
                      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 
                          treeifyBin(tab, hash); 
                      break; 
                  }if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 
                      break; 
                  p = e; 
              } 
          }if (e != null) { // existing mapping for key 
              V oldValue = e.value; 
              if (!onlyIfAbsent || oldValue == null) 
                  e.value = value; 
              afterNodeAccess(e); 
              return oldValue; 
          } 
         }
    ++modCount; 
    //size不安全 
    if (++size > threshold) 
        resize(); 
    afterNodeInsertion(evict); 
    return null; 
}

其中第六行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
除此之前,还有就是代码的第38行处有个 ++size ,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前 HashMap 的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-11-27 10:09:33  更:2021-11-27 10:11:17 
 
开发: 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 15:39:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码