Java数据结构:从入门到精通(十二)

Map和Set

1. 搜索树

1.1 概念

⼆叉搜索树⼜称⼆叉排序树,它或者是⼀棵空树,或者是具有以下性质的⼆叉树:

  • 若它的左⼦树不为空,则左⼦树上所有节点的值都⼩于根节点的值
  • 若它的右⼦树不为空,则右⼦树上所有节点的值都⼤于根节点的值
  • 它的左右⼦树也分别为⼆叉搜索树

int[] array = {5, 3, 4, 1, 7, 8, 2, 6, 0, 9};

1.2 操作-查找

1.3 操作-插⼊

  1. 如果树为空树,即根 == null,直接插⼊
  1. 如果树不是空树,按照查找逻辑确定插⼊位置,插⼊新结点

1.4 操作-删除(难点)

设待删除节点为 cur,其父节点为 parent

  1. cur.left == null 时:

    • cur 是根节点,则 root = cur.right
    • cur 不是根节点:
      • curparent.left,则 parent.left = cur.right
      • curparent.right,则 parent.right = cur.right
  2. cur.right == null 时:

    • cur 是根节点,则 root = cur.left
    • cur 不是根节点:
      • curparent.left,则 parent.left = cur.left
      • curparent.right,则 parent.right = cur.left
  3. cur.left != nullcur.right != null 时:

    • 采用替换法删除:在右子树中寻找中序遍历的第一个节点(即最小关键码节点),将其值复制到被删除节点,然后处理该节点的删除问题

1.5 实现

java 复制代码
package com.demo1;

public class BinarySearchTree {
    public static class Node {
        int key;
        Node left;
        Node right;
        public Node(int key) {
            this.key = key;
        }
    }
    private Node root = null;
    /**
     * 在搜索树中查找 key,如果找到,返回 key 所在的结点,否则返回 null
     * @param key
     * @return
     */
    public Node search(int key) {
        Node cur = root;
        while (cur != null) {
            if (key == cur.key) {
                return cur;
            } else if (key < cur.key) {
                cur = cur.left;
            } else {
                cur = cur.right;
            }
        }
        return null;
    }
    /**
     * 插⼊
     * @param key
     * @return true 表⽰插⼊成功, false 表⽰插⼊失败
     */
    public boolean insert(int key) {
        if (root == null) {
            root = new Node(key);
            return true;
        }
        Node cur = root;
        Node parent = null;
        while (cur != null) {
            if (key == cur.key) {
                return false;
            } else if (key < cur.key) {
                parent = cur;
                cur = cur.left;
            } else {
                parent = cur;
                cur = cur.right;
            }
        }
        Node node = new Node(key);
        if (key < parent.key) {
            parent.left = node;
        } else {
            parent.right = node;
        }
        return true;
    }
    /**
     * 删除成功返回 true,失败返回 false
     * @param key
     * @return
     */
    public boolean remove(int key) {
        Node cur = root;
        Node parent = null;

        // 查找要删除的节点
        while (cur != null) {
            if (key == cur.key) {
                break;
            } else if (key < cur.key) {
                parent = cur;
                cur = cur.left;
            } else {
                parent = cur;
                cur = cur.right;
            }
        }

        // 该元素不在二叉搜索树中
        if (null == cur) {
            return false;
        }
    
    /*
    根据cur的孩子是否存在分四种情况
    1. cur左右孩子均不存在
    2. cur只有左孩子
    3. cur只有右孩子
    4. cur左右孩子均存在
    
    情况1可以与情况2或者3进行合并处理
    除了情况4之外,其他情况可以直接删除
    情况4需要在其子树中找一个替代节点进行删除
    */

        // 情况1:只有右孩子 或 没有孩子(右孩子可能为null)
        if (cur.left == null) {
            if (cur == root) {
                root = cur.right;
            } else {
                if (cur == parent.left) {
                    parent.left = cur.right;
                } else {
                    parent.right = cur.right;
                }
            }
            return true;
        }
        // 情况2:只有左孩子
        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;
                }
            }
            return true;
        }
        // 情况3:左右孩子都有(最复杂的情况)
        else {
            // 找到右子树中的最小节点(或左子树中的最大节点)
            Node targetParent = cur;
            Node target = cur.right;

            // 找到右子树中的最左节点
            while (target.left != null) {
                targetParent = target;
                target = target.left;
            }

            // 将目标节点的值赋给当前节点
            cur.key = target.key;

            // 删除目标节点
            // 注意:目标节点可能有右子树,但没有左子树(因为它是右子树中的最左节点)
            if (target == targetParent.left) {
                targetParent.left = target.right;
            } else {
                targetParent.right = target.right;
            }
            return true;
        }
    }
}

1.6 性能分析

二叉搜索树的插入和删除操作都需要先进行查找,因此查找效率直接影响整体性能表现。

对于包含n个节点的二叉搜索树,假设每个节点的查找概率相同,其平均查找长度取决于节点在树中的深度------节点越深,所需的比较次数就越多。

需要注意的是,即使针对相同的关键码集合,如果插入顺序不同,最终可能形成结构完全不同的二叉搜索树。

最佳情况下,二叉搜索树呈现为完全二叉树,其平均比较次数为:O(logN) 最坏情况下,二叉搜索树退化为单支树,其平均比较次数为:O(N)

问题:当二叉搜索树退化为单支树时,其性能优势将不复存在。那么是否存在改进方法,使得无论按照何种顺序插入关键码,都能保持二叉搜索树的最佳性能?

1.7版本与Java集合框架的关系

TreeMap和TreeSet是Java中基于搜索树实现的Map和Set集合。它们底层实际采用的是红黑树结构,这是一种近似平衡的二叉搜索树。红黑树在二叉搜索树的基础上增加了颜色属性,并通过特定规则来维持平衡。关于红黑树的详细实现原理,我们将在后续章节深入讲解。

2. 搜索

2.1 概念及场景

Map和Set是专门用于高效搜索的数据结构,其性能取决于具体实现的子类。

传统的搜索方法主要有:

  1. 线性遍历:时间复杂度为O(N),当数据量较大时效率较低
  2. 二分查找:时间复杂度为O(logN),但要求数据必须预先排序

这些方法更适合静态查找场景,即数据集合基本不变的情况。然而实际应用中常常需要动态查找,例如:

  1. 根据姓名查询考试成绩
  2. 通过姓名查找联系方式
  3. 检查元素是否存在于不重复集合中

这些场景往往需要在查找过程中进行插入和删除操作,使得传统方法不再适用。Map和Set正是为这类动态查找需求而设计的集合容器。

2.2 模型

在数据搜索中,我们通常将查询数据称为关键字(Key),与之对应的结果称为值(Value),这种对应关系称为Key-value键值对。基于此,数据模型主要分为两种类型:

  1. 纯Key模型:

    • 用于快速查询某个元素是否存在
    • 典型应用场景:
      • 英文词典查询单词是否存在
      • 通讯录中查找联系人姓名是否存在
  2. Key-Value模型:

    • 用于建立元素间的映射关系
    • 典型应用场景:
      • 统计文件中单词出现次数:<单词,出现次数>
      • 梁山好汉及其江湖绰号的对应关系
  3. 数据结构差异:

    • Map存储完整的键值对(Key-Value)
    • Set仅存储Key信息

3. Map 的使⽤

3.1 关于Map的说明

Map是⼀个接⼝类,该类没有继承⾃Collection,该类中存储的是<K,V>结构的键值对,并且K⼀定是唯⼀的,不能重复。

3.2 关于Map.Entry<K, V>的说明

Map.Entry<K,V> 是 Map 接口中用于存储键值对映射关系的内部类。该内部类主要提供了获取键值对、设置值以及比较键的方法。

方法 解释
K getKey() 返回 entry 中的 key
V getValue() 返回 entry 中的 value
V setValue(V value) 将键值对中的 value 替换为指定 value

注意:Map.Entry<K,V>并没有提供设置Key的⽅法

3.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

注意:

  1. Map是一个接口,不能直接实例化,只能通过其实现类(如TreeMap或HashMap)来创建对象。
  2. Map中的键(Key)具有唯一性,而值(Value)允许重复。
  3. TreeMap不允许键为空(否则会抛出NullPointerException),但值可为空;HashMap则允许键和值均为空。
  4. 由于键具有唯一性,可以将Map中的所有键提取出来存储到Set集合中进行访问。
  5. Map中的值可以全部提取出来,存储到Collection的任何子集合中(因为值允许重复)。
  6. 不能直接修改Map中的键,如需修改必须先删除原键值对再重新插入;而值可以直接修改
  7. TreeMap和HashMap的区别
Map底层结构 TreeMap HashMap
底层结构 红黑树 哈希桶
插入/删除/查找时间复杂度 O(log n) O(1)
是否有序 关于Key大小有序(按照key的自然顺序或Comparator排序) 插入顺序无序(不保证顺序)
线程安全 不安全 不安全
插入/删除/查找区别 需要进行元素比较 通过哈希函数计算哈希地址
比较与覆写 key必须能够比较,否则会抛出ClassCastException异常 自定义类型需要覆写equals和hashCode方法
应用场景 需要Key有序场景下 Key是否有序不关心,需要更高的时间性能

3.4 TreeMap的使⽤案例

java 复制代码
import java.util.TreeMap;
import java.util.Map;
public static void TestMap(){
        Map<String, String> m = new TreeMap<>();
// put(key, value):插⼊key-value的键值对
// 如果key不存在,会将key-value的键值对插⼊到map中,返回null
        m.put("林冲", "豹⼦头");
        m.put("鲁智深", "花和尚");
        m.put("武松", "⾏者");
        m.put("宋江", "及时⾬");
        String str = m.put("李逵", "⿊旋⻛");
        System.out.println(m.size());
        System.out.println(m);
// put(key,value): 注意key不能为空,但是value可以为空
// key如果为空,会抛出空指针异常
//m.put(null, "花名");
        str = m.put("⽆名", null);
        System.out.println(m.size());
// put(key, value):
// 如果key存在,会使⽤value替换原来key所对应的value,返回旧value
        str = m.put("李逵", "铁⽜");
// get(key): 返回key所对应的value
// 如果key存在,返回key所对应的value
// 如果key不存在,返回null
        System.out.println(m.get("鲁智深"));
        System.out.println(m.get("史进"));
//GetOrDefault(): 如果key存在,返回与key所对应的value,如果key不存在,返回⼀个默认
        值
        System.out.println(m.getOrDefault("李逵", "铁⽜"));
        System.out.println(m.getOrDefault("史进", "九纹⻰"));
        System.out.println(m.size());
//containKey(key):检测key是否包含在Map中,时间复杂度:O(logN)
// 按照红⿊树的性质来进⾏查找
// 找到返回true,否则返回false
        System.out.println(m.containsKey("林冲"));
        System.out.println(m.containsKey("史进"));
// containValue(value): 检测value是否包含在Map中,时间复杂度: O(N)
// 找到返回true,否则返回false
        System.out.println(m.containsValue("豹⼦头"));
        System.out.println(m.containsValue("九纹⻰"));
// 打印所有的key
// keySet是将map中的key防⽌在Set中返回的
        for(String s : m.keySet()){
        System.out.print(s + " ");
        }
        System.out.println();
// 打印所有的value
// values()是将map中的value放在collect的⼀个集合中返回的
        for(String s : m.values()){
        System.out.print(s + " ");
        }
        System.out.println();
// 打印所有的键值对
// entrySet(): 将Map中的键值对放在Set中返回了
        for(Map.Entry<String, String> entry : m.entrySet()){
        System.out.println(entry.getKey() + "--->" + entry.getValue());
        }
        System.out.println();
        }

4. Set 的说明

Set 的官⽅⽂档

Set与Map主要的不同有两点:Set是继承⾃Collection的接⼝类,Set中只存储了Key。

4.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 中,可以达到去重的效果

注意:

  1. Set是继承自Collection接口的子接口
  2. Set仅存储key,且要求每个key必须唯一
  3. TreeSet底层通过Map实现,采用key与一个默认Object对象作为键值对插入Map中
  4. Set的核心功能是对集合元素进行去重
  5. 常用Set实现类包括TreeSet、HashSet和LinkedHashSet,其中LinkedHashSet在HashSet基础上通过维护双向链表来记录元素插入顺序
  6. Set中的Key不可直接修改,需先删除原key再重新插入
  7. TreeSet不允许插入null key,而HashSet允许
  8. TreeSet与HashSet的主要区别
