文章目录

  • 1 FP-growth算法简介
  • 2 FP-growth算法原理
    • 2.1 FP树的表示方式
    • 2.2 FP树的构建过程
      • (1) 统计原始事务集中各元素项出现的频率
      • (2) 支持度过滤
      • (3) 排序
      • (4) 构建FP树
  • 3 实验
    • 实验1 使用简单数据创建FP树
    • 实验2 从FP树中挖掘频繁项集
  • 参考资料

注:转载请标明原文出处链接:https://xiongyiming.blog.csdn.net/article/details/97782705

1 FP-growth算法简介

众所周知,Apriori算法在产生频繁模式完全集前需要对数据库进行多次扫描,同时产生大量的候选频繁集,这就使Apriori算法时间和空间复杂度较大。但是Apriori算法中有一个很重要的性质:频繁项集的所有非空子集都必须也是频繁的。但是Apriori算法在挖掘额长频繁模式的时候性能往往低下,Jiawei Han提出了FP-Growth算法,它采取如下分治策略:将提供频繁项集的数据库压缩到一棵频繁模式树(FP-tree),但仍保留项集关联信息。
在算法中使用了一种称为频繁模式树(Frequent Pattern Tree)的数据结构。FP-tree是一种特殊的前缀树,由频繁项头表和项前缀树构成。FP-Growth算法基于以上的结构加快整个挖掘过程。
(以上均来自百度百科)

2 FP-growth算法原理

使用百度搜索时,当你输入一个词语的时候,搜索引擎就会帮你自动补全你可能要查询的词项。比如,下图中所示,当我输入"机器学习"时,百度自动给出了"机器学习算法"、“机器学习python”、“机器学习 周志华”、"机器学习 周志华 pdf"这四个词条供我们参考。

对于百度的研究人员来说,想要给出这些推荐查询的词项,他们需要查看互联网上的用词并快速地找出经常出现在一起的词对,因此他们需要一种更加高效的发现频繁项集的方法。
Apriori算法是一种很好的发现频繁项集的方法,但是每次增加频繁项集的大小,Apriori算法都会重新扫描整个数据集,当数据量很大的时候,这毫无疑问会成为Apriori算法最大的缺点频繁项集发现的速度太慢。FPgrowth算法其实是在Apriori算法基础上进行了优化得到的算法,下面看一下两者的对比:


FP-growth算法只需要对数据库进行了两次扫描,而Apriori算法对于每个潜在的频繁项集都会扫描数据集判定给定模式是否频繁,因此FP-growth算法的速度要比Apriori算法快。在小规模数据集上,这不是什么问题,但是当处理大规模数据集时,就会产生很大的区别。
关于FP-growth算法需要注意的两点是:
(1) 该算法采用了与Apriori完全不同的方法来发现频繁项集;
(2) 该算法虽然能更为高效地发现频繁项集,但不能用于发现关联规则。

从FP-growth算法挖掘频繁项集这个流程图中可以看出,FP-growth算法步骤为

FP-growth算法主要有两个步骤:

  1. 构建FP树
  2. 从FP树中挖掘频繁项集

2.1 FP树的表示方式

FP是频繁模式 (Frequent Pattern)的缩写。FP-growth算法将数据存储在一种称为FP树的的紧凑数据结构中,并直接从该结构中提取频繁项集。一棵FP树的结构如下图所示,和其他树结构不同之处是FP树通过链接(link)来连接相似元素,被连起来的元素项可以看成一个链表。可以说,FP树是一种用于编码数据集的有效方式。
为了快速访问树中的相同项,还需要维护一个连接具有相同项的节点的指针列表(headTable),每个列表元素包括:数据项、该项的全局最小支持度、指向FP树中该项链表的表头的指针。

FP树的特点

  1. 一个元素项可以在一棵FP树中出现多次;
  2. 项集以路径+频率的方式存储在FP树中;
  3. 树节点中的频率表示的是集合中单个元素在其序列中出现的次数;
  4. 一条路径表示的是一个事务,如果事务完全一致,则路径可以重合;
  5. 相似项之间的链接即为节点链接(link),主要是用来快速发现相似项的位置.

2.2 FP树的构建过程

为了更好地理解FP树,我们通过一个例子构建FP树,简单数据集如下表所示

FP树的构建的步骤

  1. 统计原始事务集中各元素项出现的频率
  2. 支持度过滤
  3. 排序
  4. 构建FP树

具体步骤如下

(1) 统计原始事务集中各元素项出现的频率

