ArrayDeque 源码解析(JDK1.8)

news2024/11/8 6:45:01

目录

一. 前言

二. 源码解析

2.1. 概览

2.2. 属性

2.3. 构造方法

2.4. 入队

2.4.1. addFirst(E, e)

2.4.2. add(E e) & addLast(E e)

2.4.3. offer(E e)

2.5. 扩容

2.6. 出队

2.6.1. poll() & pollFirst()

2.6.2. pollLast()

2.7. 删除元素

2.8. 获取元素

2.9. 栈操作


一. 前言

    ArrayDeque 和
LinkedList 是Deque的两个通用实现,由于官方更推荐使用ArrayDeque用作栈和队列,由于作者已经讲解过 LinkedList,本文将着重讲解ArrayDeque的具体实现。

    双端队列是一种特殊的队列,它的两端都可以进出元素,故而得名双端队列。ArrayDeque 是一种以数组方式实现的双端队列,它是非线程安全的。由其名字可以看出,其是一个由数组实现的双端队列,对比 LinkedList 是由链表实现的双端队列。

    从名字可以看出ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。ArrayDeque是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要程序员手动同步;另外,该容器不允许放入null元素。

上图中我们看到,head指向首端第一个有效元素,tail指向尾端第一个可以插入元素的空位。因为是循环数组,所以head不一定总等于0,tail也不一定总是比head大。

二. 源码解析

2.1. 概览

通过继承体系可以看到,ArrayDeque 实现了 Deque 接口,Deque 接口继承自 Queue 接口,它是对 Queue 的一种增强。

public interface Deque<E> extends Queue<E> {
    // 添加元素到队列头
    void addFirst(E e);
    // 添加元素到队列尾
    void addLast(E e);
    // 添加元素到队列头
    boolean offerFirst(E e);
    // 添加元素到队列尾
    boolean offerLast(E e);
    // 从队列头移除元素
    E removeFirst();
    // 从队列尾移除元素
    E removeLast();
    // 从队列头移除元素
    E pollFirst();
    // 从队列尾移除元素
    E pollLast();
    // 查看队列头元素
    E getFirst();
    // 查看队列尾元素
    E getLast();
    // 查看队列头元素
    E peekFirst();
    // 查看队列尾元素
    E peekLast();
    // 从队列头向后遍历移除指定元素
    boolean removeFirstOccurrence(Object o);
    // 从队列尾向前遍历移除指定元素
    boolean removeLastOccurrence(Object o);
    /*
     * 队列中的方法
     */  
    // 添加元素,等于addLast(e)
    boolean add(E e);
     // 添加元素,等于offerLast(e)
    boolean offer(E e);
    // 移除元素,等于removeFirst()
    E remove();
    // 移除元素,等于pollFirst()
    E poll();
    // 查看元素,等于getFirst()
    E element();
    // 查看元素,等于peekFirst()
    E peek();
    /*
     * 栈方法
     */  
    // 入栈,等于addFirst(e)
    void push(E e);
    // 出栈,等于removeFirst()
    E pop();
	/*
     * Collection中的方法
     */  
    // 删除指定元素,等于removeFirstOccurrence(o)
    boolean remove(Object o);
    // 检查是否包含某个元素
    boolean contains(Object o);
    // 元素个数
    public int size();
    // 迭代器
    Iterator<E> iterator();
    // 反向迭代器
    Iterator<E> descendingIterator();
}

Deque 中新增了以下几类方法:
    *First,表示从队列头操作元素;
    *Last,表示从队列尾操作元素;
    push(e),pop(),以栈的方式操作元素的方法。

2.2. 属性

// 存储元素的数组
transient Object[] elements;

// 头指针
transient int head;

// 尾指针
transient int tail;

// 默认最小容量(注意:elements的长度一定是2的次方幂)
private static final int MIN_INITIAL_CAPACITY = 8;

从属性我们可以看到,ArrayDeque 使用数组存储元素,并使用头尾指针标识队列的头和尾,其最小容量是 8。

ArrayDeque底层是使用数组实现的,而且数组的长度必须是2的整数次幂,这么操作的原因是为了后面位运算好操作。在ArrayDeque当中有两个整形变量head和tail,分别指向右侧的第一个进入队列的数据和左侧第一个进入队列的数据,整个内存布局如下图所示:

