我最近编写了一堆不同的二叉搜索树实现(AVL,splay,treap),如果有一种特别“好”的方法来编写迭代器来遍历这些结构,我很好奇。我现在使用的解决方案是让BST中的每个节点都存储指向树中下一个和前一个元素的指针,这会将迭代减少到标准的链表迭代。但是,我对这个答案并不满意。它通过两个指针(下一个和前一个)增加每个节点的空间使用量,在某种意义上它只是作弊。
我知道一种构建二进制搜索树迭代器的方法,该迭代器使用O(h)辅助存储空间(其中h是树的高度),通过使用堆栈来跟踪要在以后探索的边界节点,但由于内存的使用,我拒绝对此进行编码。我希望有一些方法可以构建一个只使用常量空间的迭代器。
我的问题是 - 有没有办法在具有以下属性的二叉搜索树上设计迭代器?
next()
和hasNext()
查询在O(1)时间内运行。为了使它变得更容易,如果你假设树结构在迭代过程中没有改变形状(即没有插入,删除或旋转),那就没关系了,但是如果有一个解决方案可以确实真的很酷处理这个。
答案 0 :(得分:30)
最简单的迭代器存储最后看到的键,然后在下一次迭代中,在树中搜索该键的最小上限。迭代次数为O(log n)。这具有非常简单的优点。如果键很小,那么迭代器也很小。当然,它的缺点是迭代树的速度相对较慢。它也不适用于非唯一序列。
有些树完全使用您已经使用的实现,因为扫描速度非常快,因此对于特定用途而言非常重要。如果每个节点中的密钥数量很大,则存储兄弟指针的代价并不太严重。大多数B树使用这种方法。
许多搜索树实现在每个节点上保留父指针以简化其他操作。如果你有,那么你可以使用指向最后一个节点的简单指针作为迭代器的状态。在每次迭代中,您将查找最后一个节点的父节点中的下一个子节点。如果没有兄弟姐妹,那么你再上一级。
如果这些技术都不适合您,您可以使用存储在迭代器中的一堆节点。当正常迭代搜索树时,它与函数调用堆栈具有相同的功能,但是不是循环遍历兄弟节点并在子节点上递归,而是将子节点推送到堆栈并返回每个连续的兄弟节点。
答案 1 :(得分:18)
正如TokenMacGuy所提到的,你可以使用存储在迭代器中的堆栈。 以下是Java中快速测试的实现:
/**
* An iterator that iterates through a tree using in-order tree traversal
* allowing a sorted sequence.
*
*/
public class Iterator {
private Stack<Node> stack = new Stack<>();
private Node current;
private Iterator(Node argRoot) {
current = argRoot;
}
public Node next() {
while (current != null) {
stack.push(current);
current = current.left;
}
current = stack.pop();
Node node = current;
current = current.right;
return node;
}
public boolean hasNext() {
return (!stack.isEmpty() || current != null);
}
public static Iterator iterator(Node root) {
return new Iterator(root);
}
}
其他变体是在构建时遍历树并将遍历保存到列表中。您可以在之后使用列表迭代器。
答案 2 :(得分:3)
好的,我知道这已经过时了,但有一段时间我在接受微软的采访时被问到这一点,我决定对它进行一些研究。我测试了这个并且效果很好。
template <typename E>
class BSTIterator
{
BSTNode<E> * m_curNode;
std::stack<BSTNode<E>*> m_recurseIter;
public:
BSTIterator( BSTNode<E> * binTree )
{
BSTNode<E>* root = binTree;
while(root != NULL)
{
m_recurseIter.push(root);
root = root->GetLeft();
}
if(m_recurseIter.size() > 0)
{
m_curNode = m_recurseIter.top();
m_recurseIter.pop();
}
else
m_curNode = NULL;
}
BSTNode<E> & operator*() { return *m_curNode; }
bool operator==(const BSTIterator<E>& other)
{
return m_curNode == other.m_curNode;
}
bool operator!=(const BSTIterator<E>& other)
{
return !(*this == other);
}
BSTIterator<E> & operator++()
{
if(m_curNode->GetRight())
{
m_recurseIter.push(m_curNode->GetRight());
if(m_curNode->GetRight()->GetLeft())
m_recurseIter.push(m_curNode->GetRight()->GetLeft());
}
if( m_recurseIter.size() == 0)
{
m_curNode = NULL;
return *this;
}
m_curNode = m_recurseIter.top();
m_recurseIter.pop();
return *this;
}
BSTIterator<E> operator++ ( int )
{
BSTIterator<E> cpy = *this;
if(m_curNode->GetRight())
{
m_recurseIter.push(m_curNode->GetRight());
if(m_curNode->GetRight()->GetLeft())
m_recurseIter.push(m_curNode->GetRight()->GetLeft());
}
if( m_recurseIter.size() == 0)
{
m_curNode = NULL;
return *this;
}
m_curNode = m_recurseIter.top();
m_recurseIter.pop();
return cpy;
}
};
答案 3 :(得分:1)
所有示例实现都需要调用堆栈空间与树的高度成比例。在一棵不平衡的树中,这可能是相当可观的。
我们可以通过在每个节点中维护父指针或通过线程化树来删除堆栈需求。在使用线程的情况下,这将允许极大地改进inorder遍历,尽管检索预订和后序遍历所需的父节点将比基于简单堆栈的算法慢。
在文章中有一些用于迭代的伪代码,具有O(1)状态,可以很容易地适应迭代器。
答案 4 :(得分:0)
使用深度优先搜索技术怎么样?迭代器对象必须有一堆已经访问过的节点。
答案 5 :(得分:0)
使用O(1)空格,这意味着我们不会使用O(h)堆栈。
开始:
hasNext()? current.val&lt; = endNode.val来检查树是否完全遍历。
通过最左边找到最小值:我们可以找到最左边的找到下一个最小值。
选中最左边的min(将其命名为current
)。下一分钟将是2个案例:
如果current.right!= null,我们可以继续寻找current.right的最左边的孩子,作为下一分钟。
或者,我们需要向后看父母。使用二叉搜索树查找当前的父节点。
注意:在对父级进行二进制搜索时,请确保它满足parent.left = current。
因为:如果parent.right == current,则必须先访问该父级。在二叉搜索树中, 我们知道parent.val&lt; parent.right.val。我们需要跳过这个特例,因为它会导致 到ifinite循环。
public class BSTIterator {
public TreeNode root;
public TreeNode current;
public TreeNode endNode;
//@param root: The root of binary tree.
public BSTIterator(TreeNode root) {
if (root == null) {
return;
}
this.root = root;
this.current = root;
this.endNode = root;
while (endNode != null && endNode.right != null) {
endNode = endNode.right;
}
while (current != null && current.left != null) {
current = current.left;
}
}
//@return: True if there has next node, or false
public boolean hasNext() {
return current != null && current.val <= endNode.val;
}
//@return: return next node
public TreeNode next() {
TreeNode rst = current;
//current node has right child
if (current.right != null) {
current = current.right;
while (current.left != null) {
current = current.left;
}
} else {//Current node does not have right child.
current = findParent();
}
return rst;
}
//Find current's parent, where parent.left == current.
public TreeNode findParent(){
TreeNode node = root;
TreeNode parent = null;
int val = current.val;
if (val == endNode.val) {
return null;
}
while (node != null) {
if (val < node.val) {
parent = node;
node = node.left;
} else if (val > node.val) {
node = node.right;
} else {//node.val == current.val
break;
}
}
return parent;
}
}
答案 6 :(得分:0)
根据定义,next()和hasNext()不可能在O(1)时间内运行。当您查看BST中的特定节点时,您不知道其他节点的高度和结构,因此您不能“跳转”到正确的下一个节点。
但是,空间复杂度可以降低到O(1)(除了BST本身的存储器)。这是我在C中的方式:
struct node{
int value;
struct node *left, *right, *parent;
int visited;
};
struct node* iter_next(struct node* node){
struct node* rightResult = NULL;
if(node==NULL)
return NULL;
while(node->left && !(node->left->visited))
node = node->left;
if(!(node->visited))
return node;
//move right
rightResult = iter_next(node->right);
if(rightResult)
return rightResult;
while(node && node->visited)
node = node->parent;
return node;
}
技巧是为每个节点同时提供父链接和访问标记。在我看来,我们可以争辩说这不是额外的空间使用,它只是节点结构的一部分。很明显,必须在没有树结构状态改变的情况下调用iter_next()(当然),而且“访问”标志也不会改变值。
以下是调用iter_next()的tester函数,每次为此树打印值:
27
/ \
20 62
/ \ / \
15 25 40 71
\ /
16 21
int main(){
//right root subtree
struct node node40 = {40, NULL, NULL, NULL, 0};
struct node node71 = {71, NULL, NULL, NULL, 0};
struct node node62 = {62, &node40, &node71, NULL, 0};
//left root subtree
struct node node16 = {16, NULL, NULL, NULL, 0};
struct node node21 = {21, NULL, NULL, NULL, 0};
struct node node15 = {15, NULL, &node16, NULL, 0};
struct node node25 = {25, &node21, NULL, NULL, 0};
struct node node20 = {20, &node15, &node25, NULL, 0};
//root
struct node node27 = {27, &node20, &node62, NULL, 0};
//set parents
node16.parent = &node15;
node21.parent = &node25;
node15.parent = &node20;
node25.parent = &node20;
node20.parent = &node27;
node40.parent = &node62;
node71.parent = &node62;
node62.parent = &node27;
struct node *iter_node = &node27;
while((iter_node = iter_next(iter_node)) != NULL){
printf("%d ", iter_node->value);
iter_node->visited = 1;
}
printf("\n");
return 1;
}
将按排序顺序打印值:
15 16 20 21 25 27 40 62 71