(待续...)
目录
二叉搜索树中第K小的元素
【题目】
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1
输出:1示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3 输出:3提示:
树中的节点数为 n 。
1 <= k <= n <= 104
0 <= Node.val <= 104
进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?
【我的方法】
因为元素按中序遍历从小到大排列,所以中序遍历元素,当遍历到第k个元素时,返回该元素。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def kthSmallest(self, root: TreeNode, k: int) -> int:
stack = []
i = 0
while(stack or root):
if root:
stack.append(root)
root = root.left
else:
root = stack.pop()
i += 1
if i == k:
return root.val
root = root.right
执行结果:
执行用时:60 ms, 在所有 Python3 提交中击败了71.21%的用户
内存消耗:18.6 MB, 在所有 Python3 提交中击败了38.45%的用户
【其他方法】
@li-fang-fang-fang-fang-ge 的评论是一个有趣的思路,但执行结果好像一般:
查找左子树节点个数为leftN,如果K<=leftN,则所查找节点在左子树上.
若K=leftN+1,则所查找节点为根节点
若K>leftN+1,则所查找节点在右子树上,按照同样方法查找右子树第K-leftN个节点
# #借鉴该思路写出的代码
class Solution:
def kthSmallest(self, root: TreeNode, k: int) -> int:
leftN = self.countChild(root.left)
if leftN+1 == k:
return root.val
elif k<=leftN:
return self.kthSmallest(root.left, k)
else:
return self.kthSmallest(root.right, k-leftN-1)
def countChild(self,root):
if not root:
return 0
return self.countChild(root.left) + self.countChild(root.right) + 1
执行结果:
执行用时:72 ms, 在所有 Python3 提交中击败了21.14%的用户
内存消耗:18.6 MB, 在所有 Python3 提交中击败了49.66%的用户
二叉树的中序遍历
【题目】
给定一个二叉树的根节点 root ,返回它的 中序 遍历。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]示例 2:
输入:root = []
输出:[]提示:
树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
【我的代码】
递归算法实现
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
if not root:
return []
elif not root.left and not root.right:
return [root.val]
else:
return self.inorderTraversal(root.left) + [root.val] + self.inorderTraversal(root.right)
执行结果:
执行用时:52 ms, 在所有 Python3 提交中击败了5.71%的用户
内存消耗:14.8 MB, 在所有 Python3 提交中击败了67.73%的用户
【其他方法】
@weih1121L1的评论:
- 中序遍历 先遍历左子树->根节点->右子树
- 如果是递归做法则递归遍历左子树,访问根节点,递归遍历右子树
- 非递归过程即:先访问..最左子树..结点,再访问其父节点,再访问其兄弟
- while循环条件 中序遍历需先判断当前结点是否存在,若存在则将该节点放入栈中,再将当前结点设置为结点的左孩子,
- 若不存在则取栈顶元素为cur,当且仅当栈空cur也为空,循环结束。
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
stack, ret = [], []
cur = root
while stack or cur:
if cur:
stack.append(cur)
cur = cur.left
else:
cur = stack.pop()
ret.append(cur.val)
cur = cur.right
return ret
执行结果:
执行用时:36 ms, 在所有 Python3 提交中击败了82.89%的用户
内存消耗:14.8 MB, 在所有 Python3 提交中击败了56.01%的用户
【整理之递归VS迭代】
- 迭代是逐渐逼近,用新值覆盖旧值,直到满足条件后结束,不保存中间值,空间利用率高。
- 递归,简讲就是自己调用自己,自己包含自己。将一个问题分解为若干相对小一点的问题,遇到递归出口再原路返回,因此必须保存相关的中间值,这些中间值压入栈保存,问题规模较大时会占用大量内存。
深究递归和迭代的区别、联系、优缺点及实例对比(https://blog.youkuaiyun.com/laoyang360/article/details/7855860)
文章有总结两者之间的关系:
1) 递归中一定有迭代,但是迭代中不一定有递归,大部分可以相互转换。
2) 能用迭代的不用递归,递归调用函数,浪费空间,并且递归太深容易造成堆栈的溢出./*相对*/
括号生成
【题目】
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:输入:n = 1
输出:["()"]
提示:
1 <= n <= 8
【我的代码】
采用了比较暴力的方法:
1、以n=3为例,存在一次性先放入'((()'、'(()'、'()' 这三种情况,剩下的继续遍历
2、而且剩余的'(' 数量不能大于剩余的 ')' 数量
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
res = []
left = ['(']*n
right = [')']*n
def backtrack(left, right, output):
if len(left) > len(right):
return
if len(left) == 0:
output += ''.join(right)
if output not in res:
res.append(output)
return
for i in range(len(left)):
for j in range(len(right)):
backtrack(left[i+1:],right[j+1:],output+''.join(left[:i+1])+''.join(right[:j+1]))
backtrack(left, right, '')
return res
执行结果:
执行用时:96 ms, 在所有 Python3 提交中击败了7.10%的用户
内存消耗:15.2 MB, 在所有 Python3 提交中击败了31.79%的用户
【其他代码】
@ChengMingL3 的评论:
这次比较震惊,我的方法战胜了100%的用户,感觉就是DFS+少量的剪枝,剪枝的条件为:左括号的数目一旦小于右括号的数目,以及,左括号的数目和右括号数目均小于n。
# 借鉴该思路改进的代码:
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
res = []
left = ['(']*n
right = [')']*n
def backtrack(left, right, output):
if len(left) > len(right) or len(left) < 0 or len(right) < 0:
return
if len(left) == 0:
output += ''.join(right)
res.append(output)
return
backtrack(left[1:], right, output + '(')
backtrack(left, right[1:], output + ')')
backtrack(left, right, '')
return res
执行结果:
执行用时:44 ms, 在所有 Python3 提交中击败了54.65%的用户
内存消耗:15.1 MB, 在所有 Python3 提交中击败了43.13%的用户
【整理之深度优先遍历】
深度优先遍历:
当前左右括号都有大于 0 个可以使用的时候,才产生分支;
产生左分支的时候,只看当前是否还有左括号可以使用;
产生右分支的时候,还受到左分支的限制,右边剩余可以使用的括号数量一定得在严格大于左边剩余的数量的时候,才可以产生分支;
在左边和右边剩余的括号数都等于 0 的时候结算。作者:liweiwei1419
链接:https://leetcode-cn.com/problems/generate-parentheses/solution/hui-su-suan-fa-by-liweiwei1419/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。回溯也算是深度优先遍历的一种。
全排列
【题目】
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
【其他代码】
@powcaiL6 的评论:
使用回溯算法。
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
def backtrack(nums, tmp):
if not nums:
res.append(tmp)
return
for i in range(len(nums)):
backtrack(nums[:i] + nums[i+1:], tmp + [nums[i]])
backtrack(nums, [])
return res
执行结果:
执行用时:36 ms, 在所有 Python3 提交中击败了92.04%的用户
内存消耗:14.8 MB, 在所有 Python3 提交中击败了93.31%的用户
【整理之回溯算法】
我们定义递归函数 backtrack(first, output) 表示从左往右填到第 first 个位置,当前排列为 output。 那么整个递归函数分为两个情况:
- 如果first==n,则说明我们已经填完了n个位置(下标从0开始),即找到了可行解,我们将output放入答案组中,递归结束。
- 如果first<n,我们只能考虑未填过的数,因此很容易想到的一个处理手段是我们定义一个标记数组 vis[] 来标记已经填过的数,那么在填第 first 个数的时候我们遍历题目给定的 n 个数,如果这个数没有被标记过,我们就尝试填入,并将其标记,继续尝试填下一个位置,即调用函数backtrack(first+1, output)。回溯的时候要撤销这一个位置填的数以及标记,并继续尝试其他没被标记过的数。
- 去掉标记组的办法:
我们可以将题目给定的 n 个数的数组 nums 划分成左右两个部分,左边的表示已经填过的数,右边表示待填的数,我们在回溯的时候只要动态维护这个数组即可。
最长公共子序列
【题目】
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-common-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
【我的代码】
1、公共子序列必须是两者都拥有的,因此将对方没有的字符去掉
2、采用递归方法
- 终止递归的条件:min-text的长度为0,则返回0,min-text的长度为1,且min-text包含在max-text中,则返回1
- 减小规模:将问题分解为min-text[1:]与max-text的公共子序列和min-text[1:]与max-text[max-text.find(min-text[0]+1:]的公共子序列的比较
3、先计算两字符串前面最大的重复字段长度为i,然后返回值加上这个i
*注:未通过测试
class Solution(object):
def longestCommonSubsequence(self, text1, text2):
"""
:type text1: str
:type text2: str
:rtype: int
"""
if len(text1)==0:
return 0
elif len(text1)==1:
if text1 in text2:
return 1
else:
return 0
temp = text1
for i in text1:
if i not in text2:
temp = temp.replace(i,'')
text1 = temp
temp = text2
for i in text2:
if i not in text1:
temp = temp.replace(i,'')
text2 = temp
if len(text1) > len(text2):
temp = text1
text1 = text2
text2 = temp
if len(text1)==0:
return 0
else:
if text1 in text2:
return len(text1)
else:
return max(1+self.longestCommonSubsequence(text1[1:],\
text2[text2.find(text1[0])+1:]),\
self.longestCommonSubsequence(text1[1:],text2))
执行结果:
状态:超出时间限制
最后执行的输入:
"kvwrkharmnqpwxyhejgvybifmncdorglsfqlidupyvcnypwvglormj" "irmdqnwnelyturkdobypezwvonqpubedetansrkjgzyzgpuxajgdaji"
【其他代码】
@小鱼爱吃香干L3 的评论:
做了几个dp的题之后,总结了dp需要注意的几个要素:
1、 明确dp二维数组表示的含义
2、base case
3、状态的转移:对于回文/LCS之类的问题则是考虑当前字串和已经计算过的子串之间的关系
4、由状态的转移来确定 loop的边界
5、 由loop的边界打出表格 可得出最后一个dp的状态值,即结果。
# 借鉴该思路的代码
class Solution(object):
def longestCommonSubsequence(self, text1, text2):
"""
:type text1: str
:type text2: str
:rtype: int
"""
n = len(text1)
m = len(text2)
dp = [[0]*(m+1) for _ in range(n+1)]
# dp s[1..i] s[1..j] LCS长度为 dp[i][j]
for i in range(n+1):
for j in range(m+1):
dp[0][j] = dp[i][0] = 0
for i in range(1,n+1):
for j in range(1,m+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[-1][-1]
执行结果:
执行用时:424 ms, 在所有 Python 提交中击败了15.63%的用户
内存消耗:20.6 MB, 在所有 Python 提交中击败了93.26%的用户
【整理之动态规划问题】
最长公共子序列问题是典型的二维动态规划问题。
1、对于s1[1..i] s2[1..j] LCS长度为 dp[i][j]
2、边界情况:一个字符串和空字符串的LCS为0,即dp[0][j] = dp[i][0] = 0
3、当i>0且j>0时:
当s1的第i个字符等于s2的第j个字符时,则dp[i][j] = dp[i-1][j-1] + 1
否则比较以下两种情况的最长:
s1[1...(i-1)]和s2[1...j]的LCS
s1[1...i]和s2[1...(j-1)]的LCS
4、复杂度分析:
- 空间复杂度:O(mn),其中m和n分别是字符串s1和s2的长度,因为二维数组dp有(m+1)行和(n+1)列
- 时间复杂度:O(mn),其中m和n分别是字符串s1和s2的长度,因为需要对dp中的每个元素进行计算
子集
【题目】
给你一个整数数组
nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
【我的代码】
思路是从最小单元开始拼接,每次只和上一轮新产生的子集拼接。
*注:未通过测试
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
if len(nums)==1:
return [[],nums]
nums.sort()
smallUnit = [[i] for i in nums]
res = [[i] for i in nums]
count = [2,0]
while count[0]<len(nums):
for i in res[count[1]:]:
for j in smallUnit:
if j[-1]>i[-1]:
res.append(i+j)
count[1] += 1
count[0] += 1
res.append([])
res.append(nums)
return res
执行结果:
状态:解答错误
输入:
[3,2,4,1]
输出:
[[1],[2],[3],[4],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4],[2,3,4],[],[1,2,3,4]]
预期:
[[],[3],[2],[2,3],[4],[3,4],[2,4],[2,3,4],[1],[1,3],[1,2],[1,2,3],[1,4],[1,3,4],[1,2,4],[1,2,3,4]]
【其他代码】
@很特么生气L3 的评论:
参考论坛大神的python解法:
- 直接从后遍历,遇到一个数就把所有子集加上该数组成新的子集
- 注意代码中res[:]是必须的,因为切片是引用新的对象,此时在循环中res[:]是不更新的,而res是不断有元素push进去的,很trick
# 借鉴该思路写出的代码
class Solution:
def subsets(self, nums):
if len(nums) == 1:
return [[], nums]
nums.sort()
res = [[]]
for i in range(len(nums) - 1, -1, -1):
print(i)
for subres in res[:]:
res.append(subres + [nums[i]])
return res
执行结果:
执行用时:32 ms, 在所有 Python3 提交中击败了95.08%的用户
内存消耗:15 MB, 在所有 Python3 提交中击败了39.27%的用户