java数据结构之HashMap

news2024/11/29 8:53:18

目录

前言

1、初始化

        1.1、初始化

        1.2、插入第一条数据

2、数组 + 链表

        2.1、插入数据:没有hash冲突

        2.2、插入数据:Key不同,但产生hash冲突

        2.3、插入数据:Key相同

3、数组 + 红黑树

        3.1、链表如何转化为红黑树?

        3.2、为什么要转化为红黑树?

4、数组resize

5、后记


前言

做过java开发的同学应该都知道HashMap是使用数组+链表的数据结构进行数据的存储,后来演变到jdk8之后,为了提高插入和查询效率,在插入的数据超过某个阈值之后,会将数组+链表的结构转化成数组+红黑树的数据结构,也即如下图:

HashMap 数据结构示意图

最近闲来无事,准备将HashMap插入数据的过程,以及其数据结构的转化过程,再回顾一下,故写此篇文章,以是记录。

1、初始化

        对于HashMap,在我们初始化以及插入第一条数据的时候,内部发生了什么事情呢?

        1.1、初始化

                当我们执行了如下一个初始化操作:

HashMap<String,String> map = new HashMap<String,String>();

                从源代码层面看,只是对内部一个叫做loadFactor的属性进行了初始化0.75,那么这个loadFactor参数是做什么用的呢?

                我们前言中说过,HashMap是用的一个数组+链表的形式进行数据的存储,那么这个数组的长度是如何决定的呢?什么时候要对数组进行扩充,什么时候又要对数组进行缩减呢?那么这个判断依据就是根据loadFactor来决定的。具体来说就是:当插入数据的个数 / 数组的长度 >= loadFactor时,我们就要对数组的长度进行扩展啦。因为当插入的数据越来越大的时候,在数组长度不变的情况下,hash冲突会越来越严重,数组节点下的数据就会越来越多,进而导致查询效率越来越差。

        另外需要说明的是:在扩展数组的时候,数组的长度都是2^n个,在初始化的时候,数组长度是2^4 = 16,每一次扩展,都增加一倍。

        1.2、插入第一条数据

                当我们执行如下操作,插入第一条数据的时候,hashmap的内部又发生什么事情了呢?

                map.put("chenza","30")

                第一步:先计算字符串"chenza"的hash值,作为判断插入数据哪个节点的依据。

                第二步:  初始化数组表table,如上所述,第一次初始化的时候,数组表table的长度是2^4 = 16

                第三步:用"chenza"的hash值 和 数组表table的长度(n - 1) 做"且"运算,判断当前数据应该插入到数组的哪个位置。(注:做且运算的原因保证插入的位置是[0,n-1]之间,不会数组越界)

                第四步:size字段+1,并判断是否超阈值threshold,如果超过阈值threshold,则进行数组table的扩展,且以后每插入一条数据,都会进行阈值判断。(注:这里的threshold = 当前的数组table长度 * loadFactor,与上节描述一致)

代码执行逻辑如下:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true); //计算key的hash值
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length; //初始化数组table,长度16
        if ((p = tab[i = (n - 1) & hash]) == null)  //计算数据应该插入的位置
            tab[i] = newNode(hash, key, value, null);  //在数组第i个位置插入数据
        else {
            ....... //暂时省略
        }

        ++modCount;
        if (++size > threshold)  //size记录插入的数据的个数,并判断是否超阈值。
            resize();
        ......

至此,我们的初始化及第一条数据插入已经完成。从上所述,我们得知,我们一共做了三方面的工作:

(1)、初始化参数loadFactor

(2)、完成数组table的初始化,将其长度置为2^4 = 16

(3)、将数据插入到数组table对应的节点上。

到目前位置,我们的HashMap的结构如下:

map.put("chenza","30")

 

2、数组 + 链表

        现在,在我们的HashMap中,已经有了一条数据,那么我们接下来插入第二条、第三条...第n条数据,看看HashMap中到底经历了怎样的过程。

        2.1、插入数据:没有hash冲突

                何为hash冲突,在HashMap中所说的hash冲突,指的是:i = hash(key) & (n - 1)之后,数组table在i处已经有数据,代表两个不同的key需要放到数组的同一个节点下。

                当没有hash冲突时,和插入第一条数据一样,直接将数据插入到数组对应的节点即可。

                无hash冲突时的代码执行逻辑:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);  //无hash冲突,直接将数据插入到数组中
        else {
            ......
        }

