2023年7月16日,HashMap

news2025/2/4 11:05:24

HashMap

HashMap存储的是一组无序的键值对。存储时是根据键的哈希码来计算存储的位置,因为对象的哈希码是不确定的,因此HashMap存储的元素是无序的。

Map用于保存具有映射关系的数据,Map里保存着两组数据:keyvalue,它们都可以使任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value

请添加图片描述

哈希碰撞:

多个元素的hash值相同,计算出来在同一个位置上,这就是哈希碰撞

HashMap解决hash碰撞是采用了链地址法,也就是数组+链表的方式

1. HashMap底层

HashMap底层使用一个数组(Entry[ ])来存储数据。数组中的每个元素都是一个链表的头节点,称为桶(Bucket)。每个桶都是一个链表,链表节点(Entry)包含键值对的信息。

  1. Hash表数组,HashMap内部使用一个数组来存储数据。数组的每个元素都是一个链表(有些情况下会转化为红黑树),用于解决哈希冲突。
    interface Entry<K,V> {

        K getKey();

        V getValue();

        V setValue(V value);
    }

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
//用于版本控制的静态常量,它确保序列化和反序列化的一致性。
    private static final long serialVersionUID = 362498820763181265L;
//默认初始容量,它的值是 16。
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//哈希表的最大容量,它的值是 2^30。
    static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子,用于控制哈希表的负载程度,它的值为 0.75。
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
//哈希表树化的阈值,当链表长度超过该阈值时,将链表转换为红黑树。
    static final int TREEIFY_THRESHOLD = 8;
//哈希表退化为链表的阈值,当红黑树节点数量小于该阈值时,将红黑树转换为链表。
    static final int UNTREEIFY_THRESHOLD = 6;
//是红黑树最小容量,即进行树化操作时所需的最小容量。
    static final int MIN_TREEIFY_CAPACITY = 64;
//哈希表中的节点
//hash表是通过数组+链表的方式存储数据
//Node是一个静态的嵌套类,表示哈希表中的节点
        static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    } 
}
  1. 哈希函数:当要插入或查找一个键值对时,会先对键进行哈希运算,得到一个哈希值(即数组索引)。哈希函数的作用是尽可能均匀地将键映射到数组的索引上,以提高查找效率。
  2. 哈希冲突处理:由于不同的键可能计算得到相同的哈希值,产生哈希冲突。当发生冲突时,使用链表的方式将具有相同哈希值的键值对链接在一起。在Java 8及以后的版本中,如果链表长度超过一定阈值(TREEIFYTHRESHOLD),链表就会被转化成红黑树,以提高查找效率。当链表长度较短(小于一定阈值UNTREEIFYTHRESHOLD),红黑树将会转化回链表。
  3. 插入操作:当要插入一个键值对时,首先计算键的哈希值,找到对应的索引位置。如果该位置为空,则直接插入。如果该位置已经存在其他键值对(可能是链表或红黑树),则需要进行适当的插入操作。

在Java中,i = (n - 1) & hash 是一种常见的计算索引的方法,通常用于确定元素在HashMap底层数组中的存储位置。

在这个表达式中,n 是数组的长度,hash 是元素的哈希码。通过对 n - 1 进行位与(AND)操作,可以确保计算结果落在合法的索引范围内,即从 0 到 n - 1

位与操作的原理是将两个二进制数的对应位进行逻辑与运算,结果为 1 的位表示对应位置上的两个二进制数都为 1。因为 n 是数组的长度,是一个2的幂次方,所以 n - 1 的二进制表示中,所有位都是 1。

通过与 n - 1 进行位与操作,可以将 hash 的高位(超出索引范围的部分)忽略掉,只保留低位,得到一个有效的索引。这种操作相当于对 hash 取模 n,但是位运算的效率更高。

