数据结构与算法05:跳表和散列表

news2024/11/19 4:44:14

目录

【跳表】

跳表的实现原理

如何确定跳表的层高?

【散列表】

散列函数的设计

散列冲突

(1)开放寻址法(Open Addressing)

(2)链表法(chaining)

装载因子

如何设计一个比较合理高效的散列表?

散列表的应用:单词拼写检查

散列表的应用:LRU缓存淘汰算法

【每日一练:整数和罗马数字互转】


【跳表】

在 数据结构与算法02:数组和链表_浮尘笔记的博客-CSDN博客 中说过链表插入和删除的时间复杂度是O(1),但是查找数据的时间复杂度是O(n),即使是有序的链表也是如此,那么有没有办法优化一下查找的时间复杂度呢?当然有,可以对有序链表增加“索引”,改造后的数据结构就叫做 跳表(跳跃表),如下图所示:

96313eea59fc474a97d04b225519d8a8.png

原始的链表中如果要查找48,需要经历8次查询,但是添加了两级索引之后,只需要4次即可查到。注意:跳表的前提必须是一个有序的链表

想象一个场景,网上购物填写收货地址的时候,如果把全国所有的区县都平铺开到一个下拉菜单里面去找,会相当费劲,但是使用了“省-市-区”三级联动之后就可以很方便的找到自己所在的区县,这里前两层的“省和市”,就可以理解为跳表的索引。 

跳表可以支持快速的插入、删除、查找操作,从上面的示例图中可以看出来,跳表的空间复杂度是 O(n),在跳表中查询任意数据的时间复杂度是O(logn),跳表中插入和删除操作的时间复杂度也是 O(logn)。跳表实际上运用了“空间换时间”的思维,在链表的基础上增加了索引。如果将包含 n 个结点的单链表构造成跳表,就需要额外再用接近 n 个结点的存储空间,当实际存储的数据对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。想象一下,为什么在数据量大的MySQL表中一定要建立索引呢?其实是同样的道理。

为什么说跳表中插入和删除操作的时间复杂度也是 O(logn)呢?因为如果只是单链表,插入和删除的时间复杂度是O(1),但是现在需要在插入和删除的时候把索引的变动也维护进去。那么,你是否想到了在MySQL中为什么最好不要建立太多的索引?也是同样的道理。

跳表的实现原理

由于在跳表中查找数据是从高往底、从左往右查找的,所以跳表需要记录跳表的数据值、用于排序的分数、层高(索引的高度)、递归存储每一层前进的指针。使用Go语言实现一个跳表的核心 代码 如下:

// 跳表节点结构体
type skipListNode struct {
	v        interface{}     //跳表保存的值
	score    int             //用于排序的分数
	level    int             //层高
	forwards []*skipListNode //每层前进指针,递归
}

// 新建跳表节点
func newSkipListNode(v interface{}, score, level int) *skipListNode {
	return &skipListNode{
		v:        v,
		score:    score,
		forwards: make([]*skipListNode, level, level),
		level:    level,
	}
}

// 跳表结构体
type SkipList struct {
	head   *skipListNode //跳表头结点
	level  int           //跳表当前层高
	length int           //跳表长度
}

// 实例化跳表对象
func NewSkipList() *SkipList {
	// 初始化头结点数据
	head := newSkipListNode(0, 0, MAX_LEVEL) //&{0 0 3 [<nil> <nil> <nil>]}
	return &SkipList{head, 1, 0}
}

// 查找跳表中的元素
func (sl *SkipList) Find(v interface{}, score int) *skipListNode {
	if nil == v || sl.length == 0 {
		return nil
	}
	cur := sl.head
	for i := sl.level - 1; i >= 0; i-- {
		for nil != cur.forwards[i] {
			if cur.forwards[i].score == score && cur.forwards[i].v == v {
				return cur.forwards[i]
			} else if cur.forwards[i].score > score {
				break
			}
			cur = cur.forwards[i]
		}
	}
	return nil
}

如何确定跳表的层高?

