分享一行 Python 代码 `reduce(add, zip(nums[:n], nums[n:]))` - 来自解决 Leetcode #1470. Shuffle the Array 一题

2020-07-25 14:07:02 +08:00
 oahebky

本来是不会想要分享发出来的。

但是因为在 comment 区发了解法,然后有人问解释一下这行代码,所以就解释了一下。

本着解释都解释了,敲了那么多行字,所以不如分享出来给更多的人看到。


(the)Problem

[问题] :Leetcode#1470. Shuffle the Array

[描述] :

给你一个数组 nums,数组中有 2n 个元素,按 [x1,x2,...,xn,y1,y2,...,yn] 的格式排列。

请你将数组按 [x1,y1,x2,y2,...,xn,yn] 格式重新排列,返回重排后的数组。

示例 1:

输入:nums = [2,5,1,3,4,7], n = 3
输出:[2,3,5,4,1,7] 
解释:由于 x1=2, x2=5, x3=1, y1=3, y2=4, y3=7,所以答案为 [2,3,5,4,1,7]

示例 2:N/A

示例 3:

输入:nums = [1,1,2,2], n = 2
输出:[1,2,1,2]

[提示] :

来源:力扣( LeetCode ) 链接: https://leetcode-cn.com/problems/shuffle-the-array 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


Solution

我的一行解法:

class Solution:
    def shuffle(self, nums: List[int], n: int) -> List[int]:
        return reduce(add, zip(nums[:n], nums[n:]))

解释

对这一行代码的 explain:

https://leetcode.com/problems/shuffle-the-array/discuss/751957/Python-one-line-(Memory-Usage-less-then-100.00):-the-more-pythonic-way/629230


3897 次点击
所在节点    Python
53 条回复
msg7086
2020-07-25 14:14:46 +08:00
顺便 Ruby 里可以用:
nums.each_slice(n).to_a.transpose.flatten

当然也可以用很朴素的:
nums[0..n-1].zip(nums[n..-1]).flatten
jmc891205
2020-07-25 14:36:47 +08:00
本来遍历一遍就可以解决的问题,用了 reduce 和 zip 等于遍历了两遍。
本来开一个数组就可以解决的问题,用了 zip 和 slice 就需要额外内存去保存他们的返回值。

时间和内存都浪费了,还降低了代码可读性。
oahebky
2020-07-25 15:03:06 +08:00
@jmc891205

“额外内存去保存他们的返回值”???
“降低了代码可读性”???

你就是传说中的杠精?

这个是发在 Python 主题下的帖子,我写的是 python3 的代码,不懂 python3 的特性还有不了解高阶函数就不要出来秀智商了。
carlclone
2020-07-25 15:05:06 +08:00
这是算法题不是接口调用题
oahebky
2020-07-25 15:12:13 +08:00
@carlclone

那么你是觉得有多少人写不出这道题的与编程语言无关的通用解法?

别一上来就杠。看懂这一行代码背后的执行过程了吗?

看懂的人也就不会说出你这样的话了。
jmc891205
2020-07-25 15:25:08 +08:00
@oahebky
那请教一下,python3 有什么高级特性,可以在求 zip(nums[:n], nums[n:])时不使用额外的 O(n)内存保存两个 slice 产生的新 list,而直接求出 zip 的结果?

代码可读性见仁见智,没有一个评价标准。不过对于一个 trival solution,应该没有人会读不懂还要请你解释一下。
marquina
2020-07-25 15:34:44 +08:00
楼上说得没错。当执行 nums[:n]和 nums[n:]的时候,就相当于复制了一遍原数组,这就是额外内存消耗。
zip 确实是迭代器,不需要额外内存,但 zip 返回的是 tuple,对 tuple 做 add 需要生成新 tuple+销毁旧 tuple (因为是不可变对象),这又是额外的内存+性能消耗。
最要命的是,最后该函数返回的是 tuple,但题目要求的是 list,如果判题器加个 ret type checker,或需要对返回值进行 append 操作,那就完蛋了。
总之,一行代码看起来很 nb,但很多时候也只是看起来 nb 罢了。
gjquoiai
2020-07-25 15:47:13 +08:00
没有指针的语言就是蛋疼。。把一个数组分成两半遍历都麻烦的不行
leeshuai
2020-07-25 15:57:03 +08:00
他扣帽子一直可以的
ChanKc
2020-07-25 16:11:47 +08:00
其实第一时间能想到这个写法的话,在周赛里面还是很有用的
fishCatcher
2020-07-25 16:15:08 +08:00
任何反对 lz 的声音都被归类为一上来就杠
oahebky
2020-07-25 16:32:45 +08:00
@marquina

复制一遍数组没错。
不过如果是 C/C++ 之类的静态语言,也是第一部要创建一个同样长度的数组用来放值返回吧?

另外一定要这么扣细节的话,写成 `(nums[i] for i in range(n))` 够不够再省内存?
(实际上这个 solution 在 python3 的所有提交中已经是 less 100.00% 的 memory usage 了。)