这种计算桶索引的方法可以快速地确定元素在数组中的存储位置,是HashMap实现中常用的技巧。它利用了数组长度是2的幂次方的特性,保证了元素在数组中分布均匀,减少了哈希碰撞的概率,提高了HashMap的性能。

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数组是否为空,如果为空,表示HashMap还没有初始化或者已经被清空,需要进行初始化或者扩容操作。
        if ((tab = table) == null || (n = tab.length) == 0)
            //如果table数组为空,调用resize()方法进行初始化或扩容操作,并将新数组赋值给tab,同时更新n为新数组的长度。
            n = (tab = resize()).length;
    //通过计算哈希值与table的长度进行与运算,得到索引位置i,然后将索引位置i处的节点赋值给p。如果该位置为空,表示当前哈希槽为空,可以直接插入新节点。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //检查该位置的节点是否与待插入节点的键相等。如果相等,说明键已经存在,更新节点的值即可。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果节点是树节点,说明哈希冲突比较严重,会通过树结构进行解决,调用树节点的putTreeVal方法来处理插入操作。
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //如果节点是链表节点,说明哈希冲突较轻,使用链表进行解决。
            else {
                //遍历链表,查找是否存在相同的键。
                for (int binCount = 0; ; ++binCount) {
                    //如果链表的下一个节点为空,说明已经遍历到了链表的末尾,可以将新节点直接插入到链表尾部,并根据一定的条件进行树化操作(当链表长度达到一定阈值时)。
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //在遍历链表过程中,如果存在相同的键,就跳出循环
                    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;
                //如果标志位onlyIfAbsent为false(表示不仅仅在值为空的情况下更新值),或者旧值为null(说明之前的键没有对应的值),则更新节点的值为新值。
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
    //用于记录HashMap结构发生变化的次数。
        ++modCount;
    //自增size,并与threshold进行比较。如果size超过了threshold,说明元素个数接近容量的上限,需要进行扩容操作。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  1. 查找操作:当要查找一个键值对时,首先计算键的哈希值,找到对应的索引位置。然后在该位置的链表或红黑树中进行查找操作,找到对应的值。

  2. 扩容:当哈希表中存储的键值对数量达到一定阈值(加载因子 * 当前容量),会触发扩容操作。扩容会创建一个更大的数组,并将旧数组中的所有键值对重新插入到新数组中,以减少哈希冲突,提高性能。

//这是扩容方法的声明,返回一个泛型数组 
final Node<K,V>[] resize() {
    //将当前的哈希表数组赋值给 oldTab。
        Node<K,V>[] oldTab = table;
    //获取当前哈希表数组的长度作为 oldCap。如果当前数组为空,则将长度设置为0。
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //获取当前哈希表的阈值(threshold)。
        int oldThr = threshold;
    //创建变量 newCap 和 newThr,用于存储新的哈希表容量和阈值。
        int newCap, newThr = 0;
    //如果旧的哈希表容量大于0,则进行以下操作:
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果旧的哈希表容量的2倍仍然小于最大容量,并且旧的哈希表容量大于等于默认的初始容量(DEFAULT_INITIAL_CAPACITY),则将新的哈希表容量设置为旧容量的2倍,并将新的阈值设置为旧阈值的2倍。这样扩容后,哈希表的容量和阈值都会增加一倍。
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
    //如果旧阈值大于0,表示初始容量是通过阈值设置的:将新的哈希表容量设置为旧的阈值。
        else if (oldThr > 0) 
            newCap = oldThr;
    //如果旧的哈希表容量和阈值都为0,表示使用默认值
        else {               
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    //如果新的阈值为0(表明阈值尚未被设置)
        if (newThr == 0) {
            //将新的哈希表容量乘以加载因子得到一个浮点数 ft。
            float ft = (float)newCap * loadFactor;
            //将新的阈值设置为 ft,如果新的容量小于最大容量且 ft 小于最大容量,则将 ft 转换为整数作为新的阈值;否则,将阈值设置为整数的最大值。
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
    //将阈值设置为新的阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
    //创建一个新的哈希表数组 newTab,将其转换为泛型数组 Node<K,V>[]。
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //将哈希表的引用指向新的哈希表数组。
        table = newTab;
    //如果旧的哈希表数组不为空
        if (oldTab != null) {
            //使用循环遍历旧的哈希表数组 oldTab
            for (int j = 0; j < oldCap; ++j) {
                //创建临时变量 e 用于存储旧哈希表数组中的每个元素。
                Node<K,V> e;
                //检查当前位置的元素是否为空
                if ((e = oldTab[j]) != null) {
                    //将旧哈希表数组中当前位置的元素置为 null。
                    oldTab[j] = null;
                    //如果当前元素的下一个元素为空,表示当前位置只有一个元素
                    if (e.next == null)
                        //将当前元素放入新的哈希表数组的对应位置。这里通过位运算的方式确定元素应该放在新的哈希表数组的哪个位置。
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果当前元素是一个树节点
                    else if (e instanceof TreeNode)
                        //表示该位置的元素形成了一棵树节点链表,需要进行树形化操作。
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //表示当前位置的元素是一个普通节点
                    else { 
                        //创建两个变量 loHead 和 loTail 用于存储小于等于旧容量的节点。
                        Node<K,V> loHead = null, loTail = null;
                        //创建两个变量 hiHead 和 hiTail 用于存储大于旧容量的节点。
                        Node<K,V> hiHead = null, hiTail = null;
                        
                        Node<K,V> next;
                        do {
                            将当前元素的下一个元素赋值给 next
                            next = e.next;
                            //如果 loTail 为空,表示还未存储小于等于旧容量的节点,则将当前元素设为 loHead 和 loTail
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    //将当前元素追加到 loTail 的后面,并更新 loTail
                                    loTail.next = e;
                                loTail = e;
                            }
                            //将当前元素追加到 loTail 的后面,并更新 loTail
                            else {
                                // 如果 hiTail 为空,表示还未存储大于旧容量的节点,则将当前元素设为 hiHead 和 hiTail。
                                if (hiTail == null)
                                    hiHead = e;
                                //将当前元素追加到 hiTail 的后面,并更新 hiTail。
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //循环结束后,会得到两个链表 loHead 和 hiHead 分别存储小于等于旧容量的节点和大于旧容量的节点。
                        //如果 loHead 不为空,表示存在小于等于旧容量的节点。
                        if (loTail != null) {
                            loTail.next = null;
                            //将链表 loHead 放入新哈希表数组 newTab 的对应位置。
                            newTab[j] = loHead;
                        }
                        //如果 hiHead 不为空,表示存在大于旧容量的节点。
                        if (hiTail != null) {
                            hiTail.next = null;
                            //将链表 hiHead 放入新哈希表数组 newTab 的对应位置。
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
    //返回新的哈希表数组 newTab
        return newTab;
    }

总结起来,HashMap通过使用哈希表提供了高效的插入、查找和删除操作。尽管HashMap的常规操作复杂度是常数级别的(O(1)),但是在最坏情况下,由于哈希冲突,链表长度可能会变得很长,导致操作的复杂度变为线性级别(O(n))。因此,在使用HashMap时,需要注意良好的哈希函数选择和适当的负载因子设置,以避免性能退化。

package com.wz;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

public class HashMapTest {
    public static void main(String[] args) {
        Map<String,Integer> map =new HashMap<>();
        map.put("A",10);
        map.put("D",20);
        map.put("C",15);
        map.put("B",13);
        map.put("E",13);
        map.put("B",20);
//        for (Map.Entry<String,Integer> entry:map.entrySet()){
//            String key = entry.getKey();
//            Integer value = entry.getValue();
//            System.out.println(key+"="+value);
//        }
//        System.out.println("-------------");
//        //遍历key的集合
//        for (String key:map.keySet()){
//            Integer value = map.get(key);
//            System.out.println(key+"="+value);
//        }
//        System.out.println("-------------");
//        //获取值的集合
//        Collection<Integer> values = map.values();
//        for (Integer value:values){
//            System.out.println(value);
//        }
//        System.out.println("-------------------");
//        map.forEach((k,v)-> System.out.println(k+"="+v));
//        System.out.println("----------------------");
        map.entrySet().forEach(System.out::println);
        System.out.println("---------------------");
        //如果map中不村子啊key "F" ,那么就将"F"存进去
        if (!map.containsKey("F")){
            map.put("F",18);
        }

        int size = map.size();
        boolean empty = map.isEmpty();
        /**
         * 对存入的key进行计算,计算的方式就是后面的类型转换接口实现的功能,
         * 这里使用的是将字符串转换为整数的方式。
         * 将key="10"现状换成整数10,然后将"10"=10存入map
         */
//        map.computeIfAbsent("10", new Function<String, Integer>() {
//            @Override
//            public Integer apply(String s) {
//                return Integer.parseInt(s);
//            }
//        });
        map.computeIfAbsent("10", Integer::parseInt);
        map.forEach((k,v)-> System.out.println(k+"="+v));
    }
}

结果

请添加图片描述

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

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

相关文章

Leetcode-每日一题【2487.从链表中移除节点】

题目 给你一个链表的头节点 head 。 对于列表中的每个节点 node &#xff0c;如果其右侧存在一个具有 严格更大 值的节点&#xff0c;则移除 node 。 返回修改后链表的头节点 head 。 示例 1&#xff1a; 输入&#xff1a;head [5,2,13,3,8]输出&#xff1a;[13,8]解释&…

拒绝被其他域名恶意解析到你的服务器上

拒绝被其他域名恶意解析到你的服务器上 备案问题恶意解析解决方案后记 备案问题 新的一周开始了&#xff0c;又是一个摸鱼的好时候。。。。结果&#xff0c;刚刚坐到工位上&#xff0c;机房客服发来了一个 excel &#xff0c;说。。。你的备案信息没完善。。。 啥&#xff1f…

C语言进阶之程序环境和预处理

程序环境和预处理 1. 程序的翻译环境和执行环境2. 详解编译链接2.1 翻译环境2.2 编译的几个阶段2.3 运行环境 3. 预处理详解3.1 预定义符号3.2 #define3.2.1 #define 定义标识符3.2.2 #define 定义宏3.2.3 #define 替换规则3.2.4 #和##3.2.5 带副作用的宏参数3.2.6 宏和函数对比…

快速搭建接口自动化测试框架

1 接口测试 接口测试是对系统或组件之间的接口进行测试&#xff0c;主要是校验数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及相互逻辑依赖关系。 接口自动化相对于UI自动化来说&#xff0c;属于更底层的测试&#xff0c;这样带来的好处就是测试收益更大&#xff…

Python基础教程:数据结构

Python是一门广泛使用的编程语言&#xff0c;它的丰富的数据结构使得编写程序变得异常简单和方便。在本篇教程中&#xff0c;我将详细介绍Python中的四种主要数据结构&#xff1a;列表、元组、字典、集合。 1.列表&#xff08;List&#xff09; 列表是Python中最常用的数据结…

Nginx代理Grafana,鉴权访问以及Grafan免登录访问

✨概述 在使用grafana做页面嵌入的场景中&#xff0c;通常需要grafana与前端在同域下&#xff0c;方便鉴权、解决跨域。 Nginx代理Grafana后&#xff0c;就不能使用Grafana中默认配置的端口和路径进行访问&#xff0c;必须通过Nginx访问Grafana。 如果需要做Iframe嵌入自研系…

RabbitMQ ---- 发布高级确认

RabbitMQ ---- 发布高级确认 1. 发布确认 springboot 版本1.1 确认机制方案1.2 代码架构图1.3 配置文件1.4 添加配置类1.5 消息生产者1.6 回调接口1.7 消息消费者1.8 结果分析 2. 回退消息2.1 Mandatory 参数2.2 回调接口2.3 结果分析 3. 备份交换机3.1 代码架构图3.2 修改配置…

CAD可以转换成PDF吗?教你简单好用的转换方法

PDF格式是一种通用格式&#xff0c;可以在不同的设备和操作系统上轻松打开和查看&#xff0c;这使得共享和协作变得更加容易和高效。尤其是在远程工作的情况下&#xff0c;PDF格式能够让团队成员更方便地分享和合作&#xff0c;不受地理位置和设备的限制。那么怎么将CAD文件转换…

7. Java + Selenium 环境搭建

前提&#xff1a;Java 版本最低要求为 8&#xff1b;推荐使用 chrome 浏览器 chrome Java 1. 下载 chrome 浏览器&#xff08;推荐&#xff09; 2. 查看 chrome 浏览器版本 重点记住前两位即可。 3. 下载 chrome 浏览器驱动 下载链接&#xff1a; https://chromedriver.…

IPD跟敏捷、DevOps一样吗?有什么区别?

1992年在激烈的全球市场竞争下&#xff0c;IBM遭遇到了严重的财政困难&#xff0c;公司销售收入停止增长&#xff0c;利润急剧下降。经过内部分析&#xff0c;IBM发现他们在研发费用、研发损失费用和产品上市时间等几个方面远远落后于业界最佳。为了重新获得市场竞争优势&#…

SpringBoot源码分析(6)--SpringBootExceptionReporter/异常报告器

文章目录 一、前言二、异常报告器介绍2.1、作用2.2、接口定义2.3、FailureAnalyzer错误分析器2.4、FailureAnalysisReporter错误报告器 三 、SpringBootExceptionReporter源码分析四、shutdownHook介绍4.1、背景4.2、什么是Shutdown Hook4.3、什么时候会调用Shutdown Hook4.4、…

MYSQL 5.7.17 安装版 的配置文件

解压版解压后都有 my.ini配置文件&#xff0c;安装版要查找这个配置文件可以查看 MYSQL Workbench --> 左侧 INSTANCE --> Options File &#xff0c;然后可以看到底部 Configuration File所处的位置&#xff0c;即为my.ini的路径。

医疗设备如何保障?蓄电池自动监测,简直太牛了!

蓄电池监控在医院中扮演着重要的角色&#xff0c;确保在电力故障或断电时医院能够继续供电&#xff0c;保障医疗设备和关键系统的正常运行。 通过监测蓄电池的状态、充电状态和容量&#xff0c;以及触发警报和提醒&#xff0c;监控系统可以提前发现蓄电池的故障或异常情况&…

计算机网络 day8 动态路由 - NAT - SNAT实验 - VMware的网卡的3种模式

目录 动态路由&#xff1a;IGP 和 EGP 参考网课&#xff1a;4.6.1 路由选择协议概述_哔哩哔哩_bilibili ​编辑 IGP&#xff08;Interior Gateway Protocol&#xff09;内部网关协议&#xff1a; EGP&#xff08;Interior Gateway Protocol&#xff09;外部网关协议&#x…

专精特新如何养成?先搞清楚成长路径和核心能力激活高质量发展!

头雁勤&#xff0c;群雁便能“春风一夜到衡阳”。群雁齐飞&#xff0c;最重要的是头雁引领。 当前加快中小企业数字化转型正当其时&#xff0c;“专精特新”企业势必将肩负起“领头雁”之任&#xff0c;为中小企业转型发展做出表率。 装备制造业 专精特新“主力军” 纵观目前…

SpringBoot Data JPA 集成多租户

背景&#xff1a; ​ iot-kit项目用的是SpringBoot JPA&#xff0c;不是Mybatis&#xff0c;项目中需要引入多租户。 文章中心思想&#xff1a; 通过Hibernate Filters 和AspectJ 切面编程&#xff0c;实现SpringBoot JPA多租户 什么是多租户 ​ 多租户我理解就是一个网站允…

【EXCEL】通过url获取网页表格数据

目录 0.环境 1.背景 2.具体操作 0.环境 windows excel2021 1.背景 之前我用python的flask框架的爬虫爬取过豆瓣网的电影信息&#xff0c;没想到excel可以直接通过url去获取网页表格内的信息&#xff0c;比如下图这是电影信息界面 即将上映电影 (douban.com) 通过excel操作&…

Cache——让CPU更快地执行你的代码

概要 Cache对性能的影响 首先我们要知道&#xff0c;CPU访问内存时&#xff0c;不是直接去访问内存的&#xff0c;而是先访问缓存&#xff08;cache&#xff09;。 当缓存中已经有了我们要的数据时&#xff0c;CPU就会直接从缓存中读数据&#xff0c;而不是从内存中读。 CPU…

Python基础编程案例之编写交互式博客系统

文章目录 1、博客系统的需求描述2、面向用户层面各功能的设计思路与代码编写2.1.定义文章库2.2.文章的发布2.3.删除文章2.4.修改文章的标题以及内容2.5.在评论区添加评论2.6.删除文章中的某条评论2.7.阅读文章2.8.对文章进行点赞2.9.对文章进行收藏2.10.对文章进行打赏2.11.查询…

WorkPlus AI助理:结合ChatGPT对话能力与企业数据,助力企业级AI构建!

WorkPlus AI助理是基于GPT和私有数据构建智能知识库和个性化AI&#xff0c;能够帮助企业生成博客、白皮书、社交媒体帖子、新闻稿等等&#xff0c;这些内容可以用于推广产品、服务&#xff0c;增强品牌形象和知名度。此外&#xff0c;利用WorkPlus AI助理还可以生成电子邮件、利…