参考资料:http://blog.csdn.net/zone_programming/article/details/42032309
更多数据挖掘代码:https://github.com/linyiqun/DataMiningAlgorithm
介绍
GSP算法是序列模式挖掘算法的一种,他是一种类Apriori的一种,整个过程与Apriori算法比较类似,不过在细节上会略有不同,在下面的描述中,将会有所描述。GSP在原有的频繁模式定义的概念下,增加了3个的概念。1、加入时间约束min_gap,max_gap,要求原来的连续变为只要满足在规定的min_gap到max_gap之间即可。
2、加入time_windows_size,只要在windows_size内的item,都可以被认为是同一ItemSet。
3、加入分类标准。
以上3点新的中的第一条特征将会在后面的算法中着重展现。
算法原理
1、根据所输入的序列,找出所有的单项集,即1频繁模式,这里会经过最小支持度阈值的判断。
2、根据1频繁模式进行连接运算,产生2频繁模式,这里会有进行最小阈值的判断。
3、根据2频繁模式连接产生3频繁模式,会经过最小支持度判断和剪枝操作,剪枝操作的原理在于判断他的所有子集是否也全是频繁模式。
4、3频繁模式不断的挖掘知道不能够产生出候选集为止。
连接操作的原理
2个序列,全部变为item列表的形式,如果a序列去掉第1个元素后,b序列去掉最后1个序列,2个序列的item完全一致,则代表可以连接,由b的最后一个元素加入到a中,至于是以独立项集的身份加入还是加入到a中最后1个项集中取决于b中的最后一个元素所属项集是否为单项项集。
时间约束计算
这个是用在支持度计数使用的,GSP算法的支持度计算不是那么简单,比如序列判断<2, <3, 4>>是否在序列<(1,5), 2 , <3, 4>, 2>,这就不能仅仅判断序列中是否只包含2,<3, 4>就行了,还要满足时间间隔约束,这就要把2,和<3,4>的所有出现时间都找出来,然后再里面找出一条满足时间约束的路径就算包含。时间的定义是从左往右起1.2,3...继续,以1个项集为单位,所有2的时间有2个分别为t=2和t=4,然后同理,因为<3,4>在序列中只有1次,所以时间为t=3,所以问题就变为了下面一个数组的问题
2 4
3
从时间数组的上往下,通过对多个时间的组合,找出1条满足时间约束的方案,这里的方案只有2-3,4-3,然后判断时间间隔,如果存在这样的方式,则代表此序列支持所给定序列,支持度值加1,这个算法在程序的实现中是比较复杂的。
算法的代码实现
测试数据输入(格式:事务ID item数 item1 item2.....):
1 2 1 5
1 1 2
1 1 3
1 1 4
2 1 1
2 1 3
2 1 4
2 2 3 5
3 1 1
3 1 2
3 1 3
3 1 4
3 1 5
4 1 1
4 1 3
4 1 5
5 1 4
5 1 5
最后组成的序列为:
<(1,5) 2 3 4>
<1 3 4 (3,5)>
<1 2 3 4 5>
<1 3 5>
<4 5>
也就是说同一序列都是同事务的。下面是关键的类
Sequence.java:
package DataMining_GSP;
import java.util.ArrayList;
/**
* 序列,每个序列内部包含多组ItemSet项集
*
* @author lyq
*
*/
public class Sequence implements Comparable<Sequence>, Cloneable {
// 序列所属事务ID
private int trsanctionID;
// 项集列表
private ArrayList<ItemSet> itemSetList;
public Sequence(int trsanctionID) {
this.trsanctionID = trsanctionID;
this.itemSetList = new ArrayList<>();
}
public Sequence() {
this.itemSetList = new ArrayList<>();
}
public int getTrsanctionID() {
return trsanctionID;
}
public void setTrsanctionID(int trsanctionID) {
this.trsanctionID = trsanctionID;
}
public ArrayList<ItemSet> getItemSetList() {
return itemSetList;
}
public void setItemSetList(ArrayList<ItemSet> itemSetList) {
this.itemSetList = itemSetList;
}
/**
* 取出序列中第一个项集的第一个元素
*
* @return
*/
public Integer getFirstItemSetNum() {
return this.getItemSetList().get(0).getItems().get(0);
}
/**
* 获取序列中最后一个项集
*
* @return
*/
public ItemSet getLastItemSet() {
return getItemSetList().get(getItemSetList().size() - 1);
}
/**
* 获取序列中最后一个项集的最后一个一个元素
*
* @return
*/
public Integer getLastItemSetNum() {
ItemSet lastItemSet = getItemSetList().get(getItemSetList().size() - 1);
int lastItemNum = lastItemSet.getItems().get(
lastItemSet.getItems().size() - 1);
return lastItemNum;
}
/**
* 判断序列中最后一个项集是否为单一的值
*
* @return
*/
public boolean isLastItemSetSingleNum() {
ItemSet lastItemSet = getItemSetList().get(getItemSetList().size() - 1);
int size = lastItemSet.getItems().size();
return size == 1 ? true : false;
}
@Override
public int compareTo(Sequence o) {
// TODO Auto-generated method stub
return this.getFirstItemSetNum().compareTo(o.getFirstItemSetNum());
}
@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
/**
* 拷贝一份一模一样的序列
*/
public Sequence copySeqence(){
Sequence copySeq = new Sequence();
for(ItemSet itemSet: this.itemSetList){
copySeq.getItemSetList().add(new ItemSet(itemSet.copyItems()));
}
return copySeq;
}
/**
* 比较2个序列是否相等,需要判断内部的每个项集是否完全一致
*
* @param seq
* 比较的序列对象
* @return
*/
public boolean compareIsSame(Sequence seq) {
boolean result = true;
ArrayList<ItemSet> itemSetList2 = seq.getItemSetList();
ItemSet tempItemSet1;
ItemSet tempItemSet2;
if (itemSetList2.size() != this.itemSetList.size()) {
return false;
}
for (int i = 0; i < itemSetList2.size(); i++) {
tempItemSet1 = this.itemSetList.get(i);
tempItemSet2 = itemSetList2.get(i);
if (!tempItemSet1.compareIsSame(tempItemSet2)) {
// 只要不相等,直接退出函数
result = false;
break;
}
}
return result;
}
/**
* 生成此序列的所有子序列
*
* @return
*/
public ArrayList<Sequence> createChildSeqs() {
ArrayList<Sequence> childSeqs = new ArrayList<>();
ArrayList<Integer> tempItems;
Sequence tempSeq = null;
ItemSet tempItemSet;
for (int i = 0; i < this.itemSetList.size(); i++) {
tempItemSet = itemSetList.get(i);
if (tempItemSet.getItems().size() == 1) {
tempSeq = this.copySeqence();
// 如果只有项集中只有1个元素,则直接移除
tempSeq.itemSetList.remove(i);
childSeqs.add(tempSeq);
} else {
tempItems = tempItemSet.getItems();
for (int j = 0; j < tempItems.size(); j++) {
tempSeq = this.copySeqence();
// 在拷贝的序列中移除一个数字
tempSeq.getItemSetList().get(i).getItems().remove(j);
childSeqs.add(tempSeq);
}
}
}
return childSeqs;
}
}
ItemSet.java:
package DataMining_GSP;
import java.util.ArrayList;
/**
* 序列中的子项集
*
* @author lyq
*
*/
public class ItemSet {
/**
* 项集中保存的是数字项数组
*/
private ArrayList<Integer> items;
public ItemSet(String[] itemStr) {
items = new ArrayList<>();
for (String s : itemStr) {
items.add(Integer.parseInt(s));
}
}
public ItemSet(int[] itemNum) {
items = new ArrayList<>();
for (int num : itemNum) {
items.add(num);
}
}
public ItemSet(ArrayList<Integer> itemNum) {
this.items = itemNum;
}
public ArrayList<Integer> getItems() {
return items;
}
public void setItems(ArrayList<Integer> items) {
this.items = items;
}
/**
* 判断2个项集是否相等
*
* @param itemSet
* 比较对象
* @return
*/
public boolean compareIsSame(ItemSet itemSet) {
boolean result = true;
if (this.items.size() != itemSet.items.size()) {
return false;
}
for (int i = 0; i < itemSet.items.size(); i++) {
if (this.items.get(i) != itemSet.items.get(i)) {
// 只要有值不相等,直接算作不相等
result = false;
break;
}
}
return result;
}
/**
* 拷贝项集中同样的数据一份
*
* @return
*/
public ArrayList<Integer> copyItems() {
ArrayList<Integer> copyItems = new ArrayList<>();
for (int num : this.items) {
copyItems.add(num);
}
return copyItems;
}
}
GSPTool.java(算法工具类):package DataMining_GSP;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* GSP序列模式分析算法
*
* @author lyq
*
*/
public class GSPTool {
// 测试数据文件地址
private String filePath;
// 最小支持度阈值
private int minSupportCount;
// 时间最小间隔
private int min_gap;
// 时间最大间隔
private int max_gap;
// 原始数据序列
private ArrayList<Sequence> totalSequences;
// GSP算法中产生的所有的频繁项集序列
private ArrayList<Sequence> totalFrequencySeqs;
// 序列项数字对时间的映射图容器
private ArrayList<ArrayList<HashMap<Integer, Integer>>> itemNum2Time;
public GSPTool(String filePath, int minSupportCount, int min_gap,
int max_gap) {
this.filePath = filePath;
this.minSupportCount = minSupportCount;
this.min_gap = min_gap;
this.max_gap = max_gap;
totalFrequencySeqs = new ArrayList<>();
readDataFile();
}
/**
* 从文件中读取数据
*/
private void readDataFile() {
File file = new File(filePath);
ArrayList<String[]> dataArray = new ArrayList<String[]>();
try {
BufferedReader in = new BufferedReader(new FileReader(file));
String str;
String[] tempArray;
while ((str = in.readLine()) != null) {
tempArray = str.split(" ");
dataArray.add(tempArray);
}
in.close();
} catch (IOException e) {
e.getStackTrace();
}
HashMap<Integer, Sequence> mapSeq = new HashMap<>();
Sequence seq;
ItemSet itemSet;
int tID;
String[] itemStr;
for (String[] str : dataArray) {
tID = Integer.parseInt(str[0]);
itemStr = new String[Integer.parseInt(str[1])];
System.arraycopy(str, 2, itemStr, 0, itemStr.length);
itemSet = new ItemSet(itemStr);
if (mapSeq.containsKey(tID)) {
seq = mapSeq.get(tID);
} else {
seq = new Sequence(tID);
}
seq.getItemSetList().add(itemSet);
mapSeq.put(tID, seq);
}
// 将序列图加入到序列List中
totalSequences = new ArrayList<>();
for (Map.Entry entry : mapSeq.entrySet()) {
totalSequences.add((Sequence) entry.getValue());
}
}
/**
* 生成1频繁项集
*
* @return
*/
private ArrayList<Sequence> generateOneFrequencyItem() {
int count = 0;
int currentTransanctionID = 0;
Sequence tempSeq;
ItemSet tempItemSet;
HashMap<Integer, Integer> itemNumMap = new HashMap<>();
ArrayList<Sequence> seqList = new ArrayList<>();
for (Sequence seq : totalSequences) {
for (ItemSet itemSet : seq.getItemSetList()) {
for (int num : itemSet.getItems()) {
// 如果没有此种类型项,则进行添加操作
if (!itemNumMap.containsKey(num)) {
itemNumMap.put(num, 1);
}
}
}
}
boolean isContain = false;
int number = 0;
for (Map.Entry entry : itemNumMap.entrySet()) {
count = 0;
number = (int) entry.getKey();
for (Sequence seq : totalSequences) {
isContain = false;
for (ItemSet itemSet : seq.getItemSetList()) {
for (int num : itemSet.getItems()) {
if (num == number) {
isContain = true;
break;
}
}
if(isContain){
break;
}
}
if(isContain){
count++;
}
}
itemNumMap.put(number, count);
}
for (Map.Entry entry : itemNumMap.entrySet()) {
count = (int) entry.getValue();
if (count >= minSupportCount) {
tempSeq = new Sequence();
tempItemSet = new ItemSet(new int[] { (int) entry.getKey() });
tempSeq.getItemSetList().add(tempItemSet);
seqList.add(tempSeq);
}
}
// 将序列升序排列
Collections.sort(seqList);
// 将频繁1项集加入总频繁项集列表中
totalFrequencySeqs.addAll(seqList);
return seqList;
}
/**
* 通过1频繁项集连接产生2频繁项集
*
* @param oneSeq
* 1频繁项集序列
* @return
*/
private ArrayList<Sequence> generateTwoFrequencyItem(
ArrayList<Sequence> oneSeq) {
Sequence tempSeq;
ArrayList<Sequence> resultSeq = new ArrayList<>();
ItemSet tempItemSet;
int num1;
int num2;
// 假如将<a>,<b>2个1频繁项集做连接组合,可以分为<a a>,<a b>,<b a>,<b b>4个序列模式
// 注意此时的每个序列中包含2个独立项集
for (int i = 0; i < oneSeq.size(); i++) {
num1 = oneSeq.get(i).getFirstItemSetNum();
for (int j = 0; j < oneSeq.size(); j++) {
num2 = oneSeq.get(j).getFirstItemSetNum();
tempSeq = new Sequence();
tempItemSet = new ItemSet(new int[] { num1 });
tempSeq.getItemSetList().add(tempItemSet);
tempItemSet = new ItemSet(new int[] { num2 });
tempSeq.getItemSetList().add(tempItemSet);
if (countSupport(tempSeq) >= minSupportCount) {
resultSeq.add(tempSeq);
}
}
}
// 上面连接还有1种情况是每个序列中只包含有一个项集的情况,此时a,b的划分则是<(a,a)> <(a,b)> <(b,b)>
for (int i = 0; i < oneSeq.size(); i++) {
num1 = oneSeq.get(i).getFirstItemSetNum();
for (int j = i; j < oneSeq.size(); j++) {
num2 = oneSeq.get(j).getFirstItemSetNum();
tempSeq = new Sequence();
tempItemSet = new ItemSet(new int[] { num1, num2 });
tempSeq.getItemSetList().add(tempItemSet);
if (countSupport(tempSeq) >= minSupportCount) {
resultSeq.add(tempSeq);
}
}
}
// 同样将2频繁项集加入到总频繁项集中
totalFrequencySeqs.addAll(resultSeq);
return resultSeq;
}
/**
* 根据上次的频繁集连接产生新的侯选集
*
* @param seqList
* 上次产生的候选集
* @return
*/
private ArrayList<Sequence> generateCandidateItem(
ArrayList<Sequence> seqList) {
Sequence tempSeq;
ArrayList<Integer> tempNumArray;
ArrayList<Sequence> resultSeq = new ArrayList<>();
// 序列数字项列表
ArrayList<ArrayList<Integer>> seqNums = new ArrayList<>();
for (int i = 0; i < seqList.size(); i++) {
tempNumArray = new ArrayList<>();
tempSeq = seqList.get(i);
for (ItemSet itemSet : tempSeq.getItemSetList()) {
tempNumArray.addAll(itemSet.copyItems());
}
seqNums.add(tempNumArray);
}
ArrayList<Integer> array1;
ArrayList<Integer> array2;
// 序列i,j的拷贝
Sequence seqi = null;
Sequence seqj = null;
// 判断是否能够连接,默认能连接
boolean canConnect = true;
// 进行连接运算,包括自己与自己连接
for (int i = 0; i < seqNums.size(); i++) {
for (int j = 0; j < seqNums.size(); j++) {
array1 = (ArrayList<Integer>) seqNums.get(i).clone();
array2 = (ArrayList<Integer>) seqNums.get(j).clone();
// 将第一个数字组去掉第一个,第二个数字组去掉最后一个,如果剩下的部分相等,则可以连接
array1.remove(0);
array2.remove(array2.size() - 1);
canConnect = true;
for (int k = 0; k < array1.size(); k++) {
if (array1.get(k) != array2.get(k)) {
canConnect = false;
break;
}
}
if (canConnect) {
seqi = seqList.get(i).copySeqence();
seqj = seqList.get(j).copySeqence();
int lastItemNum = seqj.getLastItemSetNum();
if (seqj.isLastItemSetSingleNum()) {
// 如果j序列的最后项集为单一值,则最后一个数字以独立项集加入i序列
ItemSet itemSet = new ItemSet(new int[] { lastItemNum });
seqi.getItemSetList().add(itemSet);
} else {
// 如果j序列的最后项集为非单一值,则最后一个数字加入i序列最后一个项集中
ItemSet itemSet = seqi.getLastItemSet();
itemSet.getItems().add(lastItemNum);
}
// 判断是否超过最小支持度阈值
if (isChildSeqContained(seqi)
&& countSupport(seqi) >= minSupportCount) {
resultSeq.add(seqi);
}
}
}
}
totalFrequencySeqs.addAll(resultSeq);
return resultSeq;
}
/**
* 判断此序列的所有子序列是否也是频繁序列
*
* @param seq
* 待比较序列
* @return
*/
private boolean isChildSeqContained(Sequence seq) {
boolean isContained = false;
ArrayList<Sequence> childSeqs;
childSeqs = seq.createChildSeqs();
for (Sequence tempSeq : childSeqs) {
isContained = false;
for (Sequence frequencySeq : totalFrequencySeqs) {
if (tempSeq.compareIsSame(frequencySeq)) {
isContained = true;
break;
}
}
if (!isContained) {
break;
}
}
return isContained;
}
/**
* 候选集判断支持度的值
*
* @param seq
* 待判断序列
* @return
*/
private int countSupport(Sequence seq) {
int count = 0;
int matchNum = 0;
Sequence tempSeq;
ItemSet tempItemSet;
HashMap<Integer, Integer> timeMap;
ArrayList<ItemSet> itemSetList;
ArrayList<ArrayList<Integer>> numArray = new ArrayList<>();
// 每项集对应的时间链表
ArrayList<ArrayList<Integer>> timeArray = new ArrayList<>();
for (ItemSet itemSet : seq.getItemSetList()) {
numArray.add(itemSet.getItems());
}
for (int i = 0; i < totalSequences.size(); i++) {
timeArray = new ArrayList<>();
for (int s = 0; s < numArray.size(); s++) {
ArrayList<Integer> childNum = numArray.get(s);
ArrayList<Integer> localTime = new ArrayList<>();
tempSeq = totalSequences.get(i);
itemSetList = tempSeq.getItemSetList();
for (int j = 0; j < itemSetList.size(); j++) {
tempItemSet = itemSetList.get(j);
matchNum = 0;
int t = 0;
if (tempItemSet.getItems().size() == childNum.size()) {
timeMap = itemNum2Time.get(i).get(j);
// 只有当项集长度匹配时才匹配
for (int k = 0; k < childNum.size(); k++) {
if (timeMap.containsKey(childNum.get(k))) {
matchNum++;
t = timeMap.get(childNum.get(k));
}
}
// 如果完全匹配,则记录时间
if (matchNum == childNum.size()) {
localTime.add(t);
}
}
}
if (localTime.size() > 0) {
timeArray.add(localTime);
}
}
// 判断时间是否满足时间最大最小约束,如果满足,则此条事务包含候选事务
if (timeArray.size() == numArray.size()
&& judgeTimeInGap(timeArray)) {
count++;
}
}
return count;
}
/**
* 判断事务是否满足时间约束
*
* @param timeArray
* 时间数组,每行代表各项集的在事务中的发生时间链表
* @return
*/
private boolean judgeTimeInGap(ArrayList<ArrayList<Integer>> timeArray) {
boolean result = false;
int preTime = 0;
ArrayList<Integer> firstTimes = timeArray.get(0);
timeArray.remove(0);
if (timeArray.size() == 0) {
return false;
}
for (int i = 0; i < firstTimes.size(); i++) {
preTime = firstTimes.get(i);
if (dfsJudgeTime(preTime, timeArray)) {
result = true;
break;
}
}
return result;
}
/**
* 深度优先遍历时间,判断是否有符合条件的时间间隔
*
* @param preTime
* @param timeArray
* @return
*/
private boolean dfsJudgeTime(int preTime,
ArrayList<ArrayList<Integer>> timeArray) {
boolean result = false;
ArrayList<ArrayList<Integer>> timeArrayClone = (ArrayList<ArrayList<Integer>>) timeArray
.clone();
ArrayList<Integer> firstItemItem = timeArrayClone.get(0);
for (int i = 0; i < firstItemItem.size(); i++) {
if (firstItemItem.get(i) - preTime >= min_gap
&& firstItemItem.get(i) - preTime <= max_gap) {
// 如果此2项间隔时间满足时间约束,则继续往下递归
preTime = firstItemItem.get(i);
timeArrayClone.remove(0);
if (timeArrayClone.size() == 0) {
return true;
} else {
result = dfsJudgeTime(preTime, timeArrayClone);
if (result) {
return true;
}
}
}
}
return result;
}
/**
* 初始化序列项到时间的序列图,为了后面的时间约束计算
*/
private void initItemNumToTimeMap() {
Sequence seq;
itemNum2Time = new ArrayList<>();
HashMap<Integer, Integer> tempMap;
ArrayList<HashMap<Integer, Integer>> tempMapList;
for (int i = 0; i < totalSequences.size(); i++) {
seq = totalSequences.get(i);
tempMapList = new ArrayList<>();
for (int j = 0; j < seq.getItemSetList().size(); j++) {
ItemSet itemSet = seq.getItemSetList().get(j);
tempMap = new HashMap<>();
for (int itemNum : itemSet.getItems()) {
tempMap.put(itemNum, j + 1);
}
tempMapList.add(tempMap);
}
itemNum2Time.add(tempMapList);
}
}
/**
* 进行GSP算法计算
*/
public void gspCalculate() {
ArrayList<Sequence> oneSeq;
ArrayList<Sequence> twoSeq;
ArrayList<Sequence> candidateSeq;
initItemNumToTimeMap();
oneSeq = generateOneFrequencyItem();
twoSeq = generateTwoFrequencyItem(oneSeq);
candidateSeq = twoSeq;
// 不断连接生产候选集,直到没有产生出侯选集
for (;;) {
candidateSeq = generateCandidateItem(candidateSeq);
if (candidateSeq.size() == 0) {
break;
}
}
outputSeqence(totalFrequencySeqs);
}
/**
* 输出序列列表信息
*
* @param outputSeqList
* 待输出序列列表
*/
private void outputSeqence(ArrayList<Sequence> outputSeqList) {
for (Sequence seq : outputSeqList) {
System.out.print("<");
for (ItemSet itemSet : seq.getItemSetList()) {
System.out.print("(");
for (int num : itemSet.getItems()) {
System.out.print(num + ",");
}
System.out.print("), ");
}
System.out.println(">");
}
}
}
调用类Client.java:
package DataMining_GSP;
/**
* GSP序列模式分析算法
* @author lyq
*
*/
public class Client {
public static void main(String[] args){
String filePath = "C:\Users\lyq\Desktop\icon\testInput.txt";
//最小支持度阈值
int minSupportCount = 2;
//时间最小间隔
int min_gap = 1;
//施加最大间隔
int max_gap = 5;
GSPTool tool = new GSPTool(filePath, minSupportCount, min_gap, max_gap);
tool.gspCalculate();
}
}
算法的输出(挖掘出的所有频繁模式):
<(1,), >
<(2,), >
<(3,), >
<(4,), >
<(5,), >
<(1,), (3,), >
<(1,), (4,), >
<(1,), (5,), >
<(2,), (3,), >
<(2,), (4,), >
<(3,), (4,), >
<(3,), (5,), >
<(4,), (5,), >
<(1,), (3,), (4,), >
<(1,), (3,), (5,), >
<(2,), (3,), (4,), >
算法实现的难点
1、算法花费了几天的时间,难点首先在于对算法原理本身的理解,网上对于此算法的资料特别少,而且不同的人所表达的意思 都有少许的不同,讲的也不是很详细,于是就通过阅读别人的代码理解GSP算法的原理,我的代码实现也是参考了参考资料的C语言的实现。
2、在实现时间约束的支持度计数统计的时候,调试了一段时间,做时间统计容易出错,因为层级实在太多容易搞晕。
3、还有1个是Sequence和ItemSet的拷贝时的引用问题,在产生新的序列时一定要深拷贝1个否则导致同一引用会把原数据给改掉的。
GSP算法和Apriori算法的比较
我是都实现过了GSP算法和Apriori算法的,后者是被称为关联规则挖掘算法,偏向于挖掘关联规则的,2个算法在连接的操作上有不一样的地方,还有在数据的构成方式上,Apriori的数据会简单一点,都是单项单项构成的,而且在做支持度统计的时候只需判断存在与否即可。不需要考虑时间约束。Apriori算法给定K项集,连接到K-1项集算法就停止了,而GSP算法是直到不能够产生候选集为止。