其中 tail 指的位置没有数据,head 指的位置存在数据。

2.3. 构造方法

1. 调用无参构造器时,默认创建一个长度为16的数组。
2. 调用传入初始容量 n 的构造器,当 n 小于 8 时,会初始化一个长度为 8 的一个数组。
3. 当 n 大于等于 8 时,会初始化一个长度为 大于n的最小的2的幂 的数组(比如传入 3 算出来是 8,传入 9 算出来是 16,传入 16 算出来是 32)。

通过构造方法,我们知道默认初始容量是 16,最小容量是 8。

/*
 * 空参构造器,底层初始化一个长度为16的数组
 */
public ArrayDeque() {
	elements = new Object[16];
}

/*
 * 指定元素个数初始化
 * 传入初始容量,注意最终的容量是大于(没有等于)numElements的最大的2的幂
 * 然后会创建出来。
 */
public ArrayDeque(int numElements) {
	allocateElements(numElements);
}

/* 
 * 传入一个集合,将集合c中的元素初始化到数组中
 * 创建一个长度为<小于等于c.size的最大的2的幂>的数组
 * 然后将c中的元素添加到elements中。
 */
public ArrayDeque(Collection<? extends E> c) {
	allocateElements(c.size());
	addAll(c);
}
// 构造一个长度为 严格大于numElements的最小的2的幂 的一个数组
private void allocateElements(int numElements) {
	elements = new Object[calculateSize(numElements)];
}

// 返回严格大于numElements的最小的2的幂 (当numElements小于8时,返回8)
private static int calculateSize(int numElements) {
	// MIN_INITIAL_CAPACITY = 8
	int initialCapacity = MIN_INITIAL_CAPACITY;
	// 当numElements大于等于8时,计算出大于numElements的最小的2的幂
	if (numElements >= initialCapacity) {
		initialCapacity = numElements;
		initialCapacity |= (initialCapacity >>>  1);
		initialCapacity |= (initialCapacity >>>  2);
		initialCapacity |= (initialCapacity >>>  4);
		initialCapacity |= (initialCapacity >>>  8);
		initialCapacity |= (initialCapacity >>> 16);
		initialCapacity++;
		// 条件成立:说明爆int了,需要缩小数据,将initialCapacity无符号右移一位,相当于/2
		if (initialCapacity < 0)   // Too many elements, must back off
			initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
	}
	// 这里如果numElements小于8时,直接返回8
	return initialCapacity;
}

2.4. 入队

2.4.1. addFirst(E, e)

// 从队头入队
public void addFirst(E e) {
	// 不允许null元素
	if (e == null)
		throw new NullPointerException();
	/*
	 * 将head指针减1并与数组长度减1取模
	 * 因为element.length一定是2的幂,2的幂-1的二进制从低位起是一串1,高位都是0
	 * 初始时head = 0,0 - 1 = -1 ,-1 & 15 = 15,此时head = 15
	 *      下一次 15 - 1 = 14,14 & 15 = 14,此时head = 14
	 *      再下一次 14 - 1 = 13,13 & 15 = 13,此时head = 13
	 *      ...
	 * 这是为了防止数组到头了边界溢出。
	 * 最终如果到头了,且数组未满时,就从尾再向前,相当于循环利用数组。
	 * 即head指向的是当前队头元素。
	 */
	elements[head = (head - 1) & (elements.length - 1)] = e;
	// tail指向的是头元素的下一个位置。判断head == tail即判断数组是否满了,需要扩容。
	if (head == tail)
		// 从方法名可以看出,扩容为原数组长度2倍。
		doubleCapacity();
}

2.4.2. add(E e) & addLast(E e)

public boolean add(E e) {
    addLast(e);
    return true;
}

// 从队尾入队
public void addLast(E e) {
	// 不允许null元素
	if (e == null)
		throw new NullPointerException();
	// 初始时tail为0,直接入队,此时tail指向的是从队尾入队队列的头元素的下一个位置。
	elements[tail] = e;
	/*
	 * head指向的是队头元素的位置
	 * tail + 1指向队头的下一个元素,判断是否 == head,即判断数组是否满了。
	 * 即是否走扩容的逻辑。
	 */
	if ( (tail = (tail + 1) & (elements.length - 1)) == head)
		doubleCapacity();
}