这一步主要作用是生成FP树的树节点,统计好的元素项及其频率即为我们所需要的树节点。根据原始事务集,我们可以计算中共有17个元素项,然后分别计算每个元素项在事务集中一共出现了多少次。统计结果如下表所示:

(2) 支持度过滤

上表计算出来的所有树节点并非全部都是我们需要的树节点。我们构建FP树的主要目的是为了挖掘频繁项集,所以为了减少后续的计算量,在这一步我们就可以把不满足最小支持度的元素项给剔除了。因为我们知道,如果一个元素项是不频繁的,那么包含该元素项的超集也是不频繁的,所以就不需要考虑这些超集了。
假设这里我们定义最小支持度为3,那么进行支持度过滤之后得到的元素项为:


从上图可以看到,经过支持度过滤之后,元素项由原来的17个变为了6个,可以大大减少后续的计算量。

(3) 排序

根据频率大小,将支持度过滤后的元素项(即频繁元素项)进行排序,并根据排序后的元素项,将原始事务集也进行排序。这样做的原因是,我们构建的FP树中,相同项只会出现一次,但是大家都知道集合是无序的,对于集合{x, y, z}和集合{z, y, x}我们可以一眼看出这两个集合是一样的,但是对于FP树来说,它会把这两个集合当做两个不同的集合,然后生成两条路径,这样的结果不是我们想要的。所以为了解决这样的问题,我们需要将集合添加到树之前对它们进行排序。
频繁元素项排序结果:


原始事务集支持度过滤及重排序后的结果:


这里需要注意的是:事务集在重排序的时候,我们进行了两步操作:
第一步:按照频率大小对元素项进行排序;
第二步:对于相同频率的元素项,则对关键字进行降序排列(顺序会影响FP的结构)。

(4) 构建FP树

在对事务集过滤和重排序后,就可以构建FP树了。首先从空集(∅)开始,向其中不断添加频繁项集。如果树中已存在现有元素,则增加现有元素的值。如果现有元素不存在,则向树中添加一个分支。具体过程如下所示:



把六个事务都添加进去之后,我们的FP树就建立好了。我们可以这棵树跟我们一开始示意的FP树不太一样。原因是:我们对频率相同的元素项进行了关键字排序处理。这说明,对项的关键字排序将会影响FP树的结构。进而,树的结构也将影响后续发现频繁项的结果。

3 实验

实验1 使用简单数据创建FP树

(1) 创建FP树的数据结构
由于FP树比之前建立的决策树要复杂,所以我们这里创建一个类来保存树的每一个节点。类中包含用于存放节点名字的变量和1个计数值,nodeLink变量用于链接相似的元素项,parent变量用于指向当前父节点(如果从上到下迭代访问节点的话不需要这个变量,此处使用是为后面的内容做铺垫),空字典变量用于存放节点的子节点。

类中包含了两个方法:

  1. inc()方法的功能是对count变量增加给定值;
  2. disp()方法的功能是将树以文本形式显示,主要是方便调试。

函数定义及测试

class treeNode:def __init__(self, nameValue, numOccur, parentNode):self.name = nameValue #名字变量self.count = numOccur #计数变量(频率)self.nodeLink = None  #链接相似元素项self.parent = parentNode   #当前父节点self.children = {}         #用于存放子节点# inc 函数 对count变量增加给定值;def inc(self, numOccur):self.count += numOccur# disp()方法的功能是将树以文本形式显示,主要是方便调试def disp(self, ind=1):print( '   ' *ind, self.name, ' ', self.count)for child in self.children.values():child.disp(ind +1)   #子节点向右缩减#测试
# 显示节点#创建一个单节点
rootNode = treeNode('这是父节点',9,None)
#创建一个子节点
rootNode.children['这是子节点'] = treeNode('这是子节点',13,None)
#显示节点
rootNode.disp()print("---------------------------------")#再增加一个子节点
rootNode.children['这是另一个子节点'] = treeNode('这是另一个子节点',3,None)
#显示节点
rootNode.disp()

结果如下

(2) 创建简单数据集

# 创建简单数据集
def loadSimpDat():simpDat = [ ['r', 'z', 'h', 'j', 'p'],['z', 'y', 'x', 'w', 'v', 'u', 't', 's'],['z'],['r', 'x', 'n', 'o', 's'],['y', 'r', 'x', 'z', 'q', 't', 'p'],['y', 'z', 'x', 'e', 'q', 's', 't', 'm']]return simpDatdata=loadSimpDat()
print("data=",data)

