手撕HashMap源码2

目录

引言 

putTreeVal红黑树添加结点方法讲解

treeifyBin进行树化的方法(虚假的树化)

treeify真正的树化操作

从扩容的部分来分析红黑树的代码

split红黑树扩容迁移的方法

untreeify链化(退树成链)

红黑树代码分析

rotateLeft|Right红黑树的左旋与右旋

balanceInsertion红黑树的插入结点调整

 balanceDeletion红黑树删除结点调整

常见的问题总结


引言 

之前写了1,1里面重点讲了初始化过程,扩容过程,讲了一下链表的迁移等处理,今天这篇文章重点放在树化处理上,也就是红黑树,建议在阅读这篇文章之前,先去看我写的文章叫树之手撕红黑树,彻底理解红黑原理再来看才能看懂红黑树操作的部分,如果不想弄懂红黑树的操作部分,那就直接看代码逻辑

先从初始化一张表的时候来分析,初始化一张表,都会走进resize()这个方法里面,但是并不会涉及到树的操作,涉及到树的操作部分应该是我们在调用putVal方法的时候,当我们插入的数据在哈希表上面有相同的位置的时候,但是key不一样,就会在一个桶位形成链表或者红黑树

其实就是下面这个位置

当前插入的桶位结点如果是一棵红黑树的实例,那么就按照红黑树的方式进行数据的插入

现在我们先追进putTreeVal方法里面

