Java面试题set map

Set

Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对

象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要

让两个不同的对象视为相等的,就必须覆盖 Object  hashCode 方法和 equals 方法。HashSetHash 表)

哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是

按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取

, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为

true HashSet 就视为同一个元素。如果 equals  false 就不是同一个元素。

哈希值相同 equals  false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元

素放在一个哈希桶中)。也就是哈希一样的存一列。如图 1 表示 hashCode 值不相同的情况;图 2 表示

hashCode 值相同,但 equals 不相同的情况。

HashSet 通过 hashCode 值来确定元素在内存中的位置。一个 hashCode 位置上可以存放多个元素。

TreeSet(二叉树)

1. TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个

对象都会进行排序,将对象插入的二叉树指定的位置。

2. Integer  String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义

的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用。

3. 在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序

4. 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或

正整数。

LinkHashSetHashSet+LinkedHashMap)对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。

LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上

又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一

个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类

HashSet 的操作相同,直接调用父类 HashSet 的方法即可。

Map

HashMap(数组+链表+红黑树)

HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访

问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为

nullHashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一

致。如果需要满足线程安全,可以用 Collections  synchronizedMap 方法使 HashMap 具有线程安全

的能力,或者使用 ConcurrentHashMap。我们用下面这张图来介绍

HashMap 的结构。

JAVA7 实现

大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色

的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next

1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。

2. loadFactor:负载因子,默认为 0.753. threshold:扩容的阈值,等于 capacity * loadFactor

JAVA8实现

Java8  HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组

成。

根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下

标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决

于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将

链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)

ConcurrentHashMap

Segment

ConcurrentHashMap  HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整

 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表部分一段的意思,所以很多地

方都会将其描述为分段锁。注意,行文中,我很多地方用了来代表一个 segment

线程安全Segment 继承 ReentrantLock 加锁)

简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进

行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全

的,也就实现了全局的线程安全。并行度 concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16

也就是说 ConcurrentHashMap  16  Segments,所以理论上,这个时候,最多可以同时支持 16

线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他

值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像

之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。

Java8 实现 (引入了红黑树)

Java8  ConcurrentHashMap 进行了比较大的改动,Java8 也引入了红黑树。

Java8实现(../../../../../0马士兵/新建文件夹/BAT面试突击资料(1)/整理/BAT面试突击资料/06-JAVA面试

核心知识点整理(时间较多的同学全面复习).assets/Java8实现(引入了红黑树).jpg)

HashTable(线程安全)

Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是

线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为

ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以

 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。

3.4.4. TreeMap(可排序)

TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以

指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,

建议使用 TreeMap

在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的

Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。

LinkHashMap(记录插入顺序)

LinkedHashMap  HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历

LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。TreeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速

检索指定节点。

TreeSet  TreeMap 的关系

为了让大家了解 TreeMap  TreeSet 之间的关系,下面先看 TreeSet 类的部分源代码:

public class TreeSet<E> extends AbstractSet<E>

implements NavigableSet<E>, Cloneable, java.io.Serializable