此时的数据结构如下:

        

 

        2.2、插入数据:Key不同,但产生hash冲突

               当Key不同,但是产生hash冲突的情况下,就会产生链表操作了。此时会顺着冲突处的数组节点的链表一个一个的去找,直到找到叶子节点位置,将新数据作为新的叶子节点挂在链表下面,如下图所示:

         

 

        当然在寻找叶子节点的过程中,如果发现当前数组节点下的链表长度超过一定的阈值时,就会自动将链表转化为红黑树进行存储(转化为红黑树的操作,参见第三节数组+红黑树 )。HashMap默认的链表长度不能超TREEIFY_THRESHOLD = 8.代码逻辑如下:

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;
	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); //如果链表长度超过8,则将链表转化为红黑树
				break;
			}
			if (e.hash == hash &&
				((k = e.key) == key || (key != null && key.equals(k))))
				break;
			p = e;
		}
	}

        2.3、插入数据:Key相同

                key相同,hash(Key)自然相同,自然也会有hash冲突。所以key相同是hash冲突的一种特殊情况。

                第一种情况:新数据的key和数组table的节点key相同,则直接替换数组table节点的value值即可。

                

 

                第一种情况:新数据的key和链表下某个节点的key相同,遍历链表,替换key相同处的value值即可。 

                

   代码执行逻辑:

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;  // 和数组table节点的值相同,直接替换value即可,value的替换见最下面的代码e.value = value
	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) { //在这里进行了e的赋值,value的替换见最下面的代码e.value = value
				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;  // 循环遍历链表,寻找key值相同的链表节点,找到直接挑出循环。
			p = e;
		}
	}


    if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;  //替换key相同的value
                afterNodeAccess(e);
                return oldValue;
            }

3、数组 + 红黑树

        至此,我们在每个数组节点下插入的数据都不超过8个,所以一直是链表的形式进行数据的存储。

        在第二节中,我们也预埋了伏笔,当数组table某个节点下的链表超过8个以后,HashMap就会调用treeifyBin函数,将链表转化成红黑树的结构,本节,我们就来看看他是如何转化为红黑树的,以及为什么要转化为红黑树?

        3.1、链表如何转化为红黑树?

         链表转化为红黑树的过程,就写在treeifyBin函数中,我们先看一下这块儿代码,然后再逐个分析。

//链表转化为红黑树的过程代码
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

         第一步:转化成红黑树的条件: n = (tab.length) < MIN_TREEIFY_CAPACITY(64),也就是说当数组table的长度小于64时,不会使用红黑树,只会对数组table进行扩展,重新分配数据的存放位置。

         第二步:当数组table长度大于64时,才开启转化红黑树的执行逻辑。代码中有一个while循环,注意这个while循环只是把链表节点Node转化为红黑树的节点TreeNode,这个循环中并不进行生成红黑树。这就引入一个问题,他是如何把Node转化为TreeNode的呢?其实从继承关系中可以看到TreeNode是继承自Node,TreeNode和Node的结构如下,从图上看,TreeNode比Node多了几个属性:parent,left,right,prev,red    

      

         第三步:当把所有的Node节点转化为TreeNode节点之后,继续调用hd.treeify(tab),这个才是真正的把链表转化为红黑树。

         

 

        3.2、为什么要转化为红黑树?

         (1)、从链表和红黑树的结构就可以看到,红黑树的层级更低,在进行查询时,链表的查询时间复杂度是O(n),红黑树的查询时间复杂度是O(log(n)),在数据量很大的情况下,显然红黑树的查询效率比链表要好的多。

         (2)、我们知道,平衡二叉树的结构更加平衡,左右子树的高度差不会超过1,为什么不使用平衡二叉树呢? 原因是我们还要考虑插入数据的性能,平衡二叉树的结构的确更加均衡,但是为了维持这种均衡,在插入数据时,需要不断的进行平衡二叉树的旋转调整,插入效率相比红黑树来说,差的多。根据权威资料显示:插入操作是红黑树的优势之一,红黑树的插入操作可以在 O(log(n)) 的时间内完成,而平衡二叉树的插入操作在最坏情况下需要 O(n)的时间。

