数据结构之Map和Set总结
一、概念与模型1.概念:Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。以前常见的搜索方式有: ①直接遍历,时间复杂度为O(N),元素如果比较多效率会非常慢 ②二分查找,时间复杂度为O(logN),但搜索前必须要求序列是有序的 。上述排序比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的查找比如: ①根据姓名查询考试成绩 ②通讯录
目录
一、概念与模型
二、Map的使用
方法
|
解释
|
K getKey()
|
返回 entry 中的 key
|
V getValue()
|
返回 entry 中的 value
|
V setValue(V value)
|
将键值对中的 value 替换为指定 value
|
3.Map 的常用方法说明
方法
|
解释
|
V get(Object key)
|
返回 key 对应的 value
|
V getOrDefault(Object key, V defaultValue)
|
返回 key 对应的 value , key 不存在,返回默认值
|
V put(K key, V value)
|
设置 key 对应的 value
|
V remove(Object key)
|
删除 key 对应的映射关系
|
Set<K> keySet()
|
返回所有 key 的不重复集合
|
Collection<V> values()
|
返回所有 value 的可重复集合
|
Set<Map.Entry<K, V>> entrySet()
|
返回所有的 key-value 映射关系
|
boolean containsKey(Object key)
|
判断是否包含 key
|
boolean containsValue(Object value)
|
判断是否包含 value
|
Map 底层结构
|
TreeMap
|
HashMap
|
底层结构
|
红黑树 |
哈希桶
|
插入 / 删除 / 查找时间复杂度
|
O(logN) |
O(1)
|
是否有序
|
关于 Key 有序
|
无序
|
线程安全
|
不安全
|
不安全
|
插入 / 删除 / 查找区别
|
需要进行元素比较
|
通过哈希函数计算哈希地址
|
比较与覆写
|
key 必须能够比较,否则会抛出ClassCastException异常
|
自定义类型需要覆写 equals 和hashCode方法
|
应用场景
|
需要 Key 有序场景下
|
Key 是否有序不关心,需要更高的时间性能
|
三、Set的说明
1.一些常用的方法
方法
|
解释
|
boolean add(E e)
|
添加元素,但重复元素不会被添加成功
|
void clear()
|
清空集合
|
boolean contains(Object o)
|
判断 o 是否在集合中
|
Iterator<E> iterator()
|
返回迭代器
|
boolean remove(Object o)
|
删除集合中的 o
|
int size()
|
返回 set 中元素的个数
|
boolean isEmpty()
|
检测 set 是否为空,空返回 true ,否则返回 false
|
Object[] toArray()
|
将 set 中的元素转换为数组返回
|
boolean containsAll(Collection<?> c)
|
集合 c 中的元素是否在 set 中全部存在,是返回 true ,否则返回false
|
boolean addAll(Collection<? extends E> c)
|
将集合 c 中的元素添加到 set 中,可以达到去重的效果
|

Set 底层结构
|
TreeSet
|
HashSet
|
底层结构
|
红黑树
|
哈希桶
|
插入 / 删除 / 查找时间复杂度
|
O(logN) |
O(1)
|
是否有序
|
关于 Key 有序
|
不一定有序
|
线程安全
|
不安全
|
不安全
|
插入 / 删除 / 查找区别
|
按照红黑树的特性来进行插入和删除
|
①先计算 key 哈希地址② 然后进行插入和删除
|
比较与覆写
|
key 必须能够比较,否则会抛出ClassCastException异常
|
自定义类型需要覆写 equals 和hashCode方法
|
应用场景
|
需要 Key 有序场景下
|
Key 是否有序不关心,需要更高的时间性能
|
四、一些小练习
1.给定10W个数据,统计每个数据出现的次数
public class TestDemo {
public static Map<Integer,Integer> func(int[] arr){
Map<Integer,Integer> map = new HashMap<>();
for (int x:arr) {
if(map.get(x) == null){
map.put(x,1);
}else{
int val = map.get(x);
map.put(x,val+1);
}
}
return map;
}
public static void main(String[] args) {
int[] arr = new int[10_0000];
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(1000);
}
Map<Integer,Integer> map = func(arr);
System.out.println(map);
}
}
2.将10W个数据中的数据去重
把数据放到Set即可
public static Set<Integer> func2(int[] arr){
HashSet<Integer> set = new HashSet<>();
for (int x:arr) {
set.add(x);
}
return set;
}
3.从10W个数据中找到第一个重复的数据
public static int func3(int[] arr){
HashSet<Integer> set = new HashSet<>();
for (int x:arr) {
if(set.contains(x)){
return x;
}
set.add(x);
}
return -1;
}
五、搜索树
1.概念
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
2.查找操作
class Node{
public int val;
public Node left;
public Node right;
public Node(int val){
this.val = val;
}
}
public class BinarySearchTree {
Node root = null;
public Node search(int key){
Node cur = root;
while(cur != null){
if(cur.val < key){
cur = cur.right;
}else if(cur.val > key){
cur = cur.left;
}else{
return cur;
}
}
return null;//没有这个数据
}
}
3.插入操作
class Node{
public int val;
public Node left;
public Node right;
public Node(int val){
this.val = val;
}
}
public class BinarySearchTree {
Node root = null;
public boolean insert(int val){
if(root == null){
root = new Node(val);
return true;
}
Node cur = root;
Node parent = null;
while(cur != null){
if(cur.val < val){
parent = cur;
cur = cur.right;
}else if(cur.val == val){
return false;//不能有相同数据进行插入
}else{
parent = cur;
cur = cur.left;
}
}
Node node = new Node(val);
if(parent.val < val){
parent.right = node;
}else{
parent.left = node;
}
return true;
}
}
4.删除操作



class Node{
public int val;
public Node left;
public Node right;
public Node(int val){
this.val = val;
}
}
public class BinarySearchTree {
Node root = null;
public void remove(int key){
Node cur = root;
Node parent = null;
while(cur != null){
if(cur.val == key){
removeNode(cur,parent);
break;
}else if(cur.val < key){
parent = cur;
cur = cur.right;
}else{
parent = cur;
cur = cur.left;
}
}
}
public void removeNode(Node cur,Node parent){
if(cur.left == null){
if(cur == root){
root = cur.right;
}else if(cur == parent.left){
parent.left = cur.right;
}else{
parent.right = cur.right;
}
}else if(cur.right == null){
if(cur == root){
root = cur.left;
}else if(cur == parent.left){
parent.left = cur.left;
}else{
parent.right = cur.left;
}
}else{
Node targetParent = cur;
Node target = cur.right;
while(target.left != null){
targetParent = target;
target = target.left;
}
cur.val = target.val;
if(target == targetParent.left){
targetParent.left = target.right;
}else {
targetParent.right = target.right;
}
}
}
public void inOrder(Node root){
if(root == null){
return;
}
inOrder(root.left);
System.out.print(root.val + " ");
inOrder(root.right);
}
}
六、哈希表
1.概念
2.冲突
①哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
②哈希函数计算出来的地址能均匀分布在整个空间中
③哈希函数应该比较简单
常见的哈希函数:
①直接定制法(常用):取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况
我们来看一条题:
代码实现:
class Solution {
public int firstUniqChar(String s) {
if(s == null){
return -1;
}
int[] arr = new int[26];
for(int i = 0; i < s.length(); i++){
char ch = s.charAt(i);
arr[ch-97]++;
}
for(int i = 0; i < s.length(); i++){
char ch = s.charAt(i);
if(arr[ch-97] == 1){
return i;
}
}
return -1;
}
}


- 通过哈希函数获取待插入元素在哈希表中的位置
- 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
-
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4; 如果直接删除掉, 44 查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
-
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。 因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷
(ii)开散列

代码实现:
public class HashBuck {
static class Node{
public int key;
public int val;
public Node next;
public Node(int key, int val){
this.key = key;
this.val = val;
}
}
public Node[] arr;
public int usedSize;
public static final double DEFAULT_LOADFactor = 0.75;
public HashBuck(){
this.arr = new Node[10];
}
public void put(int key, int val){
//找到key所在的位置
int index = key % this.arr.length;
//遍历下标的链表,看看是不是有相同的key,如果有,要更新val
Node cur = arr[index];
while(cur != null){
if(cur.key == key){
cur.val = val;
return;
}
cur = cur.next;
}
//没有key这个元素,头插法插入
Node node = new Node(key,val);
node.next = arr[index];
arr[index] = node;
this.usedSize++;
//插入元素成功后,检查当前散列表的负载因子
if(loadFactor() > DEFAULT_LOADFactor){
resize();//扩容----->不是简单的把数组扩大,数组里面的每个链表的每个元素必须重新进行哈希
//14%10=4 14%20=14
}
}
private void resize(){
Node[] newArr = new Node[arr.length*2];
for(int i = 0; i < this.arr.length; i++){
Node cur = arr[i];
while(cur != null){
int index = cur.key % newArr.length;//获取新的下标
//把cur这个节点,以头插或者尾插的方式插入到新的数组对应下标的链表当中
Node curNext = cur.next;
cur.next = newArr[index];//先绑定前面
newArr[index] = cur;//再绑定后面
cur = curNext;
}
}
arr = newArr;
}
private double loadFactor(){
return 1.0 * usedSize / this.arr.length;
}
public int get(int key){
//找到key所在的位置
int index = key % this.arr.length;
//遍历下标的链表,看看是不是有相同的key,如果有,要更新val
Node cur = arr[index];
while(cur != null){
if(cur.key == key){
return cur.key;
}
cur = cur.next;
}
return -1;
}
}
import java.util.HashMap;
import java.util.Objects;
class Person{
public String ID;
public Person(String ID){
this.ID = ID;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(ID, person.ID);
}
@Override
public int hashCode() {
return Objects.hash(ID);
}
@Override
public String toString() {
return "Person{" +
"ID='" + ID + '\'' +
'}';
}
}
public class HashBuck2<K,V> {
static class Node<K,V>{
public K key;
public V val;
public Node<K,V> next;
public Node(K key, V val){
this.key = key;
this.val = val;
}
}
public Node<K,V>[] arr = (Node<K,V>[])new Node[10];
public int usedSize;
public void put(K key, V val){
int hash = key.hashCode();//hashcode()寻找位置
int index = hash % arr.length;
Node<K,V> cur = arr[index];
while(cur != null){
if(cur.key.equals(key)){//equals()负责查看有没有一样的元素
cur.val = val;
return;
}
cur = cur.next;
}
Node<K,V> node = new Node(key,val);
node.next = arr[index];
arr[index] = node;
this.usedSize++;
}
//hashcode()一样,equals()不一定一样;equals()一样,hashcode()一定一样
public V get(K key){
int hash = key.hashCode();//hashcode()寻找位置
int index = hash % arr.length;
Node<K,V> cur = arr[index];
while(cur != null){
if(cur.key.equals(key)){//equals()负责查看有没有一样的元素
return cur.val;
}
cur = cur.next;
}
return null;
}
public static void main(String[] args) {
Person person1 = new Person("123");
Person person2 = new Person("123");
HashBuck2<Person,String> hashBuck2 = new HashBuck2();
hashBuck2.put(person1,"abc");
System.out.println(hashBuck2.get(person2));
}
}
编译运行该代码,输出如下:
abc

(4)一个小题
求下面的查找成功的平均长度以及查找不成功的平均长度:

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐
所有评论(0)