本文主要总结面试中常问的java集合数据结构


文章目录

  • 一、List
  • ArrayList
  • LinkedList
  • Vector
  • 二、Map
  • HashMap
  • LinkedHashMap
  • HashTable
  • TreeMap
  • 三、Set
  • HashSet
  • LinkedHashSet
  • TreeSet
  • 时间复杂度



JAVA校招问数据结构 java中数据结构面试题_链表

一、List

ArrayList

底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长

数据结构-线性表的顺序存储,在指定位置插入/删除元素的时间复杂度为O(n),求表长以及在数组末尾增加元素,取第 i 元素的时间复杂度为O(1),因为数组的内存是连续的,想要访问那个元素,直接从数组的首地址处向后偏移就可以访问到了

  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
  • ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速访问-随机存取。
  • ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。
  • ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。

ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。

LinkedList

LinkedList底层是双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)

  • LinkedList是一个实现了List接口和Deque接口的双端链表。
  • LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性;
  • LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:

ArrayList和LinkedList的底层分别对应数组和链表,这两种结构在内存存储上的表现不一样,所以也有各自的特点

数组
数组的特点

  1. 在内存中,数组是一块连续的区域
  2. 数组需要预留空间
  • 在使用前需要提前申请所占内存的大小,由于预先可能不知道需要多大的空间,若多申请了会浪费内存空间,空间利用率低;
  1. 在数组起始位置处,插入数据和删除数据效率低
  • 插入数据时,待插入位置的的元素和它后面的所有元素都需要向后搬移
  • 删除数据时,待删除位置后面的所有元素都需要向前搬移
  1. 随机访问效率很高,时间复杂度可以达到O(1)
  2. 数组开辟的空间,在不够使用的时候需要扩容,扩容的话,就会涉及到需要把旧数组中的所有元素向新数组中搬移
  3. 数组的空间是从栈分配的

数组的优点

随机访问性强,查找速度快,时间复杂度为O(1)

数组的缺点

  1. 头插和头删的效率低,时间复杂度为O(N)
  2. 空间利用率不高
  3. 内存空间要求高,必须有足够的连续的内存空间
  4. 数组空间的大小固定,不能动态拓展

链表
链表的特点

  1. 在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续
  2. 链表中的元素都会两个属性,一个是元素的值,另一个是指针,此指针标记了下一个元素的地址
  3. 查找数据时效率低,时间复杂度为O(N)
  • 由于链表的空间是分散的特点,不具有随机访问性,要访问某个位置的数据,需要从第一个数据开始找起,利用next指针依次往后遍历,直到找到待查询的位置,时间复杂度达到O(N)
  1. 空间不需要提前指定大小,是动态申请的,根据需求动态的申请和删除内存空间,扩展方便,故空间的利用率较高
  2. 任意位置插入元素和删除元素效率较高,时间复杂度为O(1)
  3. 链表的空间是从堆中分配的

链表的优点

  1. 任意位置插入元素和删除元素的速度快,时间复杂度为O(1)
  2. 内存利用率高,不会浪费内存
  3. 链表的空间大小不固定,可以动态拓展

链表的缺点

随机访问效率低,时间复杂度为0(N)


Vector

Vector 类实现了一个动态数组。和 ArrayList 很相似,都是封装了一个Object[],但是两者是不同的:

  • Vector 是同步访问的
  • Vector 包含了许多传统的方法,这些方法不属于集合框架。
  • Vector是线程安全的,ArrayList是非线程安全的,但性能上Vector比ArrayList低

Vector 主要用在事先不知道数组的大小,或者只是需要一个可以改变大小的数组的情况

二、Map

HashMap

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树。
为什么要这样设计呢?好处就是避免在最极端的情况下链表变得很长很长,在查询的时候,效率会非常慢

  • 红黑树查询:其访问性能近似于折半查找,时间复杂度 O(logn);
  • 链表查询:在极端情况下,需要遍历全部元素才行,时间复杂度 O(n);

JAVA校招问数据结构 java中数据结构面试题_JAVA校招问数据结构_02

HashMap中的put()和get()的实现原理:

  1. map.put(k,v)实现原理

(1)首先将k,v封装到Node对象当中(节点)。
(2)然后它的底层会调用K的hashCode()方法得出hash值。
(3)通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。

  1. map.get(k)实现原理

(1) 先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(2) 通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

map.remove(k)的实现原理

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
  }

这里调用了hash方法和removeNode方法