最理想的状态下,跳表的每一层都应该包含下一层一半的节点,且同一层指针跨越的节点数量是一样的,就像上面图中所示那样,从上到下的节点数量是2、5、9、17....,也就是(2^n)+1,层数一共是 logN 层,在每一层中最多只会跳跃一次,每一层最多访问两个节点,整体搜索时间复杂度为 O(logN)。但是这样会存在一个问题,那就是在跳表中动态插入和删除的时候,需要不断地调整每一个节点的层数,因为这个层数完全取决于该节点处于链表中的第几个位置。有可能在某个位置插入一个新元素,就要对大量的索引进行调整,性能肯定会下降。

为了避免这个情况,可以采用一定的算法来决定每一层跳跃多少个节点,比如可以使用一定数值范围的随机数,或者50%概率(第一层时 100% 会被插入,第二层只有 50% 的概率会被插入,第三层是 25% 的概率会被插入),这样一来每一层节点之间的间距也会相对均匀,在更新和查找之间取了一个平衡。

比如使用下面4层结构的一个跳表,要插入元素87,过程如下:

这样插入之后,假如将来需要删除87这个节点,也只会删除1、2、3层,第4层就不用改动了。 关于添加元素的核心代码如下:

// 给跳表中插入元素和索引
func (sl *SkipList) Insert(v interface{}, score int) int {
	if nil == v {
		return 1
	}
	cur := sl.head                       //当前需要插入的位置,也就是头结点信息
	update := [MAX_LEVEL]*skipListNode{} //每一层需要更新的数据,组成一个数组

	i := MAX_LEVEL - 1
	for ; i >= 0; i-- {
		for nil != cur.forwards[i] {
			//... 省略边界校验的逻辑
			cur = cur.forwards[i]
		}
		if nil == cur.forwards[i] {
			update[i] = cur
		}
	}

	//通过随机算法获取该节点层数
	level := 1
	for i := 1; i < MAX_LEVEL; i++ {
		if rand.Int31()%7 == 1 {
			level++
		}
	}

	//创建一个新的跳表节点
	newNode := newSkipListNode(v, score, level)

	//原有节点连接
	for i := 0; i <= level-1; i++ {
		next := update[i].forwards[i]
		update[i].forwards[i] = newNode
		newNode.forwards[i] = next
	}

	//如果当前节点的层数大于之前跳表的层数
	//更新当前跳表层数
	if level > sl.level {
		sl.level = level
	}

	//更新跳表长度
	sl.length++

	return 0
}

【问】Redis 为什么选择用跳表实现有序集合(Sorted Set)?为什么不用红黑树呢?

【答】Redis 的有序集合有个重要的功能就是按照区间Score查找数据,可以参考 redis笔记04-无序集合和有序集合_有序集合无序集合_浮尘笔记的博客-CSDN博客 了解详细用法。对于按照区间查找数据的操作,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。虽然红黑树也可以实现快速的插入、删除和查找操作,但是效率没有跳表高。还有一个原因就是跳表实现起来相对比较容易。关于红黑树后面再说。

【散列表】

对于一个相对较大的任意长度的数据,而且这个数据可能不是存储在连续空间,把这个数据映射到一个相对较小空间的数组里,这里面提到的较小空间的数组就是一个散列表,也可以叫做“哈希表”或者“Hash表”,这个实现映射的过程就是一个散列函数,可以用 hash(key)=value 来表示。散列表的本质是一个数组,可以在O(1)的时间复杂度查找元素。

上面的概念听上去有点绕,我举个例子你感受下。比如现在有个散列函数是对输入的编号数字%100(对100取余数),将得到的余数存储到一个散列表数组中,效果如下:

 

正常情况下哈希值算出来应该是一个正确的数组的索引值,如果哈希值是负数,说明这个哈希算法设计的有问题。 