各位看官觉得是 zip(nums[:n], nums[n:]) 好呢?
还是 zip((nums[i] for i in range(n)), (nums[j] for j in range(n, len(nums)))) 好呢?

----

返回 tuple 和 题目要求 list 确实值得注意,不过 python 中写代码,类型本质是“鸭子类型”和“蟒蛇类型”区别,不然也就不是动态类型了。

----

我从头到尾都没有说这个解法性能好。
我的目的也不在于性能,就原题 `1<=n<=500` 的题目,遍历一遍和遍历两遍关系很大吗?


----

“一行代码看起来了很 nb”?

`functools.reduce(operator.add, zip(nums[:n], nums[n:]))` 这一行代码是为了 nb 吗?
如果你是这么觉得的话,那么就没有讨论代码的意义了,因为从一开始就预设立场,把我分享这行代码的意图曲解成为了 nb 。
YuTengjing
2020-07-25 16:41:39 +08:00
感叹一下,刷了将近 300 道题的我,现在看到很多文章的原题大都可以很快想出最优解。以前没过刷题的时候特别怕面试被问算法题,觉得自己很虚,觉得自己很容易被替代~
oahebky
2020-07-25 16:49:52 +08:00
@gjquoiai

不知道
for i in range(n)
for j in range(n, len(nums))

这样分两半哪里麻烦?


可以说几个 python 没有指针很麻烦的实例,个人比较感兴趣,可以学习下。
gwy15
2020-07-25 16:59:31 +08:00
正常的写法:空间(额外) O(1),时间 O(n)
你的“一行”写法:空间(额外) O(n),时间 O(n^2),伴随大量 GC

我写了个 benchmark,你的“一行解法”仅仅 n=100k 的规模,就要 30s 一次,对比正常写法 24ms,慢了一千倍以上。

https://imgchr.com/i/UzxYLQ

https://gist.github.com/gwy15/22616306f560f65d77b6ca23954e91a3

你当然可以说“题目就是 n~500,这么写也能过”。能过是一回事,明明有性能更好也更好理解的代码不用,非要这么写,那以后系统瓶颈的时候你是打算拿着 cProfile 慢慢找瓶颈吗?

至于拿 leetcode 的排行说性能,leetcode 的运行时间打底就是 40ms,能说明啥?你加个 time.sleep(0.03) 都可能是 40ms 。

拿高阶函数写又不是什么很有优越感的事情,楼主上来就“杠精”、“不懂 python3”、“不了解高阶函数”、“不要出来秀智商”,这个行为让人觉得很莫名其妙。
monkeymonkey
2020-07-25 17:13:09 +08:00
lz 典型的学了一点小技巧出来 showoff 被打脸,一瓶子不满半瓶子晃荡。所谓的 python“高级”特性也不过是程序员基本功而已,楼上各位提到时间复杂度空间复杂度,GC,profiling,这些才是真正的“高级”内容。
lysS
2020-07-25 17:23:32 +08:00
楼主学习到了一个小技巧,感觉自己的知识又提升了;你们居然不迎合楼主,可恶🐶
oahebky
2020-07-25 17:29:43 +08:00
@gwy15

实际生产环境,能有 n = 100k 的规模我会这么写:

def func(nums, n):
____for i in range(n):
________yield nums[i]
________yield nums[n+i]

我发这行没有说是最优解,也没有它是最优解的潜意思。

各位急着贬的网友这么想我也没有办法,只能说我只想给其它 python programmer 多了解一些使用 Python 特性的不同的 solution 。

另外,不是算法的时间是 O(n^2)。
调用函数就要了解函数,如果不了解确实不适合这么干。

上面网友说的对,这个解法确实有大量 GC 。时间是在 tuple 的 `+` 运算上。
换句话说,

```python
def sum1(iterable):
____ans = 0
____for i in iterable:
________ans += i
____return ans
```

的“通用”写法和

```python
def sum2(iterable):
____functools.reduce(operator.add, iterable)
```

都是 O(n) 的解法,而不是用了 reduce 或者 add 就变成 O(n^2) 的解法。

----

而是 operator.add((1, 2), (1, 2)) 与:
```
l = [1, 2]
l.append(1)
l.append(2)
```
的区别。


类似是
```
l = [1, 2]
l.append(1)
```

```
l = [1, 2]
l = l + [1]
```
的区别。

所以你要是说“不懂不要乱用”,那确实如此。
但是你要说因为“多行写成一行代码”复杂度会 O(n) 变 O(n^2)

那我建议你再仔细了解一下背后的原因。
huskar
2020-07-25 17:30:27 +08:00
说实在的,这解法真不怎么样…性能和可读性一样也不占
msg7086
2020-07-25 17:43:45 +08:00
说性能问题也就算了,这题还能怎么写得比 zip()更可读?
虽然我起手写了 transpose 但是我觉得 zip 比 transpose 要更可读一些。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/693010

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX