HashMap常用方法及底层原理

news2024/12/23 9:45:02

目录

    • 一、什么是HashMap
    • 二、HashMap的链表与红黑树
      • 1、数据结构
      • 2、链表转为红黑树
      • 3、红黑树退化为链表
    • 三、存储(put)操作
    • 四、读取(get)操作
    • 五、扩容(resize)操作
    • 六、HashMap的线程安全与顺序
      • 1、线程安全
      • 2、有序的Map

一、什么是HashMap

    HashMap 是 Java 中的一个关键数据结构,属于 java.util 包。它基于哈希表的实现,提供了快速查找、插入和删除操作。以下是 HashMap 的一些主要特性:

  1. 键值对存储:HashMap 存储键值对(key-value pairs),其中键(key)是唯一的。

  2. 非同步:HashMap 不是线程安全的。在多线程环境中,如果多个线程同时修改 HashMap,而没有适当的同步措施,可能会导致不可预知的行为。

  3. 允许空键和空值:HashMap 允许键或值为 null。

  4. 非有序:HashMap 不保证元素的顺序,特别是它不保证该顺序恒久不变。

  5. 初始容量和负载因子:可以通过构造函数设置 HashMap 的初始容量和负载因子。初始容量是哈希表中桶的数量,负载因子是一个影响哈希表性能的参数,它定义了哈希表在其容量自动增加之前可以达到多满。

  6. 哈希冲突解决:当两个对象具有相同的哈希码时,会发生哈希冲突。HashMap 使用链表(在 Java 8 之前)或链表和红黑树(Java 8 及之后)来解决冲突。

  7. 性能:HashMap 提供了常数时间的性能(即 O(1) 时间复杂度)对于 get 和 put 操作,假设哈希函数良好且哈希表没有过载。

  8. 迭代器:HashMap 提供了键集(key set)、值集(values)和键值对集(entry set)的视图,它们都可以被迭代。

  9. 默认构造方法:如果使用无参构造方法创建 HashMap,它会使用默认的初始容量(16)和默认的负载因子(0.75)。

主要特性总结key-value形式的键值对;无序不重复;线程不安全

    

二、HashMap的链表与红黑树

1、数据结构

HashMap是一种存取高效但不保证有序的集合,它的数据结构在1.7里面是 数组+链表在1.8之后是数组+链表+红黑树

在这里插入图片描述
    
默认初始容量16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默认负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

将链表转为红黑树的阈值

 static final int TREEIFY_THRESHOLD = 8;

将红黑树转化为链表的阈值

 static final int UNTREEIFY_THRESHOLD = 6;

将链表转化为红黑树时,数组的大小必须大于等于这个值。否则如果 TREEIFY_THRESHOLD 大于8,将扩容,而不是转为红黑树

static final int MIN_TREEIFY_CAPACITY = 64;

    

2、链表转为红黑树

在 Java 8 及以后的版本中,HashMap 在某些情况下会将链表转换为红黑树,以提高搜索效率。这种转换发生在以下两个条件同时满足时:

  1. 树化阈值:HashMap 有一个树化阈值(treeify threshold),当链表的长度超过这个值时,链表会被转换为红黑树。在 Java 8 中,这个值默认是 8。

  2. 桶(bucket)的数量:如果桶的数量小于 64,即使链表长度达到 8,也不会立即转换为红黑树。这是为了避免在哈希表较小时过度优化,因为小规模的哈希表中,链表的性能已经足够好。

    
转换过程
    当 HashMap 进行扩容时,如果桶的数量增加到 64 或更多,那么在重新计算哈希值并重新插入元素的过程中,任何长度大于等于 8 的链表都会被转换为红黑树。这个转换是在扩容过程中自动完成的。

    
为什么有这个限制

  • 性能考虑:在哈希表较小时,链表的性能通常已经足够好,不需要额外的复杂性来维护红黑树。
  • 内存考虑:红黑树比链表占用更多的内存,因此在小规模的哈希表中使用链表可以节省内存。

    在实际应用中,这意味着如果你的 HashMap 初始容量设置得较小,或者在插入数据时没有触发扩容,那么即使某些链表的长度超过了 8,它们也可能不会立即转换为红黑树。只有当哈希表的容量增加到一定大小,且链表长度满足条件时,才会进行转换
    

