数据结构 - Set 与 Map 接口介绍(TreeMap,HashMap,TreeSet,HashSet类)

本文介绍了Java集合框架中的Set与Map接口,特别是TreeSet和TreeMap的实现,它们基于红黑树。文章还探讨了简单的二叉搜索树的查找、插入和删除操作,并展示了HashSet和HashMap的哈希表结构及其解决哈希冲突的方法。此外,提供了代码示例来演示这些数据结构的使用。
摘要由CSDN通过智能技术生成

✨✨✨学习的道路很枯燥,希望我们能并肩走下来!

编程真是一件很奇妙的东西。你只是浅尝辄止,那么只会觉得枯燥乏味,像对待任务似的应付它。但你如果深入探索,就会发现其中的奇妙,了解许多所不知道的原理。知识的力量让你沉醉,甘愿深陷其中并发现宝藏。


前言

本篇是介绍数据结构Set与Map接口,了解TreeMap,HashMap,TreeSet,HashSet类;如有错误,请在评论区指正,让我们一起交流,共同进步!


本文开始

1. Set / Map接口

Set接口继承Collection,Map是独立的接口
进一步认识TreeSet和TreeMap就需要知道底层搜索树是如何查找,插入,删除的;
红黑树过于复杂,先认识一下简单的二叉搜索树,为以后学习做一个铺垫;

简单二叉搜索树特点:
左孩子 < 根节点,右孩子 > 根节点;

简单二叉搜索树查找代码:

//查找key是否存在: 二叉搜索树特点:左孩子小于根节点,右孩子大于根节点
    public TreeNode search(int key) {
        //重新定义访问的头节点
        TreeNode cur = root;
        //遍历二叉树,查找值
        while (cur != null) {
            if(cur.key == key) {
                return cur;
            }else if(cur.key < key) {
                //节点值大于查找值
                cur = cur.right;
            }else {
                //节点值小于查找值
                cur = cur.left;
            }
        }
        return null;//没找到
    }

简单二叉搜索树插入代码

    //定义根节点
    public TreeNode root;
    //插入一个元素:发现插入二叉搜索树的元素都会插入叶子节点
    public boolean insert(int key) {
   		 //空树直接插入
        if(root == null) {
            root = new TreeNode(key);
            return true;
        }
        TreeNode cur = root;
        TreeNode parent = null;
        //遍历, 并记录父节点位置;
        //真正找到插入位置,cur==null无法插入,就需要记录它的父节点
        while (cur != null) {
            if(cur.key < key) {
                //先记录父节点
                parent = cur;
                cur = cur.right;
            }else if(cur.key == key) {
                //插入一样的直接返回
                return false;
            }else {
                 //cur.key > key
                parent = cur;
                cur = cur.left;
            }
        }
        //找到位置,判断在父节点左还是右
        if(parent.key < key) {
            parent.right = new TreeNode(key);
        }else {
            parent.left = new TreeNode(key);
        }
        return true;
    }
 