{

// 使用 NavigableMap key 来保存 Set 集合的元素

private transient NavigableMap<E,Object> m;

// 使用一个 PRESENT 作为 Map 集合的所有 value

private static final Object PRESENT = new Object();

// 包访问权限的构造器,以指定的 NavigableMap 对象创建 Set 集合

TreeSet(NavigableMap<E,Object> m)

{

this.m = m;

}

public TreeSet() //

{

// 以自然排序方式创建一个新的 TreeMap

// 根据该 TreeSet 创建一个 TreeSet

// 使用该 TreeMap key 来保存 Set 集合的元素

this(new TreeMap<E,Object>());

}

public TreeSet(Comparator<? super E> comparator) //

{

// 以定制排序方式创建一个新的 TreeMap

// 根据该 TreeSet 创建一个 TreeSet

// 使用该 TreeMap key 来保存 Set 集合的元素

this(new TreeMap<E,Object>(comparator));

}

public TreeSet(Collection<? extends E> c)

{

// 调用号构造器创建一个 TreeSet,底层以 TreeMap 保存集合元素

this();

// TreeSet 中添加 Collection 集合 c 里的所有元素

addAll(c);

}

public TreeSet(SortedSet<E> s)

{

// 调用号构造器创建一个 TreeSet,底层以 TreeMap 保存集合元素

this(s.comparator());

// TreeSet 中添加 SortedSet 集合 s 里的所有元素

addAll(s);

}

//TreeSet 的其他方法都只是直接调用 TreeMap 的方法来提供实现

...

public boolean addAll(Collection<? extends E> c)

{

if (m.size() == 0 && c.size() > 0 &&

c instanceof SortedSet &&

m instanceof TreeMap)

{// c 集合强制转换为 SortedSet 集合

SortedSet<? extends E> set = (SortedSet<? extends E>) c;

// m 集合强制转换为 TreeMap 集合

TreeMap<E,Object> map = (TreeMap<E, Object>) m;

Comparator<? super E> cc = (Comparator<? super E>) set.comparator();

Comparator<? super E> mc = map.comparator();

// 如果 cc mc 两个 Comparator 相等

if (cc == mc || (cc != null && cc.equals(mc)))

{

// Collection 中所有元素添加成 TreeMap 集合的 key

map.addAllForTreeSet(set, PRESENT);

return true;

}

}

// 直接调用父类的 addAll() 方法来实现

return super.addAll(c);

}

...

}

显示更多

从上面代码可以看出,TreeSet 的 ① 号、② 号构造器的都是新建一个 TreeMap 作为实际存储 Set 元素

的容器,而另外 2 个构造器则分别依赖于 ① 号和 ② 号构造器,由此可见,TreeSet 底层实际使用的存

储容器就是 TreeMap

 HashSet 完全类似的是,TreeSet 里绝大部分方法都是直接调用 TreeMap 的方法来实现的,这一点

读者可以自行参阅 TreeSet 的源代码,此处就不再给出了。

对于 TreeMap 而言,它采用一种被称为红黑树的排序二叉树来保存 Map 中每个 Entry —— 每个

Entry 都被当成红黑树的一个节点对待。例如对于如下程序而言:

public class TreeMapTest

{

public static void main(String[] args)

{

TreeMap<String , Double> map =

new TreeMap<String , Double>();

map.put("ccc" , 89.0);

map.put("aaa" , 80.0);

map.put("zzz" , 80.0);

map.put("bbb" , 89.0);

System.out.println(map);

}

}

显示更多

当程序执行 map.put(“ccc” , 89.0); 时,系统将直接把 “ccc”-89.0 这个 Entry 放入 Map 中,这个 Entry

就是该红黑树的根节点。接着程序执行 map.put(“aaa” , 80.0); 时,程序会将 “aaa”-80.0 作为新节点添

加到已有的红黑树中。

以后每向 TreeMap 中放入一个 key-value 对,系统都需要将该 Entry 当成一个新节点,添加成已有红黑

树中,通过这种方式就可保证 TreeMap 中所有 key 总是由小到大地排列。例如我们输出上面程序,将

看到如下结果(所有 key 由小到大地排列):

{aaa=80.0, bbb=89.0, ccc=89.0, zzz=80.0}显示更多

TreeMap 的添加节点

对于 TreeMap 而言,由于它底层采用一棵红黑树来保存集合中的 Entry,这意味这 TreeMap 添加元

素、取出元素的性能都比 HashMap 低:当 TreeMap 添加元素时,需要通过循环找到新增 Entry 的插入

位置,因此比较耗性能;当从 TreeMap 中取出元素时,需要通过循环才能找到合适的 Entry,也比较耗

性能。但 TreeMapTreeSet  HashMapHashSet 的优势在于:TreeMap 中的所有 Entry 总是按

key 根据指定排序规则保持有序状态,TreeSet 中所有元素总是根据指定排序规则保持有序状态。

