Java笔记 - Set集合

Set集合是无序的,元素不可重复的。
Set接口中的方法和Collection一致。
Set集合有三个重要子类HashSet,LinkedHashSet和TreeSet

1.HashSet
HashSet类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变(Set底层的存储方式是由算法来完成的,所以说一定哪一天升级后算法就变化了,元素的存储位置也就改变了)。此类允许使用 null 元素。 是不同步的。

什么是哈希表?

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数

简单讲,哈希函数是一种算法,通过这种算法算出来很多元素的地址值,把这些值存储起来就叫做哈希表。
其实,哈希表里面存储的还是数组,如果一个数组想要查找元素,就要遍历数组,一个一个去比较。而哈希这种算法对数组进行了优化。它将要存储的元素代入到哈希函数中去,计算出一个位置,然后就把这个元素存储到这个位置上,如果想要查找元素,就再算一遍位置,然后直接到这个位置上去获取。

例如要存储字符串“ab”到数组中去,一般情况下就直接把“ab”放到0角标上就行了。但是这种方法在查询的时候需要遍历数组逐个比较,速度慢。所以就根据元素自身的特点,定义一个函数,把“ab”代入到这个函数中去,对元素进行计算,获取计算结果,这个结果就是“ab”在数组中的位置。这种方式的好处就是在查找“ab”在数组中的位置时,就不需要遍历了,直接用“ab”再算一遍位置,然后去找这个位置就行了。这个算法就是哈希算法。

每个对象都有自己的哈希值,因为每个对象都是Object类的子类,Object类中有方法int hashCode()返回该对象的哈希码值,这个方法就是用来算对象哈希值的方法。这个方法是由Windows实现的,我们不用管,但是我们自己的对象可以覆盖这个方法,建立对象自身的哈希值。

常用的哈希算法
1. 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a•key + b,其中a和b为常数(这种散列函数叫做自身函数)
2. 数字分析法
3. 平方取中法
4. 折叠法
5. 随机数法
6. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key % p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。

如果哈希算法是字符Unicode码求和再取余
function(element){
//自定义哈希算法
97+98=195
returen 195%10;//结果是5
}
那么“ab”的地址就是5,把“ab”放在数组角标为5的位置上,查找的时候直接用“ab”再进行运算,算出结果是5,直接去角标5的位置上查找元素,然后判断该元素是不是“ab”,如果不是,那么这个数组中就没有“ab”;如果已经把“ab”放在角标为5的位置上,还要再存一个“ab”这个时候会先计算ab的位置,然后判断是否相同,如果相同话就不再保存。所以这个算法提高了查询效率,但是缺点就是不能保存重复数据。

怎么判断两个元素的方式是否相同?判断哈希值是否相同是使用hashCode方法,判断内容是否相同是使用equals方法。

注意:如果hashCode不同,就不再判断equals了,因为肯定是两个不同的元素。

如果想要存储“ba”,首先计算哈希值,结果相同,然后判断内容,结果不相同,这种情况叫做哈希冲突

散列冲突的解决方案
1. 开方定址法
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突是,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p1为基础,产生另一个哈希地址p2……直到找出一个不冲突的哈希地址pi,将相应元素存入其中。
2. 再哈希法
这种方法是同时构造多个不同的哈希函数,当哈希地址冲突时,在用别的哈希函数进行计算,如果冲突再换哈希函数,直到算出一个不冲突的哈希地址。
3. 链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个成为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
4.建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

存储字符串对象

HashSet hs = new HashSet();

hs.add("ABC1");
hs.add("ABC2");
hs.add("ABC3");
hs.add("ABC4");
hs.add("ABC2");
hs.add("ABC3");
Iterator it = hs.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

输出结果:
ABC1
ABC4
ABC2
ABC3
Set集合中存储对象是无序的,并且相同的元素不会进行保存。
Set集合只有一种取出元素的方式,就是Iterator迭代器。

存储自定义对象
在开发中,我们使用更多的是自定义对象而不是字符串对象

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        super();
        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;
    }

}


