Java数据结构8(map和set)

📅 2026/7/15 20:05:22 👁️ 阅读次数 📝 编程学习
Java数据结构8(map和set)

目录

1,搜索树

2,哈希表

3,注意点

一,搜索树

1,前提引入

可以通过这张表看出,set和map分别都有通过红黑树或者哈希表来实现的,我们先从红黑树来讲起,红黑树的底层就是搜索树,我们先自己从自己创建的搜索树开始

2,概念

可以是一颗空树,或者具有这些性质:左边的都比根结点要小,右边都比根结点要大

3,方法的实现

(1)查询

//查找 public static TreeNode search(TreeNode root,int key){ if (root==null){ return null; } TreeNode cur=root; while (cur!=null){ if (key>cur.val){ cur=cur.right; }else if (cur.val==key){ return cur; }else { cur=cur.left; } } return null; }

(2)增加元素

//增加元素 public static void insert(TreeNode root,int key){ if (root==null){ root=new TreeNode(key); } TreeNode node=new TreeNode(key); TreeNode cur=root; TreeNode parent=null; while (cur!=null){ if (key>cur.val){ parent=cur; cur=cur.right; }else if (cur.val==key){ return; }else { parent=cur; cur=cur.left; } } if (parent.val>key){ parent.left=node; }else if (parent.val<key){ parent.right=node; } }

(3)删除元素

//删除 public static void remove(TreeNode root,int key){ if (root==null){ return; } TreeNode cur=root; TreeNode parent=null; //先要找到cur的位置在进行删除 while (cur!=null){ if (key> cur.val){ parent=cur; cur=cur.right; }else if (key==cur.val){ removecur(parent,cur); return; }else{ parent=cur; cur=cur.left; } } } private static void removecur(TreeNode parent, TreeNode cur) { //核心就是要让变的部分等于cur.right if (cur.left==null){ //cur为root if (cur==root){ root=cur.right; }else if (cur==parent.left){ parent.left=cur.right; }else if (cur==parent.right){ parent.right=cur.right; } }//核心就是要让变的部分等于cur.left else if (cur.right==null){ if (cur==root){ root=cur.left; }else if (cur==parent.left){ parent.left=cur.left; }else if (cur==parent.right){ parent.right=cur.left; } }//左右都不为空的情况 //我们选择替换->让右边的最左树替换掉cur else { TreeNode target=cur.right; TreeNode targetparent=cur; while (target.left!=null){ targetparent=parent; target=target.left; } cur.val=target.val; //也就是有左树 if (target==targetparent.left){ targetparent.left=target.right; }//没有左树,只有右树,那么target就是合适的替换人选 else { targetparent.right=target.right; } } }

4,模型的介绍

(1)K 模型(只存 Key,对应 Set)

容器里只存储关键字 Key,没有配套的 value,唯一作用:判断某个 key 是否存在、自动去重

(2)KV 模型(Key+Value 成对存储,对应 Map)

容器存储一对数据 <Key, Value>,key 唯一,value 是 key 附带的业务信息,通过 key 快速查找对应 value。

map的存储是kv模型,而set是纯k模型

5,treemap的方法实现

(1)常见的方法:

方法名解释
get(Object key)返回 key 对应的 value
getOrDefault(Object key, V defaultValue)返回 key 对应的 value,key 不存在,返回默认值
put(K key, V value)设置 key 对应的 value
remove(Object key)删除 key 对应的映射关系
keySet()返回所有 key 的不重复集合
values()返回所有 value 的可重复集合
entrySet()返回所有的 key-value 映射关系
containsKey(Object key)判断是否包含 key
containsValue(Object value)判断是否包含 value
Map.Entry<a,b>是map内部类用来实现存放kv键值对映射关系的内部类,下面的方法只有单个map.entry才能实现,用set来点是可以发现没有对应的方法的,因为我下面代码中的set已经表达的是一个map.entry的一个集合,里面装着左右的map.entry,所以我们要拿出一个的话可以使用循环或者迭代器来实现
方法解释
K getKey()返回 entry 中的 key
V getValue()返回 entry 中的 value
V setValue(V value)将键值对中的 value 替换为指定 value
public static void main(String[] args) { Map<String,Integer> map=new TreeMap<>(); map.put("ab",1); map.put("cd",2); map.put("ef",2); map.put("hello",5); Set<String> strings=map.keySet(); System.out.println(strings); Collection<Integer> collection=map.values(); System.out.println(collection); Set<Map.Entry<String,Integer>> set=map.entrySet(); for (Map.Entry<String, Integer> entry : set) { System.out.println("key:"+entry.getKey()+" "+"val:"+entry.getValue()); } //System.out.println(set); }

(2)注意点

1,Map是⼀个接⼝,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者 ,HashMap

2,Map中存放键值对的Key是唯⼀的,value是可以重复的

3,在TreeMap中插⼊键值对时,key不能为空,否则就会抛NullPointerException异常,value可以
为空。但是HashMap的key和value都可以为空。
4,Map中的Key可以全部分离出来,存储到Set中来进⾏访问(因为Key不能重复)。
5,Map中的value可以全部分离出来,存储在Collection的任何⼀个⼦集合中(value可能有重复)。
6,Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进⾏重新插⼊。

6,treeset的方法实现

(1)常见的方法:

方法解释
boolean add(E e)添加元素,重复元素不会添加成功
void clear()清空集合中所有元素
boolean contains(Object o)判断元素 o 是否存在于集合
Iterator<E> iterator()返回集合的迭代器,用于遍历元素
boolean remove(Object o)删除集合中的指定元素 o
int size()返回集合内元素的总个数
boolean isEmpty()判断集合是否为空,空返回 true,否则 false
Object[] toArray()将集合所有元素转为 Object 数组并返回
boolean containsAll(Collection<?> c)判断传入集合 c 的所有元素是否都在当前 Set 中,全部存在返回 true
boolean addAll(Collection<? extends E> c)将集合 c 全部元素添加进当前 Set,自动去重
public static void main(String[] args) { Set<Integer> set=new TreeSet<>(); set.add(2); set.remove(2); System.out.println(set.contains(2)); }

(2)注意点

1,key要唯一

2,本质也就是treemap,treemap里面的key就是唯一的,所以也就相当于是对应的,但是treemap里面不是还有一个val项,treeset默认传给一个object类

二,哈希表

1,概念

我们通过哈希函数来放数据,构造出来的结构称为哈希表

我们常用的函数为key/capacity

2,冲突-避免

面对上面说的冲突,我们有避免和解决两种方法,我们首先来讲避免

(1)改善哈希函数

面对不同的要求,我们可以定制不同的哈希函数,但是哈希函数一般我们都不会去改变他,我们都是用的常用的,也就是除留余数法:key/capacity

(2)调节负载因子

散列表的负载因子:填入表中的元素个数/散列表的长度

负载因子和冲突率的函数图:

可以发现负载因子越大,冲突率越大

3,冲突-解决

解决我们一般有两种方法:闭散列开散列

(1)闭散列

(1.1)线性探测

直接理解就是我们找到冲突后面的第一个空的元素填进去

这样会导致冲突的元素都聚集在一起,删除元素的时候会很麻烦,因为如果我们要删除4的时候,4下标的位置就空出来了,当我们要找14的时候,我们理应根据公式找到4下标,但是我们会发现4下标是没有的,所以这时候我们要删除的时候要标记一下那个下标是否已经被删除过了

(1.2)二次探测

这个方法比线性探测好一点,但是元素多了也会导致矛盾元素的聚集,也称为二次聚集,也不太好

(2)开散列(重点)

开散列是我们重点要掌握的方法,也俗称哈希桶

(3)自己实现的哈希桶

我们插入方法选择头插法

1,int类型
public class HushBuck { static class Node{ //成员 public int key; public int val;//默认给999 public Node next; //构造方法 public Node(int key, int val) { this.key = key; this.val = val; } } public static Node[] array; public int usedSize; public static final double LOAD_FACTOR = 0.75; public HushBuck(){ array=new Node[10]; } public void push(int key, int val){ //确定下标 int index=key%array.length; //更新val Node cur=array[index]; while (cur!=null){ if (cur.key==key){ cur.val=val; return; } cur=cur.next; } //头插法 Node newNode=new Node(key, val); newNode.next=array[index]; array[index]=newNode; usedSize++; //确认负载因子 if (usedSize*1.0/array.length>=LOAD_FACTOR){ //进行扩容并且重新哈希 resize(); } } private void resize(){ Node[] newArray=new Node[array.length*2]; for (int i = 0; i < array.length; i++) { Node cur=array[i]; while (cur!=null){ int newIndex=cur.key%newArray.length; Node curN=cur.next; cur.next=newArray[newIndex]; newArray[newIndex]=cur; cur=curN; } } array=newArray; } public int get(int key){ int index = key % array.length; Node cur = array[index]; while (cur != null) { if(cur.key == key) { return cur.val; } cur = cur.next; } return -1; } }
2,泛型
public class HushBuck2 <K,V>{ static class Node<K,V>{ //成员 public K key; public V val;//默认给999 public Node<K,V> next; //构造方法 public Node(K key, V val) { this.key = key; this.val = val; } } public Node<K,V>[] array; public int usedSize; public static final double LOAD_FACTOR = 0.75; public HushBuck2(){ array=(Node<K,V>[]) new Node[10]; } public void push(K key, V val){ //确定下标 int hash=key.hashCode(); int index=hash%array.length; //更新val Node<K,V> cur=array[index]; while (cur!=null){ if (cur.key.equals(key)){ cur.val=val; return; } cur=cur.next; } //头插法 Node<K,V> newNode=new Node<K,V>(key, val); newNode.next=array[index]; array[index]=newNode; usedSize++; //确认负载因子 if (usedSize*1.0/array.length>=LOAD_FACTOR){ //进行扩容并且重新哈希 resize(); } } private void resize(){ Node<K,V>[] newArray=new Node[array.length*2]; for (Node<K, V> kvNode : array) { Node<K, V> cur = kvNode; while (cur != null) { int newIndex = cur.key.hashCode() % newArray.length; Node<K, V> curN = cur.next; cur.next = newArray[newIndex]; newArray[newIndex] = cur; cur = curN; } } array=newArray; } public V get(K key){ int index = key.hashCode() % array.length; Node<K,V> cur = array[index]; while (cur != null) { if(cur.key.equals(key)) { return cur.val; } cur = cur.next; } return null; } }

三,注意点

1,hashcode 和 equals 的区别:

当我们想加自己写的类的时候,我们需要再类里面重写hashcode和equals方法,保证一样的key哈希出来的值是一样的

(1)equals负责判断内容相等

(2)hashCode负责哈希容器快速分区

(3)相等对象哈希值必须相同,哈希值相同对象不一定相等;

(4)重写 equals,一定要同步重写 hashCode,否则哈希集合失效。

2,系统里的哈希方法只有在你put的时候才会第一次定义数组的长度,才会分配内存,调用不带参数的构造方法,第一次的容量会是16,用带参数传容量的构造方法,会给到最接近你数字的二次幂,比如你给15,会给你16,你要20,会给你32

3,当你扩容的时候记得要二次哈希