红黑树

红黑树是一种自平衡排序二叉树,树中每个节点的值,都大于或等于在它的左子树中的所有节点的

值,并且小于或等于在它的右子树中的所有节点的值,这确保红黑树运行时可以快速地在树中查找

和定位的所需节点。

为了理解 TreeMap 的底层实现,必须先介绍排序二叉树和红黑树这两种数据结构。其中红黑树又是一

种特殊的排序二叉树。

排序二叉树是一种特殊结构的二叉树,可以非常方便地对树中所有节点进行排序和检索。

排序二叉树要么是一棵空二叉树,要么是具有下列性质的二叉树:

若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;

若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;

它的左、右子树也分别为排序二叉树。

 1 显示了一棵排序二叉树:

 1. 排序二叉树对排序二叉树,若按中序遍历就可以得到由小到大的有序序列。如图 1 所示二叉树,中序遍历得:

{23489910131518}

创建排序二叉树的步骤,也就是不断地向排序二叉树添加节点的过程,向排序二叉树添加节点的步骤如

下:

1. 以根节点当前节点开始搜索。

2. 拿新节点的值和当前节点的值比较。

3. 如果新节点的值更大,则以当前节点的右子节点作为新的当前节点;如果新节点的值更小,则以当

前节点的左子节点作为新的当前节点。

4. 重复 23 两个步骤,直到搜索到合适的叶子节点为止。

5. 将新节点添加为第 4 步找到的叶子节点的子节点;如果新节点更大,则添加为右子节点;否则添加

为左子节点。

掌握上面理论之后,下面我们来分析 TreeMap 添加节点(TreeMap 中使用 Entry 内部类代表节点)的

实现,TreeMap 集合的 put(K key, V value) 方法实现了将 Entry 放入排序二叉树中,下面是该方法的源

代码:

public V put(K key, V value)

{

// 先以 t 保存链表的 root 节点

Entry<K,V> t = root;

// 如果 t==null,表明是一个空链表,即该 TreeMap 里没有任何 Entry

if (t == null)

{

// 将新的 key-value 创建一个 Entry,并将该 Entry 作为 root

root = new Entry<K,V>(key, value, null);

// 设置该 Map 集合的 size 1,代表包含一个 Entry

size = 1;

// 记录修改次数为 1

modCount++;

return null;

}

int cmp;

Entry<K,V> parent;

Comparator<? super K> cpr = comparator;

// 如果比较器 cpr 不为 null,即表明采用定制排序

if (cpr != null)

{

do {

// 使用 parent 上次循环后的 t 所引用的 Entry

parent = t;

// 拿新插入 key t key 进行比较

cmp = cpr.compare(key, t.key);

// 如果新插入的 key 小于 t keyt 等于 t 的左边节点

if (cmp < 0)

t = t.left;

// 如果新插入的 key 大于 t keyt 等于 t 的右边节点

else if (cmp > 0)

t = t.right;

// 如果两个 key 相等,新的 value 覆盖原有的 value

// 并返回原有的 value

else

return t.setValue(value);

} while (t != null);显示更多

上面程序中粗体字代码就是实现排序二叉树的关键算法,每当程序希望添加新节点时:系统总是从树

的根节点开始比较 —— 即将根节点当成当前节点,如果新增节点大于当前节点、并且当前节点的右子节

点存在,则以右子节点作为当前节点;如果新增节点小于当前节点、并且当前节点的左子节点存在,则

以左子节点作为当前节点;如果新增节点等于当前节点,则用新增节点覆盖当前节点,并结束循环 ——

直到找到某个节点的左、右子节点不存在,将新节点添加该节点的子节点 —— 如果新节点比该节点大,

则添加为右子节点;如果新节点比该节点小,则添加为左子节点。

TreeMap 的删除节点

当程序从排序二叉树中删除一个节点之后,为了让它依然保持为排序二叉树,程序必须对该排序二叉树

进行维护。维护可分为如下几种情况:

1)被删除的节点是叶子节点,则只需将它从其父节点中删除即可。

2)被删除节点 p 只有左子树,将 p 的左子树 pL 添加成 p 的父节点的左子树即可;被删除节点 p