散列表的优势:可以非常快速的插入、删除、查找元素,无论多少数据,插入和删除的时间复杂度都接近常量;散列表的查找速度比树还要快。散列表的不足之处:散列表中的数据不是有序的,所以不能以一种固定的方式(比如从小到大)来遍历其中的元素,而且散列表中的key是不允许重复的。总结就是:散列表实现了关键字到数组地址的映射,可以在常数时间复杂度内通过关键字查找到数据。

散列函数的设计

散列函数的设计不能太复杂,否则会消耗很多计算时间,也就间接的影响到散列表的性能。散列函数生成的值要尽可能随机并且均匀分布,这样才能让散列冲突尽可能降低。

一般可以参考下面几种方式来设计一个散列函数:

  • 直接寻址法:哈希函数为关键字到地址的线性函数。如 F(key)=a*key+b,这里 a和b是设置好的常数。
  • 数字分析法:假设关键字集合中的每个关键字key都是由s位数字组成(k1, k2, ..., Ks),可以从中提取分布均匀的若干位组成哈希地址,比如手机号后四位作为散列值。
  • 平方取中法:如果关键字的每一位都有某些数字重复出现,并且频率很高,可以先求关键字的平方值,通过平方扩大差异,然后取中间几位作为最终存储地址。
  • 折叠法:如果关键字的位数很多,可以将关键字分割为几个等长的部分,取它们的叠加和的值(舍去进位)作为哈希地址。
  • 计算余数法:预先设置一个数x, 然后对关键字进行取余运算,即 key % x,也就是上面图中所示的方法。

关于散列函数的三个特点:

  • 在同一个散列函数中输入一个任意的原始数据,都可以得到一个相应的哈希值;
  • 在同一个散列函数中输入两个相同的原始数据,它们总会得到相同的哈希值;
  • 在同一个散列函数中输入两个不同的原始数据,它们也有可能得到相同的哈希值,也就是:散列冲突,比如上面图中的余数为2的就存在两个原始数据。

散列冲突

不同的两个或者多个原始数据经过散列函数计算后,是有可能得到一个相同的哈希值,这就是散列冲突。因为数组的存储空间有限,也会加大散列冲突的概率。

常用的散列冲突解决方法有两类,分别是:开放寻址法链表法

(1)开放寻址法(Open Addressing)

开放寻址法 是在数组中寻找一个还未被使用的位置,然后将新的值插入,其实是尽可能的利用数组原本的空间而不去开辟额外的空间来保存值。最简单的实现方法就是沿着数组索引往下一个一个地去寻找还未被使用的空间,这种方法也叫做 线性探测(Linear Probing)。当数据量比较小、装载因子小的时候,适合采用开放寻址法。

比如上面示例中的 030502 在散列后发现位置2已经被占用了,那么就继续向后寻找空闲空间,找到了3还没被使用,就会把它插入到3的位置,如下图所示。假如再来一个应该散列到2的位置的数据,此时发现3也被占用了,那么会继续向后寻找。

实际上这种方法是存在一些问题的,比如向后寻找到数组的一个新的位置,就需要额外记录原来本身的散列信息,才能查找到对应的数据。而且如果散列表已经满了,还得考虑动态扩容的问题。查找元素的时候如果在散列表中的对应位置没有找到,那么还要不断的往后遍历。

当散列表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置会越来越少,线性探测的时间就会越来越久,极端情况下可能需要探测整个散列表,所以最坏情况下的时间复杂度为 O(n)。

除了上面说的线性探测之外,还有另外两种探测方法,分别是 二次探测(Quadratic probing)双重散列(Double hashing),其实原理都差不多。二次探测是在探测的时候步长变成了原来的“二次方”,探测的下标序列是 hash(key)+0,hash(key)+1^2,hash(key)+2^2,... ;双重散列就是要使用一组散列函数 hash1(key),hash2(key),hash3(key)…… 如果用第一个散列函数计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。

(2)链表法(chaining)

相比开放寻址法,链表法相对来说简单一些,是一种更常用的散列冲突解决办法。就是在散列表的数组中再维护一个链表,如下图所示:

当插入元素的时候只需要通过散列函数计算出对应的散列槽位,然后将其插入到对应链表中即可,所以插入的时间复杂度是 O(1);当查找和删除一个元素时同样通过散列函数计算出对应的槽位,然后遍历链表查找或者删除,查找或删除操作的时间复杂度跟链表的长度 k 成正比,也就是 O(k)。极端情况下,如果有人恶意攻击,所有的数据都散列到了同一个槽位中,那么散列表就会退化为链表,查询的时间复杂度就会退化为O(n)。

如果散列表中有 10 万个数据,退化成链表后的查询效率就下降了 10 万倍。如果之前运行 100 次查询只需要 0.1 秒,那现在就需要 1 万秒。这样就有可能因为查询操作消耗大量 CPU 或者线程资源,导致系统无法响应。这就是散列表碰撞攻击的基本原理。

这种基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,相比开放寻址法更加灵活,比如可以继续使用“空间换时间”的策略,把散列表中每个槽位里面的链表改造成“跳表”、“二叉树”、“红黑树”等其它数据结构。

装载因子

当散列表中空闲位置不多的时候,散列冲突的概率就会提高,一般用装载因子(load factor)来表示空位的多少。装载因子越大,说明空闲位置越少,也就是冲突越多,散列表的性能会下降。

装载因子的计算公式是:填入表中的元素个数 / 散列表的长度,比如当前散列表的长度是100,已填入表中的元素个数为80,那么装载因子就是0.8。

装载因子越来越大的时候,可以重新申请一个更大的散列表(动态扩容),并且将数据搬移到这个新散列表中。假设每次扩容都申请一个原来散列表大小两倍的空间,如果原来的装载因子是0.8,那么扩容后的装载因子就变成了 0.4。散列表扩容后,由于散列表的大小变了,数据的存储位置也变了,所以需要通过散列函数重新计算每个数据的存储位置并搬移数据,所以时间复杂度是 O(n)。

扩容的时机如何控制?当插入数据的时候检测到装载因子过大,最好不要在这个时候一次性搬移所有数据,因为有可能非常耗时导致服务瘫痪。比较合理的做法是:将扩容操作穿插在插入操作的过程中分批次完成,当装载因子到达设定的阈值之后,只申请新空间但并不将老的数据搬移到新散列表中,当有新数据要插入时将这个新数据插入到新散列表中的同时,也从老的散列表中拿出一个数据放入到新散列表,每次插入新数据都重复这个操作。相当于把一次性的搬移操作分散到了多次,压力相对就比较小了。同时在查找数据的时候,如果新的散列表中没有找到,则需要在旧的散列表中再次查找,因为有可能要查找的元素还没搬移到新的散列表中。

如何设计一个比较合理高效的散列表?

可以从以下几个方面考虑:

  • 设计一个合适的散列函数,支持快速的查询、插入、删除操作,并且尽可能让散列后的值随机且均匀分布;
  • 定义装载因子阈值,并且设计动态扩容策略,需要保证内存占用合理,不能浪费过多的内存空间;
  • 选择合适的散列冲突解决方法,保证性能稳定,极端情况下散列表的性能也不会退化到无法接受的情况。

散列表的应用:单词拼写检查

在word或者代码编辑器中一般默认都会有单词检查功能,如果写错了单词会提示,如下图所示。那么这种单词拼接检查是如何高效实现的?

问题分析:常用的英文单词有 20 万个左右,假设单词的平均长度是 10 个字母,平均一个单词占用 10 个字节的内存空间,那 20 万英文单词大约占 2MB 的存储空间,就算放大 10 倍也就是 20MB。对于现在的计算机来说,这个大小完全可以放在内存里面,所以可以使用散列表来存储整个英文单词词典。

实现方法:当用户输入某个英文单词时,拿用户输入的单词去散列表中查找,如果查到了说明拼写正确;如果没有查到说明拼写可能有误。使用散列表这种数据结构,可以快速判断是否存在拼写错误。

散列表的应用:LRU缓存淘汰算法