简单二叉搜索树删除代码:
删除的7种情况:
删除节点左为空:是否为根节点;不为根节点,是父节点的左孩子还是右孩子(3种)
删除节点右为空:是否为根节点;不为根节点,是父节点的左孩子还是右孩子(3种)
删除节点左右都不为空:替换法,右树最小值与删除值替换或者左树最大值替换;最后删除替换的值;(1种)

 //删除key的值
    public boolean remove(int key) {
        TreeNode cur = root;
        TreeNode parent = null;//记录父节点,删除的情况复杂需要判断
        //先找到删除节点位置
        while (cur != null) {
            if(cur.key < key) {
                parent = cur;
                cur = cur.right;
            }else if(cur.key == key) {
                removeNode(parent,cur);
                return true;
            }else {
                parent = cur;
                cur = cur.left;
            }
        }
        return false;
    }
    //cur: 为删除节点位置,parent:为删除节点的父节点位置
    private void removeNode(TreeNode parent, TreeNode cur) {
        //判断删除节点左右是否为空
        if(cur.left == null) {
            //左为空,在判断的是否为根节点
            if(cur.key == root.key) {
                //删除节点为根节点,直接移动根节点即可
                root = cur.right;
            }else if(cur == parent.left) {
                //删除节点不为根,为父节点的左位置
                parent.left = cur.right;
            }else {
                //删除节点不为根,为父节点的右位置
                parent.right = cur.right;
            }
        }else if(cur.right == null) {
            //右为空,在判断的是否为根节点
            if(cur.key == root.key) {
                //删除节点为根节点,直接移动根节点即可
                root = cur.left;
            }else if(cur == parent.left) {
                //删除节点不为根,为父节点的左位置
                parent.left = cur.left;
            }else {
                //删除节点不为根,为父节点的右位置
                parent.right = cur.left;
            }
        }else {
            //删除节点左右孩子都不为空
            //使用替换法,记录该位置tarP,在其右子树找到最小值与删除值替换
            TreeNode tar = cur.right;
            TreeNode tarP = cur;
            //找右树最小值
            while (tar.left != null) {
                tarP = tar;
                tar = tar.left;
            }
            //替换
            cur.key = tar.key;
            //替换后删除tar位置
            if(tarP.left == tar) {
                tarP.left = tar.right;
            }else {
                //可能没有左子树位置
                tarP.right = tar.right;
            }
        }
    }

在这里插入图片描述

2. TreeSet类

TreeSet是Set接口的实现类,它的底层是一颗搜索树(红黑树),红黑树是一颗高度平衡的二叉搜索树;
时间复杂度:O(logn); - 这里是以2为底的
特点:只能存储key, 重复的值集合中不添加

Set 集合方法 代码测试:

public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>();
        //Set里增加 key - 且不重复添加
        set.add(1);
        set.add(2);
        set.add(1);
        set.add(3);
        System.out.println(set);
        //判断Set集合中是否存在key
        if(set.contains(2)) {
            System.out.println("存在");
        }
        //删除集合中的值 - key
        set.remove(1);
        System.out.println(set);
        //set集合是否为空
        System.out.println(set.isEmpty());
        //判断set集合大小
        System.out.println(set.size());
        //判断集合中的元素是否在set中全部存在
        List<Integer> list = new ArrayList<>();
        list.add(3);
        list.add(2);
        System.out.println(set.containsAll(list));
        //将集合添加到set去重
        List<Integer> list2 = new ArrayList<>();
        list2.add(4);
        list2.add(4);
        set.addAll(list2);
        System.out.println(set);
        //迭代器打印set集合
        Iterator<Integer> it = set.iterator();
        while (it.hasNext()) {
            System.out.print(it.next() + " ");
        }
        System.out.println();
    }

3. TreeMap 类

TreeMap : 底层也是搜索树 - 红黑树;
时间复杂度:O(logn)
特点:存储 key-val, key值唯一且不为空null,val值可替换;key-val可用null值
Map集合的常用方法:
【注】 map中的entrySet() =》每个元素类型 Entry<T,V> entry
entrySet获得所有键值对,可用getKey,getValue获取键或值;

public static void main(String[] args) {
        Map<String,Integer> map = new TreeMap<>();
        //Map集合中增加值
        map.put("a",1);
        map.put("ab",3);
        //map.put("ab",4);
        map.put("abc",2);
        map.put("abd",2);
        //Map集合根据key,获得val
        System.out.println(map.get("a"));
        //key不存在,返回默认值
        System.out.println(map.getOrDefault("c", 666));
        //map集合中的keySet(): 返回集合中所有不重复的key
        for (String s : map.keySet()) {
            System.out.print(s + " ");
        }
        System.out.println();
        //s
        // map.values将集合中的val返回 - val可重复
        for (int x : map.values()) {
            System.out.print(x + " ");
        }
        System.out.println();
        //entrySet() - 返回集合中的所有key-val
        for (Map.Entry<String,Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + "---" + entry.getValue());
        }
        //移除map集合中的值,返回被移除的值val
        System.out.println(map.remove("ab"));
        //判断是否包含key
        System.out.println(map.containsKey("abc"));
    }