4、数组resize

        最后,我想给大家重放一下数组resize的过程,为什么呢?在我们实际使用HashMap的过程中,如果数据量很大,免不了要进行数组的resize。resize不仅仅要对数组table进行扩展,还要对数组table节点上下挂的数据进行重新分配,所以是一个非常消耗性能的过程。

        有同学可能要问了,为什么要对数组table节点上下挂的数据进行重新分配,让他们安静的待在原来的节点下不是挺好的吗?为什么要翻来覆去的去折腾?对于这样的同学,送给你三个字:"要多想"

        原因很简单,数组table进行了扩展,那么数组的长度n也发生了变化,我们判断数据放在数组哪个节点之下用的是 hash(key) & (n - 1),现在数组table长度n变了,数据存放的位置自然也要发生变化,不然查询key时定位的数组节点位置不一样,无法从HashMap中查出key对应的数据。

        另外在走读源码的过程中,发现一个很有意思的设计:那就是原来一个数组节点下的链表(红黑树)数据,在进行resize之后,数据会被重新分配到几个新的节点上呢?答案是两个。这个点在没看源代码之前是没有想到的,看了源代码之后,感觉这个点设计的太精妙了。原因是什么呢?待我慢慢道来。

        我们知道,数组table的长度每次resize都是扩展一倍,且长度还都是2^n,也就是说数组table的长度只会是16,32,64,128,256...;而数据存放到数组哪个节点是有由hash(key) & (length - 1) 来决定的。在这两个条件下,就会发生上述一个节点数据分裂成两个节点数据的情况。我们以扩展前数组table长度是16和扩展后数组table长度是32为例,来说明具体原因。

        hash(key) 是不会变的,变的只有length - 1;

        当length = 16时,length - 1就是15,15的二进制是00001111,

        当length = 32时,length - 1就是31,31的二进制是00011111,

        看出来什么特殊之处了吗?对,就是31的二进制比15的二进制的上一位多了一个1,这就导致(hash(key) & 31) (hash(key) & 15) ,要么相等(数据保留在数组原位置),要么最高位多一个1(也就是数组位置+16);

        32扩展到64; 64扩展到128... 原理类似。

        看到这里,真是不得不感叹二进制世界的奇妙和jdk编程人员对二进制的运用达到了炉火纯青的地步。

        所以在扩展前后一个数组节点下的数据的分裂过程如下:       

 

        下面是代码层面链表一分二的过程(代码里有作者注释)(另:红黑树的分裂过程类似,也是将一个红黑树分裂成两个红黑树,在这里就不赘述了,大家可以看((TreeNode<K,V>)e).split(this, newTab, j, oldCap)中的split函数,里面也是两个变量loHead 和 hiHead分别存放分裂后的两个红黑树)

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //扩展前的数组长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length; 
        //扩展前的插入数据的阈值(数组长度 * 0.75)
        int oldThr = threshold; 						   
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) { //如果数组长度已经大于1 << 32,不再进行扩展,直接返回。
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // newCap = oldCap << 1,左移一位,相当于扩展一倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                //插入数据的阈值也扩展一倍。
                newThr = oldThr << 1; 
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        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 = 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 TreeNode)
                        //如果是红黑树,则进行红黑树的分裂
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 
                    else {//如果是链表,则进行链表的分裂
						/**
						 *由分析知,一个链表只可能分裂成两个链表。
						 *loHead:存放的是位置不动的数据
						 *hiHead:存放的是位置+16(以分析为例)的数据
						*/
                        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;
							//j不动,loHead还存放在原来的数组位置
                            newTab[j] = loHead; 
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
							//j + 16(以分析为例),hiHead存放在j + 16位置
                            newTab[j + oldCap] = hiHead; 
                        }
                    }
                }
            }
        }
        return newTab;
    }

5、后记

        断断续续写了好久,一边走读源码,一边画图,一边写文章,终于把HashMap的内部结构分析完了。相对其他java数据结构来说,HashMap结构算是稍微比较复杂的,但是HashMap并不是多线程安全的,在多线程使用的情况下,要注意线程安全问题。

        了解了HashMap之后,其实也就了解了HashSet,因为HashSet就是内部构建了一个HashMap对象,使用了HashMap的key不会重复的特性来进行数据的去重。

        另外本篇文章旨在探究HashMap的结构,对其中的红黑树并没有过多着墨进行进一步深究,因为红黑树的生成条件、插入数据时节点的旋转、红黑节点的变化等等,还是比较复杂的,如果写在这篇文章中,有点鸠占鹊巢的意思。所以,红黑树的内容完全可以单独抽离一篇文章进行探究,就不在HashMap文章中进行赘述了,后面有时间,我会再学习一下红黑树,再来跟大家唠叨唠叨。

        

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

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

