关于n个数里选k个的不同方法及一些思考

给出两个整数n和k,返回从1到n中取k个数字的所有可能的组合
例如:
如果n=4,k=2,结果为
[↵ [2,4],↵ [3,4],↵ [2,3],↵ [1,2],↵ [1,3],↵ [1,4],↵]

这题本身并不是特别的难,但是不同方法的复杂度差很多,而且响应了之前碰到的那句:

求所有符合的结果用深度递归(及时修剪),求最优结果或结果数量用动态规划;

最先想到的方式:
方法一:
用走迷宫的思路暴力递归,n个数每一个都可以看做迷宫的一步,有选和不选两种选择:

class Solution {
    public ArrayList> result = new ArrayList>();
    public void go(int n,int k,int i,ArrayList list){
        if(list.size()==k){
             result.add(list);
             return;
        }
        if(i>n) return;
        // 不把当前位置的数选入
        go(n,k,i+1,list);
        ArrayList newList = new ArrayList(list);
        newList.add(i);
        // 把当前位置的数选入
        go(n,k,i+1,newList);
    }
    public List> combine(int n, int k) {
        go(n,k,1,new ArrayList());
        return result;
    }
}

这种方式表面上像一颗二叉树一样展开n个数有2^n次方种递归,但是对二叉树进行适当裁剪,对不要对枝叶不再扩展,勉强通过了测试55 ms 42.3 MB;

方法二:
层序递归的思路,一层层的去求,求f(n,1),f(n,2),......,f(n,k)---->n个数选1个的集合,选2个的集合....k个的集合;这种方式会建立大量的集合;
一开始我这样做,0-k的每一位上有1-n种选择,表面上感觉是k*n的复杂度,但是由于是不重复的,每次为了避免前面选过的数再次被选我用的HashSet不断判断的方式来避免重复加入,最后直接超时了;

class Solution {
    public List> combine(int n, int k) {
        // 
        HashSet> result = new HashSet>();
        result.add(new HashSet());
        for(int i=1;i<=k;i++){
            HashSet> newResult = new HashSet>();
            for(int j=1;j<=n;j++){
                for(Set set:result){
                    if(!set.contains(j)){
                        HashSet newSet = new HashSet(set);
                        newSet.add(j);
                        newResult.add(newSet);
                    }
                }
            }
            result=newResult;
        }
        ArrayList> res = new ArrayList>();
        for(Set set:result){
            ArrayList temp = new ArrayList(set);
            res.add(temp);
        }
        return res;
    }
}

方法三:
动态规划的背包思路: dp[i][j]--->∑dp[i-1][j]+∑f(dp[i-1][j-1],i);利用前面的结果,二维的一格格的求,(方法二相当于一维的f(n,1)->f(n,2)->f(n,3)->.....->f(n,k),n恒定,一层层的求), 这种方式比方法二快,但比走迷宫暴力并剪枝的方式慢4倍, 213 ms -270.3 MB勉强通过;
(之前华为那道题两个数组互相交换,最后最小值,那道题则用动态规划转为背包问题最好,映证了那句话:求所有符合的结果用深度递归(及时修剪),求最优结果或结果数量用动态规划;)

class Solution {
    public List> combine(int n, int k) {
        // dp[i][j]--->dp[i-1][j]+f(dp[i-1][j-1],i);
        ArrayList>[][] dp= new ArrayList[n+1][k+1];
        for(int i=0;i<=n;i++){
            for(int j=0;j<=k;j++){
                if(i==0||j==0){
                    ArrayList> temp = new ArrayList>();
                    temp.add(new ArrayList());
                    dp[i][j]=temp;
                }else{
                    ArrayList> newResult = new ArrayList>();
                    for(List list:dp[i-1][j-1]){
                        ArrayList newList = new ArrayList(list);
                        newList.add(i);
                        newResult.add(newList);
                    }

                    for(List list:dp[i-1][j]){
                        if(list.size()==j){
                            newResult.add(new ArrayList(list));
                        }
                    }
                    dp[i][j]=newResult;
                }
            }
        }
        return dp[n][k];
    }
}

方法四:
用DFS深度优先遍历,类似于走迷宫但是每一步横向铺开,方法一的每一步都只考虑两种情况,选和不选,这里直接扩散到 i到n-(k-size)+1 (其中size是前面递归过来的已经选了的情况数;
)中的任意位置,类似于棋盘不只上下左右走了,棋子♟可以跳到任意位置;
在每一次递归前list.add(i),执行go(next)递归之后,又及时的进行删除动作,list.remove(last);只在最后符合要求时clone这个list,并放入结果集合;(不保存中间结果)
最后只用了2ms 42MB;

class Solution {
    public ArrayList> result = new ArrayList>();
    public void go(int n,int k,int i,ArrayList list){
        if(list.size()==k){
            // 这里与方法一不同,需要克隆后放入结果集;因为递归后还要撤销给for的下一个用;
             result.add(new ArrayList(list));
             return;
        }
        if(i>n) return;
        int size = list.size();
        for(int index = i;index<=n-(k-size)+1;index++){
            list.add(index);
            go(n,k,index+1,list);
            // 每一层及每一次调用都及时删除,方便下一次for循环恢复;
            list.remove(size);
        }
    }
    public List> combine(int n, int k) {
        go(n,k,1,new ArrayList());
        return result;
    }
}

你可能感兴趣的:(关于n个数里选k个的不同方法及一些思考)