4. HashSet 与 HashMap

4.1 HashSet / HashMap 底层哈希表

HashSet / HashMap: 底层是哈希表一种数组 + 链表 + 红黑树的结构
时间复杂度:O(1)
哈希表特点:数组中存储每个链表的头节点,根据源码,当链表长度超过8,数组长度超过64,链表会变成红黑树;
哈希表:在数组中插入元素,需要根据得到的哈希函数得到在数组中的存储地址存储元素,例如:存储元素 / 数组长度 = 数组中存储地址;

计算哈希地址,可能得到一样的地址,就会产生哈希冲突 <=>地址冲突(地址一样)

接下来看一下如何解决?

4.2 解决哈希冲突

方式一:闭散列(开地址法)
①线性探测:
存入数组的数字为3,33,根据 哈希函数(index = 插入的数字 % 数组长度) 得到的余数下标插入,假设长度为10,3插在3位置,33根据哈希函数得到的位置也是3位置,在3位置之后依次寻找没有插入的位置插入即可(3位置后面有地方就插入,没有就接着往后找);

在这里插入图片描述

②二次探测:
当根据哈希函数找到地址一样时(哈希冲突),找下一个地址根据Hi = (H0 + i^2) % len; (i = 1,2,3…), Hi => 下一个地址,H0 => 根据哈希函数计算得到的位置,len: 数组长度 ;

在这里插入图片描述

方式二:开散列(链地址法)
哈希表:一个数组,数组中每个元素存储单链表的头节点(每个元素都是链表);

代码实现简单的哈希桶:实现put(插入),get(获取)方法

public class HashBucket {
    //数组每个元素为链表,链表每个元素为节点 =》定义节点
    private static class Node {
        private int key;
        private int value;
        Node next;

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    private Node[]  array;
    private int size;   // 当前的数据个数
    private static final double LOAD_FACTOR = 0.75;//默认的负载因子,不能超过0.75
    private static final int DEFAULT_SIZE = 8;//默认桶的大小,扩容时需要

    public int put(int key, int value) {
        //获取插入的地址:根据哈希函数计算
        //除留余数法
        int index = key % array.length;
        //找到位置,遍历该位置链表,元素是否插入过
        Node cur = array[index];//重头遍历
        while (cur != null) {
            //判断元素是否一样
            if(cur.key == key) {
                //找到,就更新
                cur.value = value;
                return cur.value;
            }
            cur = cur.next;
        }
        //没找到一样的,头插元素
        Node node = new Node(key,value);
        node.next = array[index];
        array[index] = node;//移动头节点
        size++;//计数+1
        //判断是否需要扩容: 超过负载因子需要扩容
        if(loadFactor() > LOAD_FACTOR) {
            resize();
        }
        return cur.value;
    }


    private void resize() {
        //扩容时数组长度改变,会产生新的插入位置,所以原数组中所有元素需要重新插入
        //扩容
        Node[] newArr = new Node[2*array.length];
        //遍历原来数组
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];//每个链表的头节点
            //就地址更改进新数组
            while (cur != null) {
                //记录下一个节点位置
                Node curNext = cur.next;
                //获取插入新数组中的地址
                int index = cur.key % newArr.length;
                //重新头插
                cur.next = newArr[index];
                newArr[index] = cur;
                cur = curNext;
            }
        }
        array = newArr;//新数组给旧数组,扩容完毕
    }

    //计数当前负载因子:数组中元素个数 / 数组长度;
    private double loadFactor() {
        return size * 1.0 / array.length;
    }


    public HashBucket() {
        //初始化数组可以存储几个元素
        Node[] array = new Node[10];
    }

    public int get(int key) {
        //获取得到节点下标
        int index = key % array.length;
        Node cur = array[index];
        while (cur != null) {
            //判断是否得到节点
            if(cur.key == key) {
                //得到返回值
                return cur.value;
            }
            cur = cur.next;
        }
        return -1;//没找到
    }
}


总结

✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值