相关文章

Postman测试实践笔记

Postman测试实践 文章目录 Postman测试实践一、Postman安装与使用1.1 Postman下载及安装1.1.2 Postman Mac版 1.2 Postman 更新1.2.1 mac 版更新 1.3 Postman 其他问题 二、网络相关知识2.1 接口2.1.1 软件为什么需要接口 2.2 接口测试2.2.1 什么是接口测试&#xff1a;2.2.2 为…

VTK下载并安装

去官网下载https://vtk.org/download/ 选择最新稳定版本 然后点击source后边的压缩包进行下载。 下载完成后将其解压到特定的文件夹下&#xff0c;然后打开cmake-gui.exe&#xff0c;第一行选择刚刚解压的文件夹&#xff0c;这个文件夹下有一个CMakeLists.txt文件&#xff0c…

【6. 激光雷达接入ROS】

欢迎大家阅读2345VOR的博客【6. 激光雷达接入ROS】&#x1f973;&#x1f973;&#x1f973; 2345VOR鹏鹏主页&#xff1a; 已获得CSDN《嵌入式领域优质创作者》称号&#x1f47b;&#x1f47b;&#x1f47b;&#xff0c;座右铭&#xff1a;脚踏实地&#xff0c;仰望星空&#…

Go | 一分钟掌握Go | 8 - 并发

作者&#xff1a;Mars酱 声明&#xff1a;本文章由Mars酱编写&#xff0c;部分内容来源于网络&#xff0c;如有疑问请联系本人。 转载&#xff1a;欢迎转载&#xff0c;转载前先请联系我&#xff01; 前言 当今编程界&#xff0c;一个好的编译型语言如果不支持并发&#xff0c…

工控老司机告诉你热电偶和RTD的区别

热电偶和热电阻都是温度传感器&#xff0c;但它们的原理、功能特性和应用场景有所不同。 一、原理区别 首先&#xff0c;热电偶是利用两种不同金属之间的热电效应来测量温度的。其原理是利用温度差引起的金属之间的热电势差进行测量。两种金属之间存在一种热电势&#xff08;…

Yolov8优化:卷积变体---分布移位卷积(DSConv),提高卷积层的内存效率和速度

论文: https://arxiv.org/pdf/1901.01928v1.pdf 摘要:提出了一种卷积的变体,称为DSConv(分布偏移卷积),其可以容易地替换进标准神经网络体系结构并且实现较低的存储器使用和较高的计算速度。 DSConv将传统的卷积内核分解为两个组件:可变量化内核(VQK)和分布偏移。 通过…

双亲委派机制的原理和作用

双亲委派机制&#xff0c;就必须弄清楚Java的类加载器。 什么是类加载器 Java类加载器(ClassLoader)是Java运行时环境(JRE)的一部分&#xff0c;负责动态的将Java类加载到Java虚拟机的内存空间。 类加载器有哪些 主要有三个&#xff1a; 引导类加载器(Bootstrap ClassLoade…

前端开发在本地开发与后台进行联调阶段时,接口自动重定向https、HSTS 与 307 状态码

开发者在本地开发与后台进行联调阶段时&#xff0c;Chrome 浏览器上出现 307 状态码&#xff0c;并跳转到 https 版 但是 307 代码是什么含义呢&#xff1f;页面又为何会出现 307 状态码呢&#xff1f;我之前都没见过这个状态码&#xff0c;查了才知道原来它也是一种重定向。 …

数字三角形+包子凑数(蓝桥杯JAVA解法)

数字三角形&#xff1a;用户登录 题目描述 上图给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径&#xff0c;把路径上面的数加起来可以得到一个和&#xff0c;你的任务就是找到最大的和&#xff08;路径上的每一步只可沿左斜线向下或右斜线向下走…

ArduPilot之开源代码Sensor Drivers设计

