将集合的映射转换为所有组合的列表

14

我遇到了一个简单任务的难题。我想要做的是将 Map<K,Set<V>> 转换为 List<Map<K,V>>,获取所有可能的组合:

Map {
  {'k1' => set{'v11', 'v12'}},
  {'k2' => set{'v21', 'v22', 'v23'}},
  {'k3' => set{'v31'}}
}

期望结果:

List
{
  Map{'k1'=>'v11', 'k2'=>'v21', 'k3'=>'v31'}, 
  Map{'k1'=>'v11', 'k2'=>'v22', 'k3'=>'v31'},  
  Map{'k1'=>'v11', 'k2'=>'v23', 'k3'=>'v31'}, 
  Map{'k1'=>'v12', 'k2'=>'v21', 'k3'=>'v31'}, 
  Map{'k1'=>'v12', 'k2'=>'v22', 'k3'=>'v31'}, 
  Map{'k1'=>'v12', 'k2'=>'v23', 'k3'=>'v31'}
}

新 Map 中的键(key)和值(value)将是什么? - maks
@maks,感谢您的关注,如果有必要,两者都是任意对象。 - andbi
输入映射中是否恰好有两个条目?根据您的描述,我理解您也希望在示例结果中看到map{'k2'=>,'k1'=>}形式的条目 - 特别是如果有多于两个输入条目。如果不是,请解释应该有哪些组合,哪些不应该有。也许输出在语义上真的只是无序对的列表或双向映射 - 不是真正的单向映射?这可以解释。 (还有,删除我的答案-它假设是作业。) - Ed Staub
1
@Ed Staub,是的,问题假设有任意数量的键,每个键具有可变数量的值。输出列表大小是相应参数集大小的幂,对于我的情况是23=6。如果我有第三个名为“k3”的键,其中有4个集合条目,则输出列表大小将为23*4=24。至于您的答案-让我们忘记它,并且我很抱歉对它发表了非常粗糙的评论。 - andbi
6个回答

8
使用递归!因此,在每个递归级别上,您都可以查看地图的 keyset()中的另一个键。您将按迭代方式向要添加到列表中的当前地图中添加该键的 Set<V>中的元素。
您可以将其视为树。在根节点处,您有一个空列表。然后,树的每个后续级别i表示从第i个集合中选择哪个元素。
以下是包含测试用例的主方法代码:
import java.util.*;

public class Test {
    // method called to generate combinations using map, putting the combinations in list
    public static <K,V> void combinations( Map<K,Set<V>> map, List<Map<K,V>> list ) {
        recurse( map, new LinkedList<K>( map.keySet() ).listIterator(), new HashMap<K,V>(), list );
    }

    // helper method to do the recursion
    private static <K,V> void recurse( Map<K,Set<V>> map, ListIterator<K> iter, Map<K,V> cur, List<Map<K,V>> list ) {
            // we're at a leaf node in the recursion tree, add solution to list
        if( !iter.hasNext() ) {
            Map<K,V> entry = new HashMap<K,V>();

            for( K key : cur.keySet() ) {
                entry.put( key, cur.get( key ) );
            }

            list.add( entry );
        } else {
            K key = iter.next();
            Set<V> set = map.get( key );

            for( V value : set ) {
                cur.put( key, value );
                recurse( map, iter, cur, list );
                cur.remove( key );
            }

            iter.previous();
        }
    }

    public static void main( String[] args ) {
        Map<Integer,Set<Integer>> map = new HashMap<Integer,Set<Integer>>() {{
            put( 1, new HashSet<Integer>() {{
                add( 11 );
                add( 12 );
            }} );
            put( 2, new HashSet<Integer>() {{
                add( 21 );
                add( 22 );
                add( 23 );
            }} );
            put( 3, new HashSet<Integer>() {{
                add( 31 );
            }} );
        }};
        List<Map<Integer,Integer>> list = new LinkedList<Map<Integer,Integer>>();
        combinations( map, list );

        for( Map<Integer,Integer> combination : list ) {
            System.out.println( combination );
        }
    }
}

