HashTable源码解析

news2024/9/24 16:23:59

HashTable源码解析

简介

HashTable 是一个古老的(JDK1.0 时就已存在)线程安全的容器,其核心方法都是 synchronized 修饰的。

相反 HashMap 不是线程安全的。

HashTable与HashMap对比

二者继承体系

HashTable

20210119205607375

HashMap

20210119205714438

从图中可以对比得出,二者都是源于 Map 接口,都实现了 Cloneable 和 Serializable接口,二者都可以克隆和序列化。

但 HashMap 的父类是 AbstractMap,HashTable父类是 Dictionary。

Dictionary 类是一个已经被废弃的类(见其源码中的注释)。父类被废弃,自然其子类 Hashtable 也用的比较少了。

image-20221205160206599

elments() & contains()

HashTable 比 HashMap 多提供了 elments()contains() 两个方法。

  • elments() 方法继承自父类 Dictionnary。

    • elements() 方法用于返回此 Hashtable 中的 value 的枚举。
  • contains() 方法判断该 Hashtable 中是否包含传入的 value。

    • 它的作用与 containsValue() 一致。
    • 事实上,containsValue() 就只是调用了一下 contains() 方法。
    // 判断HashTable中是否存在value,存在返回true,否则返回false
    public synchronized boolean contains(Object value) {
        if (value == null) {
            throw new NullPointerException();
        }
        Entry<?,?> tab[] = table;
        for (int i = tab.length ; i-- > 0 ;) {
            for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
                if (e.value.equals(value)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    public boolean containsValue(Object value) {
        return contains(value);
    }
    

key 和 value 是否可以为 null

  • HashTable 不允许 key 或者 value 为 null。

        public synchronized V put(K key, V value) {
            // value不能为null,否则抛出空指针异常
            if (value == null) {
                throw new NullPointerException();
            }
            // Makes sure the key is not already in the hashtable.
            Entry<?,?> tab[] = table;
            // key不可以为null,因为当key为null时候,null调用hashCode()会报空指针异常
            int hash = key.hashCode();
            int index = (hash & 0x7FFFFFFF) % tab.length;
            @SuppressWarnings("unchecked")
            Entry<K,V> entry = (Entry<K,V>)tab[index];
            for(; entry != null ; entry = entry.next) {
                if ((entry.hash == hash) && entry.key.equals(key)) {
                    V old = entry.value;
                    entry.value = value;
                    return old;
                }
            }
            addEntry(hash, key, value, index);
            return null;
        }
    
  • HashMap 允许存在一个 key 为 null 的 Entry,但是 value 为 null 的 Entry 的个数没有限制。

    	static final int hash(Object key) {
            int h;
        	// 如果key为null那么哈希值为0
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    

线程安全

  • Hashtable 是 线程安全 的,它的每个方法中都加入了 synchronized 关键字。在多线程并发的环境下,可以直接使用 Hashtable,不需要自己为它的方法实现同步。
  • HashMap 是 线程不安全 的,在多线程并发的环境下,可能会产生死锁等问题。使用 HashMap 时就必须要自己增加同步处理。

虽然 HashMap 不是线程安全的,但是它的效率会比 Hashtable 要好很多。这样设计是合理的。在我们的日常使用当中,大部分时间是单线程操作的。HashMap 把这部分操作解放出来了。当需要多线程操作的时候可以使用线程安全的 ConcurrentHashMap

ConcurrentHashMap 也是线程安全的,它的效率比 Hashtable 要高好多倍。因为 ConcurrentHashMap 使用了分段锁,并不对整个数据进行锁定。

初始容量和扩容大小

  • Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2 n + 1 2n+1 2n+1
  • HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 2 2 倍。

创建时,如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小。也就是说 Hashtable 会尽量使用素数、奇数。而 HashMap 则总是使用 2 的幂作为哈希表的大小。

之所以会有这样的不同,是因为 Hashtable 和 HashMap 设计时的侧重点不同。

  • Hashtable 的侧重点是哈希的结果更加均匀,使得哈希冲突减少。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。
  • 而 HashMap 则更加关注 hash 的计算效率问题。在取模计算时,如果模数是 2 的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。HashMap 为了加快 hash 的速度,将哈希表的大小固定为了 2 的幂。

当然这引入了哈希分布不均匀的问题,所以 HashMap 为解决这问题,又对 hash 算法做了一些改动。这从而导致了 Hashtable 和 HashMap 的计算 hash 值的方法不同。

hash值

为了得到元素的位置,首先需要根据元素的 key 计算出一个 hash值,然后再用这个 hash 值来计算得到最终的位置。

Hashtable 直接使用对象的 hashCode。hashCode 是 JDK 根据对象的地址或者字符串或者数字算出来的 int 类型的数值。然后再使用除留余数法来获得最终的位置。

image-20221205172656506

在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。

HashMap 为了提高计算效率,将哈希表的大小固定为了 2 的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。

HashMap 的效率虽然提高了,但是 hash 冲突却也增加了。因为它得出的 hash 值的低位相同的概率比较高。

为了解决这个问题,HashMap 重新根据 hashcode 计算 hash 值后,又对 hash 值做了一些运算来打散数据。使得取得的位置更加分散,从而减少了 hash 冲突。当然了,为了高效,HashMap 只做了一些简单的位处理。从而不至于把使用 2 的幂次方带来的效率提升给抵消掉。

image-20221205172507415

源码解析

属性

    // 底层的Entry[]数组
	private transient Entry<?,?>[] table;
	
	// 元素(Entry节点)个数
    private transient int count;
	
	// 扩容阈值 (判断是否需要扩容 threshold = 哈希表长度 * 加载因子)
    private int threshold;
	
	// 加载因子
    private float loadFactor;
	
	/*
	 * Java中的一种fail-fast(快速失败)机制,每次添加或删除元素(修改不会)modCount都会+1,
	 * 然后使用迭代器遍历时会先讲modCount的值赋给expectedModCount,然后在遍历的时候会检查两者是否还相同
	 * (不相同说明在遍历期间有其他线程添加或者删除了元素,这时就会抛出ConcurrentModificationException异常)
	 */
    private transient int modCount = 0;

内部类Entry

    private static class Entry<K,V> implements Map.Entry<K,V> {
        // key的hash值
        final int hash;
        final K key;
        V value;
        // 产生hash冲突时要形成链表,next节点
        Entry<K,V> next;		
        
        protected Entry(int hash, K key, V value, Entry<K,V> next) {
            this.hash = hash;
            this.key =  key;
            this.value = value;
            this.next = next;
        }
        // ...
    }

构造方法

双参构造方法

   	/*
   	 * @param initialCapacity:初始化容量
   	 * @param loadFactor:加载因子
   	 * 就是根据传入的初始容量构造一个Entry数组,然后计算扩容阈值。
   	 */
	public Hashtable(int initialCapacity, float loadFactor) {
        // 判断是否合法
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);
		
        // 传入的初始容量为0,赋值为1
        if (initialCapacity==0)
            initialCapacity = 1;
        
        this.loadFactor = loadFactor;
        
        // 初始化Entry数组赋值给table
        // 直接根据传入的initialCapacity大小创建table
        table = new Entry<?,?>[initialCapacity];
        
        /*
         * MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
         * 阈值 = 数组长度 * 加载因子,这里跟INF - 7取一个min。
         */
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

其它构造器,调用的还是上面的双参构造方法。

	// 传入初始容量,调用的还是上面的双参构造器,加载因子默认为0.75。
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

	// 空参构造器,默认的初始容量为11,加载因子为0.75。
    public Hashtable() {
        this(11, 0.75f);
    }
	
	// 传入一个Map,默认的初始容量为max(2 * t.size(), 11),加载因子为0.75。
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

小总结

  • 这里跟 HashMap 的区别就是,HashMap 调用无参构造器时,不会初始化 Entry 数组(懒加载),只会为加载因子赋值为0.75,只有第一次 put 时才会创建 Entry 数组,且默认的数组长度为 16
  • HashTable 调用无参构造器时,就直接创建一个长度为 11 的 Entry 数组。

put(K key, V value)

  	/*
  	 * 同步方法
  	 */
	public synchronized V put(K key, V value) {
        // value不允许为null
        if (value == null) {
            throw new NullPointerException();
        }
        
        Entry<?,?> tab[] = table;
        // 获取key的hashCode值
        int hash = key.hashCode();
        /*
         * 寻址算法
         * 0x7FFFFFFF = Integer.MAX_VALUE
         * (hash & 0x7FFFFFFF)的作用是将hash变为一个正整数
         * 直接对table.length进行取余,得到的值的范围就在[0, len - 1]。
         */
        int index = (hash & 0x7FFFFFFF) % tab.length;
        
        @SuppressWarnings("unchecked")      
        // 获取index位置的entry
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        // 遍历桶位,查找当前key是否已经存在于桶位的链表中
        for(; entry != null ; entry = entry.next) {
            // hash值相等 并且 key的equals()结果也相等,进行替换
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                // 替换
                entry.value = value;
                // 返回旧的value
                return old;
            }
        }
		// 能走到这里,说明当前桶位中没有key相同的entry,需要将当前entry插入进去。
        addEntry(hash, key, value, index);
        // 返回null
        return null;
    }
			||
			\/
    /*
  	 * 真正插入节点的方法
  	 */
    private void addEntry(int hash, K key, V value, int index) {
        // 添加操作 modCount + 1
        modCount++;

        Entry<?,?> tab[] = table;
        
        // count = 当前哈希表中的元素个数,大于扩容阈值,所以需要扩容。
        if (count >= threshold) {
            // 扩容操作,下面详细分析。
            rehash();
           	/*
           	 * 扩容之后,当前entry寻址后的index会发生变化
           	 * 所以重新计算。
           	 */
            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

      	// 获取当前index桶位的头结点
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index]; 
        // 将当前的Entry插入到桶位的头结点,它的next节点是e(原头结点)
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

put() => addEntry() 流程总结

  • 前置条件:key 和 value 都不允许为 null
  • 调用 key 的 hashCode 获取 hash 值,对 length 取余寻址,获取桶位索引 index;
  • 遍历当前桶位,判断是否存在相同 key 的 entry,存在直接替换 value,然后返回旧的 value;
  • 不存在,调用 addEntry(),判断是否需要扩容,需要扩容就去扩容,然后重新寻;
  • 获取寻址后的桶位 index,将当前 Entry 直接插入到桶位头节点。

流程图

在这里插入图片描述

与 HashMap 的几点区别

  • HashTable 中 Entry 的 key 的最终的 hash 值就是其 hashCode()方法的返回值,而 HashMap 中的 Entry 的 key 的最终的 hash 值是 hashCode ^ (hashCode >>> 16)
  • HashTable 的寻址算法为 (hash % tab.length),而 HashMap 的寻址算法是 (hash & tab.length - 1)

rehash()

  • 扩容方法,相当于 HashMap 中的 resize() 方法。
    @SuppressWarnings("unchecked")
    protected void rehash() {
        // 原哈希表长度
        int oldCapacity = table.length;
        // oldMap引用原哈希表
        Entry<?,?>[] oldMap = table;

        /*
         * 一般情况:扩容为原oldCapacity * 2 + 1
         */ 
        int newCapacity = (oldCapacity << 1) + 1;
        
  		// 情况很少,一般size不会是INF级别的
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        
        // 根据newCapacity创建一个新Entry数组
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
		
        modCount++;
        // 重新计算扩容阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        // table引用新的Entry数组
        table = newMap;
        /*
         * 遍历原哈希表,将所有的entry重新寻址插入到新的哈希表中
         */
        for (int i = oldCapacity ; i-- > 0 ;) {
            // 遍历每一个桶位
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                // e指向当前节点
                Entry<K,V> e = old;
                // old向后走
                old = old.next;
                // 重新寻址
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                // 当前节点指向桶位头结点
                e.next = (Entry<K,V>)newMap[index];
                // 桶位头节点变为当前节点,完成插入操作。
                newMap[index] = e;
            }
        }
    }

remove(Object key)

    /*
     * 同步方法,删除元素。
     * 寻址,然后遍历桶位寻找待删除节点,找到后直接删除即可。
     */
	public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        /*
         * 获取hash值然后寻址
         */
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 获取桶位头结点
        Entry<K,V> e = (Entry<K,V>)tab[index];
        /*
         * prev指向当前节点的前驱节点 e指向当前节点
         */
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            // 找到了要删除的entry
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                // prev != null 表示e非头结点
                if (prev != null) {
                    // 直接将e干掉
                    prev.next = e.next;
                // e是头结点
                } else {
                    // 将头结点变为e的next
                    tab[index] = e.next;
                }
                // 元素个数-1
                count--;
                // 获取value
                V oldValue = e.value;
                // 将e.value置为null,help GC 
                e.value = null;
                // 将旧值返回
                return oldValue;
            }
        }
        // 不存在 返回null
        return null;
    }