3、红黑树退化为链表

     当一个桶中的红黑树的节点数量减少到一定阈值以下时,HashMap 会将这个红黑树转换回链表。这个阈值默认是 6。这意味着,如果一个桶中的红黑树的节点数量降到 6 个或更少,那么这个红黑树会被转换回链表。
    
为什么需要转换

  • 性能优化:链表在节点数量较少时,其操作(如搜索、插入、删除)的性能通常优于红黑树,因为链表的结构更简单,没有红黑树的复杂性。
  • 内存使用:红黑树比链表占用更多的内存,因为它需要存储额外的指针(用于维护树的结构)。当节点数量较少时,使用链表可以减少内存的使用。

    
转换过程

转换过程通常发生在以下情况下:

 1. 删除操作:当从 HashMap 中删除元素时,如果某个桶中的红黑树节点数量减少到 6 个或更少,这个红黑树会被转换回链表。
 2. 扩容操作:在 HashMap 进行扩容时,如果桶的数量增加,那么在重新计算哈希值并重新插入元素的过程中,如果某个桶中的红黑树节点数量减少到 6 个或更少,这个红黑树会被转换回链表。

    

三、存储(put)操作

    
    jdk1.7采用的是头插法 ,因头插法在扩容时导致的死循环,jdk1.8中链表插入采用的是尾插法。