static final int hash(Object key) {
        int h;
//		如果对象不为null怎返回传入对象调用hashCode的返回值
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

分析hash方法,请看方法体三目运算符传入的key值不为空,则执行:后的代码 只要Key对象hashCode相同,则调用改方法的返回值一定相同。所以hash传入相同的对象就会得到相同的结果

removeNode参数列表:
hash:key的hash
key: key值
value: 如果matchValue==false,忽略value,否则必须满足key-value同时输入正确

JAVA校招问数据结构 java中数据结构面试题_java_03


JAVA校招问数据结构 java中数据结构面试题_JAVA校招问数据结构_04


JAVA校招问数据结构 java中数据结构面试题_面试_05


JAVA校招问数据结构 java中数据结构面试题_JAVA校招问数据结构_06

HashMap如何定位桶数组的位置
HashMap在通过key,get、put键值对的时候,会先对key调用hash(key)的处理,然后才会是一般的做取模运算(&(n - 1))定位桶数组的位置。最后将键值对添加到链表或者红黑树中。

hash(key)的源码如下:

/**
 * 计算键的 hash 值
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在 Java 中,hashCode 方法产生的 hash 是 int 类型,32 位宽。前16位为高位,后16位为低位。hash(key)的含义就是将key的高位与低位做异或运算,其目的是让key的高位也参与到取模运算中,使得键值对分布的更加的均匀。

HashMap在多线程下的线程不安全问题

HashMap在多线程情况下,在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

  • 另一个键值存储集合**HashTable,它是线程安全的,它在所有涉及到多线程操作的都加上了synchronized**关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的。
  • 其实HashTable有很多的优化空间,锁住整个table这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table,而这就**ConcurrentHashMap**,并发环境下推荐使用 ConcurrentHashMap 。

LinkedHashMap

LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组+链表+红黑树组成。
LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。

  • 链表的建立过程是在插入键值对节点时开始的,初始情况下,让 LinkedHashMap 的 head 和 tail 引用同时指向新节点,链表就算建立起来了。随后不断有新节点插入,通过将新节点接在 tail 引用指向节点的后面,即可实现链表的更新
  • 链表结点的删除过程
    删除的过程并不复杂,上面这么多代码其实就做了三件事:
    1. 根据 hash 定位到桶位置
    2. 遍历链表或调用红黑树相关的删除方法
    3. 从 LinkedHashMap 维护的双链表中移除要删除的节点
  • LinkedHashMap 允许使用null值和null键, 线程是不安全的,虽然底层使用了双线链表,但是增删相快了。因为他底层的Entity 保留了hashMap node 的next 属性

JAVA校招问数据结构 java中数据结构面试题_JAVA校招问数据结构_07

HashTable

HashTable类中,保存实际数据的,依然是Entry对象。其数据结构与HashMap是相同的。
HashTable类继承自Dictionary类,实现了三个接口,分别是Map,Cloneable和java.io.Serializable。
HashTable的主要方法的源码实现逻辑,与HashMap中非常相似,有一点重大区别就是所有的操作都是通过synchronized锁保护的。只有获得了对应的锁,才能进行后续的读写等操作。

  1. put方法

(1) 先获取synchronized锁
    put方法不允许null值,如果发现是null,则直接抛出异常。
(2) 计算key的哈希值和index
(3) 更新value或添加节点
    - 遍历对应位置的链表,如果发现已经存在相同的hash和key,则更新value,并返回旧值。
    - 如果不存在相同的key的Entry节点,则调用addEntry方法增加节点。
    - addEntry方法中,如果需要则进行扩容,之后添加新节点到链表头部。

  1. get方法

(1) 先获取synchronized锁。
(2) 计算key的哈希值和index。
(3) 返回值
    - 在对应位置的链表中寻找具有相同hash和key的节点,返回节点的value。
    - 如果遍历结束都没有找到节点,则返回null。

HashMap和HashTable的区别

  1. HashMap 不是线程安全的,HashTable 是线程安全 Collection。
  2. HashMap允许将 null 作为一个 entry 的 key 或者 value,而 Hashtable 不允许。
  3. HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsValue 和 containsKey。
  4. HashTable 继承自 Dictionary 类,而 HashMap 是 Map接口的一个实现。
  5. HashTable 的方法是 Synchronize 的,而 HashMap 不是,也就是说在多个线程访问中,不用专门的操作就安全地可以使用Hashtable 了;而对于HashMap,则需要额外的同步机制。

TreeMap

TreeMap实现了SotredMap接口,它是有序的集合。而且是一个红黑树结构,每个key-value都作为一个红黑树的节点。如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序,如果指定了比较器则按照比较器来进行排序。
结点的增删改查都能在 O(lgn) 时间复杂度内完成,如果按树的中序遍历就能得到一个按 键-key 大小排序的序列。
在插入K,V时他会根据红黑树的特性,根据compare方法返回的值在left,right中遍历找到对应的位置插入Entry或替换V。

  • TreeMap 底层结构为红黑树
  • 红黑树的Node排序是根据Key进行比较
  • 每次新增删除节点,都可能导致红黑树的重排
  • 红黑树中不支持两个or已上的Node节点对应红黑值相等

三、Set

HashSet

HashSet内部基于HashMap来实现的,底层采用HashMap来保存元素。Set集合无序并不可重复。HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个private static final Object PRESENT = new Object();。HashSet跟HashMap一样,都是一个存放链表的数组

  • 实现了 Serializable 接口,表明它支持序列化。
  • 实现了 Cloneable 接口,表明它支持克隆,可以调用超类的 clone()方法进行浅拷贝。
  • 继承了 AbstractSet 抽象类,和 ArrayList 和 LinkedList 一样,在他们的抽象父类中,都提供了 equals() 方法和 hashCode() 方法。它们自身并不实现这两个方法
  • 实现了 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持,不能保证元素的顺序。

HashSet 特性

  • 不能保证元素的顺序,元素是无序的
  • HashSet 不是同步的,需要外部保持线程之间的同步问题
  • 集合元素值允许为 null

HashSet的add方法

JAVA校招问数据结构 java中数据结构面试题_java_08


实质上add方法使用的是HashMap中的put方法HashSet的remove方法

JAVA校招问数据结构 java中数据结构面试题_java_09


实质上remove方法使用的是HashMap中的remove方法

时间复杂度

  • add() 复杂度为 O(1)
  • remove() 复杂度为 O(1)
  • contains() 复杂度为 O(1)

LinkedHashSet

LinkedHashSet 继承于 HashSet,底层是一个LinkedHashMap, 维护了一个数组 + 双向链表。 hashSet的存取是随机的,但是LinkedHashSet的存取是有序的。在保证元素唯一性的情况下还可以保证遍历顺序是插入顺序

LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的,同时LinkedHashSet不允许添加重复元素。

元素添加

  1. 在LInkedHashSet中维护了一个hash表和双向链表,LinkedHashSet中有head和tail,分别指向链表的头和尾
  2. 每一个节点有before和after属性,这样可以形成双向链表
  3. 在添加一个元素时,先求hash值,再求索引,确定该元素在table表中的位置,然后将添加的元素加入到双向链表(如果该元素已经存在,则不添加)
    tail.next = newElement
    newElement.pre = tail
  4. 这样在遍历LinkedHashSet时也能确保插入顺序和遍历顺序一致

TreeSet

  • TreeSet的底层是TreeMap,所以它的数据结构是红黑树添加的数据存入了map的key的位置,而value则固定是PRESENT。TreeSet中的元素是有序且不重复的,因为TreeMap中的key是有序且不重复的。
  • TreeSet有序、不可重复、非线程安全
  • TreeMap底层使用红黑树的结构进行数据的增删改查,红黑树是一种自平衡的二叉查找树,二叉查找树是一种有序树,即进行中序遍历可以得到一组有序序列,所以TreeMap也是有序的Map集合。
  • 在红黑树的加持下,TreeMap的众多方法,如:containsKey、get、put和reomve,都能保证log(n)的时间复杂度,这个效率可以说是相当高了。

有序性如何保证

TreeMap使用两种方法来保证有序性:Comparator和Comparable。

1. Comparator

你可以把它看做一个比较器,它的compare方法可以比较两个传入类型的对象,至于比较的规则是你实现这个接口后自己重写。从上面的代码看到,TreeMap中有一个全局comparator属性,你可以在构造其中传入自己的实现类。后面再put、get时就会调用comparator的compare方法来比较你的key和已存在的key,以此来决定存或取的位置。

public interface Comparator<T> {
    int compare(T o1, T o2);
    ......
}

2. Comparable

TreeMap规定,put、get和remove等方法传入的参数key必须是实现了Comparable接口的,否则在调用这些方法的时候会抛出类型转换的异常,因为要调用compareTo方法就必须把key强制转换成Comparable类型,即:(Comparable<? super K>)key。

所以这种比较方式就是:key1.compareTo(key2)。

public interface Comparable<T> {
    public int compareTo(T o);
    ......
}

时间复杂度

List

ArrayList

  • get() 直接读取下标,复杂度 O(1)
  • add(E) 直接在队尾添加,复杂度 O(1)
  • add(index, E) 在第n个元素后插入,n后面的元素需要向后移动,复杂度 O(n)
  • remove() 删除元素后面的元素需要逐个前移,复杂度 O(n)

LinkedList

  • addFirst() 添加队列头部,复杂度 O(1)
  • removeFirst() 删除队列头部,复杂度 O(1)
  • addLast() 添加队列尾部,复杂度 O(1)
  • removeLast() 删除队列尾部,复杂度 O(1)
  • getFirst() 获取队列头部,复杂度 O(1)
  • getLast() 获取队列尾部,复杂度 O(1)
  • get() 获取第n个元素,依次遍历,复杂度O(n)
  • add(E) 添加到队列尾部,复杂度O(1)
  • add(index, E) 添加到第n个元素后,需要先查找到第n个元素,复杂度O(n)
  • remove() 删除元素,修改前后元素节点指针,复杂度O(1)

Set

HashSet

  • add() 复杂度为 O(1)
  • remove() 复杂度为 O(1)
  • contains() 复杂度为 O(1)

TreeSet(基于红黑树)

  • add() 复杂度为 O(log (n))
  • remove() 复杂度为 O(log (n))
  • contains() 复杂度为 O(log (n))

map

TreeMap(基于红黑树)

  • 平均时间复杂度 O(log n)

HashMap

  • 正常时间复杂度 O(1)~O(n)
  • 红黑树后 O(log n)

LinkedHashMap

  • 能以时间复杂度 O(1) 查找元素,又能够保证key的有序性

引用自:
作者:Devil_566      java-集合框架底层数据结构总结 作者:阿晴招生笔记      java hashset 时间复杂度_Java 集合时间复杂度