get(Object key)

	/*
	 * 同步方法,获取指定key的value。
 	 */
    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        // 寻址
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 遍历当前桶位
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            // 查找成功,返回value。
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        // 查找失败,返回null。
        return null;
    }

总结

  • Hashtable 是线程安全的容器(方法都加了synchronized),底层只有 (数组 + 链表)
  • Hashtable 不允许 key 或者 value 为 null
  • Hashtable 存在 fast-fail 机制,modCount 实现;
  • hash 值是 key 的 hashCode() 的返回值;
  • 寻址算法 hash % table.length
  • 调用无参构造器默认初始容量为11,加载因子为0.75
  • 一般情况下,扩容为 oldCap * 2 + 1



参考文章

  • shstart7_Hashtable源码解析
  • 兴趣使然的草帽路飞_JDK集合源码之HashTable解析

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

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

相关文章

零基础的小白如何学习编程,该怎么入手学习?

零基础的小白如何学习编程&#xff0c;该怎么入手学习&#xff1f;这是一个被问烂透而有很有趣的话题了。听到这个问题时&#xff0c;小编的第一反应就是要弄清楚对方为什么要学习编程&#xff0c;这是一个很好地起点&#xff0c;清楚自己想要什么&#xff0c;才能去努力实现。…

【JY】 ABAQUS子程序UEL的有限元原理与应用

