小结构与小算法:利用树结构求集合的幂集
warn:本文没有多少显而易见的软件工程上的意义,请谨慎投资您的时间与精力!
求集合的幂集,这个问题本身并不难理解,至少在写程序时我们可以很容易就想到穷举法,然后就算是用很暴力的心态很粗暴地去编码都能够写出一个能有漂亮输出结果的程序,但闲得蛋疼的我想试下能不能让穷举法不那么粗暴,然后这个世界上就诞生了一段代码几张稿纸和一篇令人蛋疼的CSDN博文。
在弄清问题的本质后接下来就要考虑如何用计算机世界的语言去描述问题对象本身,在这里的问题对象当然就是“集合”,我们要求的是集合的幂集,也就是一个由集合的全体子集构成的集合,所以我们希望这个数据结构既能描述集合本身又同时能将集合的所有子集表现出来,就算只是很隐含地表现。只要找到这么一个数据结构,那么就可以按某种算法遍历该结构并输出其中所有元素,最后神奇地发现它输出的刚好就是集合的幂集。
数据结构很简单,是Tree,而且不是什么牛叉的二叉树满二叉树完全二叉树红黑树,相信我,就仅仅是一棵树,甚至这棵树还长得相当不平衡
算法也很简单:树结构某种遍历算法
首先假设我们遭遇了这么一个集合{0,1,2,3},完了它说要是我们不把它的子集求出来它就要对我们怎样怎样,好吧,只能怪我RP不好遇上这种事,还连累了大家。
我们先用树结构描述该集合以及它的一切子集:
0
┣1
┃┣2━3
┃┗3
┣2━3
┗3
================2013.01.12的补充======================
上面那棵树我们不妨称它为"幂集树",下面给出幂集树的构造方法
给定如下集合{0,1,2,3},构造一棵幂集树。
1.根据集合元素的顺序从高到低为元素设置优先级,这里0的优先级最高,3的优先级最低。
2.按优先级从低到高的顺序构造幂集树,算法伪代码如下:
for(e : set.iterator().end())//逆序遍历集合元素
if(e == 集合中优先级最低的元素){
e.createSubTree(); //将该元素单独作为一棵只有一个节点的子树
}
else{
e.createSubTree();//以该元素为根节点建立一棵子树
//按优先级从高到低的顺序遍历set中比元素e优先级低的元素
Iterator begin = set.iterator(e) + 1;//begin代表位于e相邻下一位的迭代器位置
Iterator end = set.iterator().end();//end代表集合中最后一个元素
for(e1 : set.iterator_From_To(begin,end)){
e.addSubTree(e1.getTreePointer());
}
}
}
以上算法中,假设元素的数据类型有一个tree成员。
3.取集合中第一个 元素的tree数据成员,即可得该集合的幂集树。
突然想起这部分是被我一笔带过的,但其实是我花费时间最多的部分
不写清楚真是坑人坑己
可能不大直观,请见谅
还剩计算机网络与数据库两科没考,天亮了继续复习~
========================================================
这样的话集合就出来了,而且该集合的所有子集也出现在了里面,不多也不少,当然要让所有子集现身我们还需要一个算法运行于该数据结构之上
这个算法剽窃于树的后序遍历算法,其实这么说也不大严格,但至少在遍历路径上我觉得它和后序遍历算法最接近,附上代码:
private void getAllSubset(TreeNode head){
route.add(head);
for(TreeNode tn :route) System.out.print(tn.data + " ");
System.out.println();
for(TreeNode kid : head.children)
getAllSubset(kid);
route.pollLast();
}
以上是一段没头没尾的JAVA代码(不过方法内代码是完整的),大家权且将其视为伪代码吧
我把这个函数…呃…好像该叫方法…命名为getAllSubset,其中route是一个链表,记录遍历路径,head是遍历开始的父节点,基本思想如下:从父节点开始按后序遍历的路径进行遍历,每到一个节点就将其加入route,接下来输出route的内容,然后对该节点的所有子叶节点递归调用getAllSubset方法,最后将该节点从route中删除。不过这样还不是算法的全部,因为少了该方法的调用方式,那么继续附上代码:
for(int i = 0; i <subsetTree.size(); i++)
getAllSubset(subsetTree.get(i));
subsetTree就是上面的树结构,其实是只一个链表,只是通过在每个节点中添加指向同类型数据的指针…哦,应该叫引用,让这些引用指向subsetTree链表中的其它节点,从而在逻辑上将一个链表变成树结构,算法最顶层的思想就是:对subsetTree中的每一个元素调用getAllSubset,无论该元素是不是树的根节点,无论该元素是谁的子节点,无论该元素有无子节点,一视同仁地全部抓去当getAllSubset的参数。
然后输出如下:
0
0 1
0 1 2
0 1 2 3
0 1 3
0 2
0 2 3
0 3
1
1 2
1 2 3
1 3
2
2 3
3
集合{0,1,2,3}的所有子集都在上面,应该没错吧。什么,还少了一个空集?好吧好吧这个空集是皇帝的新空集,只有聪明人才看得到,如果你和我一样也看不到那我也木有办法,同病相怜吧。
以下全部源代码:
import java.util.LinkedList;
public class GetSubset {
private LinkedList<TreeNode>subsetTree =new LinkedList<TreeNode>();
private LinkedList<TreeNode>route =new LinkedList<TreeNode>();
public GetSubset(LinkedList<Integer> set){
for(Integer data : set){
subsetTree.add(new TreeNode(data));
}
for(int i = 0; i <subsetTree.size(); i++){
int j = i;
while(++j <subsetTree.size()){
subsetTree.get(i).addChild(j);
}
}
for(int i = 0; i <subsetTree.size(); i++){
getAllSubset(subsetTree.get(i));
}
}
privatevoid getAllSubset(TreeNode head){
//System.out.println("[" + head.data + "]");
route.add(head);
for(TreeNode tn :route) System.out.print(tn.data +" ");
System.out.println();/**/
//if(head.children.size() == 0) ccPrintln();
//else for(TreeNode kid : head.children){
// getAllSubset(kid);
//}
for(TreeNode kid : head.children)
getAllSubset(kid);
route.pollLast();
}/**/
//private void ccPrintln(){
// for(int i = route.size()-1; i >=0; i--)
// System.out.print(route.get(i).data + " ");
// System.out.println();
// for(int i = route.size()-1; i >0; i--)
// System.out.print(route.get(i).data + " ");
// System.out.println();
//}
privateclass TreeNode{
Integer data;
LinkedList<TreeNode> children;
TreeNode(Integer data){
this.data = data;
children =new LinkedList<TreeNode>();
}
void addChild(int i){
children.add(subsetTree.get(i));
}
}
publicstaticvoid main(String[] args){
LinkedList<Integer> set =new LinkedList<Integer>();
for(int i = 0; i < 18; i++){
set.add(new Integer(i));
}
GetSubset gss = new GetSubset(set);
}
}
我没写成泛型代码,懒的……测试数据可以在main方法里面改,不过先说明这本身就是一个效率很低的算法(具体是什么指数级我就没去算了,在下微积分苦手一枚,在此也求大神帮忙算下),不要用太大的数据去测试,我刚刚就傻到拿了个100去试,结果就是呆在那看着屏幕上连续不断的输出(估计什么福利彩票的中奖码都被输出一遍了),听着机器“哄哄~”的惨叫,然后才回过神来赶紧把程序停了,可怜了我那台连cs1.6人多了都卡的本本啊!总之我的耐心只是到了17这个数字,不过我的耐心比较特殊,因为17这个耐心极限我是用二分法给找出来的^_^
注:被注释掉的代码是原来的输出算法,很傻很暴力的代码,不过想想还是留着给大家做做反面教材吧。
最后,由于水平有限、时间仓促(我也知道第二个理由很扯淡),错误在所难免,还望大家不吝赐教!