FP-Tree算法和Apriori算法都属于基于关联规则的分类算法,前者在实现时采用树形结构,避免了产生候选集的过程,使算法效率得到提升。
1.题材数据
动作 战争
喜剧 爱情
剧情 动作 犯罪
剧情 动作 战争
科幻 灾难
喜剧 爱情 奇幻
动作 战争
喜剧 奇幻
剧情
剧情
2.事务存储
对于1中的数据,需要使用List<List<String>>的结构进行存储。
List<List<String>> datasList = new ArrayList<>();
//1.读文件
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(new File("trolley.txt"))));
String datasLine = null;
while((datasLine = bufferedReader.readLine())!=null){
String[] datas = datasLine.split(",");
List<String> dataList = new ArrayList<>();
for (String s:datas){
dataList.add(s);
System.out.print(s+" ");
}
System.out.println();
datasList.add(dataList);
}
bufferedReader.close();
3.建树
(1)获取属性集和属性频度
Map<String,Integer> freqOneMap = new HashMap<>();
for (List<String> datalist: datasList){
for (String s:datalist){
if (freqOneMap.keySet().contains(s)){
freqOneMap.put(s, freqOneMap.get(s)+1);
}else{
freqOneMap.put(s,1);
}
}
}
freqOneMap 中包含所有属性和其频度,之后的建树、链表只取其中支持度合格的属性。
(2)将频繁一项集的属性作为链表头
Map<String,TreeNode> headers = new HashMap<>();
for (Map.Entry<String,Integer> entry: freqOneMap.entrySet()){
if (entry.getValue() >= SUPPORT){
TreeNode header = new TreeNode(entry.getKey(), entry.getValue());
headers.put(header.getName(), header);
}
}
(3)为每一个事务中的属性进行排序(排序规则是按属性的频度)
for (List<String> dataList:datasList){
Collections.sort(dataList, new Comparator<String>() {
@Override
//o2大于o1则返回正数
public int compare(String o1, String o2) {
return freqOneMap.get(o2) - freqOneMap.get(o1);
}
});
}
(4)扫描每个事务中的属性,建树
扫描时会出现2种情况:树中有当前节点(沿路径存在),则为为该节点的count值加一;
树中无当前节点,则该属性与在其之后的属性(同一事务下)都需要进行建立节点。
TreeNode root = new TreeNode();
for (List<String> dataList :datasList){
//需要 添加或修改 节点的父节点
TreeNode subTreeRoot = root;
//需要将频度高的先进行连接,故之前进行排序
//判断是否有孩子节点,有的话需要进行比较
if (root.getChildren() != null){
//当树中已存在节点时,无需添加,只为该节点count++
while(!dataList.isEmpty() && root.getChildren().contains(dataList.get(0))){
for (TreeNode treeNode: root.getChildren()){
if (treeNode.getName().equals(dataList.get(0))){
int count = treeNode.getCount()+1;
treeNode.setCount(count);
dataList.remove(0);
subTreeRoot = treeNode;
}
}
}
}
//树中不存在该节点时,需要添加新的节点,递归创建剩余节点
addNode(subTreeRoot,dataList,headers);
}
(5)创建节点方法
每个节点既是树中的节点,也是链表中的节点,因此每个节点可分为三个域看待:数据域,树指针域,链表指针域。
private static void addNode(TreeNode subTreeRoot, List<String> dataList, Map<String,TreeNode> headers) {
while(!dataList.isEmpty()){
String data = dataList.get(0);
dataList.remove(0);
//需要在频繁集(链表头集)中,其它数据直接抛弃
if (headers.containsKey(data)){
//数据域
TreeNode treeNode = new TreeNode(data, 1);
//指针域
//树
treeNode.setParent(subTreeRoot);
subTreeRoot.addChild(treeNode);
subTreeRoot = treeNode;
//链表
TreeNode header = headers.get(data);
if (header.getTail() == null){
header.setNextHomonym(treeNode);
}else{
header.getTail().setNextHomonym(treeNode);
}
header.setTail(treeNode);
//递归
addNode(subTreeRoot,dataList,headers);
}
}
}
建立节点时采用递归建立,直至将该事务中剩余的属性全部(需要是频繁属性,不频繁属性直接跳过建立)建立节点。
节点的数据域包括属性名,属性频度;树指针域包括孩子节点(多个)、父节点;链表指针域包括尾节点、下一个节点;
private String name;
private int count;
private List<TreeNode> children;
private TreeNode parent;
private TreeNode tail;//链表尾节点
private TreeNode nextHomonym;//链表下一个节点
(6)缩小树
for (TreeNode header: headers.values()){
//添加关联规则 且可作为下次递归的postModel(后缀模式)
List<String> ruleList = new ArrayList<>();
ruleList.add(header.getName());
if (postModel!=null){
ruleList.addAll(postModel);
}
//添加到关联规则集中
frequentMap.put(ruleList, header.getCount());
//获得新的CPB(条件模式基)
List<List<String>> newDatasList = new ArrayList<>();
TreeNode nextNode = header;
while((nextNode = nextNode.getNextHomonym())!= null){
TreeNode parent = nextNode.getParent();
//为了保持父节点原有顺序,需要使用链表进行存储
List<String> tempList = new LinkedList<>();
while(parent.getName() != null){
((LinkedList<String>) tempList).push(parent.getName());
parent = parent.getParent();
}
int count = nextNode.getCount();
while(count--> 0){
newDatasList.add((List<String>) ((LinkedList<String>) tempList).clone());
}
}
//递归
FPTreeRun(newDatasList, ruleList);
}
根据扫描“链表头集合”,扫描时根据每个链表头遍历该链表,得到规则,并产生新的条件模式基和后缀模式。
本文参考自https://www.cnblogs.com/zhangchaoyang/articles/2198946.html