不等待即关注【简述ABAQUS中UEL子程序】ABAQUS作为成熟的商用有限元软件&#xff0c;可为高级用户提供特定的分析需求。ABAQUS常见的二次开发子程序包括&#xff1a;UMAT、VUMAT、UGENS、UEL和VUEL等。其中UEL/VUEL分别适用于ABAQUS的Standard/Explicit求解器。只有清楚有限元分…

零基础怎么学Python编程,新手常犯哪些错误?

Python是人工智能时代最佳的编程语言&#xff0c;入门简单、功能强大&#xff0c;深获初学者的喜爱。 很多零基础学习Python开发的人都会忽视一些小细节&#xff0c;进而导致整个程序出现错误。下面就给大家介绍一下Python开发者常犯的几个错误。 1、错误的使用变量。 在Pyt…

华为网工入门之eNSP小实验(5)--VLAN间相互通信的三种方法

VLAN间相互通信 实际网络部署中一般会将不同IP地址段划分到不同的VLAN。同VLAN且同网段的PC之间可直接进行通信&#xff0c;无需借助三层转发设备&#xff0c;该通信方式被称为二层通信。VLAN之间需要通过三层通信实现互访&#xff0c;三层通信需借助三层设备(路由器,三层交换…

高可用系列文章之二 - 传统分层架构技术方案

