数据结构7:Map & Set 、Hash & BinarySearchTree

======================================================================================================

Map与Set的概念

  • Map与Set是一种专门用来搜索查找的容器或数据结构,其搜索的效率与其具体的实例化子类有关;
    在这里插入图片描述
  • 之前的使用的查找方法适合静态类型的查找,一般不会对元素插入或删除操作;
    • 1.直接遍历查找,时间复杂度为O(n),当元素很多时查找效率很低;
    • 2.二分查找,时间复杂度为O(lg(n)),前提是区间整体有序;
  • 现实中查找是动态查找,会伴随插入删除的操作。本文介绍的Map与Set就是适合动态查找的集合容器;
    • 比如在进行通讯录查找某人电话时,只需搜索关键字-姓名,即可找到值-电话;
    • 在进行学生管理系统管理时,查找某一门课的学生或者学生调课,在查找时涉及删除操作;
  • Map的设计就是基于 键值对(key-value) 模型:要搜索的数据-关键字(key),关键字对应的值(value);Map存储的是键值对(key-value) 数据;比如目录(章节-页码);梁山好汉的江湖绰号(每个好汉-江湖绰号);
  • Set的设计基于 纯关键字(key) 模型:比如实现快速查找某个人是否在群里,搜索姓名即可;Set存储的就是(key)关键字-姓名;

Map 的方法

  • Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复,后续继续插入相同的key会被覆盖。
  • Map.Entry<k,v>是Map内部用来存储键值对(key-value)映射关系的内部类;(该内部类中主
    要提供了<key, value>的获取,value的设置以及Key的比较方式)
    在这里插入图片描述
  • Map的常用方法:
    在这里插入图片描述
  • 注意:
    1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
    2. Map中存放键值对的Key是唯一的,value是可以重复的
    3. 在Map中插入键值对时,key不能为空,否则就会抛NullPointerException异常,但是value可以为空
    4. Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
    5. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)
    6. Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
    7. TreeMap和HashMap的区别:
      在这里插入图片描述
      // HashMap 允许key为null,TreeMap不允许key为null,但可以允许value为null;

Map的实例化使用——HashMap & TreeMap

// TreeMap的插入操作put(k,v), 因为key是唯一的且不为null 否则抛出空指针异常,所以后面再插入会覆盖之前的值,并且TreeMap的底层为红黑树,插入删除时会进行比较排序,内置的toString()方法按key的大小输出
在这里插入图片描述
// key的值不能为空,否则抛出空指针异常;在这里插入图片描述
// Map的遍历

  • 通过 keySet( ) 返回所有key
        Map<Integer,String> map=new TreeMap<>();
        map.put(1,"张三");
        map.put(1,"余一");
        map.put(3,"王五");
        map.put(2,"李四");
        for(Integer x:map.keySet()){
            System.out.print(x);
        }
  • 通过 values( ) 方法返回所有的值
        for(String s:map.values()){
            System.out.print(s);
        }
  • 通过Map.Entry<k,v> EntrySet( )方法返回所有键值对映射关系
        for(Map.Entry<Integer,String> entry : map.entrySet()){
            System.out.println(entry.getKey()+entry.getValue());
        }
  • 使用迭代器进行遍历
        Iterator<Integer> iterator=map.keySet().iterator();
        while(iterator.hasNext()){
            System.out.print(iterator.next());
        }
        System.out.println();
        Iterator<String> i=map.values().iterator();
        Iterator<Map.Entry<Integer,String>> o=map.entrySet().iterator();
        while(o.hasNext()){
            System.out.print(o.next());
        }

Set 的方法

  • Set是继承自Collection的接口类,Set中只存储了Key。
    在这里插入图片描述
  • 注意:
    1. Set是继承自Collection的一个接口类
    2. Set中只存储了key,并且要求key一定要唯一
    3. Set的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的。
    4. Set最大的功能就是对集合中的元素进行去重。
    5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。
    6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入。
    7. Set中不能插入null的key。
    8. TreeSet和HashSet的区别
      在这里插入图片描述

TreeSet & HashSet的使用