2.4.3. offer(E e)

public boolean offer(E e) {
    return offerLast(e);
}

public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

public boolean offerLast(E e) {
    addLast(e);
    return true;
}

小结:
1. 入队有两种方式,从队列头或者从队列尾;
2. 如果容量不够了,直接扩大为两倍;
3. 通过取模的方式让头尾指针在数组范围内循环;
4. x & (len - 1) = x % len,使用 & 位运算的方式更快;

2.5. 扩容

private void doubleCapacity() {
	// assert:断言,判断head是否等于tail
	// 值为true时,程序从断言语句处继续执行
	// 值为false时,程序从断言语句处停止执行
	assert head == tail;
	// 头指针的位置
	int p = head;
	// 数组长度
	int n = elements.length;
	// 头指针离数组尾的距离
	int r = n - p; // number of elements to the right of p
	// 新长度为旧长度的两倍
	int newCapacity = n << 1;
	// 判断是否溢出
	if (newCapacity < 0)
		throw new IllegalStateException("Sorry, deque too big");
	// 新建新数组
	Object[] a = new Object[newCapacity];
	// 将旧数组head之后的元素拷贝到新数组中
	System.arraycopy(elements, p, a, 0, r);
	// 将旧数组下标0到head之间的元素拷贝到新数组中
	System.arraycopy(elements, 0, a, r, p);
	// 赋值为新数组
	elements = a;
	// head指向0,tail指向旧数组长度表示的位置
	head = 0;
	tail = n;
}

扩容这里迁移元素可能有点绕,请看下面这张图来理解:

2.6. 出队

2.6.1. poll() & pollFirst()

public E poll() {
    return pollFirst();
}

// 从队列头出队
public E pollFirst() {
	int h = head;
	@SuppressWarnings("unchecked")
	// 取队列头元素 (head指向的就是头元素)
	E result = (E) elements[h];
	// 如果队列为空,就返回null
	if (result == null)
		return null;
	// 将队列头置为空
	elements[h] = null;
	// 队列头指针右移一位
	head = (h + 1) & (elements.length - 1);
	// 返回出队的元素
	return result;
}

2.6.2. pollLast()

// 从队列尾出队
public E pollLast() {
	// 尾指针左移一位 因为通过addLast()我们可以知道,tail指向的是头元素的下一个位置
	int t = (tail - 1) & (elements.length - 1);
	@SuppressWarnings("unchecked")
	// 取当前尾指针处元素
	E result = (E) elements[t];
	// 如果队列为空返回null
	if (result == null)
		return null;
	// 将当前尾指针处置为空
	elements[t] = null;
	// tail指向新的尾指针处
	tail = t;
	// 返回出队的元素
	return result;
}

2.7. 删除元素

此处的 remove 和 poll,前者会抛出异常,后者不会。

public E remove() { // 当作队列时默认头部删除
	return removeFirst();
}

public E poll() { // 默认头部删除
	return pollFirst();
}

public E removeFirst() {
    E x = pollFirst(); 
    if (x == null) // 此时不存在会抛出异常
        throw new NoSuchElementException(); // 没有此元素异常
    return x;
}

public E removeLast() {
	E x = pollLast();
	if (x == null)
		throw new NoSuchElementException();
	return x;
}

public E pollFirst() {
	int h = head; // 保存头部
	@SuppressWarnings("unchecked") // 未选中 告诉编译器忽略 unchecked 警告信息,意思可以是null元素,编译期会通过,因为deque接口不允许有空值
	E result = (E) elements[h]; // 保存这个元素
	// Element is null if deque empty 如果deque为空,则元素为null
	if (result == null)
		return null;
	elements[h] = null;     // Must null out slot 垃圾回收
	head = (h + 1) & (elements.length - 1); // 头指向指向下一个
	return result; // 返回这个值
}

public E pollLast() { // 尾部删除
	int t = (tail - 1) & (elements.length - 1); // 因为tail指向的是最后一个元素的下一个空位置,所以得先找到最后一个元素
	@SuppressWarnings("unchecked") // 可以是空
	E result = (E) elements[t];
	if (result == null)
		return null;
	elements[t] = null;
	tail = t;
	return result;
}
public E pop() { // 当作栈时默认头部出栈
	return removeFirst();
}

