(考研机试)从二叉树开始学习~
文章中除模板代码均可以AC哦~(Python)
一、二叉树
(一)重点内容
种类:解题过程中主要有满二叉树与完全二叉树、二叉搜索树
二叉搜索树(BST:Binary search tree):简单理解为左节点对应的值及其子树所有节点的值大于根节点的值大于右节点对应的值及其子树所有节点的值及其子树,注意子树为空的情况。
平衡二叉树(AVL):BST的基础上限制了任意节点的两个子树高度差的绝对值不超过1,性质可以为空树。
二叉树存储方式:链式与顺序存储均可
遍历方式:广度、深度优先遍历。进一步拓展为前序、中序、后序、层次遍历。
python代码实现:
(二)二叉树的递归遍历(注释应该用双引号)
1.递归三要素:
确定参数和返回值、确定终止条件、确定单层递归的逻辑。
2.递归实现:
视情况而定,对于print操作进行修改。
个人觉得递归操作用处没有迭代操作那么大,而且递归操作比较简单。
3.迭代操作(栈与标记法同用):
之前不是很懂后序遍历的迭代法,代码随想录用一个统一的模板,给我整通透了。
重点来了,例如对于前序遍历而言,是根左右,那我们只需要将根节点最后入栈,按照遍历的反顺序入栈,并在根节点入栈后插入一个None节点。然后遇到None节点就下一个元素出栈,然后添加到res列表中。这相比之前我看到的迭代算法来说,要简单不少,也便于记忆。具体实现如下:
def PreOrderTraversal(root):
stack = []
res = []
stack.append(root)
while stack:
node = stack.pop()
if node:
if node.right:
res.append(node.right)
if node.left:
stack.append(node.left)
stack.append(node)
stack.append(None)
else:
node = stack.pop()
res.append(node.val)
return res
对于中序遍历,只要将根节点入栈的顺序放在左孩子节点入栈之前。
对于后序遍历,只要将根节点入栈顺序放在右孩子节点入栈之前。
4.层序遍历:
层序遍历的递归代码不可能实现,因为使用队列作为遍历数据结构来实现的,一般都是迭代遍历的方法,具体代码比较容易理解:
需要遍历完每一层的结点,用一个level去存放每一层的结点的val值。
具体题目:
(1)二叉树的层序遍历:
同模板。
(2)107. 二叉树的层序遍历 II - 力扣(LeetCode):
同模板,但需要在return res时将调用res.reverse()。这是因为题目要求我们自底向上的遍历,可能最开始会想到用双端队列,但是更加简单的思路是直接正常的层次遍历得到一个res列表。层序遍历是从上到下,从左到右,而题目要求的是从下到上,从左到右。模板会将每层的节点值存放在同一个列表中,在将该列表作为res列表的一个子列表,因此reverse操作不会改变子列表内数据的顺序,也就是同一层遍历的顺序,只改变了每层数据的相对顺序。因此直接对于res进行reverse操作就可以了。
(3)199. 二叉树的右视图 - 力扣(LeetCode):
这道题理解为让你访问每层最后一个节点,所以只要在模板的基础上,对于添加节点值加入条件即可:
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
res = []
if not root:
return []
queue = deque([root])
while queue:
len1 = len(queue) - 1
for i in range(len(queue)):
cur = queue.popleft()
if i == len1:
res.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
return res
(4)637. 二叉树的层平均值 - 力扣(LeetCode):
使用sum与len1去记录每层总和与节点个数即可。
def averageOfLevels(self, root: Optional[TreeNode]) -> List[float]:
res = []
if not root:
return
queue = deque([root])
while queue:
sum = 0
len1 = len(queue)
for _ in range(len(queue)):
cur = queue.popleft()
sum += cur.val
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
res.append(sum / len1)
return res
记录一下小白第一次超越这么多人哈哈。
(5)429. N 叉树的层序遍历 - 力扣(LeetCode):
和模板没啥区别,直接套用就行了,只是没有left和right了,变成孩子节点了。但是有点需要注意,就是python的in与not in,之前学c或c++,完全没有接触,所以用的不熟悉。譬如这题,如果你去找节点的孩子,如果你不用in是很难把代码写的简洁的。不用in,就要把cur.children用一个列表存放,然后去遍历这个列表,实在太繁琐。
def levelOrder(self, root: 'Node') -> List[List[int]]:
res = []
if not root:
return []
queue = deque([root])
while queue:
level = []
for _ in range(len(queue)):
cur = queue.popleft()
level.append(cur.val)
for child in cur.children:
queue.append(child)
res.append(level)
return res
(6)515. 在每个树行中找最大值 - 力扣(LeetCode):
同模板,只需要添加max(level)。
class Solution:
def largestValues(self, root: Optional[TreeNode]) -> List[int]:
res = []
if not root:
return []
queue = deque([root])
while queue:
level = []
for i in range(len(queue)):
cur = queue.popleft()
level.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
res.append(max(level))
return res
(6)116. 填充每个节点的下一个右侧节点指针 - 力扣(LeetCode):
也是模板题,设置一个条件判断是否队为空,不为空就让next指向下一个节点,为空就执行None。
if not root:
return
queue = deque([root])
while queue:
len1 = len(queue)
for i in range(len1):
cur = queue.popleft()
if i == len1 - 1:
cur.next = None
else:
none = queue.popleft()
cur.next = none
queue.appendleft(none)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
return root
我这个方法有点慢了,应该用随想录里面的方法,用一个Pre指针指向前一个元素。第七题和这题一样的,所以跳过了。
(7)104. 二叉树的最大深度 - 力扣(LeetCode):
这题其实就是遍历一遍二叉树,然后用一个遍历去记录高度就好了。
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
queue = deque([root])
height = 0
while queue:
len1 = len(queue)
for i in range(len1):
cur = queue.popleft()
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
height += 1
return height
(8)111. 二叉树的最小深度 - 力扣(LeetCode) :
这题可以说是最难的,但是如果想到就不难,最小深度是什么,其实可以理解为不能在通过这个节点的左右子树继续遍历下去,所以我们可以在当某个节点的左右子树为空是,return height。
随想录是将height加1的操作放在内层循环的前面,我放在最下面,只需要在return时加1即可。
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
queue = deque([root])
height = 0
while queue:
len1 = len(queue)
for i in range(len1):
cur = queue.popleft()
if not cur.left and not cur.right:
return height + 1
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
height += 1
return height
(三)遍历方式具体应用
二叉树的题目按照我个人理解,只要找对遍历方法应该都能AC。
1.226. 翻转二叉树 - 力扣(LeetCode)(前序遍历或后序遍历):
这题我最开始想错了,认为要遍历每层,然后用栈去存节点,然后将出栈元素组成一个新树。但实际上这样是写不出来的。
正确写法,用任意一种遍历的迭代法,将每个节点的左右子树节点进行交换。而且注意,要判空!!!
我使用前序遍历迭代法进行代码书写:
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:
return None
stack = [root]
while stack:
node = stack.pop()
node.left, node.right = node.right, node.left
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return root
这题模板不太适用,因为它不需要遍历节点的值,也不需要控制左右孩子节点的入栈顺序,也不用插入None。只需要类似递归一样,遍历所有节点,其实这题用层次遍历会更加容易理解。
2.101. 对称二叉树 - 力扣(LeetCode):
这题我觉得还是用层序遍历,若数是对称的,则对于level进行前后遍历,若不相等,则返回False。
代码随想录的代码看不懂,可能还是太菜了哈哈。
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
queue = deque([root])
while queue:
level = []
for _ in range(len(queue)):
cur = queue.popleft()
if cur == None:
level.append(101)
continue
level.append(cur.val)
if cur.left:
queue.append(cur.left)
else:
queue.append(None)
if cur.right:
queue.append(cur.right)
else:
queue.append(None)
i, j = 0, len(level) - 1
while i <= j:
if level[i] == level[j]:
i += 1
j -= 1
else:
return False
return True
3.222. 完全二叉树的节点个数 - 力扣(LeetCode):
其实就是层次遍历,每访问一个节点,res++。
或者依照代码随想录的另一种方法,求左右子树的高,若左右子树高相等,此时为满二叉树。若不是则正常用递归计算个数。
class Solution:
def countNodes(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
queue = deque([root])
res = 0
while queue:
len1 = len(queue)
for i in range(len1):
cur = queue.popleft()
res += 1
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
return res
4.110. 平衡二叉树 - 力扣(LeetCode):
这题用层序遍历的迭代法,定义一个计算深度的函数cal_depth,然后在主体函数内进行遍历,每次遍历都将该节点作为参数传入cal_depth,计算左、右子树的深度,然后计算绝对值。
不知道为啥代码随想录那么喜欢用中序遍历,我觉得层序遍历会更好一点。真的看不懂递归~
def cal_depth(root):
if not root:
return 0
queue = deque([root])
depth = 0
while queue:
for _ in range(len(queue)):
cur = queue.popleft()
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
depth += 1
return depth
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
if not root:
return True
queue = deque([root])
while queue:
for _ in range(len(queue)):
cur = queue.popleft()
if abs(cal_depth(cur.left) - cal_depth(cur.right)) > 1:
return False
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
return True
5.257. 二叉树的所有路径 - 力扣(LeetCode):
这道题我想的是前序遍历的迭代写法,因为题目要求的是从根节点到每个叶子节点的路径。然后用一个path记录遍历到节点的值,path_1作为一个列表和stack一样,将根节点到目前节点的路径存放进去,res记录访问到叶子节点的路径也就是最终结果之一。注意只有当左右均为空时才能添加路劲进入res。
class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
stack, path_1, res = [root], [str(root.val)], []
while stack:
cur = stack.pop()
path = path_1.pop()
if not cur.left and not cur.right:
res.append(path)
if cur.right:
stack.append(cur.right)
path_1.append(path + '->' + str(cur.right.val))
if cur.left:
stack.append(cur.left)
path_1.append(path + '->' + str(cur.left.val))
return res
虽然这题本意是考回溯,但是不熟练递归的我,这个实在太难理解了。但回溯法又是一个很重要的思想,所以硬着头皮看完了。
其实看完还好,回溯法可以理解为在递归中加入一个返回的条件,虽然递归一定会有返回条件,但一般都是把某个东西遍历完,回溯法添加的条件并不是想遍历完二叉树,是想当左右子树均为空时,将当前路径传入result中,然后把这一条路径进行剪枝。path.pop()这一句太难想到了,只能想某一个状态,当某一条路径被遍历完,然后将对于路径出栈即可,因为该路径已经被添加至res中了。
递归法的好处体现出来了,代码简洁不过不容易理解。这辈子感觉和递归有仇~,所以我还是选择迭代法。
def solution(root):
res, path = [], []
if not root:
return res
traversal(root, path, res)
return res
def traversal(cur, path, res):
path.append(cur.val)
if not cur.left and not cur.right:
spath = path + '->' + ''.join(path)
res.append(spath)
return
if cur.left:
traversal(cur.left, path, res)
path.pop()
if cur.right:
traversal(cur.right, path, res)
6.404. 左叶子之和 - 力扣(LeetCode):
显然,利用前序遍历,只要判断遍历到的节点的左孩子的左孩子为空,该节点的左孩子的右孩子为空,则就是一个左叶子。这题比上一题要好想一点,通过率既然还低了这么多。注意要判空,对于cur.left要判断这个节点是否为空,不然可能会报NoneType has no attitude ‘left’的错误
if root is None:
return 0
stack,sum = [root], 0
while stack:
cur = stack.pop()
if cur.left and not cur.left.left and not cur.left.right:
sum += cur.left.val
if cur.right:
stack.append(cur.right)
if cur.left:
stack.append(cur.left)
return sum
7.513. 找树左下角的值 - 力扣(LeetCode):
我们用层序遍历,我们用一个res 存放每层的第一个节点。
class Solution:
def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
queue = deque([root])
res = 0
while queue:
for i in range(len(queue)):
cur = queue.popleft()
if i == 0:
res = cur.val
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
return res
257题的变体,用一个sum记录每条路径的val值之和,若等于target就return。
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
stack,sum1 = [root], [root.val]
while stack:
cur = stack.pop()
sum2 = sum1.pop()
if not cur.left and not cur.right and sum2 == targetSum:
return True
if cur.right:
stack.append(cur.right)
sum1.append(sum2 + cur.right.val)
if cur.left:
stack.append(cur.left)
sum1.append(sum2 + cur.left.val)
return False
9. 106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode):
难题来了,不会写。代码随想录的方法是用递归,利用后序遍历的最后一个节点作为切割点,切分中序和后序序列。在切分的过程中,始终要保持中序和后序的列表长度一样。若中序遍历和后序遍历的节点值一样,则是要添加的元素。
if not postorder:
return None
root = postorder[-1]
root = TreeNode(root)
cut_num1 = root.val #后序遍历的最后一个元素值
cut_index = inorder.index(cut_num1)
inorder_left = inorder[:cut_index]
inorder_right = inorder[cut_index + 1:]
postorder_left = postorder[:len(inorder_left)] #切割后序序列用的是中序序列左边部分的长度
postorder_right = postorder[len(inorder_left):len(postorder) - 1]
root.left = solution(inorder_left, postorder_left)
root.right = solution(inorder_right, postorder_right)
return root
用另外类似的题目考虑该题目,105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
同样的前序遍历的第一个去切分中序序列,要注意切片的方式:
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
if not preorder:
return None
root_val = preorder[0]
root = TreeNode(root_val)
cut_index = inorder.index(root_val)
inorder_left = inorder[:cut_index]
inorder_right = inorder[cut_index + 1:]
preorder_left = preorder[1:len(inorder_left) + 1]
preorder_right = preorder[len(inorder_left) + 1:]
root.left = self.buildTree(preorder_left, inorder_left)
root.right = self.buildTree(preorder_right, inorder_right)
return root
10.700. 二叉搜索树中的搜索 - 力扣(LeetCode):
由于是考研机试学习,所以最难就到BST了哈哈,加上又是ACM模式,二叉树树的话主要考树的构造,难题还是处在DP、贪心等。
这道题要明白BST的性质,一个递归就解决了。
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
if not root or root.val == val:
return root
if root.val > val:
return self.searchBST(root.left, val)
if root.val < val:
return self.searchBST(root.right, val)
迭代法会更加好理解一点:
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
while root:
if root.val < val: root = root.right
elif root.val == val: return root
else: root = root.left
return None
11.108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode):
不会,随想录说从中间开始找,立马就有思路了,无论nums长度是奇数还是偶数,都取len(nums) // 2,为当前节点。主要是利用二分法找打中间节点,然后不断插入就行了。迭代不知道有多麻烦哈哈,递归法真香!迭代法其实还是入队的思想,主要是将左右区间入队,实在有点麻烦,就没有看了。
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
def find_node(nums, left, right):
if left > right:
return None
mid = (left + right) // 2
node = TreeNode(nums[mid])
node.left = find_node(nums, left, mid - 1)
node.right = find_node(nums, mid + 1, right)
return node
root = find_node(nums, 0, len(nums) - 1)
return root
历时3天,二叉树过完了。以前学数据结构是很怕树的,但是这样看来,只是考研机试难度的话,树还是可以去ac一些题目的。