在这里插入图片描述
// HashSet 的key可以为null;TreeSet有序,当key为null时,抛出空指针异常;
在这里插入图片描述
在这里插入图片描述
// addAll(set)方法,可以复制一个set实例到另一个set集合,作用是去重;

    public static void main(String[] args) {
        Set<Integer> set=new TreeSet<>();
        set.add(4);
        set.add(2);
        set.add(0);
        //set.add(null);
        System.out.println(set);
        System.out.println(set.contains(3));
        System.out.println(set.size());
        System.out.println(set.isEmpty());
        set.remove(0);
        System.out.println(set);
        Set<Integer> s=new HashSet<>();
        s.add(9);
        s.add(4);
        s.add(2);
        set.addAll(s);//去重
        System.out.println(set);
        Object[] n= set.toArray();
        System.out.println(Arrays.toString(n));
    }

在这里插入图片描述

* 二叉搜索树–图解

  • 二叉搜索树又称二叉排序树,要么是一颗空树,要么是满足如下条件:
    • 左子树如果不为空,则左子树上的所有节点的值均小于根节点;本且左子树也是一颗二叉排序树;
    • 右子树如果不为空,则右子树上的所有节点的值均大于根节点;并且右子树也是一颗二叉搜索树;
  • 查找操作
    在这里插入图片描述
  • 插入操作
    • 如树本身为空,则将新节点作为树根节点即可;
    • 如果树不为空
      在这里插入图片描述
  • 删除操作
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

二叉搜索树的实现

class BinaryNode{
    public int key;
    public int value;
    public BinaryNode left;
    public BinaryNode right;

    public BinaryNode(int key, int value) {
        this.key = key;
        this.value = value;
    }
}
public class BinarySearchTreeMap {
    private BinaryNode root=null;

    //通过key获取value
    public Integer get(int key) {
        BinaryNode cur=root;
        while (cur != null) {
            if(cur.key==key){
                return cur.value;
            } else if (key < cur.key) {
                cur=cur.left;
            }else{
                cur=cur.right;
            }
        }
        return null;
    }

    //添加节点操作
    public void put(int key,int value){
        if(root==null){
            root = new BinaryNode(key, value);
        }
        BinaryNode cur=root;
        BinaryNode parent=null;
        while(cur!=null){
            if (key < cur.key) {
                parent=cur;
                cur = cur.left;
            } else if (key > cur.key) {
                parent=cur;
                cur=cur.right;
            }else{
                //key相同的时候,用新插入的value覆盖之前的值
                cur.value=value;
                return;
            }
        }
        BinaryNode newNode = new BinaryNode(key, value);
        if (parent.key > newNode.key) {
            parent.left=newNode;
        }else{
            parent.right=newNode;
        }
    }

    //删除操作
    public boolean remove(int key) {
        BinaryNode cur=root;
        BinaryNode parent=null;
        while (cur != null) {
            if (key < cur.key) {
                parent=cur;
                cur = cur.left;
            } else if (key > cur.key) {
                parent=cur;
                cur=cur.right;
            }else{
                //找到要删除节点,进行删除操作;
                removeNode(parent, cur);
                return true;
            }
        }
        return false;
    }

    private void removeNode(BinaryNode parent, BinaryNode cur) {
        if (cur.left == null) {
            if(cur==root){
                root=cur.right;
            } else if (parent.left == cur) {
                parent.left=cur.right;
            }else{
                parent.right=cur.right;
            }
        } else if (cur.right == null) {
            if (cur == root) {
                root=cur.left;
            } else if (parent.left == cur) {
                parent.left=cur.left;
            }else{
                parent.right=cur.left;
            }
        }else{//左右子树都不为空
            BinaryNode goat=cur.right; //在右子树中找到最小节点作为替罪羊节点,最后进行删除;
            BinaryNode goatParent=cur;
            while (goat.left != null) {
                goatParent=goat;
                goat = goat.left;
            }
            cur.key=goat.key;
            cur.value=goat.value;
            if(goatParent.left==goat){
                goatParent.left=goat.right;
            }else{
                goatParent.right=goat.right;
            }
        }
    }
}
  • 由于二叉搜索树的插入和删除操作都需要先查找遍历,查找效率代表搜索树各个操作的性能;
  • 最优情况下,二叉搜索树是完全二叉树,时间复杂度平均为O(lg(n));
  • 最坏情况下,二叉搜索树为一个单支树,时间复杂度为O(n);

* 哈希表

  • 为了插入删除更方便,最理想的查找搜索是直接从表中得到要搜索的值,进而操作;
  • 用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。时间复杂度O(1);
  • 可以构造一种存储结构,通过某种哈希函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素
  • 插入元素
    • 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放;
  • 搜索元素
    • 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功;
  • 哈希表:上述方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
    哈希函数一般设置为hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