前文链接 高可用系列文章之一 - 概述 - 东风微鸣技术博客 (ewhisper.cn) 三 技术方案 3.1 概述 单点是系统高可用最大的风险和敌人&#xff0c;应该尽量在系统设计的过程中避免单点。 保障系统的高可用, 方法论上&#xff0c;高可用保证的原则是「集群化」(或 「冗余」), …

LeetCode HOT 100 —— 312.戳气球

题目 有 n 个气球&#xff0c;编号为0 到 n - 1&#xff0c;每个气球上都标有一个数字&#xff0c;这些数字存在数组 nums 中。 现在要求你戳破所有的气球。戳破第 i 个气球&#xff0c;你可以获得 nums[i - 1] * nums[i] * nums[i 1] 枚硬币。 这里的 i - 1 和 i 1 代表和 i…

别只关注chatGPT能不能写论文了,它还支持49中场景,代码都给你写好了,速领

简介 chatGPT最近非常不稳定&#xff0c;访问一不小心就出现了网络错误&#xff0c;根本就不能很好的使用。那么我们该怎么办呢&#xff1f;勇哥给大家想到了一个种办法&#xff0c;就是用程序去调用openapi的接口&#xff0c;这个接口虽然是收费的&#xff0c;但是可免费使用…

linux下源码编译cloudcompare(解决无法加载pcd文件的问题)

cloudcompare是一款点云处理软件&#xff0c;里面有很多算法&#xff0c;值得大家学习研究。 下面介绍linux下源码编译cloudcompare的方法。 1.安装依赖&#xff1a; sudo apt-get install doxygen sudo apt install cmake-curses-gui2.下载&#xff1a; git clone --recurs…

Qt之天气预报——界面优化篇(含源码+注释)

一、界面优化效果 下方为界面优化完成和优化前的效果对比。 优化前&#xff1a; 优化后&#xff1a; 二、优化内容 添加标题栏添加图片&#xff08;图图标素材源自阿里巴巴矢量图标库&#xff09;更新UI内容&#xff08;微调大小、布局比例&#xff09;添加鼠标事件函数&…