我稍微修改了一下以适应我的需求(我有 Map<K1,Map<K2,V>>),现在它完美地工作了。谢谢。 - Robert Hume

3
提示:使用递归生成组合“树”。
编辑:好的,我有一些空闲时间,决定尝试一下:
/**
 * 
 * @param <K> The type of the key
 * @param <V> The type of the value
 * @param index The index of the current key to inspect
 * @param current The current map being built by recursion
 * @param map The original input
 * @param list The result
 */ 
public static <K, V> void Combine(int index, Map<K, V> current, 
                                  Map<K, Set<V>> map, 
                                  List<Map<K, V>> list) {
    
    if(index == map.size()) { // if we have gone through all keys in the map
        Map<K, V> newMap = new HashMap<K, V>();
        System.out.println(current);
        for(K key: current.keySet()) {          // copy contents to new map.    
            newMap.put(key, current.get((K)key));               
        }           
        list.add(newMap); // add to result.
    } else {
        Object currentKey = map.keySet().toArray()[index]; // take the current key
        for(V value: map.get(currentKey)) {
            current.put((K)currentKey, value); // put each value into the temporary map
            Combine(index + 1, current, map, list); // recursive call
            current.remove(currentKey); // discard and try a new value
        }
    }
}

我已经进行了几个案例的测试,认为它是正确的。如果有需要,请告诉我。 您可以从另一个只接收“map”作为输入的方法中调用此方法,并创建默认参数“index”,“current”和“list”,并将“list”作为输出返回。请保留HTML标记。 编辑:Java 8+小更新
public static <K, V> void combine(int index, Map<K, V> current, Map<K, List<V>> map, List<Map<K, V>> list) {

    if (index == map.size()) { // if we have gone through all keys in the map
        System.out.println(current);
        list.add(new HashMap<>(current)); // add to result.
    } else {
        K currentKey = map.keySet().stream().skip(index).findFirst().get(); // take the current key
        for (V value : map.get(currentKey)) {
            current.put(currentKey, value); // put each value into the temporary map
            combine(index + 1, current, map, list); // recursive call
            current.remove(currentKey); // discard and try next value
        }
    }
}

谢谢,我知道递归会帮我解决问题,但我就是看不到它。这就是问题所在。 - andbi
非常好,谢谢!我将立即将其用作“TudorOnStackoverflow.java” :) 我的所有声望奖励提案仍然有效,您和Ed的响应将得到回报。 - andbi
无论有没有赏金,我很高兴能帮助你。 :) - Tudor

1

非常更新。

下面是一个针对Guava开发者的解决方案。如果你不熟悉Guava,也很容易转换成其他工具。这个方案不是递归的。它输出了一组字符串对的列表,我觉得这正是你想要的。

static ImmutableMap<String,ImmutableSet<String>> input = ImmutableMap.of(
    "k1", ImmutableSet.of("v11", "v12"),
    "k2", ImmutableSet.of("v21", "v22", "v23"),
    "k3", ImmutableSet.of("v31", "v32", "v33", "v34"));

static class Pair<T,U>
{
    T left;        U right;
    static <T,U> Pair of(T l, U r)
    {   return new Pair(l, r);      }
    Pair(T l, U r)    { left = l; right = r; }
}

static List<Pair<Pair<String, String>,Pair<String, String>>> transform(Map<String,ImmutableSet<String>> input)
{
    List<Pair<Pair<String, String>,Pair<String, String>>> output = Lists.newArrayList();
    // Not a real copy - very cheap.
    ImmutableList<Map.Entry<String, ImmutableSet<String>>> entryList = ImmutableList.copyOf(input.entrySet());
    for ( int i = 0; i < entryList.size(); i++ )
    {
        final Map.Entry<String, ? extends Set<String>> leftEntry = entryList.get(i);
        for ( int j = i+1; j < entryList.size(); j++ )
        {
            final Map.Entry<String, ? extends Set<String>> rightEntry = entryList.get(j);
            for ( String v1 : leftEntry.getValue() )
                for ( String v2 : rightEntry.getValue() )
                    output.add(Pair.of(
                        Pair.of(leftEntry.getKey(), v1),
                        Pair.of(rightEntry.getKey(), v2)));
        }

    }
    return output;
}