put操作的步骤如下:

  1. 待存储的key进行hash计算
  2. 如果hash数组为空或者数组长度为0,则进行初始化
  3. 数组根据key计算后的hash值进行索引数组元素:
    • 如果索引出来的元素为空,则新创建一个Node节点并添加到数组中

    • 如果索引出来的元素不为空,则比较索引出来元素的key与传入的key进行"=="或equals的比较

      ① 如果一致则用新的value值替换旧的value值
      ②如果不一致就判断索引出来节点是不是树形节点,如果是则按照树形节点进行更新
      ③如果不是树形节点,则判断索引出来的节点的next节点是否为空,如果下一个节点为空,则新建下一个节点,此时需要判断链表的长度是否大于等于8,如果等于8,则要进行树化转为红黑树。
      ④ 如果下一个节点不为空,就一直循环找到节点的hash值与传入key的hash值一致,key通过"=="或equals比较是一致的。然后用新的value值替换旧的value值

   public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab;
        Node<K,V> p;
        int n, i;

        //如果table为空,则初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        //key对应的桶不存在,则创建一个新节点并添加到桶中
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

        //key对应的桶存在,则遍历桶,找到key对应的节点,如果key相同,则更新value,否则创建一个新节点
        else {
            Node<K,V> e; K k;
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 判断该节点是不是树形节点
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //节点key不相同也不是树形节点
            else {
                for (int binCount = 0; ; ++binCount) {
                    //节点的next节点为空,则新建节点并判断是否需要树化
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                   	// 节点的next节点不为空,且key相同
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    

四、读取(get)操作

    
获取数据步骤如下:

  1. 如果数组不为空,则根据hash值找到对应的桶,对应的桶为空,就返回null
  2. 对应的桶不为空,如果桶的第一个元素的key和key相等,则返回该元素
  3. 如果桶的第一个元素的key和key不相等
    • 如果桶的第一个元素的next节点不是树节点,则遍历桶,找到key对应的节点,如果key相同,则返回该节点
    • 如果桶的第一个元素是树节点,则调用树节点的getTreeNode方法
 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
final Node<K,V> getNode(int hash, Object key) {
    
    Node<K,V>[] tab;
    Node<K,V> first, e; 
    int n; 
    K k;
    
    // 如果数组不为空,则根据hash值找到对应的桶,对应的桶也不为空
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        
        // 如果桶的第一个元素的key和key相等,则返回该元素
        if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        
        // 如果桶的第一个元素的next节点不是树节点,则遍历桶,找到key对应的节点,如果key相同,则返回该节点
        if ((e = first.next) != null) {
            // 如果桶的第一个元素是树节点,则调用树节点的getTreeNode方法
            if (first instanceof HashMap.TreeNode)
                return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

    

五、扩容(resize)操作

    
函数resize()用于实现哈希表的扩容操作。具体步骤如下:

  1. 获取旧表信息

    • 获取当前哈希表table的引用。
    • 获取当前哈希表的容量oldCap。
    • 获取当前哈希表的阈值oldThr。

        

  2. 判断是否需要扩容:

    • 如果当前容量大于等于最大容量MAXIMUM_CAPACITY,设置阈值为整型最大值并返回旧表。
    • 如果当前容量大于0且小于最大容量,计算新容量newCap为旧容量的两倍,并更新阈值newThr为旧阈值的两倍。
    • 如果当前容量为0但阈值大于0,将新容量设为阈值。
    • 如果当前容量和阈值都为0,使用默认初始容量DEFAULT_INITIAL_CAPACITY并计算阈值。
          
  3. 计算新阈值

    • 如果新阈值仍为0,根据负载因子计算新阈值。
          
  4. 创建新表

    • 更新阈值threshold为newThr。
    • 创建新的哈希表newTab,大小为newCap。
          
  5. 迁移旧表数据:

    • 遍历旧表oldTab中的每个桶。
    • 对于非空的桶,将其元素迁移到新表中。
    • 如果桶中的元素只有一个,直接插入新表对应位置。
    • 如果桶中的元素是红黑树节点,调用split方法进行拆分。
    • 如果桶中的元素是链表,遍历链表并根据哈希值分成两部分,分别插入新表的不同位置。
 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;

        // 计算新的容量和阈值
        if (oldCap > 0) {
            // 如果容量大于最大容量,则将阈值设置为最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            } // 如果新容量等于旧容量的2倍且小于最大容量且旧的容量大于等于默认容量,新阈值设置为旧的容量的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold 新的阈值为旧的阈值的2倍
        }
        // 如果当前容量为0但阈值大于0,将阈值设为新容量。
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 如果当前容量和阈值都为0,使用默认初始容量DEFAULT_INITIAL_CAPACITY并计算阈值。
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        //  如果新阈值仍为0,根据负载因子计算新阈值。
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
         //创建新的哈希表newTab,大小为newCap。
        Node<K,V>[] newTab = (Node<K,V>[])new HashMap.Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof HashMap.TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

    

六、HashMap的线程安全与顺序

1、线程安全

HashMap 是非线程安全的,这意味着多个线程同时修改 HashMap 可能会导致不可预知的行为。如果需要在多线程环境中使用 HashMap,有几种方法可以使其线程安全:

  1. 使用 Collections.synchronizedMap 方法

    Java 提供了一个 Collections.synchronizedMap 方法,可以将 HashMap 包装为线程安全的 Map。

    Map<K, V> map = Collections.synchronizedMap(new HashMap<K, V>());
    
  2. 使用 ConcurrentHashMap

    ConcurrentHashMap 是 HashMap 的线程安全版本,它提供了更好的并发性能。通常,它是实现线程安全的首选方式,因为它允许多个线程同时读写而不需要外部同步。

    Map<K, V> map = new ConcurrentHashMap<K, V>();
    
  3. 使用 synchronized 块或方法

    在访问 HashMap 的方法上使用 synchronized 关键字,确保在修改 HashMap 时只有一个线程可以执行。

    public void put(K key, V value) {
        synchronized (this) {
            this.map.put(key, value);
        }
    }
    

        

2、有序的Map

     HashMap 本身不保证有序,即它不保证元素的顺序,无论是插入顺序还是自然顺序。如果需要一个有序的映射,你可以使用以下几种替代方案:

  1. LinkedHashMap:

    LinkedHashMap 是 HashMap 的一个子类,它维护了元素的插入顺序或者访问顺序。如果你想要元素按照插入顺序存储,可以使用 LinkedHashMap

  2. TreeMap:

    TreeMap 是一个基于红黑树实现的 NavigableMap 接口的类,它可以按照元素的自然顺序或者自定义比较器(Comparator)定义的顺序来存储元素。TreeMap 保证了元素的有序性,并且提供了一些导航方法,如 firstEntry()、lastEntry()、higherEntry() 等。

    Map<K, V> map = new TreeMap<K, V>();
    

    或者,如果你想要自定义排序,可以提供一个比较器:

    Comparator<K> comparator = ...;
    Map<K, V> map = new TreeMap<K, V>(comparator);
    
  3. 自定义有序 HashMap:

    如果你需要 HashMap 的某些特性,并且想要保持顺序,你可以自己实现一个有序的 HashMap。这通常涉及到在内部维护一个列表来跟踪插入顺序。
        

  4. Hashtable:

    虽然 Hashtable 是一个遗留类,但它是同步的,并且它按照插入顺序维护键的顺序。然而,由于 Hashtable 的性能通常不如 HashMap 和 LinkedHashMap,并且它不是线程安全的,所以通常不推荐使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2130241.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云计算实训49——k8s环镜搭建(续2)

一、Metrics 部署 在新版的 Kubernetes 中系统资源的采集均使⽤ Metrics-server&#xff0c;可 以通过 Metrics 采集节点和 Pod 的内存、磁盘、CPU和⽹络的使⽤ 率。 &#xff08;1&#xff09;复制证书到所有 node 节点 将 master 节点的 front-proxy-ca.crt 复制到所有 No…

Linux进阶命令-top

作者介绍&#xff1a;简历上没有一个精通的运维工程师。希望大家多多关注作者&#xff0c;下面的思维导图也是预计更新的内容和当前进度(不定时更新)。 经过上一章Linux日志的讲解&#xff0c;我们对Linux系统自带的日志服务已经有了一些了解。我们接下来将讲解一些进阶命令&am…

【计算机网络】初识网络

初识网络 初识网络网络的发展局域网广域网 网络基础IP地址端口号协议五元组协议分层OSI 七层模型TCP/IP五层模型封装和分用"客户段-服务器"结构 初识网络 网络的发展 在过去网络还没有出现的时候, 我们的计算机大部分都是独自运行的, 比如以前那些老游戏, 都是只能…

Chainlit集成Langchain并使用通义千问实现文生图网页应用

前言 本文教程如何使用通义千问的大模型服务平台的接口&#xff0c;实现图片生成的网页应用&#xff0c;主要用到的技术服务有&#xff0c;chainlit 、 langchain、 flux。合利用了大模型的工具选择调用能力。实现聊天对话生成图片的网页应用。 阿里云 大模型服务平台百炼 API…

1.SpringCloud与SpringCloud Alibaba

SpringCloud与SpringCloud Alibaba主要讲解的内容&#xff1a; 备注&#xff1a;黑色部分是springcloud社区原版&#xff0c;红色的是SpringCloud Alibaba 服务注册与发现 Consul Alibaba Nacos 服务调用和负载均衡 LoadBalancer OpenFeign 分布式事务 Alibaba Seata 服务熔…

批量插入insert到SQLServer数据库,BigDecimal精度丢失解决办法,不动代码,从驱动层面解决

概述 相信很多人都遇到过&#xff0c;使用sql server数据库&#xff0c;批量插入数据时&#xff0c;BigDecimal类型出现丢失精度的问题&#xff0c;网上也有很多人给出过解决方案&#xff0c;但一般都要修改应用代码&#xff0c;不推荐。 丢失精度的本质是官方的驱动有BUG造成…

机器学习特征-学习篇

一、特征概念 1. 什么是特征 特征是事物可供识别的特殊的征象或标志 在机器学习中&#xff0c;特征是用来描述样本的属性或观测值的变量。它们可以是任何类型的数据&#xff0c;包括数字、文本、图像、音频等。 作用&#xff1a; 特征是训练和评估机器学习模型的基础。好的特…

[基于 Vue CLI 5 + Vue 3 + Ant Design Vue 4 搭建项目] 09 集成 Ant Design Vue

我们要将 Ant Design Vue 集成到项目中 1.首先进入到我们的项目 2.然后使用下面的命令 npm i --save ant-design-vue解释一下这个命令&#xff1a; npm&#xff1a;npm 命令 i&#xff1a;install 的简写 –save&#xff1a;将其保存到 pagckage.json ant-design-vue&am…

PHP随时随地预订民宿酒店预订系统小程序源码

随时随地预订&#xff0c;民宿酒店预订系统让旅行更自由&#xff01; &#x1f30d; 说走就走的旅行&#xff0c;从预订开始 旅行&#xff0c;总是让人心生向往&#xff0c;但繁琐的预订流程却常常让人望而却步。不过&#xff0c;现在有了“随时随地预订民宿酒店预订系统”&am…

centos7安装MySQL5.7.44

下载压缩文件 命令&#xff1a; #放到在/usr/local目录下 cd /usr/local #上传命令选择安装包 rz #解压缩包 tar -zxvf mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz #给包重命名为mysql mv mysql-5.7.44-linux-glibc2.12-x86_64 mysql #查看mysql目录下有什么东西 [rootlocal…

【Python 数据分析学习】Pandas基础与应用(1)

题目 1 Pandas 简介1.1 主要特征1.2 Pandas 安装 2 Pandas中的数据结构2.1 Series 数据结构和操作2.1.1 Series的数据结构2.1.2 Seres的操作 2.2 DataFrame 数据结构和操作2.2.1 DataFrame 数据结构2.2.2 Dataframe 操作2.2.3 DateFrame 的特殊操作 2.3 Series 和 DataFrame 的…

JMeter 入门之远程启动,服务模式,多机联测,负载均衡测试

本文主要介绍 JMeter 远程启动及使用多节点完成大并发测试&#xff08;负载均衡测试&#xff09;&#xff0c;主打一个压力山大&#xff0c;借用 黑神话&#xff1a;悟空 的技能来描述就是远程开大&#xff0c;释放猴子猴孙技能。 搜了一些 jmeter 的案例或教程&#xff0c;讲的…

Windows10 如何设置电脑ip

1、首先打开控制面板 或者使用WinR 输入control 找到网络和Internet 点击网络和共享中心 点击更改适配器设置 找到你要需要设置的网络&#xff0c;右键 如果你的网口特别多&#xff0c;不确定是哪一个&#xff0c;拔插一下看看哪个以太网的标志是断开状态就可以了 点击属性…

★ C++基础篇 ★ string类的实现

Ciallo&#xff5e;(∠・ω< )⌒☆ ~ 今天&#xff0c;我将继续和大家一起学习C基础篇第五章下篇----string类的模拟实现 ~ 上篇&#xff1a;★ C基础篇 ★ string类-CSDN博客 C基础篇专栏&#xff1a;★ C基础篇 ★_椎名澄嵐的博客-CSDN博客 目录 一 基础结构 二 迭代器 …

即插即用篇 | YOLOv8 引入组装式Transformer模块AssembleFormer | arXiv 2024

本改进已同步到YOLO-Magic框架! 摘要—早期检测和准确诊断可以预测恶性疾病转化的风险,从而增加有效治疗的可能性。轻微的症状和小范围的感染区域是一种不祥的警告,是疾病早期诊断的重中之重。深度学习算法,如卷积神经网络(CNNs),已被用于分割自然或医学对象,显示出有希…

JVM源码解析

一、java虚拟机概述 1. java程序的跨平台性 之前的话&#xff0c;通过Linux或者Windows开发&#xff0c;当需要跨平台时&#xff0c;程序不能运行。java出现后&#xff0c;产生了jvm&#xff0c;针对不同的操作系统&#xff0c;产生了不同的java虚拟机。 在Java虚拟机中执行…

3D云渲染农场为何怎么贵?主要消耗成本介绍

随着对高质量3D动画的需求持续增长&#xff0c;云渲染农场对于旨在以高效速度生产高质量视觉效果的工作室来说变得至关重要。然而&#xff0c;用户经常想知道为什么渲染农场的价格如此之高&#xff0c;理解背后的原因可以帮助艺术家做出更好的选择。 什么是云渲染农场&#xff…

【Hot100】LeetCode—72. 编辑距离

目录 1- 思路题目识别动规五部曲 2- 实现⭐72. 编辑距离——题解思路 3- ACM 实现 原题链接&#xff1a;72. 编辑距离 1- 思路 题目识别 识别1 &#xff1a;两个字符串之间相互转换&#xff0c;增、删、替换 最少的操作次数 动规五部曲 1- 定义 dp 数组 dp[i][j] 代表&…

市面上有哪些高效财税自动化软件

随着科技的不断发展&#xff0c;财税自动化软件已成为许多企业和个人不可或缺的工具。这些软件可以大大提高财税处理的效率&#xff0c;减少人工错误&#xff0c;并确保合规性。目前市场上有许多高效财税自动化软件可供选择&#xff0c;本文金智维将介绍一些市场上比较受欢迎的…

可信的人类与人工智能协作:基于人类反馈和物理知识的安全自主驾驶强化学习

可信的人类与人工智能协作&#xff1a;基于人类反馈和物理知识的安全自主驾驶强化学习 Abstract 在自动驾驶领域&#xff0c;开发安全且可信赖的自动驾驶策略仍然是一项重大挑战。近年来&#xff0c;结合人类反馈的强化学习&#xff08;RLHF&#xff09;因其提升训练安全性和…