运行结果

(3) 创建FP树

a) 数据格式化
得到数据集后,我们需要遍历每一条交易数据,并把它们变成固定格式(frozenset)。这里使用了一个.setdefault()函数,该函数的功能是返回指定键的值,如果键不在字典中,将会添加键并将值设置为一个指定值,默认为None,该函数与get()函数功能类似,但是不同的是setdefault() 返回的键如果不在字典中,会添加键(更新字典),而get()不会添加键。

代码示例

# 数据格式化
# 函数的功能是返回指定键的值,如果键不在字典中,将会添加键并将值设置为一个指定值,默认为None
def createInitSet(dataSet):retDict = {}for trans in dataSet:# print("trans=",trans)fset = frozenset(trans)# print("fest=",fset)retDict.setdefault(fset, 0) #返回指定键的值,如果没有则添加一个键# print("retDict=",retDict)retDict[fset] += 1print("retDict=",retDict)return retDictcreateInitSet(data)

运行结果

{frozenset({'r', 'h', 'j', 'z', 'p'}): 1, frozenset({'x', 'v', 's', 'z', 'y', 't', 'u', 'w'}): 1, frozenset({'z'}): 1, frozenset({'r', 'x', 's', 'o', 'n'}): 1, frozenset({'r', 'q', 'x', 'z', 'p', 'y', 't'}): 1, frozenset({'q', 'e', 'x', 'z', 'y', 'm', 't', 's'}): 1}

b) 更新头指针表
updateHeader()函数的功能是确保节点链接指向树中该元素的每一个实例。从头指针表的nodeLink开始,一直沿着nodeLink知道到达链表末尾。

c) FP树的生长函数
该函数的主要功能是让树生长(这就是FP-growth中growth的来源)。首先测试事务中第一个元素项是不是子节点,如果是子节点,则更新count参数;如果不是子节点,则创建一个新的treeNode作为子节点添加到树中。此时,头指针表也要跟着更新以指向新的节点,这个更新需要调用updateHeader()函数。如果item中不止一个元素项的话,则将剩下的元素项作为参数进行迭代,注意:迭代的时候每次调用会去掉列表中的第一个元素(因为我们在前面判断过了)

d) 创建FP树
该函数主要功能是创建FP树,树构建过程中会遍历数据集两次。第一次遍历数据集并统计每个元素项出现的次数。这些信息被存储在头指针表中。接下来扫描头指针表,删掉那些不满足最小支持度minSup的元素项。在头指针表中增加一列,用来存放指向每种类型第一个元素项的指针。然后创建只包含空集合∅的根节点。第二次遍历数据集,此次只考虑频繁项集。对频繁项集进行排序,最后调用updateTree() 让树生长。

完整代码示例

