数据结构之哈希表
数据结构之哈希表 : hash (hashtable)
扩容原理 :
1. 刚创建hash结构时,底层的数组长度是16(加载因子 : 0.75) [加载因子决定了扩容时机]
当表中元素个数 >= (底层数组长度 * 加载因子) 个时, 底层的数组长度 翻倍 !
2. 当底层数组扩容后,会重新计算每一个元素应该存储的位置;
3. 当添加的新元素所存储的表位置的值 是默认值 null 时, 直接添加;不考虑扩容!
(jdk7版本是这样判断的,jdk8版本取消了此判断)
4. 当一个哈希表位置的链表元素 == 8 时, 在JDK8中会把此哈希表索引位置的链表转换成红黑树 (红黑树提高了查找效率[红黑树可以排序])
5. 当整个hash表中的元素个数<=64 时, 就算一个链表的元素个数>8 ,也不会转成红黑树
当往一个底层数据结构是 hash表的容器中 添加第1024个元素 (不考虑特殊情况),请问底层哈希表数组的长度是多少 ?
答:2048
HashSet<E>的使用
构造方法:
HashSet<E> 集合名 = new HashSet<>();
增删改查:
增 : add
boolean add(E e) : 添加元素,返回元素添加是否成功 (元素唯一)
删 : remove
boolean remove(Object obj) : 按照元素值删除元素,返回删除是否成功
void clear(): 清空集合
改 : set 因为没有索引所以不能修改指定位置的元素
查 : get 因为没有索引所以不能获取指定位置的元素
int size() : 获取长度
boolean isEmpty() : 判断集合是否为空
boolean contains(Object obj) : 判断传入的元素是否存在于集合中
遍历:
1. 转数组
Object[] toArray()
2. 普通迭代器
Iterator<E> iterator()
3. 增强for (Iterable)
集合名.for
HashSet<E>的去重原理(JDK7)
结论 :
1. 如果集合元素类型中没有重写equals 和 hashCode方法,那么Hash表(HashSet集合,HashMap集合)是按照元素的地址值进行去重; 如果地址值相同,则去重!
//一般情况下,我们更希望Hash表按照元素的 属性值去重, 属性值相同则认为是相同元素,要求添加失败
2. 在元素所在的类中重写equals 和 hashCode方法,就可以实现Hash表按照元素的属性值去重!!
HashSet去重源码解析:
//目标 : 去重 !! -> 添加失败 -> add方法的返回值是 false
HashSet集合中add方法:
public boolean add(E e) { // = stu4;
//HashSet中的add方法 调用了 HashMap中的 put 方法
//因为HashSet和HashMap 共用hash逻辑!
//put 方法是HashMap添加元素的方法, 而e 作为了HashMap的键元素传递
// HashMap的键集 其实就是一个HashSet集合
/*
map.put(e, PRESENT)==null
add 方法的结果是 map.put(e, PRESENT)调用的结果是不是null!
put方法的结果是null --> add 方法的结果就是 true -> 添加成功
put方法的结果不是null --> add 方法的结果就是 false -> 添加失败
目标转移 : 想办法让map.put方法的结果不是null
*/
return this.map.put(e, PRESENT)==null;//Map集合是键--值
}
1.7版本JDK HashMap中的put方法源码: //JDK8版本的put方法会先调用putVal方法,在putVal方法内编写添加逻辑
/*
K key = e; -> 重点!!
V value = PRESENT;
*/
public V put(K key, V value) {
//第一层 : 不能提前结束方法 -> 非重点关注
/*
this.table == EMPTY_TABLE
当前调用方法的对象.底层数组 --> 获取当前集合对象的底层数组(底层哈希表)
//现在的this 就是 HashSet对象底层的哈希表
此判断在判断 --> 此时集合底层的数组是否是一个空数组
为什么要判断底层数组是否是一个空数组: 为第一次添加做准备 , 因为第一次添加不需要去重!!
*/
if (table == EMPTY_TABLE) {//存在的意义 : 提高代码的效率
//猜inflateTable(threshold)的作用 是为 第一次添加元素做准备!!
/**
现在的情况 : 第四次添加,所以此if判断的结果一定是 false,此方法不执行!!
*/
inflateTable(threshold);
}
//第二层 : 能让方法提前结束 -> 重点关注
/*
要添加的元素 == null --> 非空校验
*/
if (key == null){
// putForNullKey(value) :
//1. 为添加元素值为null的键做操作
//2. 为第二次添加null键去重
//3. 区分hash表中的默认值null 和元素值 null
/*
HashSet 集合中可以有 null 元素,但是只能有一个null元素
HashMap 集合中可以有 null 键,但是只能有一个null键
*/
/**
现在的情况 : stu4 != null 此if判断的结果是false,不进if,不执行return
*/
return putForNullKey(value);
}
//第三层 : 不能提前结束方法 -> 非重点关注
//int hash = hash(key); -> 获取添加元素的hash值
/*
一个对象的hash值如何获取 -> 对象.hashCode方法
a. 如果对象的类没有重写hashCode方法,那么这个对象的hashCode值就是这个对象在内存中的十进制地址值
b. 如果对象的类重写hashCode方法,那么这个对象的hashCode值就是 ???? (就要去看如何重写的hashCode方法)
*/
int hash = hash(key); //hash : 要添加元素的hash值
//indexFor(hash, table.length) : Java中hash结构中的hash算法(决定每一个元素应该存放的表位置)
int i = indexFor(hash, table.length); //i : 要添加元素应该存放在底层hash表的索引位置
//第四层 : 能让方法提前结束 -> 重点关注
/*
猜测循环在干嘛 : 获取此hash表i索引位置的每个元素 --> 为了一一和新元素做比较 --> 去重准备!!
初始化语句: Entry<K,V> e = table[i]; //e -> 简单理解成: 此索引位置的老元素
判断条件语句: e != null
步进表达式(控制条件语句): e = e.next //依次获取下一个链表元素,并重新赋值给e
循环体语句:
e : 老元素
key : 新元素
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//声明了一个局部变量 Object类型 变量名 k;
Object k;
/**
e.hash == hash && ((k = e.key) == key || key.equals(k)) : 核心去重逻辑
&&: 双与 -> 有false则为false,短路效果 : 左边为false 右边不执行
&&的左边 : e.hash == hash -> 比较老元素的hash值和新元素的hash值是否相等
现实情况 : Student类中没有重写hashCode方法,所以调用Object类中的hashCode方法
-> 现在的stu1-4的hashCode值都不一样(Object中的hashCode方法是在获取对象10进制的地址值)
感谢Java提供了方法重写的特性!!
手动重写hashCode方法 -> return 1; 所有的学生对象的hash值都是1,就可以走到&&右边去
目前 : true && ...........
&&右边 (目标是: &&的右边也为true) -> (k = e.key) == key || key.equals(k)
|| : 有true则为true , 短路效果 : 左边为true 右边不执行
||的左边 : (k = e.key) == key ---> k: 代表的是老元素 key: 新元素
老元素 == 新元素 : == 一定会比较2个对象的地址值
现实情况 : 一定为false
目前 : true && (false || ....)
||的右边 : key.equals(k)
-> 新元素.equals(老元素); --> 元素所在的类equals方法的逻辑
现实情况是 Student 类中没有重写equals ,所以调用Object类中的equals
Object类中的equals : this == obj
感谢Java提供了方法重写的特性,重写equals让其比较对象的属性值而不是对象的地址值
重写完毕后,Hash表就会按照 对象的属性值 进行去重了!
*/
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//目标: 去重 想要提前结束方法的!!! -> 进if !!!
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;//大概率不是null
}
}
//第五层 : 不能提前结束方法 -> 非重点关注
modCount++; // 统计集合底层数组被修改的次数,为多线程操作服务的
addEntry(hash, key, value, i);.//双列集合中的元素准备 键值对对象!
//第六层 : 不让其走到第六层, 因为返回null, HashSet中的add就返回true,就添加成功!
return null;
}
核心代码 :
for(遍历当前hash表i索引位置的链表元素){
if(老元素的hash值 == 新元素的hash值 && (老元素 == 新元素 || 新元素.equals(老元素))){
//能进来就去重,添加失败
return oldValue;
}
}
//从for出来就添加成功
return null;
hash算法 : 如何获取一个对象的hash值逻辑就是hash算法
public int hashCode() {//高级的hash算法 : 根据对象的属性值获取对象的hash值
//张三 -- 20 (330) , 王五 -- 30(650) , 赵六 -- 51 (279 + 51 = 330)
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
LinkedHashSet<E>的使用
LinkedHashSet<E> 是 HashSet<E>的子类集合 :
底层数据结构 :链表 + hash表
//加了链表结构后,集合从存取无序变成存取有序;
数据结构之二叉树
数据结构之二叉查找树
数据结构之平衡二叉树
数据结构之红黑树
TreeSet<E>集合
TreeSet<E>集合 的底层数据结构是 红黑树结构
红黑树结构 带来了 :
1. 去重规则 -> 相减等于0则剔除元素
2. 排序 -> 相减得负数放左边,相减得正数方法右边
3. 提高了查找效率
所以 TreeSet<E>集合具备以下特点:
1. 元素唯一
2. 元素存取无序 (TreeSet带有排序功能) -> 元素能排序
3. 无索引
hash表结构在JDK8时把底层由 (数组 + 链表) 改为 (数组 + 链表 + 红黑树) 的目的在于提高元素的查找效率
构造方法:
//按照泛型元素默认提供的排序规则对元素进行排序并存储
TreeSet<E> 集合名 = new TreeSet<>(); //E类型的元素一定要提供排序规则
增删改查四类功能 :
没有新增任何方法,按照Set集合的常规方法使用
遍历:
没有新的遍历方法,按照Set集合的常规方法使用
常见类型的默认排序规则 :
1. Integer : 自然升序
2. String : ASCII码字的升序进行排序
a. 汉字"无规律" -> 不知道每一个汉字的码表值
b. 字符串中越靠前的字符优先级越高
Comparable<E> 绑定比较器
Comparable<E> 绑定比较器 -> 接口
抽象方法 : int compareTo(E e)
绑定比较器的使用步骤 :
1. 让元素类型去实现 Comparable<E> 绑定比较器 //哪个类型实现比较器接口,比较器的泛型就是那个类型
2. 重写 int compareTo(E e) 方法, 方法内的逻辑就是排序规则
排序规则 :
升序 : this - o
降序 : o - this
Comparator<E> 独立比较器
Comparable<E> 绑定比较器
抽象方法 : int compareTo(E o) //this - o 升序 o - this 降序
Comparator<E> 独立比较器 -> 接口 (级别 : 比绑定比较器Comparable高)
抽象方法 : int compare(E o1,E o2) //o1 : this (要添加的元素) o2 : o (老元素)
排序规则 :
升序 : o1 - o2
降序 : o2 - o1
如何使用独立比较器 :
场景例如 : TreeSet(Comparator<E> comparator)
其他排序功能
数组的排序:
Arrays 工具类中有 sort(数组对象) :
1. 数组对象中存储的是基本数据类型 : sort 的原理 -> 快速排序算法
2. 数组对象中存储的是引用数据类型 :
a . sort 就需要元素的类提供排序规则
b . static <T> void sort(T[] a, Comparator<T> c)
List集合排序:
Collections 单列集合操作的工具类型
1. sort(List<E> list)
2. <T> void sort(List<T> list, Comparator<T> c)
斗地主案例
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Demo {
public static void main(String[] args) {
//准备牌
String[] flowers = {"♠","♥","♣","♦"};
String[] numbers = {"3","4","5","6","7","8","9","10","J","Q","K","A","2"};
ArrayList<String> box = new ArrayList<>();//准备牌盒
for (String flower : flowers) {
for (String number : numbers) {
String poke = flower + number;
box.add(poke);
}
}
//System.out.println("box = " + box);
//添加大小王
box.add("joker");
box.add("JOKER");
//System.out.println("box = " + box);
//洗牌
Collections.shuffle(box);
//System.out.println("box = " + box);
//发牌
//准备斗地主的人
ArrayList<String> 周润发 = new ArrayList<>();
ArrayList<String> 周星驰 = new ArrayList<>();
ArrayList<String> 刘德华 = new ArrayList<>();
ArrayList<String> 地主牌 = new ArrayList<>();
//拿出地主牌
地主牌.add(box.get(box.size() - 1));
地主牌.add(box.get(box.size() - 2));
地主牌.add(box.get(box.size() - 3));
//System.out.println("地主牌 = " + 地主牌);
//发牌
for (int i = 0; i < box.size() - 3; i++) {
if (i % 3 == 0){
周润发.add(box.get(i));
}else if (i % 3 == 1){
周星驰.add(box.get(i));
}else {
刘德华.add(box.get(i));
}
}
//理牌
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"3","4","5","6","7","8","9","1","J","Q","K","A","2","o","O");
Comparator rule =new Comparator<String>(){
@Override
public int compare(String o1, String o2) {
char c1 = o1.charAt(1);
char c2 = o2.charAt(1);
int i1 = list.indexOf(String.valueOf(c1));
int i2 = list.indexOf(String.valueOf(c2));
return i1 - i2;
}
};
Collections.sort(周润发,rule);
Collections.sort(周星驰,rule);
Collections.sort(刘德华,rule);
Collections.sort(地主牌,rule);
//看牌
System.out.println("周润发 = " + 周润发);
System.out.println("周星驰 = " + 周星驰);
System.out.println("刘德华 = " + 刘德华);
System.out.println("地主牌 = " + 地主牌);
}
}
运行结果:
Collections
Collections : 操作单列集合的工具类型
静态方法 :
void sort(List<E> e) : 按照E类型元素提供的默认排序规则对List集合中的元素进行排序
void sort(List<E> e,Comparator<E> comparator) : 按照独立比较器提供的排序规则对List集合中的元素进行排序
void shuffle(List<E> e) 随机打乱List集合中的元素顺序
void addAll(Collection<E> collection , E ... element): 批量添加
int binarySearch(List<? extends Comparable<? super T>> list, T key) : 利用二分查找法查找list集合中的key元素的索引位置
//先对List集合中的元素进行排序,排完序后再用二分查找法进行元素的查找
注意 : 如果List集合的元素类型没有提供排序规则,此方法调用报错!
int binarySearch(List<? extends T> list, T key, Comparator<? super T> c) : 利用二分查找法查找list集合中的key元素的索引位置
注意 : 如果List集合的元素类型没有提供排序规则,会按照第三个参数独立比较器提供的规则对元素先排序后查找!
void copy(List<? super T> dest, List<? extends T> src) : 把src集合中的所有元素复制到dest集合中
void fill(List<? super T> list, T obj) : 把obj对象填充到list集合中
T max(Collection<T> coll) : 集合元素求最值 //要求T类型的元素必须提供排序规则
T max(Collection<T> coll, Comparator<T> comp) : : 集合元素求最值
void reverse(List<?> list) : 翻转集合中的内容
void swap(List<?> list, int i, int j) :交换list集合中i索引和j索引位置的元素