Map&Set
======================================================================================================
Map与Set的概念
- Map与Set是一种专门用来搜索查找的容器或数据结构,其搜索的效率与其具体的实例化子类有关;
- 之前的使用的查找方法适合静态类型的查找,一般不会对元素插入或删除操作;
- 1.直接遍历查找,时间复杂度为O(n),当元素很多时查找效率很低;
- 2.二分查找,时间复杂度为O(lg(n)),前提是区间整体有序;
- 现实中查找是动态查找,会伴随插入删除的操作。本文介绍的Map与Set就是适合动态查找的集合容器;
- 比如在进行通讯录查找某人电话时,只需搜索关键字-姓名,即可找到值-电话;
- 在进行学生管理系统管理时,查找某一门课的学生或者学生调课,在查找时涉及删除操作;
- Map的设计就是基于 键值对(key-value) 模型:要搜索的数据-关键字(key),关键字对应的值(value);Map存储的是键值对(key-value) 数据;比如目录(章节-页码);梁山好汉的江湖绰号(每个好汉-江湖绰号);
- Set的设计基于 纯关键字(key) 模型:比如实现快速查找某个人是否在群里,搜索姓名即可;Set存储的就是(key)关键字-姓名;
Map 的方法
- Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复,后续继续插入相同的key会被覆盖。
- Map.Entry<k,v>是Map内部用来存储键值对(key-value)映射关系的内部类;(该内部类中主
要提供了<key, value>的获取,value的设置以及Key的比较方式)
- Map的常用方法:
- 注意:
- Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
- Map中存放键值对的Key是唯一的,value是可以重复的
- 在Map中插入键值对时,key不能为空,否则就会抛NullPointerException异常,但是value可以为空
- Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
- Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
- Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
- TreeMap和HashMap的区别:
// HashMap 允许key为null,TreeMap不允许key为null,但可以允许value为null;
Map的实例化使用——HashMap & TreeMap
// TreeMap的插入操作put(k,v), 因为key是唯一的且不为null 否则抛出空指针异常,所以后面再插入会覆盖之前的值,并且TreeMap的底层为红黑树,插入删除时会进行比较排序,内置的toString()方法按key的大小输出;
// key的值不能为空,否则抛出空指针异常;
// Map的遍历:
- 通过 keySet( ) 返回所有key
Map<Integer,String> map=new TreeMap<>();
map.put(1,"张三");
map.put(1,"余一");
map.put(3,"王五");
map.put(2,"李四");
for(Integer x:map.keySet()){
System.out.print(x);
}
- 通过 values( ) 方法返回所有的值
for(String s:map.values()){
System.out.print(s);
}
- 通过Map.Entry<k,v> EntrySet( )方法返回所有键值对映射关系
for(Map.Entry<Integer,String> entry : map.entrySet()){
System.out.println(entry.getKey()+entry.getValue());
}
- 使用迭代器进行遍历
Iterator<Integer> iterator=map.keySet().iterator();
while(iterator.hasNext()){
System.out.print(iterator.next());
}
System.out.println();
Iterator<String> i=map.values().iterator();
Iterator<Map.Entry<Integer,String>> o=map.entrySet().iterator();
while(o.hasNext()){
System.out.print(o.next());
}
Set 的方法
- Set是继承自Collection的接口类,Set中只存储了Key。
- 注意:
- Set是继承自Collection的一个接口类
- Set中只存储了key,并且要求key一定要唯一
- Set的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的。
- Set最大的功能就是对集合中的元素进行去重。
- 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。
- Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入。
- Set中不能插入null的key。
- TreeSet和HashSet的区别
TreeSet & HashSet的使用
// HashSet 的key可以为null;TreeSet有序,当key为null时,抛出空指针异常;
// addAll(set)方法,可以复制一个set实例到另一个set集合,作用是去重;
public static void main(String[] args) {
Set<Integer> set=new TreeSet<>();
set.add(4);
set.add(2);
set.add(0);
//set.add(null);
System.out.println(set);
System.out.println(set.contains(3));
System.out.println(set.size());
System.out.println(set.isEmpty());
set.remove(0);
System.out.println(set);
Set<Integer> s=new HashSet<>();
s.add(9);
s.add(4);
s.add(2);
set.addAll(s);//去重
System.out.println(set);
Object[] n= set.toArray();
System.out.println(Arrays.toString(n));
}
* 二叉搜索树–图解
- 二叉搜索树又称二叉排序树,要么是一颗空树,要么是满足如下条件:
- 左子树如果不为空,则左子树上的所有节点的值均小于根节点;本且左子树也是一颗二叉排序树;
- 右子树如果不为空,则右子树上的所有节点的值均大于根节点;并且右子树也是一颗二叉搜索树;
- 查找操作:
- 插入操作:
- 如树本身为空,则将新节点作为树根节点即可;
- 如果树不为空
- 删除操作:
二叉搜索树的实现
class BinaryNode{
public int key;
public int value;
public BinaryNode left;
public BinaryNode right;
public BinaryNode(int key, int value) {
this.key = key;
this.value = value;
}
}
public class BinarySearchTreeMap {
private BinaryNode root=null;
//通过key获取value
public Integer get(int key) {
BinaryNode cur=root;
while (cur != null) {
if(cur.key==key){
return cur.value;
} else if (key < cur.key) {
cur=cur.left;
}else{
cur=cur.right;
}
}
return null;
}
//添加节点操作
public void put(int key,int value){
if(root==null){
root = new BinaryNode(key, value);
}
BinaryNode cur=root;
BinaryNode parent=null;
while(cur!=null){
if (key < cur.key) {
parent=cur;
cur = cur.left;
} else if (key > cur.key) {
parent=cur;
cur=cur.right;
}else{
//key相同的时候,用新插入的value覆盖之前的值
cur.value=value;
return;
}
}
BinaryNode newNode = new BinaryNode(key, value);
if (parent.key > newNode.key) {
parent.left=newNode;
}else{
parent.right=newNode;
}
}
//删除操作
public boolean remove(int key) {
BinaryNode cur=root;
BinaryNode parent=null;
while (cur != null) {
if (key < cur.key) {
parent=cur;
cur = cur.left;
} else if (key > cur.key) {
parent=cur;
cur=cur.right;
}else{
//找到要删除节点,进行删除操作;
removeNode(parent, cur);
return true;
}
}
return false;
}
private void removeNode(BinaryNode parent, BinaryNode cur) {
if (cur.left == null) {
if(cur==root){
root=cur.right;
} else if (parent.left == cur) {
parent.left=cur.right;
}else{
parent.right=cur.right;
}
} else if (cur.right == null) {
if (cur == root) {
root=cur.left;
} else if (parent.left == cur) {
parent.left=cur.left;
}else{
parent.right=cur.left;
}
}else{//左右子树都不为空
BinaryNode goat=cur.right; //在右子树中找到最小节点作为替罪羊节点,最后进行删除;
BinaryNode goatParent=cur;
while (goat.left != null) {
goatParent=goat;
goat = goat.left;
}
cur.key=goat.key;
cur.value=goat.value;
if(goatParent.left==goat){
goatParent.left=goat.right;
}else{
goatParent.right=goat.right;
}
}
}
}
- 由于二叉搜索树的插入和删除操作都需要先查找遍历,查找效率代表搜索树各个操作的性能;
- 最优情况下,二叉搜索树是完全二叉树,时间复杂度平均为O(lg(n));
- 最坏情况下,二叉搜索树为一个单支树,时间复杂度为O(n);
* 哈希表
- 为了插入删除更方便,最理想的查找搜索是直接从表中得到要搜索的值,进而操作;
- 用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。时间复杂度O(1);
- 可以构造一种存储结构,通过某种哈希函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
- 插入元素
- 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放;
- 搜索元素
- 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功;
- 哈希表:上述方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
哈希函数一般设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
常见哈希函数:
- 直接定制法(常用):取关键字的某个线性函数为计算散列地址函数(Hash(Key)= A*Key + B),优点(简单均匀),缺点要事先知道关键字的分布情况;适合查找比较小且连续的情况;
- 除留余数法(常用):设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转化为哈希地址;
- 平方取中法:假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址,平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况;
- 折叠法:将关键字从左到右分成等长的几部分,再将每一位叠加求和,再根据散列表表长,选取后几位作为散列地址;折叠法适合事先不知道关键字的分布,而关键字位数比较多的情况;
- 随机法:选择一个随机函数random,Hash(key) = random(key);适合关键字长度不等的情况;
- 数学分析法:关键字为等长的多位数,每一位是多种符号之一,每种符号出现机会相同,不同位出现不同符号的频率不同,或均匀或不均匀,根据散列表的长度选择各种符号分布均匀的若干位作为散列地址;比如电话号码作为关键字,选择后几位作为散列地址,而不选择前几位(区号可能相同,会发生冲突);适合事先知道关键字分布与关键字均匀分布情况、关键字位数比较大的情况;
#主要概念
- 1.哈希冲突/碰撞:不同关键字通过相同哈希哈数计算出相同的哈希地址。把具有不同关键码但具有相同哈希地址的数据元素称为“同义词”;
- 2.哈希冲突的原因:哈希函数设置不合理。哈希表底层的数组容量是设置了初始值,往往实际使用时容量小于关键字的数量,所以冲突是不可避免的。
- 3.避免冲突的方法:主要可以通过设置不同的合适的哈希转换函数;
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间;
- 哈希函数计算出来的地址能均匀分布在整个空间中;
- 哈希函数应该比较简单;
- 4.负载因子:散列表装满程度的标志因子 a=填入表中元素个数 / 散列表总长度;冲突率越大负载因子越大,但不是线性关系;通过调整哈希表的数组大小,降低负载因子,从而降低冲突率;
- 5.冲突的解决:开散列与闭散列;
- 闭散列也称开放定址法:如果发生哈希冲突时,哈希表未被装满,说明必然有空位置,可以将key放在冲突位置的下一个“空位置”去;方法一线性探测(插入时,从发生冲突的位置向后查找空位置插入);方法二二次探测(发生冲突时,通过H=(H+/-i^2)%m,即通过该公式计算下一个哈希地址);闭散列要确保插入时的负载因子不超过0.5,其缺点是空间利用率低;
- 开散列/哈希桶: 开散列可以看作在大集合中的搜索转化为在小集合中的搜索;每个桶种储存的都是该位置发生哈希冲突的元素;
哈希桶的实现
class NodeH {
public int key;
public NodeH next;
public NodeH(int key) {
this.key = key;
}
}
public class MyHeapSet {
private NodeH[] heapSet=new NodeH[10];
private int size=0;
private static final double LOAD_FACTOR=0.75;
//计算负载因子
public double loadFactor(){
return (double)size/heapSet.length;
}
//插入操作
public void add(int key){
int index=key%heapSet.length;
//判断是否已有该key
for(NodeH cur = heapSet[index]; cur!=null; cur=cur.next){
if(key==cur.key){
return;
}
}
NodeH newNode=new NodeH(key);
newNode.next=heapSet[index];
heapSet[index]=newNode;
size++;
if(loadFactor()>=LOAD_FACTOR){
reallocate();
}
}
//扩容操作
public void reallocate(){
NodeH[] newHeap=new NodeH[heapSet.length*2];
for(int i=0;i<heapSet.length;i++){
NodeH next=null;
for(NodeH cur=heapSet[i];cur!=null;cur=next){
next=cur.next;
int newIndex=cur.key%newHeap.length;
cur.next=newHeap[newIndex];
newHeap[newIndex]=cur;
}
}
heapSet=newHeap;
}
//删除操作
public boolean remove(int key){
int index=key%heapSet.length;
if(heapSet[index]==null){
return false;
}
if(key==heapSet[index].key){
heapSet[index]=heapSet[index].next;
return true;
}
for(NodeH cur=heapSet[index];cur!=null&&cur.next!=null;cur=cur.next){
if(cur.next.key==key){
cur.next=cur.next.next;
return true;
}
}
return false;
}
}