class treeNode:def __init__(self, nameValue, numOccur, parentNode):self.name = nameValue #名字变量self.count = numOccur #计数变量(频率)self.nodeLink = None  #链接相似元素项self.parent = parentNode   #当前父节点self.children = {}         #用于存放子节点# inc 函数 对count变量增加给定值;def inc(self, numOccur):self.count += numOccur# disp()方法的功能是将树以文本形式显示,主要是方便调试def disp(self, ind=1):print( '   ' *ind, self.name, ' ', self.count)for child in self.children.values():child.disp(ind +1)   #子节点向右缩减# # 显示节点
#
# #创建一个单节点
# rootNode = treeNode('这是父节点',9,None)
# #创建一个子节点
# rootNode.children['这是子节点'] = treeNode('这是子节点',13,None)
# #显示节点
# rootNode.disp()
#
# print("---------------------------------")
#
# #再增加一个子节点
# rootNode.children['这是另一个子节点'] = treeNode('这是另一个子节点',3,None)
# #显示节点
# rootNode.disp()# 创建简单数据集
def loadSimpDat():simpDat = [ ['r', 'z', 'h', 'j', 'p'],['z', 'y', 'x', 'w', 'v', 'u', 't', 's'],['z'],['r', 'x', 'n', 'o', 's'],['y', 'r', 'x', 'z', 'q', 't', 'p'],['y', 'z', 'x', 'e', 'q', 's', 't', 'm']]return simpDat# data=loadSimpDat()
# print("data=",data)# 数据格式化
# 函数的功能是返回指定键的值,如果键不在字典中,将会添加键并将值设置为一个指定值,默认为None
def createInitSet(dataSet):retDict = {}for trans in dataSet:# print("trans=",trans)fset = frozenset(trans)# print("fest=",fset)retDict.setdefault(fset, 0) #返回指定键的值,如果没有则添加一个键# print("retDict=",retDict)retDict[fset] += 1# print("retDict=",retDict)return retDict# 更新头指针表
def updateHeader(nodeToTest, targetNode):while (nodeToTest.nodeLink != None):nodeToTest = nodeToTest.nodeLinknodeToTest.nodeLink = targetNode# FP树的生长函数
def updateTree(items, myTree, headerTable, count):if items[0] in myTree.children:myTree.children[items[0]].inc(count)else:myTree.children[items[0]] = treeNode(items[0], count, myTree)if headerTable[items[0]][1] == None:headerTable[items[0]][1] = myTree.children[items[0]]else:updateHeader(headerTable[items[0]][1], myTree.children[items[0]])if len(items) > 1:updateTree(items[1:], myTree.children[items[0]], headerTable, count)# 创建FP树
def createTree(dataSet, minSup=1):headerTable = {}#第一次遍历数据集, 记录每个数据项的支持度for trans in dataSet:for item in trans:headerTable[item] = headerTable.get(item, 0) + 1#根据最小支持度过滤lessThanMinsup = list(filter(lambda k:headerTable[k] < minSup, headerTable.keys()))for k in lessThanMinsup:del(headerTable[k])freqItemSet = set(headerTable.keys())#如果所有数据都不满足最小支持度,返回None, Noneif len(freqItemSet) == 0:return None, Nonefor k in headerTable:headerTable[k] = [headerTable[k], None]myTree = treeNode('φ', 1, None)#第二次遍历数据集,构建fp-treefor tranSet, count in dataSet.items():#根据最小支持度处理一条训练样本,key:样本中的一个样例,value:该样例的的全局支持度localD = {}for item in tranSet:if item in freqItemSet:localD[item] = headerTable[item][0]if len(localD) > 0:#根据全局频繁项对每个事务中的数据进行排序,等价于 order by p[1] desc, p[0] descorderedItems = [v[0] for v in sorted(localD.items(), key=lambda p:(p[1],p[0]), reverse=True)]updateTree(orderedItems, myTree, headerTable, count)return myTree, headerTable#
data=loadSimpDat() # 读取数据
retDict = createInitSet(data) # 数据格式化
print(retDict)myTree, headerTable = createTree(retDict,minSup=3) # 设定最小支持度minSup=3,运行函数,查看运行结果print("headerTable=",headerTable)
myTree.disp()

运行结果

实验2 从FP树中挖掘频繁项集

有了FP树之后,我们就可以来挖掘频繁项集了。这里的思路和Apriori算法大致类似,首先从单元素项集合开始,然后在此基础上逐步构建更大的集合。当然,这里使用的是FP树而不是原始数据集。

从FP树中挖掘频繁项集的步骤:

  1. 从FP树中获得条件模式基;
  2. 利用条件模式基,构建一个条件FP树;
  3. 迭代:重复上述两个步骤,知道树包含一个元素项为止。

(1) 抽取条件模式基
条件模式基(conditional pattern base)就是以所查找的元素项为结尾的路径集合。每一条路径其实都是一天前缀路径(prefix path)。什么叫前缀路径呢?其实就是介于所查找元素项与根节点之间的所有内容。
下面我们根据FP树写出每个频繁项的前缀路径:

(2) 创造条件FP树——mineTree()

完整代码

