HashMap底层原理

2023-05-16

文章目录

      • 1.HashMap的概念
      • 2.底层数据结构
      • 2.JDK1.8之前存在的问题?
      • 3.问题:加载因子为什么默认值为0.75f ?
      • 4.问题:如果得到key的hash值(哈希码)
      • 5.问题:如何得到插入元素在数组中的下标
      • 6.问题: 链表转换为红黑树的条件?
      • 7.HashMap如何实现序列化和反序列化?
      • 8.put源码
      • 9.get源码
      • 10.resize源码

1.HashMap的概念

HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)HashMap不保证映射的顺序,特别是它不保证该顺序恒久不变。
在这里插入图片描述

  • HashMap和Hashtable的区别?添加链接描述

2.底层数据结构

HashMap底层采用哈希表结构(数组+链表、JDK1.8后为数组+链表+红黑树)实现,结合了数组和链表的优点:

数组优点: 通过数组下标可以快速实现对数组元素的访问,效率极高;

链表优点: 插入或删除数据不需要移动元素,只需修改节点引用,效率极高。

HashMap图示如下:
在这里插入图片描述

HashMap内部使用数组存储数据,数组中的每个元素类型为Node<K,V>:

//Node包含了四个字段:hash、key、value、next,其中next表示链表的下一个节点。
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 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;
  }
}

2.JDK1.8之前存在的问题?

JDK1.8之前,HashMap底层实现用的是数组+链表。(JDK1.8以后用数组+链表+红黑树)。

HashMap通过hash方法计算key的哈希码,然后通过(n-1)&hash 公式(n为数组长度,初始化数组默认长度为16),得到key在数组中存放的下标。

当两个key在数组中存在的下标一致时(哈希冲突,哈希碰撞),数据将以链表的方式存储。

在链表中查找数据必须从第一个元素开始一层一层往下找,直到找到为止,时间复杂度为O(N),所以当链表长度越来越长时,HashMap的效率越来越低。

哈希码如何计算?添加链接描述


如何解决?
加入红黑树
当链表中的元素超过8个(TREEIFY_THRESHOLD)并且数组长度大于64(MIN_TREEIFY_CAPACITY)时,会将链表转换为红黑树,转换后数据查询时间复杂度从O(N)变为O(logN)。

红黑树的节点使用TreeNode表示:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { 
   TreeNode<K,V> parent;  // red-black tree links    
   TreeNode<K,V> left;    
   TreeNode<K,V> right;    
   TreeNode<K,V> prev;    // needed to unlink next upon deletion    
   boolean red;    
   TreeNode(int hash, K key, V val, Node<K,V> next) {        
   super(hash, key, val, next);   
   }   
   ...
 }

HashMap包含几个重要的变量:

// 数组默认的初始化长度16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 数组最大容量,2的30次幂,即1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认加载因子值
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表转换为红黑树的长度阈值
static final int TREEIFY_THRESHOLD = 8;

// 红黑树转换为链表的长度阈值
static final int UNTREEIFY_THRESHOLD = 6;

// 链表转换为红黑树时,数组容量必须大于等于64
static final int MIN_TREEIFY_CAPACITY = 64;

// HashMap里键值对个数
transient int size;

// 扩容阈值,计算方法为 数组容量*加载因子
int threshold;

// HashMap使用数组存放数据,数组元素类型为
Node<K,V>transient Node<K,V>[] table;

// 加载因子
final float loadFactor;

// 用于快速失败,由于HashMap非线程安全,
//在对HashMap进行迭代时,如果期间其他线程的参与
//导致HashMap的结构发生变化了(比如put,remove等操作),
//直接抛出ConcurrentModificationException异常
transient int modCount;

3.问题:加载因子为什么默认值为0.75f ?

加载因子也叫扩容因子,用于决定HashMap数组何时进行扩容。比如数组容量为16,加载因子为0.75,那么扩容阈值为数组容量加载因子,160.75=12,即HashMap数据量大于等于12时,数组就会进行扩容。

我们都知道,数组容量的大小在创建的时候就确定了,所谓的扩容指的是重新创建一个指定容量的数组,然后将旧值复制到新的数组里。扩容这个过程非常耗时,会影响程序性能。

所以加载因子是基于容量和性能之间平衡的结果:
当加载因子过大时,扩容阈值也变大,也就是说扩容的门槛提高了,这样容量的占用就会降低。但这时哈希碰撞的几率就会增加,效率下降;
当加载因子过小时,扩容阈值变小,扩容门槛降低,容量占用变大。这时候哈希碰撞的几率下降,效率提高。
可以看到容量占用和性能是此消彼长的关系,它们的平衡点由加载因子决定,0.75是一个即兼顾容量又兼顾性能的经验值。

4.问题:如果得到key的hash值(哈希码)

通过hash方法计算key的哈希码添加链接描述


5.问题:如何得到插入元素在数组中的下标

HashMap通过hash方法计算key的哈希码,然后通过(n-1)&hash 公式(n为数组长度,初始化数组默认长度为16),得到key在数组中存放的下标。


6.问题: 链表转换为红黑树的条件?

长度阈值大于8,数组容量必须大于等于64

7.HashMap如何实现序列化和反序列化?

存储数据的table字段使用transient修饰,通过transient修饰的字段在序列化的时候将被排除在外,那么HashMap在序列化后进行反序列化时,是如何回复数据的呢?HashMap通过自定义的readObject/writeObject方法自定义序列化和反序列化操作。这样做出于以下两点考虑:

1)table一般不会存满,即容量大于实际键值对个数,序列化table未使用的部分不仅考虑时间也浪费空间;
2)key对应的类型如果没有重写hashCode方法,那么它将调用 Object的hashCode方法,该方法为native方法,在不同的JVM下实现可能不同;换句话讲,同一个键值在不同的JVM环境下,在table中存储的位置可能不同,那么在反序列化table操作时可能会出错。
所以在HashXXX类中(),我们可以看到,这些类用于存储数据的字段都用transient修饰,并且都自定义了readObject/writeObject方法。

8.put源码

put操作过程总结:

  1. 判断HashMap数组是否为空,是的话初始化数组(由此可见,在创建HashMap对象的时候并不会
    直接初始化数组);
  2. 通过 (n-1) & hash 计算key在数组中的存放索引;
  3. 目标索引位置为空的话,直接创建Node存储;
  4. 目标索引位置不为空的话,分下面三种情况:
    4.1. key相同,覆盖旧值;
    4.2. 该节点类型是红黑树的话,执行红黑树插入操作;
    4.3. 该节点类型是链表的话,遍历到最后一个元素尾插入,如果期间有遇到key相同的,则直接覆
    盖。如果链表长度大于等于TREEIFY_THRESHOLD(8),并且数组容量大于等于
    MIN_TREEIFY_CAPACITY(64),则将链表转换为红黑树结构;
  5. 判断HashMap元素个数是否大于等于threshold(扩容阀值 0.75*数组初始化长度16),是的话,进行扩容操作。(扩容为原来的两倍)
    put方法源码如下:
public V put(K key, V value) {    r
eturn putVal(hash(key), key, value, false, true);
}

put方法通过hash函数计算key对应的哈希值,hash函数源码如下:

static final int hash(Object key) {    
      int h;    
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }

对于 32 位的处理器,会分成高(左边的) 16 位和低(右边的) 16 位int类型的数值是4个字节的,右移16位异或可以同时保留高16位于低16位的特征

如果key为null,返回0,不为null,则通过(h = key.hashCode()) ^ (h >>> 16)公式计算得到哈希值。该公式通过hashCode的高16位异或低16位得到哈希值,主要从性能、哈希碰撞角度考虑,减少系统开销,不会造成因为高位没有参与下标计算从而引起的碰撞。得到key对应的哈希值后,再调用putVal(hash(key), key, value, false, true)方法插入元素:

//put方法首先调用一个hash方法,计算出k的hash值
	//将k的hash值,key,value传进来
 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    //这里onlyIfAbsent如果为true,表明不能修改已经存在的值,因此我们传入false
    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是否为空,如果空的话,会先调用resize扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据当前key的hash值找到它在数组中的下标,判断当前下标位置是否已经存在元素,
		//若没有,则把key、value包装成Node节点,直接添加到此位置。
		//[i = (n - 1) & hash]相当于取模运算的位运算形式
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        	//如果当前位置已经有元素了(产生了哈希碰撞),分为三种情况。
            Node<K,V> e; K k;
            //1.当前位置元素的hash值等于传过来的hash,并且他们的key值也相等,
			//则把p赋值给e,后续需要做值的覆盖处理
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //2.如果当前是红黑树结构,则把它加入到红黑树 
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
           //3.此位置已存在元素,并且是普通链表结构,则采用尾插法,把新节点加入到链表尾部
            else {
                for (int binCount = 0; ; ++binCount) {
                	//如果头结点的下一个节点为空,则插入新节点
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                    //如果在插入的过程中,链表长度超过了8,则转化为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                    //插入成功之后,跳出循环到next
                        break;
                    }
                    //若在链表中找到了相同key的话,直接退出循环,跳转到next
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // next
            // 此时新值要覆盖旧值
            if (e != null) { 
                V oldValue = e.value;
                //用新值替换旧值,并返回旧值。
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果当前数组中的元素个数超过阈值,则扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

9.get源码

下面是get操作源码:

 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;
    // 判断数组是否为空,数组长度是否大于0,目标索引位置下元素是否为空,是的话直接返回null

    if ((tab = table) != null && (n = tab.length) > 0 &&
       (first = tab[(n - 1) & hash]) != null) {
        // 如果目标索引位置元素就是要找的元素,则直接返回
        if (first.hash == hash && // always check first node
           ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 如果目标索引位置元素的下一个节点不为空
        if ((e = first.next) != null) {
            // 如果类型是红黑树,则从红黑树中查找
            if (first instanceof TreeNode)
                return ((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;
}

10.resize源码

通过put源码分析我们知道,数组的初始化和扩容都是通过调用resize方法完成的:

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;
       }
        // 扩大容量为当前容量的两倍,但不能超过 MAXIMUM_CAPACITY
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
   }
    // 当前数组没有数据,使用初始化的值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 如果初始化的值为 0,则使用默认的初始化容量,默认值为16
        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"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 开始扩容,将新的容量赋值给 table
    table = newTab;
    // 原数据不为空,将原数据复制到新 table 中
    if (oldTab != null) {
        // 根据容量循环数组,复制非空元素到新 table
        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 TreeNode)
                    // 红黑树相关的操作
                   ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 链表复制,JDK 1.8 扩容优化部分
                    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;
                       }
                        // 原索引 + oldCap
                        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;
                   }
                    // 将原索引 + oldCap 放到哈希桶中
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                   }
               }
           }
       }
 

JDK1.8在扩容时通过高位运算 e.hash & oldCap 结果是否为0来确定元素是否需要移动,主要有如下两
种情况:
情况一:
扩容前oldCap=16,hash=5, (n-1)&hash=15&5=5 , hash&oldCap=5&16=0 ;
扩容后newCap=32,hash=5, (n-1)&hash=31&5=5 , hash&oldCap=5&16=0 。
这种情况下,扩容后元素索引位置不变,并且hash&oldCap==0。
情况二:
扩容前oldCap=16,hash=18, (n-1)&hash=15&18=2 , hash&oldCap=18&16=16 ;
扩容后newCap=32,hash=18, (n-1)&hash=31&18=18 , hash&oldCap=18&16=16 。
这种情况下,扩容后元素索引位置为18,即旧索引2加16(oldCap),并且hash&oldCap!=0。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

HashMap底层原理 的相关文章

  • LXC 介绍

    转自 xff1a https www cnblogs com xidongyu p 5767020 html LXC又名Linux container xff0c 是一种虚拟化的解决方案 xff0c 这种是内核级的虚拟化 主流的解决方案Xe
  • openstack原生网络和SDN网络对比

    1 原生Neutron架构图 xff1a 2 Networking odl的架构 3 ODL 处理过程 当OpenStack Neutron API接收到用户创建网络等操作请求 xff0c 它会调用ML2的相关方法 ML2已经定义了post
  • 树莓派 Raspberry Pi 3B+安装官方系统(一)

    两年前购买的树莓派 xff08 可看当时我的记录树莓派 3B 43 重装系统并配置 WiFi YouForever xff09 已经在角落里吃灰了好长时间 xff0c 一度以为已经损坏 xff0c 几天前试着拿出来折腾一下 xff0c 竟然
  • Rime——最好的输入法

    同步发布在博客 xff1a https www zhyong cn 9422 最近迷上了五笔输入法 xff0c 于是想找一款好用的五笔输入法软件 xff0c 最终确定鼎鼎大名的Rime输入法 一款跨平台的开源输入法 xff0c 支持Wind
  • Haar特征描述算子-人脸检测

    Haar特征描述算子 人脸检测 详细资料 3 1简介 Haar like特征最早是由Papageorgiou等应用于人脸表示 xff0c 在2001年 xff0c Viola和Jones两位大牛发表了经典的 Rapid Object Det
  • HTML(css+div)登录界面

    xff08 1 xff09 这是当时做的一个课程设计 xff0c 很多人想要图片 xff0c 在这里我把用到的所有图片资源分享下 xff08 2 xff09 链接 xff1a https pan baidu com s 1nUX1DQe a
  • 基于Matlab的GPU加速---for循环处理

    采用GPU加速时 xff0c 如遇for循环 xff0c 则很容易增加代码在GPU上运行的时间开销 在编程时 xff0c 使用矩阵和向量操作或arrayfun bsxfun pagefun替换循环操作来向量化代码 1 arrayfun函数
  • android ndk extern "C"

    C 43 43 的代码里面 xff1a extern 34 C 34 这是因为生成的二进制文件中 xff0c C和C 43 43 的符号表不相同造成的 Jni是按照C的生成规则去找函数的 xff0c 所以要加上extern C使编译器把函数
  • AdaBoost中利用Haar特征进行人脸识别算法分析与总结1——Haar特征与积分图

    目前因为做人脸识别的一个小项目 xff0c 用到了AdaBoost的人脸识别算法 xff0c 因为在网上找到的所有的AdaBoost的简介都不是很清楚 xff0c 让我看看头脑发昏 xff0c 所以在这里打算花费比较长的时间做一个关于Ada
  • 智能驾驶仿真场景构建技术

    随着汽车智能化程度的不断提高 xff0c 智能汽车通过环境传感器与周边行驶环境的信息交互与互联更为密切 xff0c 需应对的行驶环境状况也越来越复杂 xff0c 包括行驶道路 周边交通和气象条件等诸多因素 xff0c 具有较强的不确定性 难
  • Web应用程序的身份验证:Session认证、Token认证

    一 Web应用程序的身份验证 1 Session认证 用户向服务器发送用户名和密码 服务器验证通过后 xff0c 在当前对话 xff08 session xff09 里面保存相关数据 xff0c 如用户角色 xff0c 登陆时间等 服务器向
  • 计算机视觉: 物体分类,场景分类,事件分类

    主要总结一下最近看的几篇场景分类文献 xff0c 顺便总结场景 物体和事件分类的关系 1 ILSVRC 2015 Scene Classi cation Challenge 冠军 xff0c 主要贡献是Relay Backpropagati
  • CSS | 替换元素(可置换元素)

    替换元素 定义 xff1a 可替换元素是指元素内容的展现不是由 CSS 来控制的 xff0c 而是外观渲染独立于 CSS 的外部对象 详见 xff1a MDN 典型的可替换元素有 xff1a lt img gt lt iframe gt l
  • 输入一个int型整数,按照从右向左的阅读顺序,返回一个不含重复数字的新的整数

    输入一个int型整数 xff0c 按照从右向左的阅读顺序 xff0c 返回一个不含重复数字的新的整数 输入 xff1a 输出代码 xff1a 输入 xff1a 9876673 输出 37689 代码 xff1a int span class
  • 树莓派温度检测

    树莓派温度检测 一 shell命令 登录树莓派之后使用指令查看CPU温度 xff0c 依次输入以下指令 xff1a 进入目录 cd sys class thermal thermal zone0 查看温度 cat temp 树莓派返回 47
  • PX4学习(1)——PX4固件版本开发环境(ROS+mavros版本)

    参考px4官网流程 xff1a https dev px4 io en setup dev env linux ubuntu html https dev px4 io en setup building px4 html 配置过程中网速一
  • 一个操作,轻松迁移 Maven 至 Gradle

    今天我们来讲讲如何将 Maven 项目转换为 Gradle 项目 这个过程还是蛮简单的 xff0c 下面通过一个例子来说明怎么转换 Gradle 设置 如果没有安装 gradle xff0c 我们可以从 Gradle 官网下载最新的版本 x
  • 不同域名访问显示不同备案号

    lt a href 61 34 http beian miit gov cn 34 id 61 34 BeiAnHao 34 style 61 34 color 999999 34 target 61 34 blank 34 gt 鲁ICP
  • Word文档从第N页开始显示页眉或页脚的方法

    1 打开想要设置页眉页脚的word文档 2 假如想从第三页开始设置页眉 xff0c 就把光标选在在第二页末尾 xff0c 点击菜单栏 页面布局 xff0c 找到 分隔符 里子选项 分节符 xff0c 下一页 把文档分成两节 xff08 以O
  • xampp 访问出现New XAMPP security concept

    New XAMPP security concept Access to the requested directory is only available from the local network This setting can b