Set底层结构 TreeSet HashSet
底层结构 红黑树 哈希桶
插入/删除/查找时间复杂度 O(log n) O(1)
是否有序 关于Key有序 不一定有序(不保证顺序)
线程安全 不安全 不安全
插入/删除/查找区别 按照红黑树的特性来进行插入和删除 1. 先计算key哈希地址 2. 然后进行插入
比较与覆写 key必须能够比较,否则会抛出ClassCastException异常 自定义类型需要覆写equals和hashCode方法
应用场景 需要Key有序场景下 Key是否有序不关心,需要更高的时间性能

4.2 TreeSet的使⽤案例

java 复制代码
import java.util.TreeSet;
import java.util.Iterator;
import java.util.Set;
public static void TestSet(){
        Set<String> s = new TreeSet<>();
// add(key): 如果key不存在,则插⼊,返回ture
// 如果key存在,返回false
        boolean isIn = s.add("apple");
        s.add("orange");
        s.add("peach");
        s.add("banana");
        System.out.println(s.size());
        System.out.println(s);
        isIn = s.add("apple");
// add(key): key如果是空,抛出空指针异常
//s.add(null);
// contains(key): 如果key存在,返回true,否则返回false
        System.out.println(s.contains("apple"));
        System.out.println(s.contains("watermelen"));
// remove(key): key存在,删除成功返回true
// key不存在,删除失败返回false
// key为空,抛出空指针异常
        s.remove("apple");
        System.out.println(s);
        s.remove("watermelen");
        System.out.println(s);
        Iterator<String> it = s.iterator();
        while(it.hasNext()){
        System.out.print(it.next() + " ");
        }
        System.out.println();
        }

5. 哈希表

5.1 概念

在顺序结构和平衡树中,元素的关键码与其存储位置之间不存在直接对应关系,因此查找元素时必须进行多次关键码比较。顺序查找的时间复杂度为O(N),平衡树的查找时间取决于树的高度,即O(logN),搜索效率主要由比较次数决定。

理想的搜索方法应该能够直接定位目标元素,无需比较。为此可以构建一种特殊的数据结构:通过哈希函数(hashFunc)建立元素关键码与存储位置的一一映射关系,实现快速查找。

操作原理:

  • 插入元素: 根据关键码计算存储位置(通过哈希函数),并将元素存入该位置。
  • 搜索元素: 使用相同哈希函数计算关键码对应的存储位置,直接获取元素并进行关键码比对,若匹配则查找成功。

这种方法称为哈希(散列)法,其中使用的转换函数称为哈希函数,构建的结构称为哈希表(Hash Table)或散列表。

示例: 数据集合{1,7,6,4,5,9} 哈希函数设为:hash(key) = key % capacity(capacity表示底层存储空间总大小)

⽤该⽅法进⾏搜索不必进⾏多次关键码的⽐较,因此搜索的速度⽐较快问题:按照上述哈希⽅式,向集合中插⼊元素44,会出现什么问题?

5.2 冲突-概念

对于两个不同的数据元素 Ki 和 Kj(i≠j),虽然 Ki≠Kj,但存在 Hash(Ki)=Hash(Kj)的情况。这种现象称为哈希冲突或哈希碰撞。我们将具有不同关键码却拥有相同哈希地址的数据元素称为"同义词"。

5.3 冲突-避免

⾸先,我们需要明确⼀点,由于我们哈希表底层数组的容量往往是⼩于实际要存储的关键字的数量

的,这就导致⼀个问题,冲突的发⽣是必然的,但我们能做的应该是尽量的降低冲突率。

5.4 冲突-避免-哈希函数设计

导致哈希冲突的一个常见原因是哈希函数设计不合理。一个良好的哈希函数应满足以下设计原则:

  1. 定义域覆盖性:哈希函数必须覆盖所有可能的关键码,当散列表容量为m时,其输出值域应严格限定在0到m-1范围内;
  2. 均匀分布性:哈希计算结果应在整个地址空间中均匀分布;
  3. 计算高效性:哈希函数应保持计算简单高效。
5.4.1 常⻅哈希函数

常用哈希方法

1. 直接定制法(常用)

原理 :使用关键字的线性函数作为散列地址:Hash(Key) = A*Key + B
优点 :实现简单、分布均匀
缺点 :需预先了解关键字的分布情况
适用场景 :适合处理较小且连续的关键字集合
面试题示例:查找字符串中第一个只出现一次的字符

2. 除留余数法(常用)

原理 :设散列表地址数为m,选取不大于m的最大质数p作为除数,哈希函数为:Hash(key) = key % p (p≤m)
特点:通过取模运算将关键码转换为哈希地址