从队列头部开始和尾部开始删除指定元素

// 从队列头向后遍历移除指定元素
public boolean removeFirstOccurrence(Object o) {
    if (o == null)
        return false;
    int mask = elements.length - 1; // 保存数组长度 mask即掩码
    int i = head; // 保存头部
    Object x; // x用于保存待删的元素
    while ( (x = elements[i]) != null) { // 从前往后遍历数组
        if (o.equals(x)) { // 如果相等
            delete(i); // 删除i
            return true;
        }
        i = (i + 1) & mask;
    }
    return false;
}

// 从队列尾向前遍历移除指定元素
public boolean removeLastOccurrence(Object o) {
	if (o == null)
		return false;
	int mask = elements.length - 1;
	int i = (tail - 1) & mask;
	Object x;
	while ( (x = elements[i]) != null) { // 从后往前遍历数组
		if (o.equals(x)) {
			delete(i);
			return true;
		}
		i = (i - 1) & mask;
	}
	return false;
}

private void checkInvariants() { // 有效性检查,
	assert elements[tail] == null; // 判断是否尾部为空,tail位置没有元素
	// 如果head == tail说明数组为空,令head位置为空,否则头部有元素,tail-1有元素
	 assert head == tail ? elements[head] == null :
		(elements[head] != null &&
		 elements[(tail - 1) & (elements.length - 1)] != null);
	 // head-1位置没有元素
	assert elements[(head - 1) & (elements.length - 1)] == null;
}

private boolean delete(int i) { // 删除 i位置元素
	checkInvariants(); // 校验不变量  有效性检查
	final Object[] elements = this.elements; // 定义一个新数组保存旧数组
	final int mask = elements.length - 1;
	final int h = head;//保存头部
	final int t = tail;//保存尾部
	final int front = (i - h) & mask; // i位置前的元素个数
	final int back  = (t - i) & mask; // i位置后的元素个数

	// Invariant: head <= i < tail mod circularity
	// 不变的是: 头小于i小于tail 保证循环性
	// 再次检验,如果i到头部的距离大于等于尾部到头部的距离,表示当前队列已经被修改了,通过最开始检测,i是不应该满足该条件。
	if (front >= ((t - h) & mask)) // 如果i不在head和tail之间
		throw new ConcurrentModificationException();
	// 判断i靠近头还是尾,尽量移动较少元素
	// Optimize for least element motion
	if (front < back) { // 如果i靠近head
		if (h <= i) { // 在进行检测 h小于等于i 在i前面
			// 直接覆盖 比如 0 1 2 3 3是待删元素,覆盖后 0 0 1 2
			System.arraycopy(elements, h, elements, h + 1, front);
		} else { // Wrap around h大于i 在i后面
			System.arraycopy(elements, 0, elements, 1, i);
			elements[0] = elements[mask];
			System.arraycopy(elements, h, elements, h + 1, mask - h);
		}
		elements[h] = null;
		head = (h + 1) & mask;
		return false; // 返回false则是 从左往右移
	} else { // i靠近tail
		if (i < t) { // Copy the null tail as well i在tail前
			System.arraycopy(elements, i + 1, elements, i, back);
			tail = t - 1;
		} else { // Wrap around  i在tail后面
			System.arraycopy(elements, i + 1, elements, i, mask - i);
			elements[mask] = elements[0];
			System.arraycopy(elements, 1, elements, 0, t);
			tail = (t - 1) & mask;
		}
		return true; // 返回true则是从右往左
	}
}

2.8. 获取元素

peekFirst() 的作用是返回但不删除Deque首端元素,也即是head位置处的元素,直接返回elements[head]即可。
peekLast() 的作用是返回但不删除Deque尾端元素,也即是tail位置前面的那个元素。

public E element() { // 获取元素 默认获取队头
    return getFirst();
}

public E getFirst() {
	@SuppressWarnings("unchecked")
	E result = (E) elements[head];
	if (result == null)
		throw new NoSuchElementException();
	return result;
}

public E getLast() {
	@SuppressWarnings("unchecked")
	E result = (E) elements[(tail - 1) & (elements.length - 1)];
	if (result == null)
		throw new NoSuchElementException();
	return result;
}

