LeetCode 78 Medium 求子集 Python

方法一:

    算法:递归/回溯

    思路

            从数学意义上看,生成一个含n个元素集合的子集,共有2^n个,组成的方法就是每个元素都有添加和

        不添加两种,同理,根据该思路来解此题。

            直接写for循环的话如果第一层用i in range(len(nums)),那么第二层也用i in range(len(nums))

        只能生成含两个元素的子集,也就是几层for循环生成含几层元素的子集,譬如,[1,2],[1,3],[2,3],况且

        这样还不好去重。而利用递归的特点就非常好控制了,因为递归的深度可以是不定的,不像for循环套几层往往

        是写定的。

            回头再看我们人手动生成子集的过程:子集内某个元素nums[i]都有加入和不加入该子集两种选择,那么

        可以将判断的过程看做是一颗二叉树,节点代表的是当前子集的构成情况,根节点为[],从第1层开始,或者说

        i+1层中就是对第i个元素nums[i]进行判定,判断nums[i]这个元素是否加入当前的子集,如果加入就会形成一个

        新的子集,不加入的话,当前子集和父节点的子集情况是一样的(因为没加入当前节点嘛)。根据该思路以及

        树的特点。便可以比较容易地想到递归式子的写法

            据此就很好写递归式子了,传入nums,以及当前判断的是第几个元素i,这样就可以nums[i]来取当前元素,

        当前集合状态subset,以及将结果带出去的变量result

        注意

            1、一般来说递归都有终止条件,满足终止条件时一定return,不管这时的return是否会将当前结果传

            给上一层但是一定会return来终止,不然的会无限循环。此外在终止条件的"else"部分,不一定非要有

            return,return的意义要么是终止递归,要么是将结果返还,但是就本题中,完全可以传入一个result

            来贯穿整个递归的过程

            2、千万小心Python的深拷贝和浅拷贝以及对可变对象传递的值是引用,也就是传入列表的话,如果不传入

            列表的拷贝,传入的就是引用,函数内改变列表会影响函数外列表的值,这里result我们希望传入的是

            引用,但是每一层的子集状态subset我们希望传入的是它的值,是拷贝,这样才能在不同节点就不同状态

            的子集,而不是每一时刻的子集都指向内存中的同一个列表对象,这样各个位置改变的都是同一个列表

            最终无法得到我们要的各不同的子集状态

 

    复杂度分析

        时间:O2^N,遍历所有O2^N个子集(也是O2^N种可能的情况)

        空间:O2^N,递归栈空间以及存储子集的result的空间

LeetCode 78 Medium 求子集 Python

LeetCode 78 Medium 求子集 Python

def subsets0( nums):

    def generate(i, nums, subset, result):

        if i >= len(nums):

            return

        #当前元素不添加

        generate(i + 1, nums, subset[:], result)

        #当前元素添加

        subset.append(nums[i])

        result.append(subset[:])

        generate(i + 1, nums, subset[:], result)

    result = [[]]

    generate(0, nums, [], result)



    return result

方法二:

    算法:位运算

    思路

            n个元素的集合,有2^n次方个子集,每个元素都有出现和不出现两种可能,很自然地可以联想到二进制

        表示,000代表空子集,001代表只有第三个位置有元素的子集,011代表只有后两个元素的子集。如此一来

        可以将原问题构造成二进制的问题,比如集合有3个元素ABC,可以求出来所有子集的表达二进制表达方式(事实

        上不用取求,只要用位运算符,就自然而然地将子集用二进制表示了)。可以将ABC分别表示为100,010,001或者

        001,010,100,表示的顺序不关键,重点是每个元素由互斥的one-hot形式构成即可。

            位运算&是逐位运算,构造one-hot形式的元素表示后,如100,与子集101做按位与运算即,100&101 = 101 = 5 >0

        只要当one-hot位置的1在子集的二进制中也有1,就代表这个元素在这个子集中,那么运算结果一定>0,由于one-hot除了

        1位是1其他位都是0,所以这样构造后可以通过按位与&的结果是否大于0判断该元素是否在该子集中

 

            其实这种解法的思路应该理解为,我已经构造出了所有的子集,2^n次个,只不过我现在拿nums把它翻译过来罢了

        So:

            1. 生成所有子集的二进制表示(只要生成所有子集的十进制形式然后后面用位运算的时候就会"转为"用二进制了)

            2. 对每一个子集的二进制形式:

                遍历nums中将one-hot编码后的元素与当前子集按位与,判断该元素是否在该子集中,在的话就subset.append

                完成"翻译"

                这里one-hot的编码顺序可以从高到低(1<<(len(nums)-1-j))也可以从低到高(1<<j)

                    从低到高:[A,B,C]->[0,1,2]->[001,010,100]

                    从高到低:[A,B,C]->[0,1,2]->[100,010,001]

    复杂度分析

        时间:NO2^N,第一层forO2^N,第二层ON

        空间:O2^N,result,O2^N,subset,ON的空间

LeetCode 78 Medium 求子集 Python

LeetCode 78 Medium 求子集 Python

def subsets1(self, nums):

    all_set = 1<<len(nums)

    result = []

    for i in range(all_set):

        subset = []

        for j in range(len(nums)):

            #或者1<<(len(nums)-1-j) & i

            if 1<<j & i :

                subset.append(nums[j])

        result.append(subset)

    return result

方法三:

    算法:循环/迭代

    思路

        类似于我们人手写生成集合的方式,逐个元素的添加到已有的结果里,并且对已有的结果更新

        迭代式地添加元素

        []

        []->[A]

            ans: []  [A]

        []->[B],[A]->[A,B]

            ans:[]  [A]  [B]  [A,B]

        []->[C],[A]->[A,C],[B]->[B,C],[A,B]->[A,B,C]

            ans: []  [A]  [B]  [A,B]  [C]  [A,C]  [B,C]  [A,B,C]

 

        利用列表生成式可以简写为ans += [x + [item] for x in ans]

    复杂度分析

        时间:NO2^N,第一层forON,第二层O2^(N-1)=O2^N,N-1是因为最后一次循环对2^(N-1)元素for循环添加

            最后一个元素行程O2^N个元素

        空间:O2^N,存储2^N个子集

def subsets2( nums):

    ans = [[]]

    for num in nums:

        for exist in ans[:]:

            ans.append([num]+exist)

        #ans += [x + [item] for x in ans]

    return ans