putTreeVal红黑树添加结点方法讲解

 /*** Tree version of putVal.* 红黑树插入结点的方法* 参数1:第一个是当前结点HashMap对象* 参数2:当前的哈希表* 后面的结点依次是哈希值,键值*/final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {//确定键的可比较类,当哈希值无法直接参与比较大小的时候//要通过这个参数去确定一下树里面是否存在一个相同的键,如果有就返回Class<?> kc = null;boolean searched = false;//是否已执行相同键的搜索//获取树的根结点TreeNode<K,V> root = (parent != null) ? root() : this;//循环遍历树并插入新的键值对for (TreeNode<K,V> p = root;;) {int dir, ph; K pk;//方向值,哈希值,键值//确定方向//结点的哈希值大,走左边,说明当前传入的结点小if ((ph = p.hash) > h)dir = -1;//结点的哈希值小,走右边,说明当前传入的结点大else if (ph < h)dir = 1;//这里就是键已经存在,直接返回当前这个值就行了else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;//上面一条路都没干进去,哈希值没比较出来,键也没比较出来//这里来说明一下comparableClassFor尝试获取键的可比较类,如果失败//返回null ,说明这里键不是可比较的,如果是这种情况,内部调用find方法//去查找是否有相同的键的存在,然后返回,避免重复插入//另外一种情况是,如果键的可比较类kc存在,就通过compareComparables方法//来比较键的大小,如果相等,dir返回0,内部还是调用find方法去找是否有相同的键值else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0) {if (!searched) {TreeNode<K,V> q, ch;searched = true;if (((ch = p.left) != null &&(q = ch.find(h, k, kc)) != null) ||((ch = p.right) != null &&(q = ch.find(h, k, kc)) != null))return q;}//这里是红黑树出现键相等的情况时 ,选择插入的方向问题dir = tieBreakOrder(k, pk);}//上面就给我们确定了方向,下面就是来放值//将当前结点保存为父节点为xpTreeNode<K,V> xp = p;//左边,或者右边有位置才会进来放值//一般在叶子结点,或者倒数第二层结点来放值,并把左边或者右边结点交给p来进行轮替if ((p = (dir <= 0) ? p.left : p.right) == null) {//创建一个新的树结点Node<K,V> xpn = xp.next;//保留next指针//添加了一个指向父亲的next指针xpnTreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);//连接上新结点xif (dir <= 0)xp.left = x;elsexp.right = x;//父亲的next指向了新的结点xxp.next = x;x.parent = x.prev = xp;if (xpn != null)//既然x的next是指向了xpn,那么xpn的prev就指向了x//这里的指向都是考虑为链表的next与prev((TreeNode<K,V>)xpn).prev = x;//插入后调整树的结构,将根结点放在桶位上,也就是链表的表头moveRootToFront(tab, balanceInsertion(root, x));return null;}}}

treeifyBin进行树化的方法(虚假的树化)

/*** Replaces all linked nodes in bin at index for given hash unless* table is too small, in which case resizes instead.* 将给定哈希值对应的索引处的链表结构转化为树结构* 除非哈希表过小,此时会选择进行扩容而不是树化*/final void treeifyBin(Node<K,V>[] tab, int hash) {//e是临时变量,用于遍历链表中的结点int n, index; Node<K,V> e;//n表示哈希表的长度,index表示计算出的索引//之前说了,一个桶位它是否进行树化,首先取决于//当前哈希表的元素是否大于默认值,也就是MIN_TREEIFY_CAPACITY//也就是是否大于64,小于64,不树化,直接进行扩容处理//所以这里其实也涉及到一个问题,当我们在对数据插入的时候,第一次在一个桶位上形成树是在什么时候//在数据进行putVal的时候,最开始的数据进来,有重复的索引位置//刚开始的时候肯定会形成链表,当这个桶位的结点数目的阈值大于了TREEIFY_THRESHOLD,//也就是8的时候,他会进入这个树化方法,也就是treeifyBin这个方法//当进入这个方法之后,他还会去判定整个链表的数据有没有达到64,如果没有达到64//就按照扩容进行处理//所以第一次形成一棵树的条件是,首先整张表的数据要达到64,其次链表里面的结点数目必须大于等于8if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();//那么这条路线肯定满足了树化的条件//注意这个点的这条路线已经在走链表了//这里判断其实多余,因为在putVal里面已经做了一次判断//下面就是把链表进行树化else if ((e = tab[index = (n - 1) & hash]) != null) {//定义一个树的头结点和树的尾结点TreeNode<K,V> hd = null, tl = null;//其实这里的循环就干了一件事儿,把每一个链表的结点//变成一棵树的结点,并且把树里面的prev与next指针按照链表的指向链接上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);//真正树化操作}}

treeify真正的树化操作

/*** Forms tree of the nodes linked from this node.* 进行树化的操作* 参数是一个哈希表* 这里是真正把left与right节点给挂上了*/final void treeify(Node<K,V>[] tab) {TreeNode<K,V> root = null;//x:拿到当前结点初始值//遍历整棵树的结点,this是拿到当前这个树结结点//其实这个操作就是整体循环了一遍链表for (TreeNode<K,V> x = this, next; x != null; x = next) {//获取当前结点的下一个结点next = (TreeNode<K,V>)x.next;//把当前结点左右结点都指向null,挂空x.left = x.right = null;//如果当前红黑树的根结点为空的时候,就把x变为根结点,颜色变黑if (root == null) {x.parent = null;x.red = false;root = x;}else {//获取插入结点的k值K k = x.key;//插入结点的hash值int h = x.hash;Class<?> kc = null;//从根结点开始遍历for (TreeNode<K,V> p = root;;) {int dir, ph;//dir:标记下个结点的方向 ph:当前结点的hash值K pk = p.key;//当前结点的key值//如果当前插入结点的hash值小于当前结点的hash值,下次查找向左查找if ((ph = p.hash) > h)dir = -1;//另外就是向右查找else if (ph < h)dir = 1;//如果上面都没比较出来,走下面的方法//或者jdk自带的方法进行比较else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0)dir = tieBreakOrder(k, pk);TreeNode<K,V> xp = p;//保留当前p结点//如果p结点左右有位值,也就是左边或者右边等于null的情况,就开始挂结点//如果左右都有结点,不等于null,然后往下面走if ((p = (dir <= 0) ? p.left : p.right) == null) {//x的父亲指向px.parent = xp;//根据方向来挂结点if (dir <= 0)xp.left = x;elsexp.right = x;//调整平衡root = balanceInsertion(root, x);break;}}}}//把头结点放到表的头部moveRootToFront(tab, root);}

上面就是整个表第一次会在一个桶位形成红黑树的分析。

从扩容的部分来分析红黑树的代码

如果发现这个节点是一个红黑树的结点,就会按照红黑树的方式进行结点的迁移

 那么我们追进去看一下split

split红黑树扩容迁移的方法

/*** Splits nodes in a tree bin into lower and upper tree bins,* or untreeifies if now too small. Called only from resize;* see above discussion about split bits and indices.** @param map the map* @param tab the table for recording bin heads* @param index the index of the table being split* @param bit the bit of hash to split on* 对几个参数做一些说明*  第一个参数是当前哈希map对象*  第二个参数是新的表*  第三个参数是当前表第一个结点在表里面的索引*  第四个参数是原来表的长度*/final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {TreeNode<K,V> b = this;//当前hash桶下标所在的第一个结点// Relink into lo and hi lists, preserving order//还是分为低位链表与高位链表来处理1TreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null;int lc = 0, hc = 0;//这里是高位表和低位表的计数//把当前结点交给e//还定义了一个中间结点nextfor (TreeNode<K,V> e = b, next; e != null; e = next) {next = (TreeNode<K,V>)e.next;//把下一个结点交给next,每次一次循环完,在把next交给e//把当前结点的next给挂空,因为已经交给next了e.next = null;//还是与原来的长度&,高位为0还是低链表//大体和链表的处理方式一样,只是这里有一个prev指针指向前面的一个结点if ((e.hash & bit) == 0) {if ((e.prev = loTail) == null)//这里的e.prev是给了一个指向前面结点的指针loHead = e;//第一次loHead指向第一个结点elseloTail.next = e;loTail = e;++lc;}else {if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.next = e;hiTail = e;++hc;}}//循环完了之后//下面就是判断进行树化还是不树化if (loHead != null) {//如果小于6,就退树成链if (lc <= UNTREEIFY_THRESHOLD)tab[index] = loHead.untreeify(map);//还是按照链表的方式处理else {tab[index] = loHead;//这里为什么判断hiHead不等于null//这里本身是已经树化的,所以从某种情况来讲,他不需要再次被树化//但是如果链表被拆为了高位链表和低位链表,就要重新进行树化if (hiHead != null)//把这个表重新进行树化loHead.treeify(tab);}}//同样的处理方式if (hiHead != null) {if (hc <= UNTREEIFY_THRESHOLD)tab[index + bit] = hiHead.untreeify(map);else {tab[index + bit] = hiHead;if (loHead != null)hiHead.treeify(tab);}}}

我们去注意一下下面这个位置

这个位置就是退树成链的方法,当高位结点或者低位结点里面的链表数据小于等于链化阈值,就退树成链

untreeify链化(退树成链)

这个方法是在扩容的时候,我们需要迁移红黑树的时候的处理方法

/*** Returns a list of non-TreeNodes replacing those linked from* this node.* 红黑树退化成链表的方法* 传入的map*/final Node<K,V> untreeify(HashMap<K,V> map) {//hd一个链表的表头,tl一个链表的表尾Node<K,V> hd = null, tl = null;//这里简单来讲是循环这棵树for (Node<K,V> q = this; q != null; q = q.next) {//把每一棵树变成一个链表结点,这里返回一个Node类型的结点Node<K,V> p = map.replacementNode(q, null);//下面把链表连接起来if (tl == null)hd = p;elsetl.next = p;tl = p;}//返回这个链表的表头return hd;}

红黑树代码分析

rotateLeft|Right红黑树的左旋与右旋

 /* ------------------------------------------------------------ */// Red-black tree methods, all adapted from CLR//左旋//参数是一个根结点,一个旋转结点static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,TreeNode<K,V> p) {TreeNode<K,V> r, pp, rl;//右边不等于null的时候,才有旋转的意义if (p != null && (r = p.right) != null) {//左有放右边(看我的红黑树旋转空口诀)//这里先搭左边的线if ((rl = p.right = r.left) != null)rl.parent = p;//把父亲也搭上//-------左边结点就搭完了---------------////开始搭根结点的父亲//if ((pp = r.parent = p.parent) == null)//如果是根结点直接颜色变黑(root = r).red = false;//false就代表黑else if (pp.left == p)//p在根结点的左边pp.left = r;else//p在根结点右边pp.right = r;//总之最后搭p.parent这个是原则r.left = p;p.parent = r;}//最后返回这个根结点return root;}//右旋和左旋是完全相反的//里面的左变右,右变左static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,TreeNode<K,V> p) {TreeNode<K,V> l, pp, lr;if (p != null && (l = p.left) != null) {if ((lr = p.left = l.right) != null)lr.parent = p;if ((pp = l.parent = p.parent) == null)(root = l).red = false;else if (pp.right == p)pp.right = l;elsepp.left = l;l.right = p;p.parent = l;}return root;}

balanceInsertion红黑树的插入结点调整

//插入的调整代码//还是一个根结点,一个插入结点//最后返回根结点static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,TreeNode<K,V> x) {x.red = true;//把结点变红,默认应该是黑色//整体的循环判定//xp:x的父亲结点; xpp:x的爷爷结点 xppl,xppr:x爷爷的左孩子和x爷爷的右孩子//这里要循环递归判定for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {//如果父亲为null,那么就是根结点,直接变黑返回根结点if ((xp = x.parent) == null) {x.red = false;return x;}//父亲为黑并且是根结点,插入为红的情况//直接返回根结点else if (!xp.red || (xpp = xp.parent) == null)return root;//这里就是左左斜的情况讨论,第三种情况插入if (xp == (xppl = xpp.left)) {//第三种情况里面的第四种插入情况的判断,也就是插入的结点有叔叔的情况//这种是建立在第三种情况,左左斜上面进行分析的//xppr叔叔不为null,并且叔叔为红色,就是第四种情况if ((xppr = xpp.right) != null && xppr.red) {xppr.red = false;//叔叔变黑xp.red = false;//父亲变黑xpp.red = true;//爷爷变红x = xpp;//以爷爷为结点往上面进行整棵树的调整}//如果不是第四种情况,也就是直接是变成四结点的情况//那就是情况三的插入else {//情况3还可能出现的一个情况是,不是笔直的左左斜//也就是x在父亲的右边if (x == xp.right) {//拿到x的父亲左旋,这里x指向饿了root = rotateLeft(root, x = xp);//这一步多余其实,重新赋值了爷爷//但是爷爷在上面的旋转过程中,又不会发生变化xpp = (xp = x.parent) == null ? null : xp.parent;}//这里做了很多安全检查//又检查了一下xp,其实这里还是没用,只要进来了这个大体的else,至少都有三个结点存在//并且xp肯定是有父亲的if (xp != null) {xp.red = false;//父亲变黑if (xpp != null) {xpp.red = true;//爷爷变红root = rotateRight(root, xpp);//以爷爷进行右旋}}}}//左左斜就做完了,下面就做右右的部分,与左左斜其实是完全相反的,就不过多解释了else {if (xppl != null && xppl.red) {xppl.red = false;xp.red = false;xpp.red = true;x = xpp;}else {if (x == xp.left) {root = rotateRight(root, x = xp);xpp = (xp = x.parent) == null ? null : xp.parent;}if (xp != null) {xp.red = false;if (xpp != null) {xpp.red = true;root = rotateLeft(root, xpp);}}}}}}

 

 balanceDeletion红黑树删除结点调整

//删除代码调整分析//给了一个根结点和删除结点static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,TreeNode<K,V> x) {//进入循环判定//但是删除不会往上进行整棵树的递归//xp:父亲结点  xpl,xpr:父亲的右边和父亲的左边for (TreeNode<K,V> xp, xpl, xpr;;) {//x等于根结点直接返回根结点//这种情况进来,只能说明它就只有一个根结点//如果x等于根结点,必须保证是黑色if (x == null || x == root)return root;//这里x的父亲等于null,那说明x就是根结点啊//这里就是保证了根结点为黑色,红色为false嘛else if ((xp = x.parent) == null) {x.red = false;return x;//直接返回根结点,这里确实只有这一个接点}//如果x是红色else if (x.red) {x.red = false;//直接变黑,这里其实就是对应删除情况1:自己能搞定的情况,自己能搞定,直接变黑,返回return root;//返回根结点结束}//这里就要去找兄弟结点借了//兄弟结点能借与不能借的情况//父亲的左边是x,那么我们就是需要找右兄弟去借else if ((xpl = xp.left) == x) {//这里判断的是不是真正的兄弟//如果右边不等于null,并且颜色为红色,就表示是真正的兄弟if ((xpr = xp.right) != null && xpr.red) {xpr.red = false;// 兄弟变黑xp.red = true;//父亲变红root = rotateLeft(root, xp);//利用父亲左旋xpr = (xp = x.parent) == null ? null : xp.right;//拿到真正的兄弟}//下面就考虑兄弟有得借是没得借的情况if (xpr == null)//如果兄弟等于null,bainchengx = xp;else {//有得借//拿到兄弟的左孩子与右孩子TreeNode<K,V> sl = xpr.left, sr = xpr.right;//这里是兄弟没得借的情况if ((sr == null || !sr.red) &&(sl == null || !sl.red)) {xpr.red = true;//兄弟变红自损x = xp;//利用父亲进行递归,父亲如果是红色,变黑,平衡}//下面就是兄弟有得借的情况else {//这里要处理的一个情况是//找兄弟借,兄弟首先必须处理一个三结点非正常右右的情况if (sr == null || !sr.red) {if (sl != null)sl.red = false;//兄弟的左边变黑xpr.red = true;//兄弟变红//其实就是兄弟与兄弟的孩子交换颜色root = rotateRight(root, xpr);//用兄弟右旋xpr = (xp = x.parent) == null ?null : xp.right;//拿到真正的兄弟}//下面就开始变色旋转//之前就说了//三结点与四节点的旋转都是共用一段代码if (xpr != null) {xpr.red = (xp == null) ? false : xp.red;//兄弟变成父亲的颜色if ((sr = xpr.right) != null)sr.red = false;//兄弟孩子变黑}if (xp != null) {xp.red = false;//父亲变黑root = rotateLeft(root, xp);//利用父亲左旋}x = root;//结束}}}else { // 下面操作与上面同理,右变左,左变右if (xpl != null && xpl.red) {xpl.red = false;xp.red = true;root = rotateRight(root, xp);xpl = (xp = x.parent) == null ? null : xp.left;}if (xpl == null)x = xp;else {TreeNode<K,V> sl = xpl.left, sr = xpl.right;if ((sl == null || !sl.red) &&(sr == null || !sr.red)) {xpl.red = true;x = xp;}else {if (sl == null || !sl.red) {if (sr != null)sr.red = false;xpl.red = true;root = rotateLeft(root, xpl);xpl = (xp = x.parent) == null ?null : xp.left;}if (xpl != null) {xpl.red = (xp == null) ? false : xp.red;if ((sl = xpl.left) != null)sl.red = false;}if (xp != null) {xp.red = false;root = rotateRight(root, xp);}x = root;}}}}}

常见的问题总结

问题1:当我们在扩容的时候,如果当前节点是红黑树的实例,他在节点迁移之后,他还会是一棵红黑树吗

答案是不一定,迁移当前桶位以及后面的节点,它是变成一棵红黑树还是单链表,取决于当前链表的迁移数目,是否大于链化阈值,如果当前链表的节点,比如低位链表的节点数目小于等于链化阈值,默认是6这个值,那么他就会把这棵树变成一个单链表存放,也就是说它会调用untreeify方法,内部会返回一个带头节点的单链表。

问题2:如果当前桶位是一个树化节点,他一定会被重新树化吗?

答案是不一定,首先这个链表会被拆分为一个高位链表和一个低位链表,如果假设这个位置,最后只有高位链表或者低位链表有数据,那么他是不会被重新树化的。

问题3:在利用红黑树putTreeVal进行数据插入的时候,相同的键是怎么处理的,是会被老值直接替换吗?

不会,在调用这个方法插入数据的时候,如果遇到相同的键,不管是能直接比较出来大小还是不能,从会直接返回原来的相同键,并不会进行替换。

 

问题4:在用红黑树的putTreeVal进行数据插入的时候,新插入的next指针的指向是指向了谁,新插入节点的父亲节点的指向又是指向了谁?

新创建的节点的next指向是指向了原来父亲的next,而父亲的next则是指向了当前这个新的节点

问题5:红黑树里面的pre节点指向的是红黑树中的父节点吗?

这里并不是,说明一下,这里是链表里面的前一个节点,这里的链表是不同的key算出来的hash值相同,然后在一个桶位形成的链表

当我们在调用putVal进行数据插入的时候,发现一个桶位达到树化条件之后,就会调用treeifyBin进行树化操作,但是这个方法内部,只会把链表的每一个节点变成一个TreeNode树节点,然后把原来的链表的next连接上,并且给原来的链表多加了一个pre指针,换句话说,就是指向了上一个节点的指针。

所以这里的pre并不是红黑树里面的父节点。

问题6:哈希表里面里面第一次形成红黑树的条件是什么?

当我们在调用putVal方法的时候,发现一个桶位中的链表数目大于了8,也就是树化的最小阈值TREEIFY_THRESHOLD,就会调用treeifyBIn进行树化操作,但是treeifyBin它不一定会去走树化这条路,它可能会调用reszie()把这个表进行扩容,扩容或者不扩容,取决于你的整个哈希表有没有最小的树化条件,也就是整个哈希表的节点必须大于MIN_TREEIFY_CAPACITY(64),才会进行树化操作。

问题7:treeify这个方法会在什么地方被调用,应用场景是什么?

首先是我们在插入的数据的时候,也就是在调用putVal的时候,当一个桶位的链表数目大于了树化阈值TREEIFY_THRESHOLD,也就是8的时候,就会调用treeifyBin这个方法,然后在treeifyBin里面还会去判断整个哈希表的结点数目大不大于一个最小的树化结点条件,也就是MIN_TREEIFY_CAPACITY(64),大于这个数目,就要调用treeify()方法把这个桶位的链表进行树化,这是第一次的调用情况

第二次的调用情况是我们在调用resize()方法进行扩容的时候,在考虑桶位后面的结点迁移的时候,如果发现此节点是一个树结点,就要调用split()方法按照红黑树的方式对结点进行迁移,那么内部还是分为了高位链表与低位链表来处理,当结点大于链化阈值并且这个桶位高位来拿表与低位链表都有结点,就要重新树化,本身其实就是树化好了的嘛,内部也会调用treeify()方法进行重新树化

好了,先写到这,祝大家早安午安晚安。

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

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

相关文章

FO with Prefix Hashing KEM Generalizations

参考文献&#xff1a; [Has88] Hastad J. Solving simultaneous modular equations of low degree[J]. siam Journal on Computing, 1988, 17(2): 336-341.[BBM00] Bellare M, Boldyreva A, Micali S. Public-key encryption in a multi-user setting: Security proofs and im…

2023 英特尔On技术创新大会直播 | AI 融合发展之旅

前言 2023 年的英特尔 On 技术创新大会中国站&#xff0c;主要聚焦最新一代增强 AI 能力的计算平台&#xff0c;深度讲解如何支持开放、多架构的软件方案&#xff0c;以赋能人工智能并推动其持续发展。 大会的目标之一是优化系统并赋能开发者&#xff0c;特别注重芯片增强技术…

国产划片机品牌众多,如何选择优质的供应商?

在半导体行业的发展浪潮中&#xff0c;划片机作为关键设备之一&#xff0c;其性能和质量对于生产过程的高效性和产品的质量具有至关重要的影响。近年来&#xff0c;国产划片机的品牌数量不断增多&#xff0c;为半导体行业提供了更多的选择。然而&#xff0c;如何从众多的品牌中…

【Python炫酷系列】一闪一闪亮星星,漫天都是小星星(完整代码)

文章目录 环境需求完整代码详细分析系列文章环境需求 python3.11.4及以上版本PyCharm Community Edition 2023.2.5pyinstaller6.2.0(可选,这个库用于打包,使程序没有python环境也可以运行,如果想发给好朋友的话需要这个库哦~)【注】 python环境搭建请见:https://want595.…

Python 爬虫之简单的爬虫(四)

爬取动态网页&#xff08;下&#xff09; 文章目录 爬取动态网页&#xff08;下&#xff09;前言一、大致内容二、基本思路三、代码编写1.引入库2.加载网页数据3.获取并保存4.保存文档 总结 前言 上篇主要讲了如何去爬取数据&#xff0c;这篇来讲一下如何在获取的同时将数据整…

每个开发人员都应该知道的六个生成式 AI 框架和工具

在快速发展的技术环境中&#xff0c;生成式人工智能是一股革命性的力量&#xff0c;它改变了开发人员处理复杂问题和创新的方式。本文深入探讨了生成式 AI 的世界&#xff0c;揭示了对每个开发人员都至关重要的框架和工具。 1. LangChain LangChain 由 Harrison Chase 开发并于…

Ansible自动化运维以及模块使用

ansible的作用&#xff1a; 远程操作主机功能 自动化运维(playbook剧本基于yaml格式书写) ansible是基于python开发的配置管理和应用部署工具。在自动化运维中&#xff0c;现在是异军突起 ansible能够批量配置、部署、管理上千台主机。类似于Xshell的一键输入工具。不需要每…

通过层进行高效学习:探索深度神经网络中的层次稀疏表示

一、介绍 深度学习中的层次稀疏表示是人工智能领域日益重要的研究领域。本文将探讨分层稀疏表示的概念、它们在深度学习中的意义、应用、挑战和未来方向。 最大限度地提高人工智能的效率和性能&#xff1a;深度学习系统中分层稀疏表示的力量。 二、理解层次稀疏表示 分层稀疏表…

JDK各个版本特性讲解-JDK19特性

JDK各个版本特性讲解-JDK19特性 一、JAVA19概述二、新特性介绍1. 记录模式(预览版本)2.Linux/RISC-V 移植3.外部函数和内存 API &#xff08;预览版&#xff09;4.虚拟线程(预览版)5.Vector API &#xff08;第四次孵化&#xff09;6.Switch 模式匹配&#xff08;第三预览版&am…

从C代码制作chm开发文档【doxygen + graphviz+winChm】

需要的工具&#xff1a; 1. doxygen 最新版本 2. graphviz 最新版本 3. winChm破解版本 1. 最后制作的效果 2. 生成HTML文档 生成hmtl文档是勾选如下2项&#xff0c;为生成chm准备&#xff1a; 需要选择如下2项&#xff1a; generate HTMLHELP 生…

C语言数据结构-排序

文章目录 1 排序的概念及运用1.1 排序的概念1.2 排序的应用 2 插入排序2.1 直接插入排序2.2 希尔排序2.3 直接排序和希尔排序对比 3 选择排序3.1 堆排序3.2 直接选择排序 4 交换排序4.1 冒泡排序4.2 快速排序4.2.1 挖坑法14.2.2 挖坑法24.2.3 挖坑法3 5 并归排序6 十万级别数据…

深入探索Git的高级技巧与神奇操作(分支,高效合并)

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 深入探索Git的高级技巧与神奇操作 前言强制推送的妙用1. 什么是强制推送&#xff1f;2. 为什么需要使用强制推送&#xff1f;3. 强制推送的风险与注意事项4. 如何正确、安全地执行强制推送步骤&#x…

vCenter HA拆分和部署

原创作者&#xff1a;运维工程师 谢晋 vCenter HA拆分和部署 拆分vCenter HA部署vCenter HA 拆分vCenter HA 客户vCenter HA内一台虚拟机出现故障无法连接&#xff0c;报错如下&#xff1a; 点击移除集群报错如下&#xff1a; 查找官方KB&#xff0c;按照官方KB进行移除…

PyCharm关闭项目很慢

我的版本&#xff1a; PyCharm 2023.2.5 (Professional Edition) 问题&#xff1a; 关闭项目的时候显示一直在关闭项目 &#xff08;单次解决&#xff1a;任务管理器里面杀掉PyCharm&#xff09; 解决方案&#xff1a; 在PyCharm中按下快捷键 CtrlShiftA。 输入Registry或…

如何同时给每张PPT插入不同的图片?这2种方法可行!

有时候创作PPT&#xff0c;我们需要把几十张图片插入到PowerPoint中&#xff0c;每张图片作为一张幻灯片&#xff0c;如果一张张手动操作&#xff0c;那就未免太花时间了。今天小编来分享2种方法&#xff0c;可以让您快速给每张PPT插入不同图片。 方法一、使用“创建相册” 1.…

第三方软件验收测试对于软件项目验收的重要性

软件公司开发出一款软件产品后需要通过一系列的验收测试才能顺利上市&#xff0c;从而被最终用户使用。验收测试是部署软件之前的最后一个测试操作。在软件产品完成了单元测试、集成测试和系统测试之后&#xff0c;产品发布之前所进行的软件测试活动。由于软件企业会将更多的精…

护肤品类小红书素人达人的推广报价是多少?

小红书是一款集社交和电商于一体的平台&#xff0c;用户可以在上面分享生活点滴、购物心得、旅游攻略等。近年来&#xff0c;随着护肤美妆市场的不断扩大&#xff0c;越来越多的品牌和商家选择在小红书上投放广告&#xff0c;借助素人达人的影响力为产品引流。那么&#xff0c;…

Word的兼容性问题很常见,禁用兼容模式虽步不是最有效的,但可以解决兼容性问题

当你在较新版本的Word应用程序中打开用较旧版本的Word创建的文档时&#xff0c;会出现兼容性问题。错误通常发生在文件名附近&#xff08;兼容模式&#xff09;。兼容性模式问题&#xff08;暂时&#xff09;禁用Word功能&#xff0c;从而限制使用较新版本Word的用户编辑文档。…

腾讯云debian服务器的连接与初始化

目录 1. 远程连接2. 软件下载3. 设置开机自启动 1. 远程连接 腾讯云给的服务器在安装好系统之后&#xff0c;只需要在防火墙里面添加一个白名单&#xff08;ip 或者域名&#xff09;就能访问了。 防火墙添加本机WLAN的IPv4白名单&#xff0c;本地用一个远程工具连接&#xff…

Java第十七章总结

数据库基础 SQL语言 1、select 语句 select 语句用于从数据中检索数据。语法如下&#xff1a; SELECT 搜选字段列表 FROM 数据表名 WHERE 条件表达式 GROUP BY 字段名 HAVING 条件表达式(指定分组的条件) ORDER BY 字段名[ASC|DESC] 2、insert 语句 insert 语句用于向表中插入…