class treeNode:def __init__(self, nameValue, numOccur, parentNode):self.name = nameValue #名字变量self.count = numOccur #计数变量(频率)self.nodeLink = None  #链接相似元素项self.parent = parentNode   #当前父节点self.children = {}         #用于存放子节点# inc 函数 对count变量增加给定值;def inc(self, numOccur):self.count += numOccur# disp()方法的功能是将树以文本形式显示,主要是方便调试def disp(self, ind=1):print( '   ' *ind, self.name, ' ', self.count)for child in self.children.values():child.disp(ind +1)   #子节点向右缩减# 创建简单数据集
def loadSimpDat():simpDat = [ ['r', 'z', 'h', 'j', 'p'],['z', 'y', 'x', 'w', 'v', 'u', 't', 's'],['z'],['r', 'x', 'n', 'o', 's'],['y', 'r', 'x', 'z', 'q', 't', 'p'],['y', 'z', 'x', 'e', 'q', 's', 't', 'm']]return simpDat# 数据格式化
# 函数的功能是返回指定键的值,如果键不在字典中,将会添加键并将值设置为一个指定值,默认为None
def createInitSet(dataSet):retDict = {}for trans in dataSet:# print("trans=",trans)fset = frozenset(trans)# print("fest=",fset)retDict.setdefault(fset, 0) #返回指定键的值,如果没有则添加一个键# print("retDict=",retDict)retDict[fset] += 1# print("retDict=",retDict)return retDict# 更新头指针表
def updateHeader(nodeToTest, targetNode):while (nodeToTest.nodeLink != None):nodeToTest = nodeToTest.nodeLinknodeToTest.nodeLink = targetNode# FP树的生长函数
def updateTree(items, myTree, headerTable, count):if items[0] in myTree.children:myTree.children[items[0]].inc(count)else:myTree.children[items[0]] = treeNode(items[0], count, myTree)if headerTable[items[0]][1] == None:headerTable[items[0]][1] = myTree.children[items[0]]else:updateHeader(headerTable[items[0]][1], myTree.children[items[0]])if len(items) > 1:updateTree(items[1:], myTree.children[items[0]], headerTable, count)# 创建FP树
def createTree(dataSet, minSup=1):headerTable = {}#第一次遍历数据集, 记录每个数据项的支持度for trans in dataSet:for item in trans:headerTable[item] = headerTable.get(item, 0) + 1#根据最小支持度过滤lessThanMinsup = list(filter(lambda k:headerTable[k] < minSup, headerTable.keys()))for k in lessThanMinsup:del(headerTable[k])freqItemSet = set(headerTable.keys())#如果所有数据都不满足最小支持度,返回None, Noneif len(freqItemSet) == 0:return None, Nonefor k in headerTable:headerTable[k] = [headerTable[k], None]myTree = treeNode('φ', 1, None)#第二次遍历数据集,构建fp-treefor tranSet, count in dataSet.items():#根据最小支持度处理一条训练样本,key:样本中的一个样例,value:该样例的的全局支持度localD = {}for item in tranSet:if item in freqItemSet:localD[item] = headerTable[item][0]if len(localD) > 0:#根据全局频繁项对每个事务中的数据进行排序,等价于 order by p[1] desc, p[0] descorderedItems = [v[0] for v in sorted(localD.items(), key=lambda p:(p[1],p[0]), reverse=True)]updateTree(orderedItems, myTree, headerTable, count)return myTree, headerTable# 抽取条件模式基
def ascendTree(leafNode, prefixPath):if leafNode.parent != None:prefixPath.append(leafNode.name)ascendTree(leafNode.parent, prefixPath)def findPrefixPath(basePat, headerTable):condPats = {}treeNode = headerTable[basePat][1]while treeNode != None:prefixPath = []ascendTree(treeNode, prefixPath)if len(prefixPath) > 1:condPats[frozenset(prefixPath[1:])] = treeNode.counttreeNode = treeNode.nodeLinkreturn condPats# 创造条件FP树
def mineTree(inTree, headerTable, minSup=1, preFix=set([]), freqItemList=[]):#排序minSup asc, value ascbigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: (p[1][0],p[0]))]for basePat in bigL:newFreqSet = preFix.copy()newFreqSet.add(basePat)freqItemList.append(newFreqSet)# 通过条件模式基找到的频繁项集condPattBases = findPrefixPath(basePat, headerTable)myCondTree, myHead = createTree(condPattBases, minSup)if myHead != None:print('condPattBases: ', basePat, condPattBases)myCondTree.disp()print('*' * 30)mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)#测试
simpDat = loadSimpDat()  # 读取数据
dictDat = createInitSet(simpDat)  # 格式化数据
myFPTree,myheader = createTree(dictDat, 3)  #创建FP树
myFPTree.disp()mineTree(myFPTree, myheader, 2)

运行结果


参考资料

[1] 机器学习实战. 人民邮电出版社.
[2] https://www.bilibili.com/video/av40754697?t=1758

