Hash表
Hash,一般翻译做“散列”,也有直接音译为“哈希”的,它是基于快速存取的角度设计的,也是一种典型的**“空间换时间”**的做法。顾名思义,该数据结构可以理解为一个线性表,但是其中的元素不是紧密排列的,而是可能存在空隙。
PS:什么是Hash表,就是 数组+链表的组合形式
散列表(Hash table,也叫哈希表),是根据关键码值**(Key value)**而直接进行访问的数据结构。也就是说,**它通过把key值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数**,存放记录的**数组叫做散列表**。比如我们存储70个元素,但我们可能为这70个元素申请了100个元素的空间。**70/100=0.7,这个数字称为负载(加载)因子**。我们之所以这样做,**也 是为了“快速存取”的目的**。我们基于一种结果**尽可能随机平均分布**的固定函数H为每个元素安排存储位置,这样就可以避免遍历性质的线性搜索,以达到快速存取。但是由于**此随机性**,也必然导致一个问题就是冲突。**所谓冲突,即两个元素通过散列函数H得到的地址相同**,**那么这两个元素称为“同义词”**。这类似于70个人去一个有100个椅子的饭店吃饭。散列函数的计算结果是一个存储单位地址**,每个存储单位称为“桶”**。设一个散列表有m个桶,则散列函数的值域应为[0,m-1]。
如何存储数据到hash表中?
这些元素是按照怎么样的规则才能到对应数组中,一般是用过**【hash(key)%len】**也就是说元素key的hash值对数组长度取余
例如:上图中数组下标是从【0~15】证明数组的长度是16 ,要存储12到这个hash表中 --》 12%16 = 12 --》要存储 28 --》28%16 =12
如何扩容?
当hash表接近装满的时候,会进行扩容【也就相当于是数组的进行一个扩容】
Java中默散列单元大小全部都是2的次方【幂】,默认初始容量是16(2的4次方),假如16条链表中75%有都用数据,则当前默认加载因子达到0.75,hash表会从新开始计算,也就是将原有的散列结构**【全部抛弃】,重新开始一个大小为32(2的5次方)的散列结构,【各个存储在原有hash表中数据都需要重新计算并存储到对应位置中】**
PS:Set存储数据是无序,一种情况时计算【hash(key)%len】,另外一种情况就是Hash表扩容【一旦扩容重新计算】
负载(加载)因子0.75 --》例如:当前hash表中提供存储空间是16,也就是说当达到12的时候就进行扩容
排重机制
假日我们有一个数据(散列码496),而此时的HashSet有16个散列单元,那么和这个数据就可以插入到数组的第【496%16 =0 】位,此时就会将当前496计算的位置进行存储,存储到对应0的位置,如果当前0的位置已经发现一个和新添加496数据一样的数据存在即(equals ()== true),这个新数据就会被视为已经存储,从而就不会重复存数据
PS:问题只写equals真的可以排重吗?
Hash表直接根据散列码和散列表的数据大小计算除余后,就得到了所在数据的位置,然后在查找链表中是否存在这个重复数据,查找的代价也就是在链表中,但是真正一条链表的数据很少【为什么少?1、没有重复值 2、不同数据会存在不同位置】,有的甚至没有数据,几乎没有什么迭代可言,所以散列表的查找效率是建立在散列单元指定的链表中**【hash表的插入和查找是很优秀】**
HashSet集合
PS:HashSet的直接接口是Set接口即HashSet是Set接口的实现,Set接口的父接口Collection,所以HashSet也是Collection接口实现
Collection接口的实现类有:ArrayList,LinkedList,HashSet
List接口的实现类: ArrayList,LinkedList
Set接口的实现类: HashSet
特点:
1.无序(添加和底层存储顺序不一样)【无序不代表随机,因为这里的无序是因为存储数据需要用 hash(key)%len,而不是随机得到一个插入值】
ps: key就是要存储值,而hash(key)
在Java中就是对应引用类型hashcode
len 是默认hashSet创建的初始容量
2.所有Set集合都具有排重作用【例如:向Set集合中存储(1,1,1,1),最终在底层得到的存结果只有一个(1)】
ps:这个排重仅限系统提供类,如果需要对自己类定义的对象进行排重,我们需要自己实现方法
3.HashSet计算方式时依托于Hash表【Hash算法实现】
HashSetAPI
package com.qfedu.Collection.Set;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
public class HashSetDemo {
public static void main(String[] args) {
//1.创建HashSet
//常用方式就是空参创建(默认大小是16 加载因子是0.75)
//所有HashSet的创建底层都是一个HashMap,HashSet中存储值,会存储到
//HashMap的key中,Map中key是唯一的【即不可重复】,所以利用了不可重复这个特点
//使用HashMap存储进行排重
HashSet<Integer> set = new HashSet<>();
//2.其余构建特别是参数是一个Collection集合对象,可以完全参考ArrayList
//像HashSet中构造方法一Collection集合对象作为参数初始化HashSet的方式都是
//将参数集合中内容存到HashSet中,而非集合对象
//3.HashSet的创建是允许提供 默认初始容量 和 默认加载因子
//只默认初始化容量,加载因子还是0.75
//API
set.add(1);//向集合中添加元素
set.add(1);
set.add(2);
set.add(3);
set.add(4);
//打印是打印,但是不代表底层存储
System.out.println(set);
//向集合中添加另外一个集合的元素
HashSet<Integer> set2 = new HashSet<>();
Collections.addAll(set2,1,2,3,4,5);
set.addAll(set2);
System.out.println(set);
//清空集合
set2.clear();
System.out.println(set2);
//判断集合中是否存在某个元素
System.out.println(set.contains(1));
//containsAll 判断集合中是否存指定集合中元素
Collections.addAll(set2,1,2,3,4,5);
boolean b = set.containsAll(set2);
System.out.println(b);
//判断集合是否为空,空指定是集合中没有元素,而非集合赋值为null
//赋值为null证明集合在内存中没有开辟地址空间
System.out.println(set.isEmpty());
//因为HashSet并没有实现数组的相关操作,所以没有下标形式
//无法使用普通for循环,不能通过方法取出单独某个值
//增强for循环
for(Integer i : set){
System.out.println(i);
}
//迭代器,这个迭代器是基本迭代器
//只要集合带有泛型,通过迭代器方法创建出来迭代器都会默认推断出泛型
Iterator<Integer> iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
//打印遍历
set.forEach(System.out::println);
set.forEach(o ->{
System.out.println(o);
});
//HashSet只能根据对象删除
set.remove(1);
System.out.println(set);
//参考ArrayList方法中实现
/*
removeAll 删除指定集合中存在的参数
retainAll 保留集合中指定参数,删除其他值
*/
//获取HashSet的长度
System.out.println(set.size());
//HashSet转变为一个数组
Object[] objects = set.toArray();
}
}
HashSet的子类LinkedHashSet
LinkedHashSet继承于HashSet子类,底层出Hash算法之外,提供链表算法
Hash表:主要用来保证数据唯一性 。 链表:用于记录元素的先后添加顺序
LinkedHashSet没有任何特殊方法,HashSet如何使用LinkedHashSet就如何使用。
PS:当初期待LinkedHashSet的诞生主要是为了,它提供了一个记录顺序方式,但是LinkedHashSet依旧没有提供直接获取对应元素的方法,所以可以了解即可
LinkeHashSet是HashSet子类,并且也是Collection 和Set集合接口的实现类
LinkedHashSet 可以维护存储顺序,底层的排重是依据与Hash表
如何对自定义对象进行排重?
排重案例:
package com.qfedu.Set;
import java.util.Objects;
public class Girl {
private String name;
private int age;
public Girl() {
}
public Girl(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
//因为要展示集合中存储的Girl对象数据,重写toString
@Override
public String toString() {
return "Girl{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
// 如果需要将自定义类所创建对象存储到HashSet中并需要排重,必须重写equals
@Override
public boolean equals(Object o) {
//此时只需要判断当前和传入对象是否相等即可【比较属性】
Girl other = (Girl)o;
return this.name.equals(other.name) && this.age == other.age;
}
//equals相等,hashcode也相等
@Override
public int hashCode() {
//属性中参与equals判断的有引用类型就调用其对应hashcode值+属性参与判断值{包装类,基本数据类型}类型值
return name.hashCode() + age;
}
}
package com.qfedu.Set;
import java.util.HashSet;
//需求:创建一个Girl类,提供两属性name和age,创建两个对象,将对象存储在Set集合中,创建的着两个对象属性要完全相同。
public class SetDemo {
public static void main(String[] args) {
//HashSet集合在存自定类所创建对象时,是不会自动排重
HashSet<Girl> set = new HashSet<>();
Girl girl1 = new Girl("小红", 18);
Girl girl2 = new Girl("小红", 18);
set.add(girl1);
set.add(girl2);
//如果认为两个对象是相等, 只要对象中属性完全相同,或部分相同【指定部分】,就认为是同一个对象
//此时存储在set集合对象中 Girl对象中属性是完全相同,所以set集合需要进行排重操作
System.out.println(new Integer(496).hashCode());
System.out.println(new Integer(496).hashCode());
System.out.println(girl1.hashCode());
System.out.println(girl2.hashCode());
System.out.println(set);
//HashSet集合中存储系统类的对象,会自动排重
HashSet<Integer> set1 = new HashSet<>();
set1.add(18);
set1.add(18);
System.out.println(set1);
HashSet<String> set2 = new HashSet<>();
set2.add("小红");
System.out.println("小红".hashCode());
set2.add("小红");
System.out.println("小红".hashCode());
System.out.println(set2);
//如果给当前自定类所创建的排重
/*
在刚刚讲解Hash表的时候说了,HashSet底层是依托于Hash表【即散列表】进行数据存储
如果需要将数据存到Hash表中,需要会进行一个 Hash函数的计算 hash(key)%len
hash(key) 对应的是 Java中hashcode【即值已经给算完】 key对应用类型而言 就是 地址 --》 例如:0xabcd
len是HashSet的初始容量值 即 16
会根据当前这个公式计算,数据需要挂在在hash表中的位置,Hash表中排重依据
1.如果存现了两相同数据,如果排重,需要让Hash表知道这个数据是相等,所以Java中需要提供 equals方法
PS:JavaAPI 基本上所有可以使用类,equals方法都是重写完毕
而自定义类是直接继承于Object,所以会继承父类Object,此时hash表中就会调用这个Object中equals方法
来比较两个对象是否相等,默认Object中equals方法是 this == obj 即比较两个引用类型地址,所以Hash表中
是无法比较出两个对象是否相等,所以不能排重
如果需要将自定义类所创建对象存储到HashSet中并需要排重,必须重写equals
2.只提供equals是无法完成HashSet中排重任务,,原因在于hashSet底层Hash表的存储
hash表的存储原则是 hash(key)%len --》相当于是Java 引用类型.hashcode%len;
此时自定义类所创建的对象都是用过new 所创建出来,每一个对象的地址都是不相同的即hashcode值也不同
那么就会造成一个问题,每一个对象通过 hash(key)%len 函数计算 得到存储位置都是不相等,所以就算重写equals也是无法比较
HashSet存储数据比较equals相等的必要原则,存储在同一个位置--》即体现在Hashcode要一致才可以比较equals
所以,才得到一个结论在重写equals的同时要重新hashcode
程序猿的结论:如果两个对象equals相等,那么他们hashcode也要相等,这样才可以被认为是同一个对象
PS:JavaAPI中提供类只要重写equals都提供了hashcode
*/
}
}
排重原则
一般而言:我们使用HashSet存储自定义类时,需要对类重写hashCode和equals方法
在HashSet中如何判断两个对象是否相同问题:
1):两个对象的equals比较相等. 返回true,则说明是相同对象.
2):两个对象的hashCode方法返回值相等.
对象的hashCode值决定了在哈希表中的存储位置.
二者:缺一不可.
当往HashSet集合中添加新的对象的时候,先回判断该对象和集合对象中的hashCode值:
1):不等: 直接把该新的对象存储到hashCode指定的位置.
2):相等: 再继续判断新对象和集合对象中的equals做比较.
1>:hashCode相同,equals为true:则视为是同一个对象,则不保存在哈希表中.
1>:hashCode相同,equals为false:非常麻烦,存储在之前对象同槽为的链表上(拒绝,操作比较麻烦).【比较特殊极端的方式才能出现】
对象的hashCode和equals方法的重要性:
每一个存储到hash表中的对象,都得提供hashCode和equals方法,用来判断是否是同一个对象.
存储在哈希表中的对象,都应该覆盖equals方法和hashCode方法,并且保证equals相等的时候,hashCode也应该相等.
equals和hashcode的重写
PS:之前我们所有equals和hashcode重写都是根据自身需求手动完成,逻辑是我们自定义。但以后的实际开发汇建议使用系统生成的,不建议手写,什么时候手写【笔试】
package com.qfedu.Set;
import java.util.Objects;
public class EqualsAndHashcode {
//IDEA中一共equals和hashcode重写,IDEA提供了两种重写,无论是哪种效果都是一样
//在使用的时候值需要选择其中一种即可,不能共存
String name;
int age;
//重写equals和hashcode【同样可以使用alt+Insert快捷键】 --》 Default模板 【默认的】
@Override
public boolean equals(Object o) {
if (this == o) return true; //相当于直接比较了两个引用类型地址,地址都相等,那么一定是同一个对象
//传入的o对象为null 那么就没有必要比较,因为传入的对象地址都没有,那么无需比较直接false
//传入o对象不为null ,但是 你字节码获取对象和传入的对象的字节码文件获取对象不一致 ,直接false【两个对象比较,都不是用一个.java源文件创建】
if (o == null || getClass() != o.getClass()) return false;
//向下转型
EqualsAndHashcode that = (EqualsAndHashcode) o;
if (age != that.age) return false; //需要判断的条件其中的某一个不相等,直接false【比较原则是属性完全相同】
//另外一个条件 先判断name是有有值, 判断 name是否不等null,证明当name值一定是有,所以他防止了出现空指针异常判断
// name.equals(that.name) 如果没有前面的 name != null 那么会出现一个空指针异常问题
//name没有赋值,并且使用是默认null或者赋值为null ,那么就会出现null.equals(that.name) --》 出现空指针异常
//name 值为null null != null 为false 所以执行 that.name = null;
//逻辑:相当于this.name 是null that.name 也是null ,所以也会认为使用一个对象
// this.name = null 但是 that.name != null 返回false 一定不用一个对象
return name != null ? name.equals(that.name) : that.name == null;
}
@Override
public int hashCode() {
//这个逻辑的主要目的就是为了防止出现重复
//它提供了一个31的质数,这个质数值经过计算验证的得来, 这个质数不大不小正好
//hash表的大小是 2的次方 默认16 --》 2的4次方 扩容 2的5次方法
//31 正好是 2的5次方法-1 即2 向左位5位
//判断引用类型是否为null,如果为null则返回0,否则返回hashcode值
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
//IDEA中第二种写法,当前必须是Java7之上API才可以,因为使用了Objects这个工具类
@Override
public boolean equals(Object o) {
if (this == o) return true; //相当于直接比较了两个引用类型地址,地址都相等,那么一定是同一个对象
//传入的o对象为null 那么就没有必要比较,因为传入的对象地址都没有,那么无需比较直接false
//传入o对象不为null ,但是 你字节码获取对象和传入的对象的字节码文件获取对象不一致 ,直接false【两个对象比较,都不是用一个.java源文件创建】
if (o == null || getClass() != o.getClass()) return false;
//向下转型
EqualsAndHashcode that = (EqualsAndHashcode) o;
//直接比较值类型【基本数据类型】值 和 引用类型的equals方法,
//这里equals是Objects提供,但是这个equals会遵守参数中重新给equals方式进行比较
//即就相当于直接使用 name.equals(that.name)
return age == that.age &&
Objects.equals(name, that.name);
}
@Override
public int hashCode() {
//hash的底层实现就是
/*
int result = name != null ? name.hashCode() : 0;
// result = 31 * result + age;
*/
return Objects.hash(name, age);
}
}
扩展:当向集合中插入对象时,如何判别在集合中是否已经存在该对象了?
也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。
面是HotSpot JVM中生成hash散列值的实现:
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
intptr_t value = 0 ;
if (hashCode == 0) {
// This form uses an unguarded global Park-Miller RNG,
// so it's possible for two threads to race and generate the same RNG.
// On MP system we'll have lots of RW access to a global, so the
// mechanism induces lots of coherency traffic.
value = os::random() ;
} else
if (hashCode == 1) {
// This variation has the property of being stable (idempotent)
// between STW operations. This can be useful in some of the 1-0
// synchronization schemes.
intptr_t addrBits = intptr_t(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
} else
if (hashCode == 2) {
value = 1 ; // for sensitivity testing
} else
if (hashCode == 3) {
value = ++GVars.hcSequence ;
} else
if (hashCode == 4) {
value = intptr_t(obj) ;
} else {
// Marsaglia's xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = Self->_hashStateX ;
t ^= (t << 11) ;
Self->_hashStateX = Self->_hashStateY ;
Self->_hashStateY = Self->_hashStateZ ;
Self->_hashStateZ = Self->_hashStateW ;
unsigned v = Self->_hashStateW ;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW = v ;
value = v ;
}
value &= markOopDesc::hash_mask;
if (value == 0) value = 0xBAD ;
assert (value != markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value;
}
TreeSet集合
TreeSet特点
1.有Set集合的特点即排重
2.自带排序功能,对存储在集合数据进行排序【默认是升序】
PS:在没有Java8之前,TreeSet是除Collections工具类中排序方法之外,唯一可以排序的集合
但是在Java8之后List集合中sort接口,所以List集合也排序,List集合提供了sort方法
不是只有TreeSet才可以对数据进行排序,List和Map集合都可以排序,我们主要学习式数据结构和排序接口的实现,但是TreeSet有一个不争的事实就是排重
3.TreeSet在Java8之前默认实现方式是 【二叉树+hash表】,在Java8之后实现方式时【红黑二叉树+hash表】
4.TreeSet的排序其实就是对树遍历【前序、中序、后序】
5.因为还有Hash表的存在,所以【添加顺序和底层存储顺序不一致,添加同时进行排序【是以树状形式展示】】
PS:TreeSet不要就是实现API,如果实现自己查看文档【因为他是一个Set集合实现,所以Set集合基本法他都有】
基础案例:
package com.qfedu.TreeSet;
import java.util.Collections;
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
//TreeSet排序 【默认升序而且排重】
TreeSet<Integer> set = new TreeSet<>();
Collections.addAll(set,34,543,543,543,5234,62,624,62,534,32,523,234,6234,62,46);
System.out.println(set);
//这样定义是没有问题,但是不要这样,有问题存在
// TreeSet set2 = new TreeSet();
// Collections.addAll(set2,34,543,543,543,5234,"62",624,62,534,32,523,234,6234,62,46);
// System.out.println(set2);
//我们使用TreeSet比较的时候,必须保证数据类型一致【使用泛型约束】,除此之外,数据与数据之间要存在可比性【可比较性】
}
}
使用自定类的对象进行比较排序
需求:创建一个学生类,提供基本属性【必须有年龄】,需要根据学生年龄进行升序排序
package com.qfedu.TreeSet;
import java.util.Comparator;
public class Student {
int age;
String name;
public Student(String name ,int age){
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
package com.qfedu.TreeSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
//存学生对象【TreeSet的比较原则是数据一致,具备可比性(需要在TreeSet中排序的对象,要么实现Comparable 或 实现Comparator)】
//到底需要实现哪个接口,这里取决于你调用按个TreeSet的构造方法
TreeSet<Student> set = new TreeSet<>();
Collections.addAll(set
,new Student("张三",18)
,new Student("李四",18)
,new Student("王五",16)
,new Student("赵六",29)
,new Student("田七",10));
set.forEach(System.out::println);
}
总结:如果需要使用TreeSet对自定类所创建对象进行排序,要么实现Comparable【自然排序】接口要么实现Comparator【自定义排序】,这个接口的实现取决于TreeSet调用哪个构造方法
TreeSet无参构造方法创建对象存储数据,那么存储数据必须实现Comparable才能排序
TreeSet有参构造方法创建对象存储数据,就需要提供Comparator接口的实现才能排序
Comparable和Comparable接口的比较规则
当前对象【this】 和 传入对象【other】
this < other 返回一个负数 (返回-1) 【降序】
this == other 返回一个零 (返回0) 【不变】
this > other 返回一个正数 (返回1) 【升序】
需要注意:
1.比较方法中,哪个属性需要排序,就比较哪个属性
2.规则要求三个条件都要满足 —》 假如:【升序 this>other】在第一个判断条件的位置
—》 假如:【降序 this<ohter】 在第一个判断条件的位置
3. this == other 的时候返回值是0 ,说明两个对象比较属性是相等,这个0还是排重要求【排重一个对象,仅保留一个】
4. 无论是Comparable 还是 Comparator 都要遵守这个比较规则,不同点在于实现方法不同,作用在TreeSet构造方法不同
Comparable接口
此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo
方法被称为它的自然比较方法。
方法摘要 | T 是 Comparable接口泛型,只要指定泛型类型,就无需向下转型,这个返回值是一个int类型能得到的结果只有三种【正数,负数和零】 |
---|---|
int | compareTo(T o) 比较此对象与指定对象的顺序。 |
PS:在Java8中Comparable接口被加入了多个方法,但是核心还这一个。
Comparable接口那个类的对象需要排序【TreeSet排序】,那么就哪个类实现这个Comparable接口并实现比较方法compareTo
对学生的年龄进行升序排序
package com.qfedu.TreeSet;
import java.util.Collections;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
//存学生对象【TreeSet的比较原则是数据一致,具备可比性(需要在TreeSet中排序的对象,要么实现Comparable 或 实现Comparator)】
//到底需要实现哪个接口,这里取决于你调用按个TreeSet的构造方法
//TreeSet的无参构造方法,要是存储在内部的数据必须实现Comparable接口才能进行比较操作
TreeSet<Student> set = new TreeSet<>();
Collections.addAll(set
,new Student("张三",18)
,new Student("李四",18)
,new Student("王五",16)
,new Student("赵六",29)
,new Student("田七",10));
set.forEach(System.out::println);
}
}
package com.qfedu.TreeSet;
import java.util.Comparator;
public class Student implements Comparable<Student>{
private int age;
private String name;
public Student(String name ,int age){
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
//比较方法,原则就是哪个属性要比较,这里就写哪个属性比较规则
//但是必须满足 正数 ,负数 和 零的操作
@Override
public int compareTo(Student other) {
//谁触发这个方法谁就是this other就是传入对象
//年龄升序
/*
当前对象 > 传入对象 返回1 【升序】
当前对象 == 传入对象 返回0 【不改变(会排重),建议如果为0需要进行一些其他比较操作】
当前对象 < 传入对象 返回 -1 【降序】
*/
// if(this.age < other.age){ //此时就是升序
// return -1;
// }else if(this.age == other.age){
// return 0;
// }else{
// return 1;
// }
//下面这种写就利用了一个规则,这个方法的返回值是一个int类型 而且得带的结果是 【正数,负数和零】
// 只要是数值类型, 做差值就可以了 包装类和String都提供Comparable接口的实现,所有直接调用compareTo方法即可
//万能操作 当前对象-传入对象 或 当前对象.compareTo(传入对象) 【升序】
// 传入对象-当前对象 或 传入对象.compareTo(当前对象) 【降序】
//TreeSet是靠这个方法排重的,排重的依据就是计算结果得到0,此时就会认为是同一个对象
// 所以如果对数据排序的时候如果有重复值,就建议在指定一个排序规则,以保证数据完整
//PS: 这个位置时使用三目运算符最多的位置
return other.age - this.age == 0 ? this.name.compareTo(other.name) : other.age - this.age;
}
}
总结:Comparable接口只适合使用在TreeSet的无参构造方法排序中,其他的排序就不建议使用这个接口
哪个类的对象需要在TreeSet中排序,哪个类就实现Comparable接口,泛型就是这个类
compareTo方法只要实现下面这个操作,任意数据都能排序,这里就是一个公式【哪个属性比较就套用这个公式即可】 例如: age属性比较 当前对象.age - 传入对象.age
//万能操作 当前对象-传入对象 或 当前对象.compareTo(传入对象) 【升序】
// 传入对象-当前对象 或 传入对象.compareTo(当前对象) 【降序】
Comparator接口
Comparator接口和Comparable接口的比较规则是一样的,不同点在于实现方法不用
方法摘要 | T Comparator的泛型,哪个数据比较就写哪个数据类型 o1就是当前对象 o2就是传入对象 |
---|---|
int | compare(T o1, T o2) 比较用来排序的两个参数。 |
Comparator多用于各种工具类中所提供排序方法,作为比较接口参数存在,它的使用范围大于Comparable,建议优先掌握Comparator的实现方式,以后所有的排序都可以简单化解。
Comparator接口作为自定义类在TreeSet中排序,那么就需要调用TreeSet的有参构造方法,方法参数是Comparator类型
PS:哪个类在TreeSet中使用Comparator进行排序,不需要这个类实现Comparator接口,需要单独见一个类实现Comparator,执行比较规则,然后将这个类的对象传入到TreeSet有参够方法中,自然就排序了
package com.qfedu.TreeSet;
import java.util.Comparator;
public class Student {
int age;
String name;
public Student(String name ,int age){
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
package com.qfedu.TreeSet;
import java.util.Comparator;
public class GZ implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
package com.qfedu.TreeSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
//存学生对象【TreeSet的比较原则是数据一致,具备可比性(需要在TreeSet中排序的对象,要么实现Comparable 或 实现Comparator)】
//到底需要实现哪个接口,这里取决于你调用按个TreeSet的构造方法
//TreeSet的有参构造方法,要是存储在内部的数据必须实现Comparator接口才能进行比较操作
//参数是实现当前Comparator接口的实现类的对象
TreeSet<Student> set = new TreeSet<>(new GZ());
Collections.addAll(set
,new Student("张三",18)
,new Student("李四",18)
,new Student("王五",16)
,new Student("赵六",29)
,new Student("田七",10));
set.forEach(System.out::println);
//推荐使用匿名内部
TreeSet<Student> set2 = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.age-o2.age;
}
});
Collections.addAll(set2
,new Student("张三",18)
,new Student("李四",18)
,new Student("王五",16)
,new Student("赵六",29)
,new Student("田七",10));
set2.forEach(System.out::println);
//lambda
TreeSet<Student> set3 = new TreeSet<>((o1,o2)->o1.age-o2.age);
Collections.addAll(set3
,new Student("张三",18)
,new Student("李四",18)
,new Student("王五",16)
,new Student("赵六",29)
,new Student("田七",10));
set3.forEach(System.out::println);
}
}
另外作用
package com.qfedu.TreeSet;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class ComparatorDemo {
public static void main(String[] args) {
//Comparator接口的另外作用,作为sort方法的参数存在
//1.数据中存储数据可以任意升序或降序【数据元素的数据类型必须是包装类】
Integer[] arr = {3,243,5243,643,6534,234,5,16,23672,2,416};
//匿名内部类
Arrays.sort(arr, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//return o1-o2;//升序
return o2-o1; //降序
}
});
//List集合以可以使用排序,List集合提供sort方法
List<Integer> list = Arrays.asList(arr);
list.sort(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//return o1-o2;//升序
return o2-o1; //降序
}
});
//PS:在Java8之前List集合是没有sort方法,所以需要对List集合进行排序,我们需要使用工具类
//Collections 这个工具类提供了 sort方法 对List集合进行排序
Collections.sort(list,new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//return o1-o2;//升序
return o2-o1; //降序
}
});
//以上所有匿名内部类的位置都可以使用lambda
//以Collections为例子
Collections.sort(list,(o1,o2)->o2-o1);
}
}
Comparator和Comparable接口总结
1.这两个接口都可以对TreeSet中排序的数据,提供排序方式,只不过对应构造方法不同
2.Comparator和Comparable的实现排序理论是一样的,所以如果使用TreeSet对数据进行排序,这两个接口只要实现其中一个即可,不需要都实现
PS:如果用TreeSet建议使用Comparable
3.Comparator接口其实比Comparable接口受众面更大,Comparator接口不仅可以作为TreeSet集合的实现,也可以作为系统一共sort方法中参数实现。
PS: 建议优先掌握Compartor接口
4.特别注意因为TreeSet的去重原则是,比较方法中返回值类0,就会认为是重复元素,所以会排重,建议如果出现0提另外一种比较规则。
Set集合
共同特点:
1.都不允许有重复数据【自动排重】
2.都是线程不安全,解决方案
Collections.synchronizedSet(Set集合对象); ---》 这个方法的返回值是一个线程安全的set集合对象
Set集合是Collection集合子类,所以Set集合实现类都是作为Collection接口类使用,所以提面向接口编程【多态的多态】
Set set = new HashSet();
Set set = new LinkedHashSet();
Set set = new TreeSet();
Collection c = new HashSet();
Collection c = new LinkedHashSet();
Collection c = new TreeSet();
PS:这里用到的泛型在使用的时候自己添加
HashSet查询效率高,但是添加数据慢【扩容阶段】
LinkedHashSet是HashSet子类,并没有任何特殊方法,HashSet如何操作它就如何操作,唯一比HashSet多出的原则就是提供了一个有序存储【记录存储顺序】(较少使用)
TreeSet是Set集合接口的实现了,TreeSet主要用来对数据进行排序但是需要实现【Comparable】或【Comparator】接口,但是随着Java8的诞生,不是只有TreeSet才可以排序的,List和Map集合都可以排序,但是需要注意,需要实现Comparator。TreeSet独一无二作用就是排重
HashSet是运用Hash表进行数据存,但是实际存储数据结构,在底层是以HashMap的形式实现
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
所有向HashSet中存储的数据,都存储到了HashMap集合中Key中,HashMap集合Key,唯一且排重
Collection集合
Collection集合是List集合和Set集合的父类【根接口】,Collection集合中提供了大部分List和Set集合的方法所以List集合和Set集合方法掌握之后Collection集合的方法也就掌握了
Collection集合创建对象,只需要使用List和Set集合接口的实现类即可
Collection c1 = new ArrayList();
Collection c2 = new LinkedList();
Collection c3 = new HashSet();
Collection c4 = new LinkedHashSet();
Collection c5 = new TreeSet();
PS:最常用的List集合【ArrayList】,最常用Set集合【HashSet】
扩展:二叉树实现
二叉树:是每个结点最多有[两个子树]的有序树,在使用二叉树的时候,数据并不是随便插入到节点中的, 一个节点
的左子节点的关键值必须小于此节点【树的左边一般是小于根节点】,右子节点的关键值必须大于或者是等于此节点**【树的右边一般是大于根节点】**,所以又称二叉查找树、二叉排序树、二叉搜索树【统称二叉树】。
完全二叉树:若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数, 第h层有叶子结
点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。 满二叉树——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。 深度——二叉树的层数,就是深度。
二叉树的特点总结:
(1)树执行查找、删除、插入的时间复杂度都是O(logN) —> 大o算法
(2)遍历二叉树的方法包括前序、中序、后序
(3)非平衡树指的是根的左右两边的子节点的数量不一致
(4)在非空二叉树中,第i层的结点总数不超过 , i>=1;
(5)深度为h的二叉树最多有个结点(h>=1),最少有h个结点;
(6)对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;
二叉树遍历分为三种
先(前)序遍历 首先访问根,再先序遍历左子树,最后先序遍历右子树 根 --> 左 – > 右
中序遍历 首先中序遍历左子树,再访问根,最后中序遍历右子树 左 --> 根 --.> 右 【升序】
后序遍历 首先后序遍历左子树,再后序遍历右子树,最后访问根 左 --> 右 --> 跟
package com.qfedu.TreeSet;
import java.math.BigInteger;
/**
* 二叉树
*/
public class BinaryTreeDemo {
private Node root;//根节点
//提供一个方法可以添加节点
public void add(int data){
//判断是否是第一次创建节点
if(root == null){
//创建根节点
root = new Node(data);
}else{//不是根节点
//分左右插入数据
root.addNode(data);
}
}
//打印树中存储数据
public void printBinaryTree(){
root.print();
}
class Node{ //节点内部类【当前表示二叉树中每一个元素】
private int data; //数据
private Node left;//左
private Node right;//右
public Node(int data){
this.data = data;
}
//添加具体的左右节点
public void addNode(int data){
if (this.data > data){//根节点数据大于传入数据
//向左
if(this.left == null){
this.left = new Node(data);//将数据插入到根节点的左边【这就是递归方法的出口】
}else{
//因为不知道左边有多少个阶段,所以这个判断无法完成
this.left.addNode(data);//递归
}
}else{//小于传入数据
//向右
if(this.right == null){
this.right = new Node(data);
}else{
this.right.addNode(data);
}
}
}
//提供打印节点值(左---》 根---》 右) -->[升序]
public void print(){
if(this.left != null){
this.left.print();
}
System.out.print(this.data+"->");
if (this.right != null){
this.right.print();
}
}
}
public static void main(String[] args) {
BinaryTreeDemo br = new BinaryTreeDemo();
br.add(8);
br.add(3);
br.add(1);
br.add(9);
br.add(10);
br.add(5);
br.add(7);
br.add(6);
br.printBinaryTree();
}
}
Map集合
PS:map集合中提供一个名词【映射】
映射在数学中的解释:
假设有A,B两个非空集合,如果存在一个法则f,使得A中每个元素按照法则f在B中有唯一确定元素与之对应,则f为从A到B的映射即 f: A->B
上图中映射关系是两个集合的连接,从A集合连接到B集合,这个之间产生的关联就是【映射】
A集合数据如何和B集合数据关联在一起,映射关系提供了一种存储方法**【key-value】 键值对**
映射关系中约束,作为key这一端的集合数据必须是唯一的且不允许重复
作为value这一端的集合数据可以不为唯一且可以重复
PS:就是因为这个原因key是唯一且不重复,所以把存储key这边集合会看做set集合
value是不唯一且可以重复,所以把存储value的这边集合会看做List集合
严格上说Map并不是集合,而是把两个集合之间产生了映射关系(Map接口并没有继承Collection),然而因为Map可以存储数据,所以我们习惯把Map称为集合
因为Map,没有继承Collection所有没有Iterable接口,所以Map集合不提供迭代器遍历,不支持普通for循环也不支持增强for循环
Map集合原则:key必须唯一且不允许重复,value可以不唯一且允许重复
例如:
key1 = value1
key2 = value2
//上面这种结构是允许
//下面这种不允许
key1 = value1
key1 = value2
//此时就违反了key是唯一的原则,所以这样操作是不允许
Map集合的特点1.Map集合中是排重,它是根据key排重 2.key和value的类型必须是引用类型
HashMap的常用方法
package com.qfedu.Map;
import java.util.Collection;
import java.util.HashMap;
import java.util.Set;
/**
* 常用API,Java8新API明天演示
*/
public class HashMapDemo {
public static void main(String[] args) {
//1.创建一个Map集合
//它有两个泛型
// 第一个泛型是key的泛型【存储key的数据类型】
// 第二个泛型是value的泛型【存储value的数据类型】
//无论是哪个泛型必须是引用类型
HashMap<String,Integer> map = new HashMap<>();
//常用API
//1.向Map集合添加数据
//第一个参数是要存储的key,第二个参数是要存储的value
map.put("key1",1);
//map集合已经重写toString,所以可以直接打印
System.out.println(map);
//编译和运行阶段都不会报错,而且是允许的,map依旧会保持key的唯一性
//put方法的另外一个作用,就是覆盖【修改】对应key存储value
map.put("key1",2);
System.out.println(map);
//判断map集合中是否存在指定key 存在则true 否则false
boolean key1 = map.containsKey("key1");
//判断map集合中是否存在指定的value 存在则true,否则false
boolean b = map.containsValue(2);
//遍历方式1 forEach方法
map.forEach((k,v)->{
System.out.println("key的值是:"+k);
System.out.println("value的值是:"+v);
});
//通过key获取对应value值【如果key不存在这取出一个null】
Integer integer = map.get("key1");
//删除map集合中的键值对 参数是key值, 只要传入key满足条件 直接删除整个键值对
map.remove("key1");
//获取map集合中所有key值存储到Set集合汇总
Set<String> strings = map.keySet();
//获取map集合中所有value值存储到Collection集合汇总
Collection<Integer> values = map.values();
}
}