一行Python解Leetcode习题
语法简单,提供大量开箱即用的工具是Python语言的一大特点,也是其受欢迎的重要特点。所谓“人生苦短,我用Python”,并不是说Python比其他语言性能好、也不是说Python比其它语言优秀,而是说它方便,易用,可用于思路验证、原型实现,也可用于快速开发;其开发效率高的特点使其的网络编程、爬虫和数据开发领域极受欢迎。
知乎上甚至有专门的话题讨论一行Python能干什么,话题下的回答有的令人大开眼界,也有的强行一行。受到话题启发,在此不完全归纳Leetcode上的能用一行Python代码解出的习题。
学习Python中的小伙伴,需要学习资料的话,可以到我的微信公众号:Python学习知识圈,后台回复:“01”,即可拿Python学习资料
什么是Leetcode
Leetcode被称为程序员最喜爱的网站之一,其主要功能是提供了一个方便、便捷的程序员做题平台。现在已经有中文网站:https://leetcode-cn.com。
什么样的代码能称为一行解题
Leetcode中,Python解题一般是给定一个Solution类,给定一个类方法,要求你填写方法体。如果方法体中只有一行代码,则称为用一行代码解了该题。
让我们开始看题吧!
349 两个数组的交集
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2]
示例 2:
输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [9,4]
说明:
输出结果中的每个元素一定是唯一的。
我们可以不考虑输出结果的顺序。
解题思路:
按照一般思路,我们可以构造一个新的列表,遍历和对比输入的两个列表,将输入列表中相同的元素加入新的列表,注意新的列表中不要加入重复元素即可。这咱思路,需要遍历输入的两个列表,还要查询要加入的元素是否已经存在于目标列表,对目标列表也要不断查询,性能并不理想,也不会是面试官能接受的答案。更进一步的思路,我们可以利用集合或字典中键的不重复性来构造目标元素的容器。具体解题方法可以参加Leetcode上本题题解。
而在Python中,提供了集合这一数据类型,其支持集合的交并补等操作,所以我们可以直接利用这一特性。
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
return list(set(nums1) & set(nums2))
可以看到,我们先将列表转成集合,再对两个集合取交,最后又转成列表返回。
557. 反转字符串中的单词 III
给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
示例 1:
输入: "Let's take LeetCode contest"
输出: "s'teL ekat edoCteeL tsetnoc"
注意:在字符串中,每个单词由单个空格分隔,并且字符串中不会有任何额外的空格。
解题思路:
在Python中,许多可迭代对象都支持[::-1]反转操作,字符串也支持这样的操作。于是我们只要利用string.split()方法将句子切割开,再对每一个单词反转即可。
class Solution:
def reverseWords(self, s: str) -> str:
return ' '.join([e[::-1] for e in s.split(' ')])
注意:此处用到的列表推导,Python中的大部分可迭代对象都支持就地推导。这也是“一行Python”中的常用操作。
561. 数组拆分 I
给定长度为 2n 的数组, 你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), ..., (an, bn) ,使得从1 到 n 的 min(ai, bi) 总和最大。
示例 1:
输入: [1,4,3,2]
输出: 4
解释: n 等于 2, 最大总和为 4 = min(1, 2) + min(3, 4).
提示:
n 是正整数,范围在 [1, 10000].
数组中的元素范围在 [-10000, 10000].
解题思路:
凭直觉,我们认为只要使得“损失”掉的量最小即可,可须对原数组排序,再再两两组合,求其中较小数的和即可。这么做可行的数学证明可见:https://leetcode-cn.com/problems/array-partition-i/solution/minshu-dui-bi-shi-you-xu-shu-lie-shang-xiang-lin-y/
class Solution:
def arrayPairSum(self, nums: List[int]) -> int:
return sum(sorted(nums)[::2])
这里用到了[::2]利用步长迭代操作。
509. 斐波那契数
斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
给定 N,计算 F(N)。
示例 1:
输入:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1.
示例 2:
输入:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2.
示例 3:
输入:4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3.
提示:
0 ≤ N ≤ 30
解题思路:
斐波那契数是常见题,是属于必须掌握的题。最常见的思路,递归就完事了。但是要谨防递归中出现的“递归灾难”,也就是对同样的输入进行重复求值。这里给出一种使用递归一行解题的实现方式。
class Solution:
def fib(self, N: int) -> int:
return N if N = 0 or N = 1 else self.fib(N - 1) + self.fib(N - 2)
注意,上面的代码中,就会出现“递归灾难”,性能较差,也是属于面试官不可接受的一种实现方式。更多常见的实现方式可自行查阅。以下给出网友提供的通项公式法(本人并没有验证过此通项公式):
# 通项公式
class Solution:
def fib(self, N):
"""
:type N: int
:rtype: int
"""
return int((5**0.5)*0.2*( ((1+5**0.5)/2)**N-((1-5**0.5)/2)**N))
476. 数字的补数
给定一个正整数,输出它的补数。补数是对该数的二进制表示取反。
注意:
给定的整数保证在32位带符号整数的范围内。 你可以假定二进制数不包含前导零位。 示例 1:
输入: 5
输出: 2
解释: 5的二进制表示为101(没有前导零位),其补数为010。所以你需要输出2。
示例 2:
输入: 1
输出: 0
解释: 1的二进制表示为1(没有前导零位),其补数为0。所以你需要输出0。
解题思路:
本题涉及到二进制操作,对于一个整数i, i >> 1 表示其二进制开式向右移一位,等同于其十进制形式除以2. 以下是一种常见思路(Go语言):
func findComplement(num int) int {
tem := num
c := 0
for tem >0 {
tem >>= 1
c = (c << 1) + 1
}
return num ^ c
}
以下是网友提供的一行解题方法:
return int(bin(num)[2:].replace('0', '2').replace('1', '0').replace('2', '1'), 2)
以上代码用到了bin()函数,它将十进制数转换成二进制数的字符串形式。再连续使用string.replace()方法,最后转成int类型。
852. 山脉数组的峰顶索引
我们把符合下列属性的数组 A 称作山脉:
A.length >= 3
存在 0 < i < A.length - 1 使得A[0] < A[1] < ... A[i-1] < A[i] > A[i+1] > ... > A[A.length - 1]
给定一个确定为山脉的数组,返回任何满足 A[0] < A[1] < ... A[i-1] < A[i] > A[i+1] > ... > A[A.length - 1] 的 i 的值。
示例 1:
输入:[0,1,0]
输出:1
示例 2:
输入:[0,2,1,0]
输出:1
提示:
3 <= A.length <= 10000 0 <= A[i] <= 10^6 A 是如上定义的山脉
**解题思路**:
脑子转得快的同学,一眼就知道本题求出数组的最大值的索引即可。
return A.index(max(A))
另一种符合直觉的想法是,从第一位开始往后数,当数字下一位开始变小时,就把当前数字的索引返回,用Go解释如下:
func peakIndexInMountainArray(A []int) int {
for i := 1; i < len(A); i++ {
if A[i] < A[i - 1] {
return i - 1
}
}
return len(A) - 1
}
## 292\. Nim 游戏
你和你的朋友,两个人一起玩 Nim 游戏:桌子上有一堆石头,每次你们轮流拿掉 1 - 3 块石头。 拿掉最后一块石头的人就是获胜者。你作为先手。
你们是聪明人,每一步都是最优解。 编写一个函数,来判断你是否可以在给定石头数量的情况下赢得游戏。
示例:
输入: 4
输出: false
解释: 如果堆中有 4 块石头,那么你永远不会赢得比赛;
因为无论你拿走 1 块、2 块 还是 3 块石头,最后一块石头总是会被你的朋友拿走。
**解题思路**:
可以用归纳法证明(可自行证明),只要给定石头数量不是4的倍数,先手稳赢。Python:
class Solution:
def canWinNim(self, n: int) -> bool:
return n % 4 != 0
## 461\. 汉明距离
两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。
给出两个整数 x 和 y,计算它们之间的汉明距离。
注意: 0 ≤ x, y < 231.
示例:
输入: x = 1, y = 4
输出: 2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
上面的箭头指出了对应二进制位不同的位置。
**解题思路**:
一个符合直觉的思路是,先把两个数都转成二进制形式,将长度较短的数的开头补成0,再依次比较每一位是否相同。以下是这种思路的一种实现:
class Solution:
def hammingDistance(self, x: int, y: int) -> int:
binx, biny = bin(x)[2:], bin(y)[2:]
length = max(len(binx), len(biny))
binx, biny = binx.zfill(length), biny.zfill(length)
distance = 0
for ex, ey in zip(binx, biny):
distance += ex != ey
return distance
更高级一点的方法,是使用位或操作,再统计位或计算后的数的二进制形式中的1的数量。
class Solution:
def hammingDistance(self, x: int, y: int) -> int:
return bin(x^y).count('1')
## 657\. 机器人能否返回原点
在二维平面上,有一个机器人从原点 (0, 0) 开始。给出它的移动顺序,判断这个机器人在完成移动后是否在 (0, 0) 处结束。
移动顺序由字符串表示。字符 move[i] 表示其第 i 次移动。机器人的有效动作有 R(右),L(左),U(上)和 D(下)。如果机器人在完成所有动作后返回原点,则返回 true。否则,返回 false。
注意:机器人“面朝”的方向无关紧要。 “R” 将始终使机器人向右移动一次,“L” 将始终向左移动等。此外,假设每次移动机器人的移动幅度相同。
示例 1:
输入: "UD"
输出: true
解释:机器人向上移动一次,然后向下移动一次。所有动作都具有相同的幅度,因此它最终回到它开始的原点。因此,我们返回 true。
示例 2:
输入: "LL"
输出: false
解释:机器人向左移动两次。它最终位于原点的左侧,距原点有两次 “移动” 的距离。我们返回 false,因为它在移动结束时没有返回原点。
**解题思路**:
脑子转得快的同学已经知道,只要机器人向左右走的步数相同且向上下走的步数相同,就肯定会回到原点。我们可以利用string.count()方法。
100%
class Solution:
def judgeCircle(self, moves: str) -> bool:
return moves.count('U') == moves.count('D') and moves.count('L') == moves.count('R')
## 832\. 翻转图像
给定一个二进制矩阵 A,我们想先水平翻转图像,然后反转图像并返回结果。
水平翻转图片就是将图片的每一行都进行翻转,即逆序。例如,水平翻转 [1, 1, 0] 的结果是 [0, 1, 1]。
反转图片的意思是图片中的 0 全部被 1 替换, 1 全部被 0 替换。例如,反转 [0, 1, 1] 的结果是 [1, 0, 0]。
示例 1:
输入: [[1,1,0],[1,0,1],[0,0,0]]
输出: [[1,0,0],[0,1,0],[1,1,1]]
解释: 首先翻转每一行: [[0,1,1],[1,0,1],[0,0,0]];
然后反转图片: [[1,0,0],[0,1,0],[1,1,1]]
示例 2:
输入: [[1,1,0,0],[1,0,0,1],[0,1,1,1],[1,0,1,0]]
输出: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
解释: 首先翻转每一行: [[0,0,1,1],[1,0,0,1],[1,1,1,0],[0,1,0,1]];
然后反转图片: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
说明:
1 <= A.length = A[0].length <= 20
0 <= A[i][j] <= 1
**解题思路**:
同学们可以自行实现一下本题,再来看一行题解。
class Solution:
def flipAndInvertImage(self, A: List[List[int]]) -> List[List[int]]:
return [[{0: 1}.get(e, 0) for e in line[::-1]] for line in A]
# return [[j ^ 1 for j in i[::-1]] for i in A]
本题使用了列表推导的嵌套,使用了位或操作,使用了[::-1]负步长反转列表,对解题人的Python特性了解程度有一定的要求。
## 237\. 删除链表中的节点
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。
现有一个链表 -- head = [4,5,1,9],它可以表示为:
![image](http://upload-images.jianshu.io/upload_images/16886563-17d3df0b9c9af02d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
示例 1:
输入: head = [4,5,1,9], node = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:
输入: head = [4,5,1,9], node = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
说明:
链表至少包含两个节点。
链表中所有节点的值都是唯一的。
给定的节点为非末尾节点并且一定是链表中的一个有效节点。
不要从你的函数中返回任何结果。
**解题思路**:
有人说本题出的巧妙,本人却并不这么认为,甚至认为本题出得相当自认聪明。核心思路是将本节点的上一节点的Next指向本节点的Next即可。
class Solution:
def deleteNode(self, node):
"""
:type node: ListNode
:rtype: void Do not return anything, modify node in-place instead.
"""
node.val, node.next = node.next.val, node.next.next
本题涉及到链表这种数据结构,如果有同学不了解这种数据结构,可以先补一补数据结构的知识,毕竟链表题是面试题的最高频的结构题。
## 905\. 按奇偶排序数组
给定一个非负整数数组 A,返回一个数组,在该数组中, A 的所有偶数元素之后跟着所有奇数元素。
你可以返回满足此条件的任何数组作为答案。
示例:
输入:[3,1,2,4]
输出:[2,4,3,1]
输出 [4,2,3,1],[2,4,1,3] 和 [4,2,1,3] 也会被接受。
提示:
1 <= A.length <= 5000 0 <= A[i] <= 5000
解题思路:
本题的实质是一个排序题,同学们可以趁机自己实现一些排序算法,但是Python本身已经提供了列表排序函数sorted()。值得注意的是,许多萌新同学并不了解这个函数可以传入一个参数key来按key排序。key是 一个函数,这只使用lambda匿名函数来实现一行定义函数。
class Solution:
def sortArrayByParity(self, A: List[int]) -> List[int]:
# return [e for e in A if e % 2 == 0] + [e for e in A if e % 2 == 1]
return sorted(A, key=lambda x: x % 2 == 1)
# return sorted(A, key=lambda x: x & 1)
x & 1是什么操作?如果不了解,可以补充一下Python位操作的知识,据我经验,判断奇偶数时,x & 1比x % 2略好。
709. 转换成小写字母
实现函数 ToLowerCase(),该函数接收一个字符串参数 str,并将该字符串中的大写字母转换成小写字母,之后返回新的字符串。
示例 1:
输入: "Hello"
输出: "hello"
示例 2:
输入: "here"
输出: "here"
示例 3:
输入: "LOVELY"
输出: "lovely"
解题思路:
没什么好说的,用Python中的string.lower()方法就好了。代码如下:
Go:
import "strings"
func toLowerCase(str string) string {
return strings.ToLower(str)
}
Python:
class Solution:
def toLowerCase(self, str: str) -> str:
return str.lower()
771. 宝石与石头
给定字符串J 代表石头中宝石的类型,和字符串 S代表你拥有的石头。 S 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。
J 中的字母不重复,J 和 S中的所有字符都是字母。字母区分大小写,因此"a"和"A"是不同类型的石头。
示例 1:
输入: J = "aA", S = "aAAbbbb"
输出: 3
示例 2:
输入: J = "z", S = "ZZ"
输出: 0
注意:
S 和 J 最多含有50个字母。J 中的字符不重复。
本题可以利用Python中字符串的就地推导以及sum()函数对可迭代对象的求和。Python中真值属于数字类型(许多语言中真值即布尔类型,布尔类型与数字类型是并列的两大类型,但在Python中,布尔型是数字类型的一种,不信的话,可以试试isinstance(True, int),看看返回什么结果),可以参与数字运算,所以以下代码中,sum(s in J for s in S)是可以求和的。
class Solution:
def numJewelsInStones(self, J: str, S: str) -> int:
return sum(s in J for s in S)
# return sum(S.count(i) for i in J)
总结
从题目难度来说,大部分题目在Leetcode中都属于“简单”,有的题目甚至让你产生“这也算习题”的感觉。
从技巧上来说,一行Python主要利用了Python的可迭代对象的就地推导特征,丰富的字符串方法,丰富的容器操作(列表、集合、字典等),丰富的内建函数(sum, bin等),以及方便的二进制操作。
但要注意的是,一行Python解题,并不意味着Pythonic,我也见过老长老长、强行一行的操作,而枉顾代码的可读性和性能。一行Python解题,也不意味着性能强悍,比如斐波那契数题中,如果使用暴力递归,会被面试官丢掉简历的;按奇偶排序数组题中,使用两次列表推导,分别求出奇数数组和偶数数组,可能还不如多写几行代码对列表按奇偶排序,以少迭代列表一次。