public class HashSetTest{
    public static void main(String[] args){
        HashSet hs = new HashSet();

        hs.add(new Person("lisi4",24));
        hs.add(new Person("lisi7",27));
        hs.add(new Person("lisi1",21));
        hs.add(new Person("lisi9",29));
        hs.add(new Person("lisi7",27));

        Iterator it = hs.iterator();
        while(it.hasNext()){
            Person p = (Person)it.next();//自定义对象要做强转动作,add接受后Person提升为Object
            System.out.println(p.getName()+"...."+p.getAge());
        }
    }
}

输出结果:
lisi1….21
lisi7….27
lisi4….24
lisi7….27
lisi9….29

HashSet集合数据结构是哈希表,所以存储元素的时候,使用元素的hashCode方法来确定位置,如果位置相同,再通过元素的equlas方法来判断元素是否相同。

在该示例中,Person对象的哈希值是通过调用Object中的hashCode方法,并且判断内容是否相同也是用的Object中的equals方法。这5个Person对象的hashCode不相同,所以HashSet集合认为他们是5个不同的元素。所以我们需要建立自己的hashCode方法来计算元素的位置和equals方法来判断元素是否相同,所以需要在Person类中复写Object的hashCode和equals方法。

public class Person{
    //定义变量,构造函数,get、set方法
    public int hashCode(){
        return name.hashCode+age();//人类的特点就是姓名和年龄,所以根据姓名和年龄计算地址值
    }

    public boolean equals(Object obj){

        Person p = (Person)obj;
        return this.name.equals(p.name) && this.age==p.age;//这里调用的equals方法是字符串name中的equals方法,比较姓名是否相同
    }
}

输出结果:
lisi1….21
lisi9….29
lisi4….24
lisi7….27

在实际开发过程中一般要复写hashCode、equals、toString方法,Eclipse中提供了快捷书写hashCode和equals方法,toString方法的选项。

public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + age;
    result = prime * result + ((name == null) ? 0 : name.hashCode());//为了防止name.hashCode+age出现重复结果,就把age随便乘以一个数
    return result;
}

public boolean equals(Object obj) {
    if (this == obj)
        return true;
    /*
    if (!obj instanceof Person){
        throw new ClassCastException("类型错误");如果传入的类型不对,直接抛出类型错误
    }
    */  
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Person other = (Person) obj;
    if (age != other.age)
        return false;
    if (name == null) {
        if (other.name != null)
            return false;
    } else if (!name.equals(other.name))
        return false;
    return true;
}

public String toString(){//如果直接使用从Object中继承的toString方法,返回结果就是哈希值,没有意义,一般情况下我们需要根据对象的实际情况,返回对对象的描述,所以需要重写toString方法。
    return name+":"+age;
}

2.TreeSet
TreeSet是按照元素的字典顺序排序,这个顺序我们不称之为有序,有序是指的存入和取出的顺序。这里的顺序我们可以叫做指定的顺序。
TreeSet是不同步的。

存储字符串对象
比较简单,略过。

存储自定义对象

1. 自然排序(实现Comparable接口)
继续使用Person类,创建TreeSet集合如下:

public class TreeSetDemo{

    public static void main(String[] args){
        TreeSet ts = new TreeSet();

        ts.add(new Person("wangwu",21));
        ts.add(new Person("zhaoliu",26));
        ts.add(new Person("zhangsan",23));
        ts.add(new Person("sunqi",27));
        ts.add(new Person("lisi",21));

        Iterator it = ts.iterator();
        while (it.hasNext()) {
            Person p = (Person)it.next();
            System.out.println(System.out.println(p.getName()+"......"+p.getAge()););
        }
    }
}

运行发现报错Person cannot be cast to java.lang.Comparable
因为TreeSet是给元素排序用的,既然排序就要进行大小的比较,但是两个Person对象不能进行比较。

Comparable接口
此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法

Person类用来描述人这类事务,如果想要把人放在TreeSet中进行排序,那么Person应该在已经具备的基本功能外还要具备一个扩展功能,就是比较的功能。这个比较的功能已经被定义在了Comparable接口中,所以如果想要Person对象具备比较性,只要让Person类实现Comparable接口就行了。

修改Person类

public class Person implements Comparable{
    //hashCode,equals,toString,set,get,构造函数

    public int compareTo(Object obj){
        Person p = (Person)obj;

        int temp = this.age - p.age;
        return temp==0?this.name.compareTo(p.name):temp;//先按年龄排序,年龄相同再按姓名排序

        //int temp = this.name.compareTo(p.name);先按姓名排序,姓名相同再按年龄排序
        //return temp==0?this.age-p.age:temp;


    }
}

输出结果:
lisi……21
wangwu……21
zhangsan……23
zhaoliu……26
sunqi……27

在上面的例子中比较name时调用的compareTo方法是字符串中的compareTo方法,其实String类也实现了接口Comparable,字符串中的compareTo方法重写了Comparable接口中的compareTo方法,所以字符串本身具备自然排序。只要对象想要进行比较,就要实现Comparable接口,并重写compareTo方法。

这个Person类中默认的比较排序方式就是自然排序

2. 比较器排序(实现Comarator接口)
但是如果我们不想要按照类中默认的方式排序,或者这个类没有排序的功能怎么办?
首先不可以修改Person类,因为这个类有可能不是我们写的,我们只是拿过来用。所以我们可以使用TreeSet第二种排序方法,就是让集合自身具备比较功能。

在第一种方法中,TreeSet集合自身并不能直接对元素进行比较,是元素自身具有比较的功能,TreeSet只是按照元素比较的结果来确定元素在集合中的位置。所以如果元素自身不具备比较的功能,可以让集合对元素进行比较。

TreeSet构造方法:
TreeSet(Comparator< ? super E> comparator) :构造一个新的空 TreeSet,它根据指定比较器进行排序。插入到该 set 的所有元素都必须能够由指定比较器进行相互比较:对于 set 中的任意两个元素 e1 和 e2,执行 comparator.compare(e1, e2)
Comparator接口就是比较器。创建比较器就是实现Comparator接口,然后覆盖其中的compare方法,把比较器作为参数传给TreeSet构造函数,TreeSet集合就具备了比较功能。如果自然排序和比较器同时存在的时候,以比较器为主。

public class ComparatorByAge implements Comparator{
    public int compare(Object o1,Object o2){
        Person p1 = (Person)o1;
        Person p2 = (Person)o2;

        int temp = p1.getAge() - p2.getAge();
        return temp==0?p1.getName().compareTo(p2.getName()):temp;
    }
}

public class TreeSetDemo{
    public static void main(String[] args){
        TreeSet ts = new TreeSet(new ComparatorByAge);
        //.....
    }
}

输出结果:
lisi……21
wangwu……21
zhangsan……23
zhaoliu……26
sunqi……27

在实际开发过程中常用的是比较器。但是一般情况下只要Person要存到集合中,除了覆盖equals,hashCode,toString方法之外也还会实现Comparable接口。Java中的很多类都实现了Comparable接口,比如String类,Integer类,所以这些类都有比较的属性,也就是说String类,Integer类本身具备自然排序。

TreeSet排序的底层原理(二叉树)
二叉树
如果TreeSet中的Person对象按照年龄来排序,首先第一个元素28放在树的最顶层,然后第二个元素如果比28小就放在左边,如果比28大就放在右边…以此类推。当放25的时候,因为25比28小,所以就放在28的左边,不需要和29再进行比较,这样就提高了效率。
但即使如此,当元素数量很多的时候速度也会变慢。因为前面所有确定位置的元素都是按照元素从小到大的顺序排列的,是有序的,为了加快效率,就可以使用二分查找,在每一次放元素之前都会对已有的有序元素进行折半,再确定新元素的位置。

二叉树判断元素大小是看返回值的,如果返回1,就说明该元素比被比较的元素要大。如果返回-1,就说明该元素要小。
依据这原理,如果想要有序,按从小到大排列,就可以固定的返回1。

public class ComparatorByName implements Comparator {

    public int compare(Object o1, Object o2) {

        Person p1 = (Person)o1;
        Person p2 = (Person)o2;
        return 1;//有序。,返回-1就是倒叙
    }
}

3.LinkedHashSet
具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。此链接列表定义了迭代顺序,即按照将元素插入到 set 中的顺序(插入顺序)进行迭代。
是链表结构的,存第一个元素,通过hashCode计算出地址,存第二个元素的时候,第一个元素记住第二个元素的地址…以此类推。
LinkedHashSet也是不同步的。
有了LinkedHashSet类,Set集合和List集合最大的不同点就是元素是否唯一了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值