Java 教程

Java 教程 Java 是由 Sun Microsystems 公司于 1995 年 5 月推出的高级程序设计语言。 Java 可运行于多个平台&#xff0c;如 Windows, Mac OS 及其他多种 UNIX 版本的系统。 本教程通过简单的实例将让大家更好的了解 Java 编程语言。 移动操作系统 Android 大部分的代码采用…

RepVGG:一个结构重参数化网络

本文来自公众号“AI大道理” ResNet、DenseNet 等复杂的多分支网络可以增强模型的表征能力&#xff0c;使得训练效果更好。但是多分支的结构在推理的时候效率严重不足。 看起来二则不可兼得。 能否两全其美&#xff1f; RepVGG通过结构重参数化的方法&#xff0c;在训练的时候…

2022 年 Kubernetes 高危漏洞盘点

2022 年&#xff0c;Kubernetes继续巩固自己作为关键基础设施领域的地位。从小型到大型组织&#xff0c;它已成为广受欢迎的选择。出于显而易见的原因&#xff0c;这种转变使 Kubernetes 更容易受到攻击。但这还没有结束&#xff0c;开发人员通常将Kubernetes 部署与其他云原生…

【2022.12.18】备战春招Day13——每日一题 + 234. 回文链表 + 139. 单词拆分

【每日一题】1703. 得到连续 K 个 1 的最少相邻交换次数 题目描述 给你一个整数数组 nums 和一个整数 k 。 nums 仅包含 0 和 1 。每一次移动&#xff0c;你可以选择 相邻 两个数字并将它们交换。 请你返回使 nums 中包含 k 个 连续 1 的 最少 交换次数 输入&#xff1a;nums …

【数据结构】堆(二)——堆排序、TOP-K问题

作者&#xff1a;一个喜欢猫咪的的程序员 专栏&#xff1a;《数据结构》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 目录 堆排序&#xff1a;&#xff08;以小堆为例&#xff09; Heapsort函数…

C语言重点解剖关键字要点速记

1.在windows中&#xff0c;双击的本质是运行该程序&#xff0c;就是将程序(.exe)加载到内存当中去。任何程序在被运行之前都必须加载到内存当中去。 2.所有的变量本质都是在内存的某个位置开辟的。变量不能定义在硬盘上&#xff0c;因为变量必须在程序运行的时候才能被开辟&am…

SPRING-了解1

1)最终address: 查找路径比较长&#xff0c;很有趣 JFrog 原始步骤1)进入 spring.io&#xff0c;点击右上角黑色标记边的标记 2)进入 git,找到 Binaries下面的 Spring Framework Artifacts 3)进一步找到Downlaoding a Distribution,下面有 https://repo.spring.io 4)x选择…

牛客SQL每日一题之SQL136 每类试卷得分前3名

文章目录SQL136 每类试卷得分前3名描述示例1输入&#xff1a;输出&#xff1a;答案SQL136 每类试卷得分前3名 描述 现有试卷信息表examination_info&#xff08;exam_id试卷ID, tag试卷类别, difficulty试卷难度, duration考试时长, release_time发布时间&#xff09;&#x…

C# 中的 Infinity 和 NaN

C# 语言中&#xff0c;对于 int&#xff0c;long 和 decimal 类型的数&#xff0c;任何数除以 0 所得的结果是无穷大&#xff0c;不在 int&#xff0c;long 和 decimal 类型的范围之内&#xff0c;所以计算 6/0 之类的表达式会出错。 但是&#xff0c;double 和 float 类型实际…

Java入门笔记补充

Java基础笔记补充数据类型数据类型整形拓展浮点型拓展字符型拓展类型转换变量变量作用域局部变量实例变量静态变量变量的命名规范数组三种初始化静态初始化动态初始化数组的默认初始化Arrays类内存分析栈 stack&#xff1a;堆 heap&#xff1a;方法区(也是堆)&#xff1a;封装继…

卷积神经网络 图像处理,卷积神经网络图片处理

1、在做图像处理时&#xff0c;如何提高识别算法的设计与效果的精度&#xff1f; 得到更多的数据 这无疑是最简单的解决办法&#xff0c;深度学习模型的强大程度取决于你带来的数据。增加验证准确性的最简单方法之一是添加更多数据。如果您没有很多训练实例&#xff0c;这将特…