public E peek() {
    return peekFirst();
}

@SuppressWarnings("unchecked")
public E peekFirst() {
	// elements[head] is null if deque empty
	return (E) elements[head];
}

@SuppressWarnings("unchecked")
public E peekLast() {
	return (E) elements[(tail - 1) & (elements.length - 1)];
}

2.9. 栈操作

前面我们介绍 Deque 的时候说过,Deque 可以直接作为栈来使用,那么 ArrayDeque 是怎么实现的呢?非常简单,看如下代码:

// 入栈
public void push(E e) {
	addFirst(e);
}

// 出栈
public E pop() {
	// 底层调用的还是pollFirst()
	return removeFirst();
}

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

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

相关文章

springweb+vue前后端分离开发,集成部署

背景&#xff1a; 在自己做测试的时候&#xff0c;由于需要项目和项目的前端页面使用同样接口访问&#xff0c;所以需要将前端代码部署到后端项目下。前端采用vue&#xff0c;后端采用springboot。 首先时建立一个vue项目&#xff0c;这个可以参照网上的案例&#xff0c;创建方…

思维模型 巴纳姆效应

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。 1 巴纳姆效应的应用 1.1 “占卜者”的故事 1 遇到占卜师的汤姆 汤姆是一位年轻的上班族&#xff0c;他一直对自己的未来感到迷茫和困惑。有一天&#xff0c;他在一家神秘的占卜店里遇到…

Python处理PDF——PyMuPDF的安装与使用详解

​​​​​​​ 1、PyMuPDF简介 1. 介绍 在介绍PyMuPDF之前&#xff0c;先来了解一下MuPDF&#xff0c;从命名形式中就可以看出&#xff0c;PyMuPDF是MuPDF的Python接口形式。 MuPDF MuPDF 是一个轻量级的 PDF、XPS和电子书查看器。MuPDF 由软件库、命令行工具和各种…

28 mysql 数据记录的 存储更新删除

前言 前面 我们探讨了 索引记录的 新增, 更新, 删除 这里 我们来看一下 mysql 的核心数据记录的 新增更新删除 这里 来看一下 增删改 的相关实现 数据记录 和 索引记录 的处理方式是一致的 mysql 数据记录的存储 新增部分参见 自增长主键的实现 以及 记录的插入 mysql…

解决提交到App Store时的ITMS-90478和ITMS-90062错误

目录 引言 正文 1. 什么是ITMS-90478和ITMS-90062错误&#xff1f; 2. 解决方法 2.1 确定当前的版本号和构建号 2.2 递增版本号和构建号 2.3 再次尝试提交应用 总结 参考资料 错误记录 摘要&#xff1a;本文为iOS技术博主分享&#xff0c;将详细介绍解决提交应用到App…

QCustomPlot添加标题头

1 效果图 2 代码 is2Label QString("add title layout element");// add title layout element:ui->customplot->plotLayout()->insertRow(0);ui->customplot->plotLayout()->addElement(0, 0, new QCPTextElement(ui->customplot, xAxis2Lab…

设备巡检管理系统有什么用?企业如何提高生产效率和生产安全?

在当今工业生产领域&#xff0c;设备巡检的重要性不言而喻。然而&#xff0c;传统巡检方式存在的诸多问题&#xff0c;如数据不规范、漏检误检等&#xff0c;严重制约了企业生产效率和产品质量。为解决这一问题&#xff0c;我们推出了一款设备巡检管理系统——“的修”工单管理…

宝塔Nginx配置反向代理后如何配置跨域?

