树作为一种基本的数据结构,也是算法题常考的题型。基本的如树的遍历,树的高度,树的变种数据结构等。
树的遍历
树的遍历有四种:前序,中序,后序,层次。都需要掌握其递归与非递归方式。
[leetcode]94.Binary Tree Inorder Traversal
中序遍历
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
p = root
stack = []
res = []
while p or stack:
while p:
stack.append(p)
p = p.left
p = stack.pop()
res.append(p.val)
p = p.right
return res
[leetcode]102.Binary Tree Level Order Traversal
层次遍历
import collections
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
res = []
if not root: return res
queue = collections.deque()
queue.append(root)
while queue:
level = []
for i in range(len(queue)):
curr = queue.popleft()
level.append(curr.val)
if curr.left:
queue.append(curr.left)
if curr.right:
queue.append(curr.right)
res.append(level)
return res
[leetcode]144.Binary Tree Preorder Traversal
前序遍历
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
# def preorderTraversal(self, root: TreeNode) -> List[int]:
# """
# :type root: TreeNode
# :rtype: List[int]
# """
# res = []
# self.dfs(root,res)
# return res
# def dfs(self,root,res):
# if not root:
# return
# res.append(root.val)
# self.dfs(root.left,res)
# self.dfs(root.right,res)
def preorderTraversal(self, root: TreeNode) -> List[int]:
res = []
stack = []
p = root
if not root:return res
while p or stack:
while p:
stack.append(p)
res.append(p.val)
p = p.left
p = stack.pop()
p = p.right
return res
[leetcode]145.Binary Tree Postorder Traversal
后序遍历
class Solution:
def postorderTraversal1(self, root: TreeNode) -> List[int]:
"""
:type root: TreeNode
:rtype: List[int]
"""
res = []
if not root:return res
self.dfs(root,res)
return res
def dfs(self,root,res):
if not root:return
self.dfs(root.left,res)
self.dfs(root.right,res)
res.append(root.val)
def postorderTraversal2(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
#后续遍历的非递归解法,双压栈版
res=[]
stack=[]
if not root:
return res
stack.append(root)
stack.append(root)
while (stack):
p=stack.pop()
if(stack and p==stack[-1]):
if (p.right):
stack.append(p.right)
stack.append(p.right)
if (p.left):
stack.append(p.left)
stack.append(p.left)
else:
res.append(p.val)
return res
def postorderTraversal3(self, root):
ans = []
if root == None:
return ans
stack = [root]
while stack:
p = stack.pop()
ans.append(p.val)
if p.left:
stack.append(p.left)
if p.right:
stack.append(p.right)
ans.reverse()
return ans
[leetcode]297.Serialize and Deserialize Binary Tree
树的序列化与去序列化。
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Codec:
def serialize(self, root):
"""Encodes a tree to a single string.
:type root: TreeNode
:rtype: str
"""
res = []
self.preorder(root, res)
# 将结点值序列转化为一个字符串
return ','.join(res)
def preorder(self, root, res):
if not root:
# 对于空结点,返回#字符加以标识
res.append('#')
return
res.append(str(root.val))
self.preorder(root.left, res)
self.preorder(root.right, res)
def deserialize(self, data):
"""Decodes your encoded data to tree.
:type data: str
:rtype: TreeNode
"""
res = data.split(',')
root = self.preorderdes(res)
return root
def preorderdes(self, res):
if not res:
return None
# 遇到#字符,直接删除,返回空结点
if res[0] == '#':
del res[0]
return None
root = TreeNode(int(res[0]))
del res[0]
root.left = self.preorderdes(res)
root.right = self.preorderdes(res)
return root
# Your Codec object will be instantiated and called as such:
# codec = Codec()
# codec.deserialize(codec.serialize(root))
[leetcode]450.Delete Node in a BST
删除二叉搜索树的一个节点。递归查找右子树的最小值节点。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def deleteNode(self, root: TreeNode, key: int) -> TreeNode:
if not root: return None
if root.val == key:
if not root.right:
return root.left
else:
right = root.right
while right.left:
right = right.left
root.val, right.val = right.val, root.val
root.left = self.deleteNode(root.left, key)
root.right = self.deleteNode(root.right, key)
return root
树+dfs
[leetcode]113.Path Sum II
深度优先搜索,查找结果。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def pathSum(self, root: TreeNode, sum: int) -> List[List[int]]:
res = []
self.dfs(root,sum,[],res)
return res
def dfs(self,root,sum,result,res):
if not root:return
if not root.left and not root.right and root.val == sum:
result.append(root.val)
res.append(result)
return
self.dfs(root.left,sum-root.val,result+[root.val],res)
self.dfs(root.right,sum-root.val,result+[root.val],res)
[leetcode]124.Binary Tree Maximum Path Sum 设置全局变量
树结构显然用递归来解,解题关键:
1、对于每一层递归,只有包含此层树根节点的值才可以返回到上层。否则路径将不连续。
2、返回的值最多为根节点加上左右子树中的一个返回值,而不能加上两个返回值。否则路径将分叉。
在这两个前提下有个需要注意的问题,最上层返回的值并不一定是满足要求的最大值,因为最大值对应的路径不一定包含root的值,可能存在于某个子树上。因此解决方案为设置全局变量maxSum,在递归过程中不断更新最大值。
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
self.res = -float('inf')
if not root:
return 0
self.dfs(root)
return self.res
def dfs(self,root):
if not root:
return 0
left = self.dfs(root.left)
right = self.dfs(root.right)
self.res = max(self.res,root.val+max(0,left)+max(0,right))
return max(left+root.val,right+root.val,root.val)
[leetcode]95.Unique Binary Search Trees II
对于本题来说,采取的是自底向上的求解过程。
1.选出根结点后应该先分别求解该根的左右子树集合,也就是根的左子树有若干种,它们组成左子树集合,根的右子树有若干种,它们组成右子树集合。
2.然后将左右子树相互配对,每一个左子树都与所有右子树匹配,每一个右子树都与所有的左子树匹配。然后将两个子树插在根结点上。
3.最后,把根结点放入链表中。
class Solution:
def generateTrees(self, n: int) -> List[TreeNode]:
if n == 0:
return []
return self.dfs(1, n+1)
def dfs(self, start, end):
result = []
for i in range(start, end):
for l in self.dfs(start, i) or [None]:
for r in self.dfs(i+1, end) or [None]:
node = TreeNode(i)
node.left, node.right = l, r
result.append(node)
return result
[leetcode]236.Lowest Common Ancestor of a Binary Tree
我们可以用递归来实现,在递归函数中,我们首先看当前结点是否为空,若为空则直接返回空,若为p或q中的任意一个,也直接返回当前结点。否则的话就对其左右子结点分别调用递归函数,由于这道题限制了p和q一定都在二叉树中存在,那么如果当前结点不等于p或q,p和q要么分别位于左右子树中,要么同时位于左子树,或者同时位于右子树
此题还有一种情况,若题目中没有明确说明p和q是否是树中的节点,如果不是,应该返回NULL,而上面的方法就不正确了
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if root == None or root == p or root == q:
return root
left = self.lowestCommonAncestor(root.left,p,q)
right = self.lowestCommonAncestor(root.right,p,q)
if left and right:
return root
else:
return left if left else(right)
[leetcode]337.House Robber III
定义dfs函数, 返回两个值: 从当前节点偷能获取的最大值rob_now和从子节点开始偷能获得的最大值rob_later.Base case是当root为空时, 返回(0, 0).如果从当前节点偷, 那么左右的子节点不能偷, 如果从子节点开始偷, 那么总金额为两个节点能偷到的最大值的和.
class Solution:
def rob(self, root: TreeNode) -> int:
return max(self.dfs(root))
def dfs(self,root):
# return the max money if rob this root and the max money if not rob this root
if not root: return (0, 0)
left, right = self.dfs(root.left), self.dfs(root.right)
rob_now = root.val + left[1] + right[1]
rob_later = max(left) + max(right)
return (rob_now, rob_later)
字典树
字典树和线段树作为高级的树结构,都有其特定的构成方式与解题思路。
字典树,又称前缀树或单词查找树。字典树主要有如下三点性质:
- 根节点不包含字符,除根节点意外每个节点只包含一个字符。
- 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符串不相同。
它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。缺点就是空间开销大。
对于N个字符串,如果其平均长度为M,则建立字典树的算法复杂度为(O(M*N)),在字典树上查找某个长度为M的字符串的算法复杂度(O(M))
下面以几个例题说明。
[leetcode]208.Implement Trie (Prefix Tree)
class Node(object):
def __init__(self):
self.children = collections.defaultdict(lambda :Node())
self.isword = False
class Trie(object):
def __init__(self):
"""
Initialize your data structure here.
"""
self.root = Node()
def insert(self, word):
"""
Inserts a word into the trie.
:type word: str
:rtype: void
"""
current = self.root
for w in word:
current = current.children[w]
current.isword = True
def search(self, word):
"""
Returns if the word is in the trie.
:type word: str
:rtype: bool
"""
current = self.root
for w in word:
# current = current.children.get(w)
# if current == None:
# return False
if w not in current.children:return False
current = current.children[w]
return current.isword
def startsWith(self, prefix):
"""
Returns if there is any word in the trie that starts with the given prefix.
:type prefix: str
:rtype: bool
"""
current = self.root
for w in prefix:
# current = current.children.get(w)
# if current == None:
# return False
if w not in current.children:return False
current = current.children[w]
return True
# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)
[leetcode]212.Word Search II
将待查找的单词存放在Trie(字典树)中,利用DFS(深度优先搜索)在board中搜索即可,每次查找成功,进行剪枝操作
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
res = []
tree = Trie()
visited = [[False for j in range(len(board[0]))] for i in range(len(board))]
for i in range(len(words)):
tree.insert(words[i])
for i in range(len(board)):
for j in range(len(board[0])):
self.dfs(tree,board,tree.root, visited,i,j,'',res)
return res
def dfs(self,tree,board,curr, visited,i,j,word,res):
if i < 0 or j < 0 or i > len(board) - 1 or j > len (board[0]) - 1 or visited[i][j]:
return
word += board[i][j]
curr = curr.children.get(board[i][j])
if not curr:
return
if curr.isword:
res.append(word)
tree.delete(word)
visited[i][j] = True
self.dfs(tree,board,curr,visited,i+1,j,word,res)
self.dfs(tree,board,curr,visited,i-1,j,word,res)
self.dfs(tree,board,curr,visited,i,j+1,word,res)
self.dfs(tree,board,curr,visited,i,j-1,word,res)
visited[i][j] = False
class Node(object):
def __init__(self):
self.children = collections.defaultdict(Node)
self.isword = False
class Trie(object):
def __init__(self):
"""
Initialize your data structure here.
"""
self.root = Node()
def insert(self, word):
"""
Inserts a word into the trie.
:type word: str
:rtype: void
"""
current = self.root
for w in word:
current = current.children[w]
current.isword = True
def search(self, word):
"""
Returns if the word is in the trie.
:type word: str
:rtype: bool
"""
current = self.root
for w in word:
current = current.children.get(w)
if current == None:
return False
# if w not in current:return
# .children[w]
return current.isword
def startsWith(self, prefix):
"""
Returns if there is any word in the trie that starts with the given prefix.
:type prefix: str
:rtype: bool
"""
current = self.root
for w in prefix:
if w not in current.children:return False
current = current.children[w]
current = current.children.get(w)
if current == None:
return False
return True
def delete(self, word):
current = self.root
stack = []
for w in word:
stack.append([current, w])
current = current.children.get(w)
if current is None:
return False
if not current.isword:
return False
if current.children:
current.isword = False
return True
else:
while stack:
node, string = stack.pop()
del node.children[string]
if node.children or node.isword:
break
return True
线段树
线段树在各个节点保存一条线段(数组中的一段子数组),主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(logn)。线段树的每个节点表示一个区间,子节点则分别表示父节点的左右半区间,例如父亲的区间是[L,R],那么(m=(L+R)/2)左儿子的区间是[L,m],右儿子的区间是[m+1,R]。
[leetcode]307.Range Sum Query - Mutable
解决方法有Segement Tree(线段树),Binary Indexed Tree(树状数组) ,和平方根分解三种办法。
以线段树为例:
#Segment tree node
class Node(object):
def __init__(self, start, end):
self.start = start
self.end = end
self.total = 0
self.left = None
self.right = None
class NumArray(object):
def __init__(self, nums):
"""
initialize your data structure here.
:type nums: List[int]
"""
#helper function to create the tree from input array
def createTree(nums, l, r):
#base case
if l > r:
return None
#leaf node
if l == r:
n = Node(l, r)
n.total = nums[l]
return n
mid = (l + r) // 2
root = Node(l, r)
#recursively build the Segment tree
root.left = createTree(nums, l, mid)
root.right = createTree(nums, mid+1, r)
#Total stores the sum of all leaves under root
#i.e.those elements lying between (start, end)
root.total = root.left.total + root.right.total
return root
self.root = createTree(nums, 0, len(nums)-1)
def update(self, i, val):
"""
:type i: int
:type val: int
:rtype: int
"""
#Helper function to update a value
def updateVal(root, i, val):
#Base case.The actual value will be updated in a leaf.
#The total is then propogated upwards
if root.start == root.end:
root.total = val
return val
mid = (root.start + root.end) // 2
#If the index is less than the mid, that leaf must be in the left subtree
if i <= mid:
updateVal(root.left, i, val)
#Otherwise, the right subtree
else:
updateVal(root.right, i, val)
#Propogate the changes after recursive call returns
root.total = root.left.total + root.right.total
return root.total
return updateVal(self.root, i, val)
def sumRange(self, i, j):
"""
sum of elements nums[i..j], inclusive.
:type i: int
:type j: int
:rtype: int
"""
#Helper function to calculate range sum
def rangeSum(root, i, j):
#If the range exactly matches the root, we already have the sum
if root.start == i and root.end == j:
return root.total
mid = (root.start + root.end) // 2
#If end of the range is less than the mid, the entire interval lies
#in the left subtree
if j <= mid:
return rangeSum(root.left, i, j)
#If start of the interval is greater than mid, the entire inteval lies
#in the right subtree
elif i >= mid + 1:
return rangeSum(root.right, i, j)
#Otherwise, the interval is split.So we calculate the sum recursively,
#by splitting the interval
else:
return rangeSum(root.left, i, mid) + rangeSum(root.right, mid+1, j)
return rangeSum(self.root, i, j)