常见哈希函数

  • 直接定制法(常用):取关键字的某个线性函数为计算散列地址函数(Hash(Key)= A*Key + B),优点(简单均匀),缺点要事先知道关键字的分布情况;适合查找比较小且连续的情况
  • 除留余数法(常用):设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转化为哈希地址;
  • 平方取中法:假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址,平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
  • 折叠法:将关键字从左到右分成等长的几部分,再将每一位叠加求和,再根据散列表表长,选取后几位作为散列地址;折叠法适合事先不知道关键字的分布,而关键字位数比较多的情况
  • 随机法:选择一个随机函数random,Hash(key) = random(key);适合关键字长度不等的情况
  • 数学分析法:关键字为等长的多位数,每一位是多种符号之一,每种符号出现机会相同,不同位出现不同符号的频率不同,或均匀或不均匀,根据散列表的长度选择各种符号分布均匀的若干位作为散列地址;比如电话号码作为关键字,选择后几位作为散列地址,而不选择前几位(区号可能相同,会发生冲突);适合事先知道关键字分布与关键字均匀分布情况、关键字位数比较大的情况

#主要概念

  • 1.哈希冲突/碰撞:不同关键字通过相同哈希哈数计算出相同的哈希地址。把具有不同关键码但具有相同哈希地址的数据元素称为“同义词”;
  • 2.哈希冲突的原因:哈希函数设置不合理。哈希表底层的数组容量是设置了初始值,往往实际使用时容量小于关键字的数量,所以冲突是不可避免的
  • 3.避免冲突的方法:主要可以通过设置不同的合适的哈希转换函数;
    • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间;
    • 哈希函数计算出来的地址能均匀分布在整个空间中;
    • 哈希函数应该比较简单;
  • 4.负载因子:散列表装满程度的标志因子 a=填入表中元素个数 / 散列表总长度;冲突率越大负载因子越大,但不是线性关系;通过调整哈希表的数组大小,降低负载因子,从而降低冲突率
  • 5.冲突的解决:开散列与闭散列
    • 闭散列也称开放定址法:如果发生哈希冲突时,哈希表未被装满,说明必然有空位置,可以将key放在冲突位置的下一个“空位置”去;方法一线性探测(插入时,从发生冲突的位置向后查找空位置插入);方法二二次探测(发生冲突时,通过H=(H+/-i^2)%m,即通过该公式计算下一个哈希地址);闭散列要确保插入时的负载因子不超过0.5,其缺点是空间利用率低;
    • 开散列/哈希桶: 开散列可以看作在大集合中的搜索转化为在小集合中的搜索;每个桶种储存的都是该位置发生哈希冲突的元素;
    • 在这里插入图片描述

哈希桶的实现

class NodeH {
    public int key;
    public NodeH next;

    public NodeH(int key) {
        this.key = key;
    }
}
public class MyHeapSet {
    private NodeH[] heapSet=new NodeH[10];
    private int size=0;
    private static final double LOAD_FACTOR=0.75;

    //计算负载因子
    public double loadFactor(){
        return (double)size/heapSet.length;
    }
    //插入操作
    public void add(int key){
        int index=key%heapSet.length;
        //判断是否已有该key
        for(NodeH cur = heapSet[index]; cur!=null; cur=cur.next){
            if(key==cur.key){
                return;
            }
        }
        NodeH newNode=new NodeH(key);
        newNode.next=heapSet[index];
        heapSet[index]=newNode;
        size++;

        if(loadFactor()>=LOAD_FACTOR){
            reallocate();
        }
    }

    //扩容操作
    public void reallocate(){
        NodeH[] newHeap=new NodeH[heapSet.length*2];
        for(int i=0;i<heapSet.length;i++){
            NodeH next=null;
            for(NodeH cur=heapSet[i];cur!=null;cur=next){
                next=cur.next;
                int newIndex=cur.key%newHeap.length;
                cur.next=newHeap[newIndex];
                newHeap[newIndex]=cur;
            }
        }
        heapSet=newHeap;
    }

    //删除操作
    public boolean remove(int key){
        int index=key%heapSet.length;
        if(heapSet[index]==null){
            return false;
        }
        if(key==heapSet[index].key){
            heapSet[index]=heapSet[index].next;
            return true;
        }
        for(NodeH cur=heapSet[index];cur!=null&&cur.next!=null;cur=cur.next){
            if(cur.next.key==key){
                cur.next=cur.next.next;
                return true;
            }
        }
        return false;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值