随机推荐

  • Runtime error 216 at xxx 故障解决一例

    故障现象 xff1a 部分Delphi项目在win7开发机上不能运行了 xff0c 提示Runtime error 216 at xxx错误信息 xff0c 但是将出错的EXE文件复制到别的win7和xp下 xff0c 均运行正常 解决办法
  • 启用了TRACE 和TRACK HTTP 方法,如何禁用?

    http wenku baidu com view 557d761ea8114431b90dd873 html http wenku baidu com view de1f4ad2195f312b3169a50d html http www
  • IP地址表示方法及网段子网掩码写法

    A类IP段 0 0 0 0 到127 255 255 255 B类IP段 128 0 0 0 到191 255 255 255 C类IP段 192 0 0 0 到223 255 255 255 XP默认分配的子网掩码每段只有255或0 xf
  • ftp连接错误——服务器发回了不可路由的地址。使用服务器地址代替。

    设置filezilla客户端的连接参数 选中某一连接项高级 加密 只使用普通ftp传输设置 传输模式 主动重新连接
  • VNC内网穿透--MAC控制windos

    提示 xff1a 文章写完后 xff0c 目录可以自动生成 xff0c 如何生成可参考右边的帮助文档 文章目录 前言SSH和VNCSSHSSH服务端SSH客户端 VNCVNC serverVNC viewer 内网穿透花生壳 前言 翻出很久
  • Excel 2016双击文件打开为空白的解决办法

    故障描述 xff1a 安装Office 2016后 xff0c 双击EXCEL文件打开后显示为空白 解决办法 xff1a 开始 运行 regedit 修改HKEY CLASSES ROOT Excel Sheet 12 shell Open
  • Linux 修改远程默认端口(22)

    如题 xff1a 在此前 xff0c 建议先查看redhat的release版本 xff0c CentOS 7的启动服务不同 xff1a more etc redhat release 正文 xff1a 1 编辑sshd配置 xff0c 修
  • pytorch环境配置(装cuda、cudnn)win10+cuda10.1+cudnn7.6.5+torch1.7.1 && 集显装pytorch

    为了装这个走了太多坑了 xff0c 所以想写一篇具体教程 xff0c 有缘人看吧 xff0c 希望能解决你的问题 xff08 第一次写文章啥也不懂 xff0c 万一冒犯了啥 xff0c 麻烦告知我改 xff09 我anaconda很早就装过
  • APM无人机软件在环仿真环境搭建

    题记 xff1a 最近做毕业设计得用到无人机仿真 xff0c 重操旧业 xff0c 搞一搞SITL仿真 给个传送门参考 xff1a 无人机SITL仿真 APM软件在环仿真 我的环境 xff1a ubuntu18 04虚拟机 1 官方教程 a
  • 无人机导航中的各类坐标系

    无人机中的各类坐标系学习笔记 xff1a 北东地坐标系 NED north east down 东北天 ENU east north up 机体坐标系 body frame 1 导航中的坐标系理解 重要参考 导航中 xff0c 最重要的两个
  • 有了Systick中断为什么还要PendSV中断?

    文章目录 问题 xff1a 原因 xff1a 1 在SysTick中断里完成任务切换会降低操作系统的实时性 xff1a 2 把systick优先级设置为最高把PendSV设置为最低的好处 xff1a 3 结语 xff1a 问题 xff1a
  • OpenCV/caffe安装流程

    公司正在做人脸识别系统 xff0c 用到了OpenCV库 xff0c 下面就是根据网上资料以及自己多次部署安装的经验整理的安装流程 xff0c 希望能给一些人一点参考 系统 xff1a Ubuntu1604 CPU架构 xff1a 一般是x
  • win10远程桌面连接ubuntu18.04

    一开始 xff0c 我是根据这个教程进行操作的 xff0c 改了设置为共享 xff0c 安装xrdp等 xff0c 一切都完成后 xff0c 当进行连接时 xff0c 也会出现那个xrdp连接界面 xff0c 但只要一登陆 xff0c 界面
  • 机器学习算法知识点整理

    1生成模型generative model和判别模型 discriminative model 已知输入变量x xff0c 生成模型通过对观测值和标注数据计算联合概率分布P x y 来达到判定估算y的目的 判别模型通过求解条件概率分布P y
  • 我的2014年总结——奔波的一年

    2014年 xff0c 发生了一些人生的大事 xff0c 这些事既有忧 xff0c 又有喜 这因为有这些事情的发生 xff0c 所以我们才越发成熟 xff0c 越发稳重 2014年技术的提升没有前2年那么突飞猛进了 xff0c 生活的事情也
  • 谈谈你对Spring Bean生命周期的理解【面试】

    前言 面试中经常会被问到Spring Bean的生命周期 xff0c 有些人说记不住 xff0c 看了一遍源码也是云里雾里的 xff0c 那是因为只看理论 xff0c 没有自己实践 xff0c 如果自己亲自写代码验证一下 xff0c 不管是
  • FreeRTOS初级篇----名称规范

    数据类型 TickType t xff1a FreeRTOS中断计数值类型 xff0c 可以是16位也可以是32位 xff0c 对于32位CPU来说TickType t最好为32位 BaseType t xff1a 是能够让CPU运行效率最
  • FreeRTOS初级篇----创建任务--动态创建、静态创建

    任务创建函数 xff1a xTaskCreate BaseType t span class token function xTaskCreate span span class token punctuation span TaskFun
  • Linux 性能测试与分析

    源自 http blog sina com cn s blog 71ad0d3f01019uzl html Linux 性能测试与分析 Revision History Version Date Author Description 1 0
  • HashMap底层原理

    文章目录 1 HashMap的概念2 底层数据结构2 JDK1 8之前存在的问题 xff1f 3 问题 xff1a 加载因子为什么默认值为0 75f xff1f 4 问题 xff1a 如果得到key的hash值 xff08 哈希码 xff0