创建二叉树
Creating Binary Tree
我搜索过的关于二叉树的大多数问题都显示了二叉搜索树的实现,而不是二叉树。完全二叉树的项是:
- 要么是一棵空树,要么它有 1 个节点和 2 children,其中每个
child 是另一个二叉树。
- 所有关卡都已满(可能最后一关除外)
- bottom-most 层的所有叶子都是
尽可能靠左。
我想出了一个概念,但它似乎没有 运行 通过正确的递归 -- 有人知道我做错了什么吗?
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def add(self, key):
if self.key:
if self.left is None:
self.left = Node(key)
else:
self.left.add(key)
if self.right is None:
self.right = Node(key)
else:
self.right.add(key)
else:
self.key = key
return (self.key)
您可以使用 sklearn 决策树,因为它们也可以设置为二元决策树。 link 到文档 here.
您的代码中的问题是您多次添加相同的值。您添加节点,然后仍然更深入地递归,在那里您执行相同的操作。
更深层次的问题是在到达树的底层之前并检测到该层不完整的地方之前,您并不知道在哪里插入节点 .找到正确的插入点可能需要遍历整棵树...这打败了您最初期望使用二叉树获得的速度增益。
我在这里提供三个解决方案,从最有效的开始:
1。使用列表作为树实现
对于完整的树,需要特别注意:如果按级别对节点进行编号,从 0 开始表示根,并且在每个级别内从左到右,您会注意到节点的编号 parent是(k-1)/2当它自己的数是k。在另一个方向:如果编号为 k 的节点有 children,那么它的左边 child 有编号 k*2+1,右边的child有一个大一的数。
因为树是完整的,所以这个编号永远不会有间隙,所以你可以将节点存储在一个列表中,并使用该列表的索引进行节点编号。现在将节点添加到树中仅意味着将其附加到该列表。而不是 Node
object,您只有树列表,该列表中的索引是您的节点引用。
这是一个实现:
class CompleteTree(list):
def add(self, key):
self.append(key)
return len(self) - 1
def left(self, i):
return i * 2 + 1 if i * 2 + 1 < len(self) else -1
def right(self, i):
return i * 2 + 2 if i * 2 + 2 < len(self) else -1
@staticmethod
def parent(i):
return (i - 1) // 2
def swapwithparent(self, i):
if i > 0:
p = self.parent(i)
self[p], self[i] = self[i], self[p]
def inorder(self, i=0):
left = self.left(i)
right = self.right(i)
if left >= 0:
yield from self.inorder(left)
yield i
if right >= 0:
yield from self.inorder(right)
@staticmethod
def depth(i):
return (i + 1).bit_length() - 1
这是一个创建示例树的演示,然后打印在 in-order 遍历中访问的键,按它们在树中的深度缩进:
tree = CompleteTree()
tree.add(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
for node in tree.inorder():
print(" " * tree.depth(node), tree[node])
当然,这意味着您必须引用与使用真实 Node
class 时略有不同的节点,但效率提升是有回报的。
2。使用额外的 属性
如果你知道一棵(子)树中有多少个节点,那么从这个数字的位表示,你就可以知道下一个节点应该添加到哪里。
例如,在您的示例树中,您有 5 个节点。想象一下,你想给那棵树加一个 6。根节点会告诉您当前有 5,因此您需要将其更新为 6。在二进制中为 110。忽略 left-most 1 位,其余位告诉您是向左还是向左正确的。在这种情况下,您应该向右走 (1),然后最后向左走 (0),在那个方向上创建节点。您可以迭代或递归地执行此操作。
这是一个递归的实现:
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.count = 1
def add(self, key):
self.count += 1
if self.left is None:
self.left = Node(key)
elif self.right is None:
self.right = Node(key)
# extract from the count the second-most significant bit:
elif self.count & (1 << (self.count.bit_length() - 2)):
self.right.add(key)
else:
self.left.add(key)
def inorder(self):
if self.left:
yield from self.left.inorder()
yield self
if self.right:
yield from self.right.inorder()
tree = Node(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
for node in tree.inorder():
print(node.key)
3。没有额外的 属性
如果 属性 不能添加到 Node
objects,则需要进行更广泛的搜索才能找到正确的插入点:
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def newparent(self):
# Finds the node that should serve as parent for a new node
# It returns a tuple:
# if parent found: [-1, parent for new node]
# if not found: [height, left-most leaf]
# In the latter case, the subtree is perfect, and its left-most
# leaf is the node to be used, unless self is a right child
# and its sibling has the insertion point.
if self.right:
right = self.right.newparent()
if right[0] == -1: # found inbalance
return right
left = self.left.newparent()
if left[0] == -1: # found inbalance
return left
if left[0] != right[0]:
return [-1, right[1]] # found inbalance
# temporary result in perfect subtree
return [left[0]+1, left[1]]
elif self.left:
return [-1, self] # found inbalance
# temporary result for leaf
return [0, self]
def add(self, key):
_, parent = self.newparent()
if not parent.left:
parent.left = Node(key)
else:
parent.right = Node(key)
def __repr__(self):
s = ""
if self.left:
s += str(self.left).replace("\n", "\n ")
s += "\n" + str(self.key)
if self.right:
s += str(self.right).replace("\n", "\n ")
return s
tree = Node(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
print(tree)
这从右到左递归搜索树,以找到要添加的节点的候选 parent。
对于大树,这可以稍微改进,方法是根据这些路径的长度在从根到叶的路径之间执行 binary-search。但它仍然不会像前两个解决方案那样有效。
您确实需要以某种方式扩充您的树。因为这不是二叉搜索树,所以关于每个节点的唯一真实信息是它是否有左和右 child。不幸的是,这对导航完整的二叉树没有帮助。想象一个有 10 个级别的完整二叉树。直到第 9 级,每个节点都有一个左 child 和一个右 child,所以你无法知道从哪条路径下到叶子。那么问题来了,你给每个节点添加什么信息呢?我会添加该树中的节点数。
维护计数很容易,因为每次下降到子树时,您都知道要在该节点的计数上加一。你要识别的是最左边的不完美子树。每个完美二叉树都有 n = 2^k - 1,其中 k 是层数,n 是节点数。有一些快速简便的方法可以检查一个数是否小于 2 的幂(参见 this question 的第一个答案),事实上,在完整的二叉树中,每个节点最多有一个 child 这不是完美二叉树的根。遵循一个简单的规则来添加节点:
- 如果左边child是None,设置
root.left = Node(key)
和return
- 否则如果右 child 是 None,设置
root.right = Node(key)
和 return
- 如果当前节点的 children 之一是不完美子树的根,则使该节点成为当前节点(沿该子树下降)
- 否则如果大小不相等,则将具有较小子树的节点作为当前节点。
- 否则,使左侧 child 成为当前节点。
通过用以该节点为根的子树的大小扩充每个节点,您在每个节点都拥有构建递归解决方案所需的所有信息。
我搜索过的关于二叉树的大多数问题都显示了二叉搜索树的实现,而不是二叉树。完全二叉树的项是:
- 要么是一棵空树,要么它有 1 个节点和 2 children,其中每个 child 是另一个二叉树。
- 所有关卡都已满(可能最后一关除外)
- bottom-most 层的所有叶子都是 尽可能靠左。
我想出了一个概念,但它似乎没有 运行 通过正确的递归 -- 有人知道我做错了什么吗?
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def add(self, key):
if self.key:
if self.left is None:
self.left = Node(key)
else:
self.left.add(key)
if self.right is None:
self.right = Node(key)
else:
self.right.add(key)
else:
self.key = key
return (self.key)
您可以使用 sklearn 决策树,因为它们也可以设置为二元决策树。 link 到文档 here.
您的代码中的问题是您多次添加相同的值。您添加节点,然后仍然更深入地递归,在那里您执行相同的操作。
更深层次的问题是在到达树的底层之前并检测到该层不完整的地方之前,您并不知道在哪里插入节点 .找到正确的插入点可能需要遍历整棵树...这打败了您最初期望使用二叉树获得的速度增益。
我在这里提供三个解决方案,从最有效的开始:
1。使用列表作为树实现
对于完整的树,需要特别注意:如果按级别对节点进行编号,从 0 开始表示根,并且在每个级别内从左到右,您会注意到节点的编号 parent是(k-1)/2当它自己的数是k。在另一个方向:如果编号为 k 的节点有 children,那么它的左边 child 有编号 k*2+1,右边的child有一个大一的数。
因为树是完整的,所以这个编号永远不会有间隙,所以你可以将节点存储在一个列表中,并使用该列表的索引进行节点编号。现在将节点添加到树中仅意味着将其附加到该列表。而不是 Node
object,您只有树列表,该列表中的索引是您的节点引用。
这是一个实现:
class CompleteTree(list):
def add(self, key):
self.append(key)
return len(self) - 1
def left(self, i):
return i * 2 + 1 if i * 2 + 1 < len(self) else -1
def right(self, i):
return i * 2 + 2 if i * 2 + 2 < len(self) else -1
@staticmethod
def parent(i):
return (i - 1) // 2
def swapwithparent(self, i):
if i > 0:
p = self.parent(i)
self[p], self[i] = self[i], self[p]
def inorder(self, i=0):
left = self.left(i)
right = self.right(i)
if left >= 0:
yield from self.inorder(left)
yield i
if right >= 0:
yield from self.inorder(right)
@staticmethod
def depth(i):
return (i + 1).bit_length() - 1
这是一个创建示例树的演示,然后打印在 in-order 遍历中访问的键,按它们在树中的深度缩进:
tree = CompleteTree()
tree.add(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
for node in tree.inorder():
print(" " * tree.depth(node), tree[node])
当然,这意味着您必须引用与使用真实 Node
class 时略有不同的节点,但效率提升是有回报的。
2。使用额外的 属性
如果你知道一棵(子)树中有多少个节点,那么从这个数字的位表示,你就可以知道下一个节点应该添加到哪里。
例如,在您的示例树中,您有 5 个节点。想象一下,你想给那棵树加一个 6。根节点会告诉您当前有 5,因此您需要将其更新为 6。在二进制中为 110。忽略 left-most 1 位,其余位告诉您是向左还是向左正确的。在这种情况下,您应该向右走 (1),然后最后向左走 (0),在那个方向上创建节点。您可以迭代或递归地执行此操作。
这是一个递归的实现:
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.count = 1
def add(self, key):
self.count += 1
if self.left is None:
self.left = Node(key)
elif self.right is None:
self.right = Node(key)
# extract from the count the second-most significant bit:
elif self.count & (1 << (self.count.bit_length() - 2)):
self.right.add(key)
else:
self.left.add(key)
def inorder(self):
if self.left:
yield from self.left.inorder()
yield self
if self.right:
yield from self.right.inorder()
tree = Node(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
for node in tree.inorder():
print(node.key)
3。没有额外的 属性
如果 属性 不能添加到 Node
objects,则需要进行更广泛的搜索才能找到正确的插入点:
class Node():
def __init__(self, key):
self.key = key
self.left = None
self.right = None
def newparent(self):
# Finds the node that should serve as parent for a new node
# It returns a tuple:
# if parent found: [-1, parent for new node]
# if not found: [height, left-most leaf]
# In the latter case, the subtree is perfect, and its left-most
# leaf is the node to be used, unless self is a right child
# and its sibling has the insertion point.
if self.right:
right = self.right.newparent()
if right[0] == -1: # found inbalance
return right
left = self.left.newparent()
if left[0] == -1: # found inbalance
return left
if left[0] != right[0]:
return [-1, right[1]] # found inbalance
# temporary result in perfect subtree
return [left[0]+1, left[1]]
elif self.left:
return [-1, self] # found inbalance
# temporary result for leaf
return [0, self]
def add(self, key):
_, parent = self.newparent()
if not parent.left:
parent.left = Node(key)
else:
parent.right = Node(key)
def __repr__(self):
s = ""
if self.left:
s += str(self.left).replace("\n", "\n ")
s += "\n" + str(self.key)
if self.right:
s += str(self.right).replace("\n", "\n ")
return s
tree = Node(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
print(tree)
这从右到左递归搜索树,以找到要添加的节点的候选 parent。
对于大树,这可以稍微改进,方法是根据这些路径的长度在从根到叶的路径之间执行 binary-search。但它仍然不会像前两个解决方案那样有效。
您确实需要以某种方式扩充您的树。因为这不是二叉搜索树,所以关于每个节点的唯一真实信息是它是否有左和右 child。不幸的是,这对导航完整的二叉树没有帮助。想象一个有 10 个级别的完整二叉树。直到第 9 级,每个节点都有一个左 child 和一个右 child,所以你无法知道从哪条路径下到叶子。那么问题来了,你给每个节点添加什么信息呢?我会添加该树中的节点数。
维护计数很容易,因为每次下降到子树时,您都知道要在该节点的计数上加一。你要识别的是最左边的不完美子树。每个完美二叉树都有 n = 2^k - 1,其中 k 是层数,n 是节点数。有一些快速简便的方法可以检查一个数是否小于 2 的幂(参见 this question 的第一个答案),事实上,在完整的二叉树中,每个节点最多有一个 child 这不是完美二叉树的根。遵循一个简单的规则来添加节点:
- 如果左边child是None,设置
root.left = Node(key)
和return - 否则如果右 child 是 None,设置
root.right = Node(key)
和 return - 如果当前节点的 children 之一是不完美子树的根,则使该节点成为当前节点(沿该子树下降)
- 否则如果大小不相等,则将具有较小子树的节点作为当前节点。
- 否则,使左侧 child 成为当前节点。
通过用以该节点为根的子树的大小扩充每个节点,您在每个节点都拥有构建递归解决方案所需的所有信息。