机器学习(11): FP-growth算法 小结及实验相关推荐

  1. Frequent Pattern 挖掘之二(FP Growth算法)(转)

    FP树构造 FP Growth算法利用了巧妙的数据结构,大大降低了Aproir挖掘算法的代价,他不需要不断得生成候选项目队列和不断得扫描整个数据库进行比对.为了达到这样的效果,它采用了一种简洁的数据结 ...

  2. MapReduce框架下的FP Growth算法概述

    转载自:http://blog.sina.com.cn/s/blog_68ffc7a40100uebi.html 前面的博客分析了关联分析中非常重要的一个算法-FP Growth.该算法根据数据库在内 ...

  3. FP Growth算法

    转载自:http://blog.sina.com.cn/s/blog_68ffc7a40100uebg.html FP树构造 FP Growth算法利用了巧妙的数据结构,大大降低了Aproir挖掘算法 ...

  4. MapReduce框架下的FP Growth算法详解

    转载自:http://blog.sina.com.cn/s/blog_68ffc7a40100uebk.html Sharding 这一步没什么好讲的,将数据库分成连续的大小相等的几个块,放置在不同的 ...

  5. FP Growth算法详解

    看了n多资料,就这篇说的比较详细,适合初学者 FP树构造 FP Growth算法利用了巧妙的数据结构,大大降低了Aproir挖掘算法的代价,他不需要不断得生成候选项目队列和不断得扫描整个数据库进行比对 ...

  6. 机器学习(7): 朴素贝叶斯算法 小结及实验

    文章目录 1 朴素贝叶斯简介 2 条件概率与全概率公式 3 贝叶斯推断 4 引例 5 朴素贝叶斯算法分类 (1) GaussianNB (2) MultinomialNB (3) BernoulliN ...

  7. 机器学习笔试面试系列算法集锦

    转自:http://blog.csdn.net/ksearch/article/details/17527857 前言: 找工作时(IT行业),除了常见的软件开发以外,机器学习岗位也可以当作是一个选择 ...

  8. 机器学习入门:聚类算法-5

    机器学习入门:聚类算法 1.实验描述 本实验先简单介绍了一下各聚类算法,然后利用鸢尾花数据集分别针对KMeans聚类.谱聚类.DBSCAN聚类建模,并训练模型:利用模型做预测,并使用相应的指标对模型进 ...

  9. FP Tree算法原理总结(转)

    FP Tree算法原理总结 转自: https://www.cnblogs.com/zhengxingpeng/p/6679280.html 总结得太好了. FP Tree算法原理总结 在Aprior ...

  10. FP Tree算法原理

    作为一个挖掘频繁项集的算法,Apriori算法需要多次扫描数据,I/O是很大的瓶颈.为了解决这个问题,FP Tree算法(也称FP Growth算法)采用了一些技巧,无论多少数据,只需要扫描两次数据集 ...

最新文章

  1. 【JS基础】类型转换——不同数据类型比较
  2. Smart Paster...great tool to paste large strings into Vs.net
  3. 【采用】无监督核心聚类算法
  4. 静态页面cors跨域问题
  5. shiro简单入门介绍
  6. 分享一篇关于奇异值分解的文章[Eng]
  7. 教你读懂Ajax的工作原理
  8. pjsip视频通信开发(上层应用)之EditText重写
  9. 区分那些是属于构架方面的C++功能
  10. python快速体验课-2020年2月
  11. 深度学习(三十五)异构计算GLSL学习笔记(1)
  12. caffe boost cuda __float128 undefined
  13. 【入门】Spring-Boot项目配置Mysql数据库
  14. 纯手工打造漂亮的垂直时间轴,使用最简单的HTML+CSS+JQUERY完成100个版本更新记录的华丽转身!...
  15. 全栈性能测试修炼宝典jmeter实战电子版_推荐一款技术人必备的接口测试神器:Apifox...
  16. html进阶css(5)
  17. 如何永久的关闭macOS 更新提示?
  18. 理解J.U.C中的ReentrantLock
  19. 中国厨房垃圾处理器(厨余粉碎机)行业深度调研与投资前景分析报告2022-2028年版
  20. js 中编码(encode)和解码(decode)的三种方法(传递是特殊符号丢失问题,如‘+’)

热门文章

  1. 百度文库无需VIP和下载券直接下载
  2. php自动发卡程序8.0_「亲测」2020新版个人自动发卡源码 php完整个人发卡网搭建源码...
  3. 计算机多余自动启动项,去掉多余的开机启动项
  4. RTI_DDS自定义插件开发 7 资源
  5. 公众号排版文章批量导出-免费公众号文章批量导出排版
  6. 中文版通用工程师软件DPS 别克雪佛兰编程改装
  7. 数学建模十大经典算法和常用算法
  8. JavaScript + jQuery 知识复习总结(附超实用jQuery中文文档)
  9. DELL服务器R730重装Window Server2012系统
  10. 基于STM32单片机设计指纹考勤机+上位机管理