一、Set
1、Set:元素不可以重复,是无序的(存入和取出的顺序不一致)
2、Set接口中的方法和Collection中的方法一致
3、Set集合的元素取出方式只有一种:迭代器iterator()
Set set = new HashSet();
Iterator it = set.iterator();
while (it.hasNext()) {
it.next();
}
4、Set接口有两个子类
(1)HashSet:内部数据结构是哈希表(哈希表底层是数组+单链表),是不同步的
(2)TreeSet:内部数据结构是二叉树,是不同步的(可以对Set集合中的元素排序。存取顺序不一致,但有指定顺序)
注:LinkedHashSet:元素唯一(哈希表),元素有序(链表)
二、HashSet
1、HashSet由哈希表(实际上是一个HashMap实例)支持,它不保证迭代顺序(无序)。因为底层的存储方式是通过算法来完成的
注:HashSet堪称查询速度最快的集合(因为其内部是hashCode()来实现的)
2、Set进行迭代所需的时间与HashSet实例的大小(元素的数量)和底层HashMap实例(桶的数量)的“容量”的和成比例。因此,如果迭代性能很重要,则不要将初始容量设置的太高(或将加载因子设置的太低)
3、HashSet用来保证元素唯一(重复的元素存不进去)。需要唯一才使用HashSet,不需要唯一应该选择ArrayList
注:需要唯一就用Set,无所谓就用List(List使用较多)。List和Set最大的区别在于元素是否唯一
4、HashSet判断元素唯一,需要覆盖hashCode()和equals()方法,建立自己的判断唯一的依据
5、无论是contains()还是remove(),关键问题是判断元素是否相同,即容器中是否有和该元素相同的元素。而容器判断相同的依据,根据容器的数据结构而定
(1)ArrayList:判断equals()
(2)HashSet:判断hashCode() + equals() (哈希表中必须首先判断hashCode())
6、构造函数
(1)HashSet():构造一个新的空set,其底层HashMap实例的默认初始容量是16,加载因子是0.75
(2)HashSet(Collection<? extends E> c):构造一个包含指定collection中的元素的新set
(3)HashSet(int initialCapacity):构造一个新的空set,其底层HashMap实例具有指定的初始容量和默认的加载因子(0.75)
(4)HashSet(int initialCapacity, float loadFactory):构造一个新的空set,其底层HashMap实例具有指定的初始容量和指定的加载因子
注:HashSet所有的构造函数都是构造出一个新的HashMap。HashSet中的所有元素都是保存在HashMap的key中,value则是使用的PRESENT对象,该对象为static final(HashSet是基于HashMap来实现的,底层采用HashMap来保存元素)
private static final Object PRESENT = new Object();
三、LinkedHashSet
1、LinkedHashSet:具有可预知迭代顺序的Set接口的哈希表和链接列表实现
2、唯一+有序。LinkedHashSet可以让客户免遭未指定的、由HashSet提供的通常杂乱无章的排序工作,而又不致引起与TreeSet关联的成本增加。使用它可以生成一个与原来顺序相同的set副本,并且与原set的实现无关,然后返回由此副本决定了顺序的结果(客户通常期望内容返回的顺序与它们出现的顺序相同)
四、TreeSet
1、TreeSet implements SortedSet:基于TreeMap的NavigableSet实现。使用元素的自然顺序对元素进行排序(Comparable),或者根据创建set时提供的Comparator进行排序,具体取决于使用的构造方法
2、TreeSet可以对集合中的元素进行排序,是不同步的
3、TreeSet集合是Set集合的子类,也可以保证元素的唯一性。TreeSet判断元素唯一的方式:根据比较方法(compareTo()或compare())的返回结果是否为0。为0,就是相同元素,不存储
注:TreeSet集合和hashCode()、equals()没有关系
4、TreeSet无序(存入和取出顺序不同),但有自己的排序方式。TreeSet对元素进行排序的方式(两种):
(1)让元素自身具备比较功能。元素需要实现Comparable接口,覆盖CompareTo()方法 -- 元素的自然排序
(2)让集合自身具备比较功能。定义一个类,实现Comparator接口,覆盖compare()方法。将该类对象(接口Comparator的子类对象)作为参数传递给TreeSet集合的构造函数 -- 比较器
注:TreeSet集合中的元素要按照指定方式排序,需要进行比较,必须要让元素自身或者集合自身具备比较功能。在实际开发中,比较器(Comparator)比较常用。因为它能避免一些弊端和不足,eg:Person本身具备比较功能A,但不是想要的,此时就必须使用比较器定义另一个比较功能B
例:字符串本身实现按元素的字典顺序排序(Java写好的,无法修改),若现在需要按字符串长度排序,就只能使用Comparator比较器
public class Test {
public static void main(String[] args) {
TreeSet ts = new TreeSet(new ComparatorByLength());
ts.add("aaaaa");
ts.add("zz");
ts.add("nbaq");
ts.add("cba");
ts.add("abc");
System.out.println(ts); //[zz, abc, cba, nbaq, aaaaa]
for (Iterator it = ts.iterator(); it.hasNext(); ) {
System.out.println(it.next());
}
}
}
/**
* 自定义类,实现Comparator接口,覆盖compare()方法
*/
class ComparatorByLength implements Comparator {
@Override
public int compare(Object obj1, Object obj2) {
//......此处省略强转前的健壮性判断
String s1 = (String) obj1;
String s2 = (String) obj2;
int temp = s1.length() - s2.length();
//如果长度相同,再按元素的字典顺序排序
return temp == 0 ? s1.compareTo(s2) : temp;
//如果只按照字符串长度比较,则长度相同的后面元素将会被舍弃
// return temp; //ts的值为:[zz, cba, nbaq, aaaaa]
}
}
5、构造函数
(1)TreeSet():构造一个新的空set,该set根据其元素的自然顺序进行排序。插入该set的所有元素都必须实现Comparable接口。另外,所有这些元素都必须是可互相比较的:对于set中的任意两个元素e1和e2,执行e1.compareTo(e2);都不得抛出ClassCastException
(2)TreeSet(Collection<? extends E> c):构造一个包含指定collection元素的新TreeSet,它按照其元素的自然顺序进行排序。插入该set的所有元素都必须实现Comparable接口。另外,所有这些元素都必须是可互相比较的:对于set中的任意两个元素e1和e2,执行e1.compareTo(e2);都不得抛出ClassCastException
(3)TreeSet(Comparator<? super E> comparator):构造一个新的空TreeSet,它根据指定比较器进行排序。插入到该set的所有元素都必须能够由指定比较器进行相互比较:对于set中的任意两个元素e1和e2,执行comparator.compare(e1, e2);都不得抛出ClassCastException
(4)TreeSet(SortedSet<E> s):构造一个与指定有序set具有相同映射关系和相同排序的新TreeSet
五、二叉树(红黑树)
1、二叉树是一种数据结构,持有左、右、父三个索引
2、用二叉树可以完成排序,并能确定元素的位置。查找顺序:左 - 中 - 右,左边小,右边大,相同的不存储
3、在存储后一个元素时,已有的元素是有序的。对已有的有序元素进行折半(折半查找/二分法查找),再确定新元素的位置,效率较高
4、用二叉树实现TreeSet 正序/倒序 存取(怎么存入就怎么取出,或者怎么存入就倒序取出)
class ComparatorByName implements Comparator {
/**
* 用二叉树实现TreeSet 正序/倒序 存取
* @param obj1
* @param obj2
* @return
*/
@Override
public int compare(Object obj1, Object obj2) {
//......此处省略强转前的健壮性判断
Person p1 = (Person) obj1;
Person p2 = (Person) obj2;
//二叉树只看正数、负数和零
//正序:存入和取出顺序一致
return 1;
//倒序:存入和取出顺序相反
// return -1;
//若 return 0; ,说明后面添加的元素都和第一个元素相同,全部舍弃
}
}
六、java.lang.Comparable
1、interface Comparable<T>:此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo()方法被称为它的自然比较方法
2、实现此接口的对象列表(和数组)可以通过Collections.sort()(和Arrays.sort())进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器
3、建议:最好使自然排序和equals()一致。即 (x.compareTo(y) == 0) == (x.equals(y))
4、方法
(1)int compareTo(T o):比较此对象与指定对象的顺序。如果该对象小于、等于、大于指定对象,则分别返回负整数、零、正整数
5、TreeSet集合用Comparable(自然排序)判断元素唯一的方式:根据比较方法compareTo()返回结果是否为0。为0,就是相同元素,不存储
/**
* 实现了Comparable接口的Person类
*/
class Person implements Comparable {
private String name;
private int age;
//...... 省略 构造方法 和 get、set方法
/**
* 覆盖 Comparable 接口的比较方法 compareTo()
* 下面方法的排序方式:按年龄从小到大排序,如果年龄相同,按名称的字典顺序从前到后
*
* 问题:如果要从大到小排序,如何修改?
* 逆序。即 改变 this 和 p 两个对象的位置(互换)
*
* @param obj
* @return
*/
@Override
public int compareTo(Object obj) {
//凡是引用数据类型强转之前,都要进行健壮性判断,否则会发生ClassCastException
if (!(obj instanceof Person)) {
throw new ClassCastException();
}
Person p = (Person) obj;
//age是int类型,可以相减
int temp = this.age - p.age;
//如果age相同,再比较name
//String类的默认比较方法compareTo(),按照元素的字典顺序排序 -- 字符串本身就实现了Comparable接口
return temp == 0 ? this.name.compareTo(p.name) : temp;
}
}
6、要保持compareTo()和equals()方法同步。实现了compareTo()方法,就有必要实现equals()方法,同时还需要确保两个方法同步。即 compareTo()和equals()两个方法的比较依据保持一致
List<Student> list = new ArrayList<>();
//Student(name, age)
list.add(new Student("chenssy1", 24));
list.add(new Student("chenssy2", 26));
//排序
Collections.sort(list);
Student student = new Student("chenssy2", 24);
//检索student在list中的位置
//indexOf()是基于equals()方法实现的
int index1 = list.indexOf(student);
//binarySearch()是基于compareTo()方法实现的
int index2 = Collections.binarySearch(list, student);
//如果compareTo()和equals()的比较依据不同,那么得到的结果很有可能会不同
//所以,compareTo()和equals()两者之间的比较依据需要保持一致
System.out.println("index1 = " + index1);
System.out.println("index2 = " + index2);
七、java.util.Comparator
1、interface Comparator<T>:强行对某个对象collection进行整体排序的比较函数。可以将Comparator传递给sort()方法(如Collections.sort()或Arrays.sort()),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序
2、当且仅当对于一组元素S中的每个e1和e2而言,c.compare(e1, e2) == 0与e1.equals(e2)具有相等的布尔值时,Comparator c 强行对S进行的排序才叫做与equals一致的排序
3、通常,让Comparator实现java.io.Serializable。因为它们在可序列化的数据结构(TreeSet、TreeMap)中可用作排序方法。为了成功地序列化数据结构,Comparator(如果已提供)必须实现Serializable ???
4、方法
(1)int compare(T o1, T o2):比较用来排序的两个参数。根据第一个参数小于、等于、大于第二个参数分别返回负整数、零、正整数
(2)boolean equals(Object obj):指示某个其他对象是否“等于”此Comparator。此方法必须遵守Object.equals(Object)的常规协定。此外,仅当指定的对象也是一个Comparator,并且强行实施与此Comparator相同的排序时,此方法才返回true(判断比较器是否相同)
注:此方法是抽象的,但不重写Object.equals(Object)方法总是安全的(默认继承Object,用覆写的equals()方法即可)。然而,在某些情况下,重写此方法可以允许程序确定两个不同的Comparator是否强行实施了相同的排序,从而提高性能
5、创建集合对象时就应该具备的功能,使用构造函数(对象是在添加的时候进行比较的,所以在添加元素之前容器就要具备比较性)
/**
* 定义一个类,实现Comparator
*/
class ComparatorByName implements Comparator {
/**
* 覆盖compare()方法。按Person的 name + age 排序
* @param obj1
* @param obj2
* @return
*/
@Override
public int compare(Object obj1, Object obj2) {
//......此处省略强转前的健壮性判断
Person p1 = (Person) obj1;
Person p2 = (Person) obj2;
//此处不能直接用 对象.属性 调用。因为属性私有,需要使用get()方法获取属性值
int temp = p1.getName().compareTo(p2.getName());
return temp == 0 ? p1.getAge() - p2.getAge() : temp;
}
}
public class TreeSetDemo{
public static void main(String[] args) {
//将Comparator接口的子类对象(ComparatorByName)作为参数传递给TreeSet的构造函数
TreeSet ts = new TreeSet(new ComparatorByName());
ts.add("xxx");
//......
}
}
八、哈希表
1、哈希表的数据结构:数组+单链表
2、哈希是一种算法,这种算法对数组进行了优化。哈希算法算出的值存储起来形成哈希表,哈希表中全是数组,该表的特点是:有对应关系
3、哈希算法:根据元素自身的特点,对元素进行运算,获取其在数组中的位置。哈希算法对查找进行了优化,性能稳定且高效(查找一个元素,先根据哈希算法算出该元素在数组中应该存储的位置,然后判断数组中该位置的元素是否是要查找的元素 -- 因为存入时也是按照哈希算法计算出来的位置存的,如果查找的元素存在,一定在计算出来的位置上)
4、每个对象都有自己的哈希值(即每个对象在内存中的位置),都是通过hashCode()计算出来的。hashCode()是用来计算对象哈希值的方法
5、哈希算法的存储过程
(1)根据哈希算法计算元素abc在数组中的位置a
(2)判断数组中位置a处是否有元素。如果没有,直接存储;如果有,进行步骤(3)
(3)判断位置a处的元素是否是abc。如果是,不存储(用新值覆盖旧值,并返回旧值);如果不是,用哈冲突的解决方式(将该元素保存在链头(最先保存的元素放在链尾))
6、哈希表如何确定元素是重复的
(1)判断哈希值(hashCode())。哈希值相同时,才会有第二次判断。如果哈希值不同,不需要判断equals()
(2)判断内容(equals())
7、哈希冲突:两个对象不同,但哈希值相同。解决方式
(1)顺延。即将数组延长
(2)串联。基于此位置继续计算,算出一个位置
8、哈希算法的好处和弊端:
(1)好处:提高查询效率
(2)弊端:不能重复
九、HashSet存储自定义对象
1、HashSet中存储的自定义对象,如果要保证存储的自定义对象唯一,或者要进行contains()、remove()等操作,最关键的是判断元素是否相同,要覆盖自定义对象的hashCode()和equals()方法,建立自己的判断相同的依据(最好同时覆盖自定义对象的toString()方法)
注:ArrayList要覆盖自定义对象的equals()方法,HashSet要覆盖自定义对象的hashCode()和equals()方法
十、判断元素唯一的方式
1、ArrayList:equals()
2、HashSet:hashCode() + equals()
3、TreeSet:compareTo() 或 compare()