3. 平方取中法(了解)

原理 :对关键字平方后取中间几位作为哈希地址
示例

  • 关键字1234 → 平方1522756 → 取中间227
  • 关键字4321 → 平方18671041 → 取中间671或710
    适用场景:关键字分布未知且位数较少时
4. 折叠法(了解)

原理

  1. 将关键字分割为等长部分(最后部分可稍短)
  2. 各部分相加求和
  3. 取和值的后几位作为散列地址
    适用场景:关键字位数较多且分布未知时
5. 随机数法(了解)

原理 :H(key) = random(key),使用随机函数生成哈希地址
适用场景:特别适合处理长度不一的关键字

6. 数学分析法(了解)

原理 :分析关键字的数字分布特征,选取分布均匀的若干位作为散列地址
示例:对于n个d位数,选择符号分布均匀的位作为哈希地址

在设计公司员工登记表的存储系统时,若采用手机号作为关键字,其前7位往往相同。这种情况下,建议选取手机号后四位作为散列地址。若仍存在冲突风险,可采用以下优化方法:

  1. 数字反转(如1234改为4321)
  2. 右循环位移(如1234改为4123)
  3. 左循环位移
  4. 前后数字叠加(如1234改为12+34=46)

数字分析法特别适用于处理位数较多的关键字,尤其当关键字的某些位分布均匀且其分布规律已知时。

需要注意的是:虽然精心设计的哈希函数能显著降低冲突概率,但完全避免哈希冲突是不可能的。

5.5 冲突-避免-负载因⼦调节(重点掌握)

负载因⼦和冲突率的关系粗略演⽰

所以当冲突率达到⼀个⽆法忍受的程度时,我们需要通过降低负载因⼦来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的⼤⼩。

5.6 冲突-解决

解决哈希冲突 两种常⻅的⽅法是:闭散列开散列

5.7 冲突-解决-闭散列

闭散列:也叫开放地址法,当发⽣哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的"下⼀个"空位置中去。那如何寻找下⼀个空位置呢?

5.7.1 线性探测

⽐如上⾯的场景,现在需要插⼊元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发⽣哈希冲突。

线性探测:从发⽣冲突的位置开始,依次向后探测,直到寻找到下⼀个空位置为⽌。

• 插入操作

◦ 首先计算待插入元素的哈希值,确定其在哈希表中的目标位置

◦ 若目标位置为空,则直接插入新元素 ◦ 若目标位置已被占用(发生哈希冲突),则采用线性 探测法依次查找后续位置,直至找到空闲位置完成插入

在采用闭散列方法处理哈希冲突时,不能直接物理删除哈希表中的已有元素,因为这种操作会影响其他元素的查找效率。例如,若直接删除元素4,后续查找元素44时可能会出现问题。为此,线性探测法采用标记伪删除的方式来处理元素的删除操作。

5.7.2 ⼆次探测

线性探测的主要缺点是容易导致冲突数据聚集,这是因为其查找下一个空位的方式是简单地逐个向后遍历。为解决这一问题,二次探测采用更智能的定位方法:下一个候选位置的计算公式为: h_i = (h_0 + i^2) % m 或 h_i = (h_0 - i^2) % m 其中i=1,2,3...,h_0是通过散列函数Hash(x)计算得到的初始位置,m表示哈希表的大小。以2.1节中的插入44为例,当发生冲突时,使用二次探测后的处理情况为:

5.7.3 总结
  1. 优点:
  • 不需要额外的数据结构(如链表),空间利⽤率⾼。
  • 实现相对简单。
  • 缓存性能好,因为数据都存储在连续的内存空间中。
  • 不需要存储指针,节省空间。
  1. 缺点:
  1. 容易产⽣聚集现象(尤其是线性探测)。
  2. 删除操作复杂(需要特殊处理以保持查找链的完整性)。
  3. 装载因⼦不能太⼤,否则性能会急剧下降。
  4. ⼆次聚集(对于⼆次探测)。

5.8 冲突-解决-开散列/哈希桶(重点掌握)

开散列法(又称链地址法或开链法)的工作原理如下:首先通过散列函数计算关键码的存储地址,将具有相同地址的关键码归入同一子集合,每个子集合称为一个桶。各桶中的元素通过单链表连接,链表的头节点则存储在哈希表中。

