【问题标题】:Binary Search Tree Inorder Traversal二叉搜索树中序遍历
【发布时间】:2018-02-25 05:59:58
【问题描述】:

我被这段代码弄糊涂了:

void in_order_traversal_iterative(BinaryTree *root) {
  stack<BinaryTree*> s;
  BinaryTree *current = root;
  while (!s.empty() || current) {
    if (current) {
      s.push(current);
      current = current->left;
    } else {
      current = s.top();
      s.pop();
      cout << current->data << " ";
      current = current->right;
    }
  }
}

我们设置了一个指向根的指针。然后如果它存在,则将当前(当前是根)推入堆栈。我不明白为什么我们最初将整个树推入堆栈,而不仅仅是节点保存的数据的值。我是完全遗漏了什么还是不明白为什么它会这样工作?我无法理解为什么我们将整个树推入,而不是单个节点...

【问题讨论】:

    标签: binary-search-tree


    【解决方案1】:

    你错过了一个事实,即在一个节点被弹出后,它的右孩子仍然必须被遍历:

      current = s.top();
      s.pop();
      cout << current->data << " ";
      current = current->right;
    

    如果堆栈中只有数据,这是不可能的。循环不变式是堆栈恰好包含那些具有未遍历的右子节点的节点。

    查看发生了什么的另一种方法是将递归遍历通过代数转换为迭代:

    traverse(node) {
      if (node) {
        traverse(node->left);
        visit(node);
        traverse(node->right);
      }
    }
    

    首先将尾调用转换为迭代。我们通过更新参数并将递归调用替换为函数开头的goto 来做到这一点:

    traverse(node) {
     start:
      if (node) {
        traverse(node->left);
        visit(node);
        node = node->right;
        goto start;
      }
    }
    

    gotoifwhile 相同,所以到目前为止

    traverse(node) {
      while (node) {
        traverse(node->left);
        visit(node);
        node = node->right;
      }
    }
    

    替换其他递归调用需要我们模拟编译器运行环境的调用栈。我们使用显式堆栈来做到这一点。

    traverse(node) {
     start:
      while (node) {
        stack.push(node);   // save the value of the argument.
        node = node->left;  // redefine it the same way the recursive call would have
        goto start;         // simulate the recursive call
                            // recursive call was here; it's gone now!
       recursive_return:    // branch here to simulate return from recursive call
        visit(node);
        node = node->right;
      }
      // simulate the recursive return: if stack has args, restore and go to return site
      if (!stack.empty()) {
        node = stack.pop();  // restore the saved parameter value
        goto recursive_return;
      }
    }
    

    虽然它很丑,但这是一种始终适用于实现递归代码的迭代版本的方法。 (如果有多个非尾递归调用会更复杂,但不会太多。)而且我相信您可以看到与您的代码的相似之处。

    我们甚至可以用更多的代数来摆脱丑陋。首先,不难看出这段代码:

     start:
      while (node) {
        stack.push(node);   // save the value of the argument.
        node = node->left;  // redefine it the same way the recursive call would have
        goto start;         // simulate the recursive call
    

    当以start开头执行时相当于

      while (node) {
        stack.push(node);   // save the value of the argument.
        node = node->left;  // redefine it the same way the recursive call would have
      }
    

    我们也可以替换

      if (!stack.empty()) {
        node = stack.pop();  // restore the saved parameter value
        goto recursive_return;
      }
    

    以下

      if (!stack.empty()) {
        node = stack.pop();  // restore the saved parameter value
        visit(node);
        node = node->right;
        goto start;
      }
    

    我们只是将recursive_return: 之后的三个指令复制到if 正文中。

    这样的话,就没有办法到达recursive_return这个标签了,所以我们可以把它连同下面两个语句一起删除:

       // Dead code!  Delete me!
       recursive_return:
        visit(node);
        node = node->right;
    

    我们现在有:

    traverse(node) {
     start:
      while (node) {
        stack.push(node);   // save the value of the argument.
        node = node->left;  // redefine it the same way the recursive call would have
      }
      if (!stack.empty()) {
        node = stack.pop();  // restore the saved parameter value
        visit(node);
        node = node->right;
        goto start;
      }
    }
    

    我们可以通过将最后一个goto start 替换为无限循环来摆脱它:

    traverse(node) {
      loop {
        while (node) {
          stack.push(node);        // save the value of the argument
          node = node->left;       // redefine it the same way the recursive call would have
        }
        if (stack.empty()) break;  // original code returns, so does this!
        node = stack.pop();        // restore the saved parameter value
        visit(node);
        node = node->right;
      }
    }
    

    请注意,我们在与前面代码相同的条件下返回:堆栈为空!

    我会让您向自己证明,这段代码的作用与您提供的相同,只是效率更高一些,因为它避免了一些比较!我们根本不需要对指针和堆栈元素进行推理。它“刚刚发生”。

    【讨论】:

    • 啊,我明白为什么不能使用数据了。这是一个非常好的和明确的观点。谢谢!堆栈是否包含孤立节点或每个节点的子节点?
    • @Brandon 诸如“孤立”之类的术语毫无意义。堆栈准确地保存了搜索已到达但尚未访问的节点。这也意味着以这些堆叠节点的右孩子为根的子树也不能被遍历。
    • 我现在明白了。我也想知道右边。非常感谢你们。这很有帮助!哇,我没有意识到我有多少不理解这个概念。祝你有美好的一天
    • 这篇文章是一座金矿。它介绍了一种系统地将递归转换为迭代的方法。多年来,我一直在寻找有关此类主题的材料,例如,如何整齐地组织复杂的嵌套循环。请问有没有什么书或笔记系统地介绍了类似的tips?还是这些方法纯粹是从个人反思中获得的?
    • @modeller Horowitz 和 Sahani 的教科书第 1 版讨论了使用显式堆栈模拟编译器对堆栈帧所做的基本思想。其他转换是您将在 David Greis 的“编程科学”中看到的那种逻辑。
    【解决方案2】:

    它不是将整个树推入堆栈,而是将树的最左边部分推入。然后它开始弹出元素并按升序推送它们最右边的对应元素。

    【讨论】:

    • 不是先推根吗?然后左右两侧都被推动。还是指针隔离了父母和孩子之间的联系?
    • 指针是一个节点。它推送根节点,是的......它有一个值(数据)和两个子节点(左右),但你没有推送子节点,它们的引用是根节点“内部”。
    • 好的,我的想法有意义吗:堆栈包含节点而不是明确的树?因为它是一个节点指针,所以我们指向节点,但到子节点的链接就在节点内,这就是我们如何在被弹出后遍历到正确的子节点?之前我想的太字面意思了。我在想,树本身被一遍又一遍地推。但我必须考虑内存位置而不是节点的文字链接。
    • 任何节点都是一棵树。或者更确切地说,树只是我们方便地称为“根”的任意节点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多