在 数据结构与算法02:数组和链表_浮尘笔记的博客-CSDN博客 这篇文章中使用原始的链表实现了一个LRU淘汰算法,由于不管缓存有没有满,都需要遍历一遍链表,所以基于链表实现LRU淘汰算法的时间复杂度为 O(n),并不是一个理想的结果。如果使用散列表来实现LRU算法,可以把添加、删除、查找的时间复杂度都降为O(1),参考下图:

  • 查找数据:散列表中查找数据的时间复杂度接近 O(1),所以通过散列表可以很快地在缓存中找到一个数据。当找到数据之后还需要将它移动到双向链表的尾部。
  • 删除数据:需要找到数据所在的结点然后将结点删除,借助散列表可以在 O(1) 时间复杂度里找到要删除的结点。因为双向链表可以通过前驱指针 O(1) 时间复杂度获取前驱结点,所以在双向链表中,删除结点只需要 O(1) 的时间复杂度。
  • 添加数据:先看这个数据是否已经在缓存中,如果已经在其中,需要将其移动到双向链表的尾部;如果不在其中,还要看缓存有没有满。如果满了,则将双向链表头部的结点删除,然后再将数据放到链表的尾部;如果没有满,就直接将数据放到链表的尾部。 

参考资料:20 | 散列表(下):为什么散列表和链表经常会一起使用?-极客时间

【每日一练:整数和罗马数字互转】

力扣12. 整数转罗马数字

罗马数字包含以下七种字符: I(1), V(5), X(10), L(50),C(100),D(500) 和 M(1000)。
例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。

示例 1: 输入: num = 3,输出: "III";
示例 2: 输入: num = 4,输出: "IV"。

思路:可以将所有罗马数字的不同符号及对应整数放在字典中。时间复杂度: O(N),空间复杂度: O(1)。

func intToRoman(num int) string {
	// 初始化了一个 一一对应的map,方便后面取出符号。
	lookupSymbol := []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}
	lookupNum := []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}
	roman := ""
	for i, symbol := range lookupSymbol {
		val := lookupNum[i]
		for num >= val {
			roman += symbol
			num -= val
		}
	}
	return roman
}

func main() {
	fmt.Println(intToRoman(3)) //III
	fmt.Println(intToRoman(4)) //IV
}

力扣13. 罗马数字转整数

示例 3: 输入: s = "IX",输出: 9
示例 4: 输入: s = "LVIII",输出: 58,解释: L = 50, V= 5, III = 3.

思路:小的数字,限于(I、X 和 C)在大的数字左边,所表示的数等于大数减去小数所得的数,例如IV = 4。所以如果当前罗马数字的值比前面一个大,说明这一段的值应当是减去上一个值。否则,应将当前值加入到最后结果中并开始下一次记录,例如:VI = 5 + 1, II = 1+1。时间复杂度: O(N) 空间复杂度: O(1)。

func romanToInt(s string) int {
	// 初始化了一个一一对应的map,方便后面取出符号。
	lookup := make(map[byte]int)
	lookup['I'] = 1
	lookup['V'] = 5
	lookup['X'] = 10
	lookup['L'] = 50
	lookup['C'] = 100
	lookup['D'] = 500
	lookup['M'] = 1000

	res := 0
	for i, _ := range s {
		if i > 0 && lookup[s[i]] > lookup[s[i-1]] {
			res += lookup[s[i]] - 2*lookup[s[i-1]]
		} else {
			res += lookup[s[i]]
		}
	}
	return res
}

func main() {
	fmt.Println(romanToInt("IX"))    //9
	fmt.Println(romanToInt("LVIII")) //58
}

代码:https://gitee.com/rxbook/go-algo-demo/blob/master/leetcode/intToRoman.go 

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

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

相关文章

如何把两个视频拼接在一起?这些工具太棒了!

如何把两个视频拼接在一起&#xff1f;对于自媒体行业的人来讲&#xff0c;平时在进行短视频的剪辑时&#xff0c;总会需要合并多个视频&#xff0c;之后再剪辑处理。不过对于大部分没有从事自媒体行业的人来讲&#xff0c;可能对合并视频并不了解。下面我就来给大家分享可以拼…