从上图可以看出,开散列中每个桶中放的都是发⽣哈希冲突的元素。
开散列,可以认为是把⼀个在⼤集合中的搜索问题转化为在⼩集合中做搜索了。

5.9 冲突严重时的解决办法

刚才我们提到了,哈希桶其实可以看作将⼤集合的搜索问题转化为⼩集合的搜索问题了,那如果冲突

严重,就意味着⼩集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的⼩集合搜索问

题继续进⾏转化,例如:

  1. 每个桶的背后是⼀棵搜索树

  2. 每个桶的背后是另⼀个哈希表

  3. 哈希函数重新设计

  4. .....

5.10 实现

java 复制代码
// key-value 模型
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;
    public int put(int key, int value) {
        int index = key % array.length;
// 在链表中查找 key 所在的结点
// 如果找到了,更新
// 所有结点都不是 key,插⼊⼀个新的结点
        for (Node cur = array[index]; cur != null; cur = cur.next) {
            if (key == cur.key) {
                int oldValue = cur.value;
                cur.value = value;
                return oldValue;
            }
        }
        Node node = new Node(key, value);
        node.next = array[index];
        array[index] = node;
        size++;
        if (loadFactor() >= LOAD_FACTOR) {
            resize();
        }
        return -1;
    }
    private void resize() {
        Node[] newArray = new Node[array.length * 2];
        for (int i = 0; i < array.length; i++) {
            Node next;
            for (Node cur = array[i]; cur != null; cur = next) {
                next = cur.next;
                int index = cur.key % newArray.length;
                cur.next = newArray[index];
                newArray[index] = cur;
            }
        }
        array = newArray;
    }
    private double loadFactor() {
        return size * 1.0 / array.length;
    }
    public HashBucket() {
        array = new Node[8];
        size = 0;
    }
    public int get(int key) {
        int index = key % array.length;
        Node head = array[index];
        for (Node cur = head; cur != null; cur = cur.next) {
            if (key == cur.key) {
                return cur.value;
            }
        }
        return -1;
    }
}

5.11 性能分析

尽管哈希表始终面临冲突问题,但在实际应用中,其冲突率通常较低且冲突数量可控。这意味着每个哈希桶中的链表长度保持在一个常数范围内。因此,在一般情况下,我们认为哈希表的插入、删除和查找操作的时间复杂度均为O(1)

5.12 和java类集的关系

  1. HashMap 和 HashSet 是 Java 中基于哈希表实现的 Map 和 Set 集合

  2. Java 采用哈希桶(链地址法)来解决哈希冲突问题

  3. 当冲突链表的长度超过特定阈值时,Java 会自动将其转换为红黑树结构以提高查询效率

  4. 在 Java 中:

    • 哈希值的计算是通过调用对象的 hashCode() 方法实现的
    • 键的相等性比较则是通过 equals() 方法进行的

    因此,若要将自定义类作为 HashMap 的键或 HashSet 的元素,必须:

    • 重写 hashCode() 和 equals() 方法
    • 确保 equals() 返回 true 的两个对象,其 hashCode() 返回值必须相同

6. OJ练习

1. 只出现⼀次的数字
2. 复制带随机指针的链表
3. 宝⽯与⽯头
4. 坏键盘打字

感谢您的观看!

相关推荐
一叶知秋066 小时前
数据结构-什么是队列?
数据结构·队列
Jasmine_llq6 小时前
《CF280C Game on Tree》
数据结构·算法·邻接表·深度优先搜索(dfs)·树的遍历 + 线性累加统计
zhongvv6 小时前
对单片机C语言指针的一些理解
c语言·数据结构·单片机·指针·汇编语言
im_AMBER7 小时前
Leetcode 102 反转链表
数据结构·c++·学习·算法·leetcode·链表
Xの哲學7 小时前
深入剖析Linux文件系统数据结构实现机制
linux·运维·网络·数据结构·算法
C雨后彩虹8 小时前
竖直四子棋
java·数据结构·算法·华为·面试
荒诞硬汉9 小时前
对象数组.
java·数据结构
散峰而望9 小时前
【算法竞赛】栈和 stack
开发语言·数据结构·c++·算法·leetcode·github·推荐算法
wen__xvn9 小时前
代码随想录算法训练营DAY13第六章 二叉树part01
数据结构