ArduPilot之开源代码Sensor Drivers设计 1. 源由2. Sensor Drivers设计2.1 front-end / back-end分层2.2 设计思想分析 3 实例理解3.1 驱动初始化3.2 业务应用代码3.3 frond-end代码3.3 back-end代码3.3.1 UART3.3.2 I2C3.3.3 SPI 4. 参考资料 1. 源由 飞控代码除了最为基础的…

《美团机器学习实践》读后感和一点思考

前言&#xff1a;最近拜读了美团算法团队出品的《美团机器学习实践》&#xff0c;这本书写于2018年&#xff0c;一个大模型还没有标配的时代。这本书侧重于工业界的实践&#xff0c;能清楚地让我们了解到工业界和学术界对机器学习的关注方向上的差异&#xff0c;值得一读。因为…

文件系统和软硬链接

文章目录 一.文件系统1.了解磁盘的物理结构2.磁盘的存储结构a.磁盘读取 3.磁盘的逻辑结构a.为什么操作系统不直接使用CHS地址&#xff1f;b.实际IO一次的大小 4.磁盘的分区管理4.1.ext文件系统a.文件查找b.文件删除 4.2目录的属性和数据 二.软硬链接软链接的建立和删除软链接的…

【VM服务管家】VM4.0软件使用_1.4 通讯类

目录 1.4.1 通讯管理&#xff1a;ModBus通信发送非整型数据的方法1.4.2 通讯管理&#xff1a;使用Modbus TCP通讯协议与流程交互 1.4.1 通讯管理&#xff1a;ModBus通信发送非整型数据的方法 描述 环境&#xff1a;VM4.0.0 现象&#xff1a;Modbus通信发送数据只能为Int类型&a…

快速搭建Electron+Vite3+Vue3+TypeScript5脚手架 (无需梯子,快速安装Electron)

一、介绍 &#x1f606; &#x1f601; &#x1f609; Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需…

【网课平台】Day13.订单支付模式:生成支付二维码与查询支付

文章目录 一、需求&#xff1a;生成支付二维码1、需求分析2、表设计3、接口定义4、接口实现5、完善controller 二、需求&#xff1a;查询支付结果1、需求分析2、表设计与模型类3、接口定义4、接口实现步骤一&#xff1a;查询支付结果步骤二&#xff1a;保存支付结果&#xff08…

如何写出一份大厂都不会拒绝的简历?

你好&#xff0c;我是宋光璠&#xff0c;今天我以过来人的身份教你写出一份惊艳面试官的简历。 简历算是我们过去经历的一个缩影&#xff0c;虽然只有短短一两页&#xff0c;但也能让人从中发现你的优点&#xff0c;一份优质的简历更是如此&#xff0c;所以今天我就带你从头到…

PLC模糊PID(梯形图实现)

博途PLC的模糊PID控制详细内容请查看下面的博客文章: Matlab仿真+博途PLC模糊PID控制完整SCL源代码参考(带模糊和普通PID切换功能)_博途怎么实现模糊pid_RXXW_Dor的博客-CSDN博客模糊PID的其它相关数学基础,理论知识大家可以参看专栏的其它文章,这里不再赘述,本文就双容…

网络安全常用术语

肉鸡 肉鸡指的就是被黑客成功入侵并取得控制权限的电脑。黑客们可以随意的控制肉鸡&#xff0c;就像在使用自己的电脑一样&#xff0c;很形象的比喻&#xff0c;就像是养的肉鸡&#xff0c;任黑客宰杀和利用。关键的是&#xff0c;在成为肉鸡后&#xff0c;只要黑客不对电脑进…

【VM服务管家】VM4.x算子SDK开发_3.4 控件嵌入类

目录 3.4.1 图片存储&#xff1a;图片保存的方法3.4.2 辅助十字线&#xff1a;给图像添加辅助十字线的方法3.4.3 控件调用&#xff1a;在WPF中使用Winform控件的方法3.4.4 图形改变事件&#xff1a;渲染控件上图形改变事件的实现方法3.4.5 鼠标事件&#xff1a;渲染控件上鼠标事…

Hive的基本操作和查询语法以及案例(大数据查询)

1、 13-Hive的基本操作和查询语法以及案例_hive分区表查询语句_大数据下的画像人的博客-CSDN博客 2、SQL 中多个 and or 的组合运算 SQL 中多个 and or 的组合运算_weixin_30611509的博客-CSDN博客sql关系型运算符优先级高到低为&#xff1a;not >and> orAND、OR运算符…