paddleclas pp-shitu v1/v2

null集开放数据、开源算法、免费算力三位一体&#xff0c;为开发者提供高效学习和开发环境、高价值高奖金竞赛项目&#xff0c;支撑高校老师轻松实现AI教学&#xff0c;并助力开发者学习交流&#xff0c;加速落地AI业务场景https://aistudio.baidu.com/aistudio/projectdetail/…

BlackIce病毒分析

概述 blackice是一个古老的感染型病毒&#xff0c;可感染系统中exe、doc和xls文件&#xff0c;通过USB设备和网络驱动器来传播&#xff0c;会向C&C下载pe执行&#xff0c;会关闭常用的杀软进程。下面找了一个样本&#xff0c;这个样本的代码结构清晰&#xff0c;用IDA pro…

客服配置-shopro

客服配置 注意事项 shopro客服系统 采用 workerman 的 gateway-worker 作为服务基础&#xff0c;请先安装 gateway-worker 扩展包shopro商城 已不再支持 workerman 在线客服插件 安装部署 安装扩展包 composer require workerman/gateway-worker:~3.0 删除禁用函数(如有未列…

Postman中加url环境变量和token全局变量

环境变量引用 语法&#xff1a;{{变量名}} 环境变量分类 1. 全局变量&#xff1a;全局有效&#xff0c;在Postman中的任何集合中都可以使用该变量&#xff0c;作用域最大。 2. 环境变量&#xff1a;要申明环境变量&#xff0c;先要创建环境&#xff0c;然后在该环境中创建变…

KPI考核对企业能起到什么作用?公司该如何建立标准化

什么是KPI考核 KPI考核是指对企业或组织的关键绩效指标进行量化的评估和监控&#xff0c;以了解其业务目标的实现情况。 KPI&#xff08;Key Performance Indicators&#xff09;是指关键绩效指标&#xff0c;是衡量企业或组织成功的基本指标。对KPI进行考核可以帮助企业或组织…

破解极域(4):万能密码法(可以获取到原密码)

破解极域&#xff08;4&#xff09;&#xff1a;万能密码法 1.思路2.实现2.1 获得密码2.2 解除控制2.3 特别注意 3.视频展示 今天来分享下破解极域的第4种方法——万能密码法 1.思路 首先&#xff0c;我们要知道的是&#xff0c;极域这个东西它有一个万能密码&#xff0c;万能…

网络安全+自学笔记

学前感言: 1.这是一条坚持的道路,三分钟的热情可以放弃往下看了.2.多练多想,不要离开了教程什么都不会了.最好看完教程自己独立完成技术方面的开发.3.有时多 google,baidu,我们往往都遇不到好心的大神,谁会无聊天天给你做解答.4.遇到实在搞不懂的,可以先放放,以后再来解决. 基…

[GUET-CTF2019]number_game[数独]

目录 题目 学到的知识点&#xff1a; 题目 在buu上看到了一道数独题&#xff0c;没见过&#xff0c;记录一下 下载附件&#xff0c;查壳&#xff0c;无壳&#xff0c;在IDA中打开&#xff0c;直接找到主函数 unsigned __int64 __fastcall main(int a1, char **a2, char **a3…

您的监控策略是否可扩展?

家公司都在努力更好地了解其运营效率&#xff0c;但他们都遇到了同样的问题&#xff1a;规模。那么&#xff0c;可扩展的监控策略是什么样的&#xff1f;您如何防范可观察性中最重要的问题&#xff1f; 什么是可扩展监控策略&#xff1f; 我们将从确定受规模影响最大的两件事…

生态系统NPP及碳源、碳汇模拟、土地利用变化、未来气候变化、空间动态模拟实践技术应用

由于全球变暖、大气中温室气体浓度逐年增加等问题的出现&#xff0c;“双碳”行动特别是碳中和已经在世界范围形成广泛影响。碳中和可以从碳排放&#xff08;碳源&#xff09;和碳固定&#xff08;碳汇&#xff09;这两个侧面来理解。陆地生态系统在全球碳循环过程中有着重要作…

SpringBootWeb---分层解耦

3. 分层解耦 3.1 三层架构 3.1.1 介绍 在我们进行程序设计以及程序开发时&#xff0c;尽可能让每一个接口、类、方法的职责更单一些&#xff08;单一职责原则&#xff09;。 单一职责原则&#xff1a;一个类或一个方法&#xff0c;就只做一件事情&#xff0c;只管一块功能。…

前端网络、JavaScript优化以及开发小技巧

一、网络优化 YSlow有23条规则&#xff0c;中文可以参考这里。这几十条规则最主要是在做消除或减少不必要的网络延迟&#xff0c;将需要传输的数据压缩至最少。 1&#xff09;合并压缩CSS、JavaScript、图片&#xff0c;静态资源CDN缓存 通过构建工具Gulp&#xff0c;可以在…

PDPS教程:机器人工作站导出为JT格式文件操作方法

目录 功能简介 功能注意事项 导出JT格式文件操作 导出JT格式文件查看 功能简介 PDPS软件不仅能够从外部导入JT格式的模型文件&#xff0c;还能够将创建好的机器人工作站/生产线导出为JT格式的模型文件。这个能够导出JT格式文件的功能就是“Export JT”命令。 使用“Expor…

零尽其用,尾随不落——探究力扣题目“移除字符串中的尾随零”的解题思路

本篇博客会讲解力扣“2710. 移除字符串中的尾随零”的解题思路&#xff0c;这是题目链接。 先来审题&#xff1a; 以下是示例&#xff1a; 以下是提示&#xff1a; 本题的思路是&#xff1a; 先遍历字符串&#xff0c;找到字符串末尾的\0。从\0开始&#xff0c;向前遍历&a…

如何写一篇让人挑不出毛病的产品需求文档?

需求”这个词是产品经理工作中的常客&#xff0c;产品需求文档也贯穿于整个产品经理的日常工作中&#xff0c;本周小编将通过什么是产品需求文档&#xff0c;产品需求文档的作用、如何写好产品需求文档等方面分享如何写出一篇让你挑不出毛病的PRD&#xff0c;让需求文档助力产品…

30分钟开发微信小程序并部署

30分钟开发微信小程序并部署 首先&#xff0c;今天我们会使用到腾讯为我们提供的云平台来协助我们小程序的开发和部署。 环境准备&#xff1a; 微信开发者账号&#xff08;自己在微信公众号平台注册一个账号&#xff09;&#xff1a;https://mp.weixin.qq.com/&#xff0c;同时…

arp欺骗(http)与dns欺骗

Arp欺骗 win7&#xff1a; ip:192.168.127.147 mac:00-0C-29-4F-1C-36 kali: ip:192.168.127.133 mac:00:0c:29:4c:4d:92 arp -a 获取网关 网关&#xff1a;ip&#xff1a;192.168.127.0 mac&#xff1a;00-0c-29-4c-4d-92 fping -g 192.168.127.0/24 192.168.127.1…

在华为眼里,ICT行业今年还有啥搞头?

大家好&#xff0c;我是老杨。 不知道你们有没有研究年报的习惯&#xff0c;一些超大厂是会每年发布年报的&#xff0c;而从年报里&#xff0c;你能看出很多行业趋势出来。 以最热门的华为举例&#xff1a; 每年&#xff0c;华为都会发布全年财报&#xff0c;对自己今年的整…

什么是低代码?国内常见的低代码平台有哪些?

一、什么是低代码开发&#xff1f; 低代码也称之为无代码或是 aPaas。要想了解低代码是什么&#xff0c;我们先来讨论低代码本质&#xff1f; 它是一种可视化软件开发方法&#xff0c;通过最少的编码更快地交付应用程序。 图形用户界面和拖放功能使开发过程的各个方面自动化…