目录
1.区别概念:
1、List
Arraylist、LinkedList、Vector
2、Set
TreeSet(有序,唯一):红黑树(自平衡的排序二叉树。)
HashSet(无序,唯一):哈希表或者叫散列集(hash table)
LinkedHashSet:链表和哈希表组成 。 由链表保证元素的排序 , 由哈希表证元素的唯一性
3、Map
TreeMap:红黑树(自平衡的排序二叉树)
HashMap:基于哈希表的Map接口实现(Map结构即映射表存放键值对)
HashTable:哈希表
LinkedHashMap:HashMap 的基础上加上了链表数据结构
DS下专门研究动态数据集查找的数据集结构:平衡搜索树、哈希表(HashMap、HashSet)
java.util.Map(key-value)
2.Map介绍
可以看到,有两个泛型类型:K(key 的类型),V(value 的类型)
V这个类型没有父接口的信息,是一个顶级接口(从语法角度(继承、实现)讲,Map 和 Collection 之间没有关系)
map 中的 key 是不允许重复的
1.TreeMap方法总结
方法 | 解释 | 备注 |
void clear() | 清空 | |
boolean isEmpty() | 判断是否为空 | |
int size() | 求长度 | |
boolean containsKey(Object key) | 判断是否包含 key,成本更低 | |
boolean containsValue(Object value) | 判断是否包含value,成本更高 | 以搜索树为例containsValue( ... ) 全遍历操作 |
V get(Object key) | 返回 key 对应的 value(查找) | 如果map 中不包含指定的 key 返回为 null |
V getOrDefault(Object key, V defaultValue) | 返回 key 对应的 value | key 不存在,返回 提前写好的 defaultValu (是不会修改map 的) key - value 的插入操作 key - value 的替换操作,用新的 value 替换映射关系(更新) |
V put(K key, V value) | 设置 key 对应的 value | 如果是插入的话:返回null 如果是更新的话:返回 旧的 value |
V remove(Object key) | 删除 key 对应的 k-v 记录 | 返回 null 说明key 不存在 删除成功:返回 key 对应的value |
Set<K> keySet() | 得到所有的 key :Set<K> | |
Collection<V> values() | 得到所有的 value:Collection<V> | |
Set<Map.Entry<K, V>> entrySet() | 得到所有的 k-v 映射记录:Set<Map.Entry<K,V>> |
遍历操作:
map 没有办法直接遍历
1.得到所有的 key :Set<K>
2.得到所有的 value:Collection<V>
3.得到所有的 k-v 映射记录:Set<Map.Entry<K,V>>
对Set、Collection 进行我们熟悉的遍历
为什么key、kv记录用Set,value 用Collection?
Set中的元素是不允许重复的,Map 中的 key 、kv 记录是不会重复的,所以用Set
Map 中的value 是可以重复的,所以用Collection
2.TreeMap的遍历
public static void main(String[] args) {
Map<Integer,String> 地支 = new TreeMap<>();
地支.put(3, "寅");
地支.put(6, "巳");
地支.put(10, "酉");
地支.put(11, "戌");
地支.put(12, "亥");
地支.put(7, "午");
地支.put(1, "子");
地支.put(2, "丑");
地支.put(8, "未");
地支.put(9, "申");
地支.put(4, "卯");
地支.put(5, "辰");
// 遍历 key
for (Integer key : 地支.keySet()) {
System.out.println(key);
}
//遍历value
System.out.println("================================");
for (String value : 地支.values()) {
System.out.println(value);
}
// 遍历key-value
System.out.println("================================");
for (Map.Entry<Integer, String> entry : 地支.entrySet()) {
Integer key = entry.getKey();
String value = entry.getValue();
System.out.println(key + " => " + value);
}
}
3.TreeSet 和TreeMap
1.一些概念:
搜索树在Java中表现为红黑树(一种平衡二叉树)
纯 key :TreeSet 实现Set 接口
key-value:TreeMap 实现Map 接口
2.TreeSet 和 TreeMap独有的特性
搜索树的结构维护的基础是: key 之间的大小关系
所以要求 key 的类型必须具备比较的能力,要么Comparable、要么Comparator
1.方法(TreeSet)
first:返回最小的 key(不是第一次插入)
last:返回最大的 key
floor:返回小于等于 e 中最大的
ceiling:返回大于等于 e 中最小的
higher:返回大于 e 中最小的一个
lower:返回小于 e 中最大的一个
这些方法能够成立,是因为BST的中序遍历是有序的。我们很容易得到关于 key 的顺序信息
2.方法(TreeMap)
和 TreeSet 同理
文档:
4.冲突
1.冲突多了是不是好事情?
不好。以衣服放在衣柜里的例子来说,冲突越多意味着这个衣柜中的衣服越多,衣服越多,找起来越慢。实际上,根据经验和计算,总是能把冲突数维持在一个常数上(一般不超过8)
2.冲突不好,能否完全避免?
理论上可以,但实际上做不到。
衣柜的数量大于衣服的数量时,就可以做到不冲突(保证每个衣柜中不会出现两件衣服)
现实中,元素个数远远大于柜子的个数,所以基本做不到完全没冲突。
3.进行冲突避免的几种思路
1.hash函数得到的结果,尽可能的均匀
2.通过扩容来实现
虽然顺序表和哈希表都有扩容机制,但两者扩容触发时机是不同的。顺序表的扩容时机是数组中放不下新的元素了。哈希表的扩容时机并不是哈希表中放不下元素了(理论上哈希表可以放无限)。哈希表的扩容只是因为超过一定阈值,使得冲突率不可接受了。
5.冲突解决
1.闭散列:
“此处不留爷自有留爷处”
元素类型是整型(int) hash函数:元素%数组长度
元素 193 的位置:index = 193 % 7 = 4 ,放在4 号下标的位置
元素 4 的位置:index = 4% 7 = 4,此时 4 号下标已经有了元素了
冲突的具体解决办法1:一个接一个往后找(带循环)
线性探测:如果对应位置已经有其他元素占用了,则正常向下一个可用位置放置。
因此元素 4 就放到了 5 号下标处。
2.开散列(哈希桶):
编程实践中常用。把所有冲突的 key 组织在一条链表中
数组的元素类型是链表的结点类型(头结点:代表整条链表)
1.根据 key 找到index 2.根据 index 从array 得到一条链表 3.在链表中查找(由于冲突率不高,所以链表的长度不会很长)
用Map 形式,实现哈希桶,规定 key 的类型是String 类型,value 类型Long类型。
哈希表中,不仅仅可以保存数字类型的 key ,也可以保存其他对象类型的 key ,因此这里选用string类型 从一般对象 -> 整型数字。在String 类型中,相等性比较要用equals。
1.HashMap的 get 和 put 过程
get(查找):
1.利用hashCode() 方法,从对象得到整形数字
备注1:hashCode() 方法来自Object 类,所以Java 中所有的对象都具有该访问
备注2:如果自定义 class,要作为HashMap 的 K 类型或者 HashSet 的元素类型
必须正确的重写 hashCode() 和 equals() 方法
int n = key.hashCode();
2.从整型数字得到一个合法的数组下标(通过设计,应该让下标的设计,尽可能均匀)
我们现在采用的是 index = n % array.length; 这个方法没有过多的考虑分布均匀的问题
int index = n % arrat.length;
3.根据下标,可以从 HashMap 的数组中使用O(1)时间复杂度得到一条链表
Q:哈希表中最外侧的数组,是否可以用链表来代替?
A: 不行,如果是链表,[index]的操作就不是O(1)时间复杂度
Node head = array[index];
4.遍历刚刚得到的整条链表,查看结点 key 是否等于要找的 key
备注:使用 equals 做相等性判断
key.equals(cur.key)
put(插入/更新):
1.利用hashCode() 方法,从对象得到整形数字
备注1:hashCode() 方法来自Object 类,所以Java 中所有的对象都具有该访问
备注2:如果自定义 class,要作为HashMap 的 K 类型或者 HashSet 的元素类型
必须正确的重写 hashCode() 和 equals() 方法
int n = key.hashCode();
2.从整型数字得到一个合法的数组下标(通过设计,应该让下标的设计,尽可能均匀)
我们现在采用的是 index = n % array.length; 这个方法没有过多的考虑分布均匀的问题
int index = n % arrat.length;
3.根据下标,可以从 HashMap 的数组中使用O(1)时间复杂度得到一条链表
Q:哈希表中最外侧的数组,是否可以用链表来代替?
A: 不行,如果是链表,[index]的操作就不是O(1)时间复杂度
Node head = array[index];
4.遍历刚刚得到的整条链表,查看结点 key 是否等于要找的 key
备注:使用 equals 做相等性判断
如果找到的话做更新操作
key.equals(cur.key)
5.如果没有找到的话,需要进行插入操作
5.1 将 key-value 装成链表结点
5.2将链表结点,插入到 array[index] 对应的链表中(理论上,头插尾插都可以)
6.size/ array.length > 扩容阈值时
为了降低冲突率,所以进行扩容。不是因为放不下才扩容,扩容的时候,需要把所有 k-v 重新计算,以得到新的下标。