直接将 27行更换成如下配置即可 上图有示例 if ( $static_fileAUWz2fmi 0 ){add_header Cache-Control no-cache;add_header Access-Control-Allow-Origin "$http_origin";add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS;add_header Acc…

ASEMI肖特基二极管MBR10100CT参数,MBR10100CT封装

编辑-Z MBR10100CT参数描述&#xff1a; 型号&#xff1a;MBR10100CT 最大重复峰值反向电压VRRM&#xff1a;100V 最大RMS电压VRMS&#xff1a;70V 最大直流阻断电压VDC&#xff1a;100V 最大平均正向电流IF&#xff1a;10A 峰值正向浪涌电流IFSM&#xff1a;150A 最大…

使用java代码调用超融合登录接口,跟着敲就完事,官方文档也没有的资源~

一、准备工作 接口地址&#xff1a; 获取公钥&#xff1a;/vapi/json/public_key 登录&#xff1a;/vapi/json/access/ticket 登陆接口和 API 调用说明&#xff1a; 这部分步骤均为官网文档&#xff0c;最后步骤则是本地测试用户列表接口 1、第一步&#xff1a;获取公钥 注意&…

Echarts自定义柱状图

目录 效果图 echarts官网找相似图 将柱状图引入html页面中 自定义柱状图 将不需要的属性删除 ​编辑 修改图形大小 grid 不显示x轴 ​编辑 不显示y轴线和相关刻度 ​编辑 y轴文字的颜色设置为自己想要的颜色 修改第一组柱子相关样式&#xff08;条状&#xff09; …

Richard Stallman 正在与癌症作战

导读为了纪念 GNU 项目成立 40 周年&#xff0c;自由软件基金会&#xff08;FSF&#xff09;已计划在 10 月 1 日&#xff08;即GNU 40&#xff09;为家庭、学生以及美国的其他人群组织一场黑客马拉松活动。 活动之前&#xff0c;GNU 项目于 9 月 27 日迎来了 40 岁生日&#…

GraalVM入门教程

GraalVM入门教程 GraalVM是什么几个名词介绍native-imagegu 基础环境搭建linux下载GraalVM配置环境变量下载安装AOT编译器 native-image安装gcc、glibc-devel 与 zlib-devel下载maven并配置环境变量 windows下载GraalVM配置环境变量下载安装AOT编译器 native-image下载配置wind…

阿里云服务器的购买方式有哪些?哪些实例规格的云服务器价格相对便宜一点?

对于还没购买过阿里云服务器的用户来说&#xff0c;如何购买阿里云服务器以及现在购买哪些实例规格的云服务器价格比较便宜是用户比较关心的&#xff0c;购买阿里云服务器的方式主要有四种&#xff0c;而目前价格比较便宜的云服务器实例规格有轻量应用服务器和和经济型、通用算…

【ROS 2 基础-常用工具】-6 Rviz基础使用

所有内容请查看&#xff1a;博客学习目录_Howe_xixi的博客-CSDN博客

图形学--shading着色

概念&#xff1a;对不同物体应用不同材质 llumination shading着色 Blinn-Phong reflectance model Diffuse Reflection 漫反射 光打到了shading point上然后光线四面八方均匀散射 光源会根据光源传播的距离进行一定的能量衰减 按球的表面积来算&#xff0c;吸收的能量和光…

分类预测 | Matlab实现WOA-GRU鲸鱼算法优化门控循环单元的数据多输入分类预测

分类预测 | Matlab实现WOA-GRU鲸鱼算法优化门控循环单元的数据多输入分类预测 目录 分类预测 | Matlab实现WOA-GRU鲸鱼算法优化门控循环单元的数据多输入分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现WOA-GRU鲸鱼算法优化门控循环单元的数据多输入…

Gradio的重要函数以及一些代码示例学习(一)

1 重要函数解析 1.1 Interface()类 参考&#xff1a; interface 最常用的基础模块构成。 应用界面&#xff1a;gr.Interface(简易场景), gr.Blocks(定制化场景)输入输出&#xff1a;gr.Image(图像), gr.Textbox(文本框), gr.DataFrame(数据框), gr.Dropdown(下拉选项), gr.…

AI-新人类电商人的AI课,用世界先进的AI帮助电商降本增效

课程下载&#xff1a;https://download.csdn.net/download/m0_66047725/88448518 比如用语言模型写短视频文案、脚本&#xff0c;用生图模型做场景、拍摄、换装&#xff0c;用数字人做直播短视频 AI电商课程目录 注意&#xff1a;由于AI发展太快&#xff0c;新内容可能不在…

如何提升多个微信个人号管理效率?

一、管理多个微信个人号 微信扫码即可登录到系统&#xff0c;支持登录多个账号&#xff0c;可以聚合聊天&#xff0c;设置快捷回复语&#xff0c;提高回复的效率&#xff0c;不用来回切换界面&#xff0c;减少封号的风险。 二、监管管理多个账号 微信登陆在系统&#xff0c;所…