有右子树,将 p 的右子树 pR 添加成 p 的父节点的右子树即可。

3)若被删除节点 p 的左、右子树均非空,有两种做法:

}

else

{

if (key == null)

throw new NullPointerException();

Comparable<? super K> k = (Comparable<? super K>) key;

do {

// 使用 parent 上次循环后的 t 所引用的 Entry

parent = t;

// 拿新插入 key t key 进行比较

cmp = k.compareTo(t.key);

// 如果新插入的 key 小于 t keyt 等于 t 的左边节点

if (cmp < 0)

t = t.left;

// 如果新插入的 key 大于 t keyt 等于 t 的右边节点

else if (cmp > 0)

t = t.right;

// 如果两个 key 相等,新的 value 覆盖原有的 value

// 并返回原有的 value

else

return t.setValue(value);

} while (t != null);

}

// 将新插入的节点作为 parent 节点的子节点

Entry<K,V> e = new Entry<K,V>(key, value, parent);

// 如果新插入 key 小于 parent key,则 e 作为 parent 的左子节点

if (cmp < 0)

parent.left = e;

// 如果新插入 key 小于 parent key,则 e 作为 parent 的右子节点

else

parent.right = e;

// 修复红黑树

fixAfterInsertion(e); //

size++;

modCount++;

return null;

} pL 设为 p 的父节点 q 的左或右子节点(取决于 p 是其父节点 q 的左、右子节点),将 pR 设为

p 节点的中序前趋节点 s 的右子节点(s  pL 最右下的节点,也就是 pL 子树中最大的节点)。

 p 节点的中序前趋或后继替代 p 所指节点,然后再从原排序二叉树中删去中序前趋或后继节点即

可。(也就是用大于 p 的最小节点或小于 p 的最大节点代替 p 节点即可)。

 2 显示了被删除节点只有左子树的示意图:

 2. 被删除节点只有左子树

 3 显示了被删除节点只有右子树的示意图:

 3. 被删除节点只有右子树

 4 显示了被删除节点既有左子节点,又有右子节点的情形,此时我们采用到是第一种方式进行维护:

 4. 被删除节点既有左子树,又有右子树

 5 显示了被删除节点既有左子树,又有右子树的情形,此时我们采用到是第二种方式进行维护: 5. 被删除节点既有左子树,又有右子树

TreeMap 删除节点采用图 5 所示右边的情形进行维护——也就是用被删除节点的右子树中最小节点与被

删节点交换的方式进行维护。

TreeMap 删除节点的方法由如下方法实现:

private void deleteEntry(Entry<K,V> p)

{

modCount++;

size--;

// 如果被删除节点的左子树、右子树都不为空

if (p.left != null && p.right != null)

{

// p 节点的中序后继节点代替 p 节点

Entry<K,V> s = successor (p);

p.key = s.key;

p.value = s.value;

p = s;

}

// 如果 p 节点的左节点存在,replacement 代表左节点;否则代表右节点。

Entry<K,V> replacement = (p.left != null ? p.left : p.right);

if (replacement != null)

{

replacement.parent = p.parent;

// 如果 p 没有父节点,则 replacemment 变成父节点

if (p.parent == null)

root = replacement;

// 如果 p 节点是其父节点的左子节点

else if (p == p.parent.left)

p.parent.left = replacement;

// 如果 p 节点是其父节点的右子节点

else

p.parent.right = replacement;

p.left = p.right = p.parent = null;

// 修复红黑树

if (p.color == BLACK)

fixAfterDeletion(replacement); //

}

// 如果 p 节点没有父节点显示更多

猜你喜欢

转载自blog.csdn.net/2301_76965813/article/details/130504181
今日推荐