Java-数构map和set
1.搜索树
1.1概念
二叉搜索树又称为二叉排序树,它或者是一颗空树要么是满足一下性质的二叉树:
- 若左子树不为空,左子树上的所有节点的值都小于根节点的值
- 若右子树不为空,那么右子树上所有节点的值都大于根节点的值
- 它左右子树也分别是二叉搜索树
1.2查找操作
public boolean search(int val){TreeNode cur=root;while(cur!=null){if(cur.val<val){cur=cur.right;}else if(cur.val>val){cur=cur.left;}else {return true;}}return false;}
1.3 插入操作
1.若是空树,即根==null,直接插入
2.若不是空入,查找到插入位置后插入,利用父亲节点保存前置节点。
public boolean insert(int val){TreeNode parent=null;TreeNode cur=root;TreeNode node=new TreeNode(val);if(root==null){root=node;return true;}while (cur!=null){if(cur.val<val){parent=cur;cur=cur.right;} else if (cur.val>val) {parent=cur;cur=cur.left;}else {return false;}}if(parent.val>val){parent.left=node;} else if (parent.val<val) {parent.right=node;}return true;}
1.4删除操作
将待删除结点设置为cur,待删除结点的双亲结点为parent。
- cur 是 root,则 root = cur.right
- cur 不是 root,cur 是 parent.left,则 parent.left = cur.right
- cur 不是 root,cur 是 parent.right,则 parent.right = cur.right
- cur 是 root,则 root = cur.left
- cur 不是 root,cur 是 parent.left,则 parent.left = cur.left
- cur 不是 root,cur 是 parent.right,则 parent.right = cur.left
public void remove(int val){TreeNode cur=root;TreeNode parent=null;while(cur!=null){//寻找要删除的结点if(cur.val<val){parent=cur;cur=cur.right;} else if (cur.val>val) {parent=cur;cur=cur.left;}else {removeNode(parent,cur);return;}}}private void removeNode(TreeNode parent, TreeNode cur) {if(cur.left==null){if(cur==root){root=cur.right;} else if (cur==parent.left) {parent.left=cur.right;} else if (cur==parent.right) {parent.right=cur.right;}} else if (cur.right==null) {if(cur==root){root=cur.left;} else if (cur==parent.left) {parent.left=cur.left;} else if (cur==parent.right) {parent.right=cur.left;}}else {//根节点左右都不为空:"替罪羊法"TreeNode targetParent=cur;TreeNode target=cur.right;while(target.left!=null){targetParent=target;target=target.left;}cur.val=target.val;//cur:95if(target==targetParent.left){targetParent.left=target.right;}else {targetParent.right=target.right;}}}
1.5性能分析
插入和删除操作都需要进行查找,而查找过程中会进行两两比较。
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为: 最差情况下,二叉搜索树会退化为单支树,其平均比较次数为
2.map的使用
Map是一个接口类,没有继承Collection该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复。
2.1关于Map.Entry<K,V>的说明
2.2map的常用方法说明
注意:
- Map是一个接口,不能实例化对象,如果要实例化对象要实现TreeMap类或者HashMap
- Map中存放键值对的Key是唯一的,value是可以重复的
- 在Treemap中插入键值对的时候,key不能为空,否则会抛出空指针异常。但是HashMap的key和value可以为空
- map中的key可以分离出来存储到set中进行访问
- map中的value可以分离出来存储到collection中的任意自己
- map中键值对key不能直接修改,value可以修改。但是只能进行重新插入。
public static void main(String[] args) {TreeMap<String,Integer> map=new TreeMap<>();map.put("this",3);map.put("phe",5);map.put("a",7);map.put("a",8888);int val = map.getOrDefault("phe8",1999);//不存在返回1999System.out.println(map.get("a"));System.out.println(val);Set<String> set=map.keySet();//返回所有key不重复的集合Collection<Integer> collection=map.values();//返回所有value的可重复集合Set<Map.Entry<String,Integer>> entries = map.entrySet();for(Map.Entry<String,Integer> entry:entries){System.out.println("key: "+entry.getKey()+"value: "+entry.getValue() );}}
2.3TreeMap和HashMap的区别
map底层结构 | TreeMap | HashMap |
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | ||
是否有序 | 关于Key有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找的区别 | 元素需要比较 | 通过哈希函数计算哈希地址 |
比较与覆写 | key必须能够比较,否则类型异常 | 自定义类要覆写equals和hashcode方法 |
应用场景 | 需要key有序场景下 | key是否有序不关心,用空间换时间 |
3.Set的使用
sei继承自collection接口类,set中只存储了key
public static void main(String[] args) {Set<String> set = new TreeSet<>();set.add("hello");set.add("abc");set.add("plo");set.remove("abc");System.out.println(set.contains("abc"));int count=0;count=set.size();System.out.println(count);System.out.println(set.isEmpty());for (String s : set) {System.out.println(s);}}
map底层结构 | TreeSet | HashSet |
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | ||
是否有序 | 关于Key有序 | 无一定有序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找的区别 | 按照红黑树特性进行插入和删除 | 1.先计算key的哈希地址2.进行插入删除操作 |
比较与覆写 | key必须能够比较,否则类型异常 | 自定义类要覆写equals和hashcode方法 |
应用场景 | 需要key有序场景下 | key是否有序不关心,用空间换时间 |
4.哈希表
4.1概念
可以不经过任何比较,一次直接从表中获得搜索的元素。如果构造一种存储结构,通过某种函数使得元素的存储位置和它的关键码之间能够建立一一映射关系,那么查找的时候通过函数可以很快的查找到该元素。
哈希方法种使用的转换函数乘坐哈希函数,构造出来的结构称为哈希表。我们一般将哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
4.2冲突的概念
4.3冲突的避免
冲突的发生是必然的,但是我们要尽可能降低冲突率。尽可能的去设计较好的哈希函数。
常见的哈希函数:
- 直接定制法:取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况。
- 除留余数法:设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
- 平方取中法
- 随机数法
- 数学分析法
4.4负载因子的调节
当冲突率达到无法忍受的程度时候,我们要通过降低负载因子来变相的降低冲突率。关键字的个数是不可变的,那么我们只能调整哈希表种数组的大小。
5.解决冲突
解决哈希冲突的两种常见的方法是:闭散列和开散列
public class HashBuck {static class Node{public int key;public int val;public Node next;public Node(int key, int val) {this.key = key;this.val = val;}}public Node[] array;public int usedSize;public HashBuck(){array=new Node[10];}public void put(int key,int val){int index=key%array.length;Node cur=array[index];while(cur!=null){if(cur.key==key){cur.val=val;return;}cur=cur.next;}Node node=new Node(key,val);node.next=array[index];array[index]=node;usedSize++;//头插法if(loadFoat()>=0.75){resize();}}private void resize() {Node[] tmpArray=new Node[array.length*2];for (int i = 0; i < array.length; i++) {Node cur=array[i];while(cur!=null){Node curNext=cur.next;int newIndex= cur.key;}}}private double loadFoat() {return usedSize*1.0/array.length;}
}
5.2性能分析
在实际过程种,哈希表的冲突率是不高的,冲突个数是可控的,也就是说每一个中的链表长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是O(1).
5.3java类集的关系
- HashMap和HashSet在Java中是利用哈希表实现的Map和Set(HashMap的默认长度树16)
- Java中使用的是哈希桶的方式解决冲突(默认负载因子是0.75)
- Java会在冲突链表长度大于一定阈值后,将链表转换为搜索树(红黑树)(链表长度超过8或者数组长度超过64则变成红黑树)
- Java中计算哈希值实际上是调用HashMap的key或者HashSet的值,必须覆写hashCode和equals方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。
public class HashBuck2 <K,V>{static class Node<K,V>{public K key;public V val;public Node<K,V>next;public Node(K key,V val){this.key=key;this.val=val;}}public Node<K,V>[] array;public int usedSize;public HashBuck2(){array=(Node<K,V>[]) new Node[10];}public void put(K key,V val){int hash=key.hashCode();int index=hash%array.length;Node<K,V> cur=array[index];while(cur!=null){if(cur.key.equals(key)){cur.val=val;return;}cur=cur.next;}Node<K,V> node=new Node<>(key, val);node.next=array[index];array[index]=node;usedSize++;}public V get(K key){int hash=key.hashCode();int index=hash%array.length;Node<K,V> cur=array[index];while(cur!=null){if(cur.key.equals(key)){return cur.val;}cur=cur.next;}return null;}
}
当不知道具体key和value的类型的时候可以借助泛型传入。