@Osw - 啊,原来如此 - 但是你会说Guava吗?请参见上文。 - Ed Staub
你刚刚让我自己生气了两次!第一次是因为我无法解决这个问题,第二次是因为我不理解你的答案 :) Pair of Pairs 让我特别疯狂。你的方法有没有可能返回一个映射列表?那么我会将奖励分给你们两个,并实现你和 Tudor 的解决方案的随机执行 :) - andbi
看看你的输出 - 每个条目都是一个有序对有序对,不是吗?对我来说,在每个映射中只有一个条目是毫无意义的 - 抱歉。我不知道这些条目在现实世界中的含义,所以我无法编写具有恰当名称的Pair类。对于外部配对,如果它们令人困惑,您可以简单地创建Pair的子类。像我一样,要么使用类,要么只使用数组,对我来说往往不够清晰。 - Ed Staub
嗯,我的输出似乎是一对对的,只因为我在示例中只使用了两个键。一般来说,在您的术语中,输出是一组成对的列表:{k1->v11, k2-v22, k3->v32, ...}。您的代码生成了正确的组合,我会再多试一下,但此时我无法理解这里出现一对对的原因 :( - andbi
@Tudor - 对于_output_中的每个条目,它只是一对对。我认为这与“输出列表大小是相应参数集大小的幂,对于我的情况是2 * 3 = 6”是一致的。如果我有第三个名为k3的键,其中包含4个集合条目,则输出列表大小将为2 * 3 * 4 = 24。这就是此代码的输出。 - Ed Staub
显示剩余6条评论

0

首先感谢您提供的案例和实现!我们遇到了同样的问题,看到它们为我们节省了大量时间!我们留下了一个Python实现,供那些可能在其他语言中处理相同问题的人使用 :)

def combinations(themap, maplist):
    return recurse(themap, themap.keys(), {}, maplist, 0)

def recurse(themap, iterator, curmap, maplist, iteratoridx):
    if(iteratoridx>=len(iterator)):
        entry = {}
        for key in curmap.keys():
            entry[key]=curmap[key]
        maplist.append(entry)
    else:
        key = iterator[iteratoridx]
        theset = themap.get(key)
        for value in theset:
            curmap[key]=value
            recurse(themap, iterator, curmap, maplist, iteratoridx+1)
            curmap.pop(key, None)
        iteratoridx=iteratoridx-1


maplist=[]
grid={"A":[1, 2, 3], "B":[4, 5, 6], "C":[7, 8, 9]}
combinations(grid, maplist)
print(maplist)

-1
假设我们有类型为Map<K,Set<V>>的变量“source”,要将其转换为所需的类型,您可以使用以下代码:
List<Map<K,V>> target = new ArrayList<Map<K,V>>();
for(K key : source.keySet()){
    Map<K,V> map = new HashMap<K, V>();
    for(V val : source.get(key)){
       map.put(key, val)
    }
    target.add(map);
}

2
你不会产生所期望的结果。在外部循环的每次迭代中,你都会生成一个带有当前键及其关联值的映射。 - Tudor

-1
List<Map<K,V>> target = new ArrayList<Map<K,V>>();
for(K key : source.keySet()){
    for(V val : source.get(key)){
       Map<K,V> map = new HashMap<K, V>();
       map.put(key, val)
       target.add(map);
    }
}

1
我刚刚重新查看了期望的输出,现在我明白这不是 OP 所要寻找的。 - Fantius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接