我最喜欢的面试问题之一是
在O(n)时间和O(1)空间中,确定链表是否包含循环。
可以使用Floyd's cycle-finding algorithm完成此操作。
我的问题是,在尝试检测二叉树是否包含循环时,是否可以获得这么好的时间和空间保证。也就是说,如果有人按照
的顺序给你一个struct
定义
struct node {
node* left;
node* right;
};
您如何有效地验证给定结构确实是二叉树,而不是DAG或包含循环的图形?
是否存在一种算法,在给定二叉树的根的情况下,可以确定该树是否包含O(n)时间并且优于O(n)空间的循环?显然,这可以使用标准DFS或BFS来完成,但这需要O(n)空间。可以在O(√n)空间内完成吗? O(log n)空间?或者(O圣)在O(1)空间?我很好奇,因为在链表的情况下,这可以在O(1)空间中完成,但我从未见过相应有效的算法。
答案 0 :(得分:7)
你甚至无法访问O(1)空间中真实的,诚实的,无循环的树的每个节点,所以你要求的东西显然是不可能的。 (沿途修改树的技巧不是O(1)空间)。
如果您愿意考虑基于堆栈的算法,那么可以根据Floyd算法轻松修改常规树步行。
答案 1 :(得分:4)
如果图的两个顶点属于同一个连通组件,则可以在对数空间中进行测试(Reingold,Omer(2008),“对数空间中的无向连通”,ACM 55(4)期刊:文章17,24页,doi:10.1145 / 1391289.1391291)。连通分量是循环的;因此,如果您可以在图中找到属于同一个连通组件的两个顶点,则图中会有一个循环。 Reingold在首次提出存在问题26年后发表了该算法(见http://en.wikipedia.org/wiki/Connected_component_%28graph_theory%29)。考虑到找到一个对数空间解决方案需要25年的时间,拥有O(1)空间算法听起来不太可能。请注意,从图中选取两个顶点并询问它们是否属于一个循环等同于询问它们是否属于连通组件。
这个算法可以扩展到具有out-degree 2(OP:“trees”)的图形的对数空间解决方案,因为它足以检查每个节点和它的一个直接兄弟节点是否属于它们对于相同的连接组件,可以使用标准递归树下降在O(log n)空间中枚举这些对。
答案 2 :(得分:2)
如果你假设循环指向“树”中相同深度或更小深度的节点,那么你可以做一个带有两个堆栈的BFS(迭代版本),一个用于乌龟(x1),一个用于野兔(x2速度)。在某些时候,Hare的堆栈将为空(无循环),或者是乌龟堆栈的子集(找到一个循环)。所需时间是O(n k),空间是O(lg n),其中n是使用节点的数量,k是检查可以由lg(n)限制的子集条件所需的时间。注意,关于循环的初始假设并不限制原始问题,因为它被假定为树,但是对于与先前节点形成循环的有限数量的弧;指向树中较深节点的链接不会形成循环,但会破坏树结构。
如果可以进一步假设循环指向祖先,则可以通过检查两个堆栈是否相等来更改子集条件,这更快。
答案 3 :(得分:1)
访问意识
你需要重新定义这样的结构(我将在此之外留下指针):
class node {
node left;
node right;
bool visited = false;
};
使用以下递归算法(如果你的树长得足够大,显然可以重新使用它来使用自定义堆栈):
bool validate(node value)
{
if (value.visited)
return (value.visited = false);
value.visited = true;
if (value.left != null && !validate(value.left))
return (value.visited = false);
if (value.right != null && !validate(value.right))
return (value.visited = false);
value.visited = false;
return true;
}
评论:技术上确实有O(n)空间;因为结构中的额外字段。如果所有值都在树的一侧并且每个值都在循环中,则最坏的情况也是O(n + 1)。
深度意识
插入树时,您可以跟踪最大深度:
struct node {
node left;
node right;
};
global int maximumDepth = 0;
void insert(node item) { insert(root, item, 1); }
void insert(node parent, node item, int depth)
{
if (depth > maximumDepth)
maximumDepth = depth;
// Do your insertion magic, ensuring to pass in depth + 1 to any other insert() calls.
}
bool validate(node value, int depth)
{
if (depth > maximumDepth)
return false;
if (value.left != null && !validate(value.left, depth + 1))
return false;
if (value.right != null && !validate(value.right, depth + 1))
return false;
return true;
}
评论:存储空间为O(n + 1),因为我们将深度存储在堆栈上(以及最大深度);时间仍然是O(n + 1)。这在无效树上会做得更好。
答案 4 :(得分:0)
正如卡尔所定义的那样,“树”没有周期。但我仍然得到了提出问题的精神。为什么在任何图形中都需要花哨的算法来检测周期。您可以简单地运行BFS或DFS,如果您访问已访问的节点,则意味着循环。这将在O(n)时间内运行,但空间复杂度也是O(n),不知道是否可以减少。
答案 5 :(得分:0)
正如ChingPing所提到的,一个简单的DFS应该可以解决问题。您需要将每个节点标记为已访问(需要执行从节点参考到整数的某些映射),如果在已访问的节点上尝试重新进入,则表示存在循环。
这在内存中是O(n)。
答案 6 :(得分:0)
乍一看,您可以看到这个问题可以通过Floyd算法的非确定性应用来解决。那么如果我们以分裂分支方式应用Floyd,会发生什么?
我们可以从基节点使用Floyd,然后在每个分支添加一个额外的Floyd。 因此,对于每个终端路径,我们有一个Floyd算法的实例,它在那里结束。如果一个循环出现,就有一只乌龟必须有相应的野兔追逐它。 所以算法结束了。 (作为副作用,每个终端节点只能到达一只野兔/龟对,因此有O(n)次访问,因此有O(n)时间。(存储已经分支的节点,这不会增加内存的数量级并防止在循环的情况下内存爆裂) 此外,这样做可确保内存占用量与终端节点数相同。终端节点的数量是O(log n),但在最坏的情况下是O(n)。
TL; DR:每次您可以选择时,应用Floyd和分支:
时间:O(n)
space:O(log n)
答案 7 :(得分:0)
我不相信存在用于行走小于O(N)空间的树的算法。并且,对于(声称的)二叉树,它不需要更多的空间/时间(以“顺序”术语)来检测循环而不是走树。我相信DFS会在O(N)时间内走一棵树,因此O(N)可能是两种测量中的极限。
答案 8 :(得分:0)
好的,经过进一步的思考,我相信我找到了办法,只要你
基本思想是使用Morris inorder tree traversal遍历树,并在访问阶段和单个前任发现阶段计算访问节点的数量。如果其中任何一个超过了节点数,那么你肯定有一个循环。如果你没有循环,那么它相当于普通的Morris遍历,你的二叉树将被恢复。
我不确定是否可以预先知道节点数量。会考虑更多。
答案 9 :(得分:0)
管理好了!
算法尝试通过将当前节点的整个左子树移动到其上方来使二进制树变平,使其成为子树的最右边节点,然后更新当前节点以在新发现的节点中找到更多的左子树。如果我们知道左子节点和当前节点的前任节点,我们可以在几个操作中移动整个子树,方式类似于将列表插入另一个节点。这样的移动保留了树的有序顺序,它总是使树更倾斜。
有三种情况取决于当前节点周围节点的本地配置:左子节点与前一节点相同,左子节点与前一节点不同,或者没有左子树。第一种情况是微不足道的。第二种情况需要找到前一种情况,第三种情况需要在右边找到一个带有左子树的节点。图形表示有助于理解它们。
在后两种情况下,我们可以遇到周期。由于我们只遍历一个正确的孩子列表,我们可以使用Floyd的周期检测算法来查找和报告循环。每个周期迟早都会轮换成这种形式。
#include <cstdio>
#include <iostream>
#include <queue>
#define null NULL
#define int32 int
using namespace std;
/**
* Binary tree node class
**/
template <class T>
class Node
{
public:
/* Public Attributes */
Node* left;
Node* right;
T value;
};
/**
* This exception is thrown when the flattener & cycle detector algorithm encounters a cycle
**/
class CycleException
{
public:
/* Public Constructors */
CycleException () {}
virtual ~CycleException () {}
};
/**
* Biny tree flattener and cycle detector class.
**/
template <class T>
class Flattener
{
public:
/* Public Constructors */
Flattener () :
root (null),
parent (null),
current (null),
top (null),
bottom (null),
turtle (null),
{}
virtual ~Flattener () {}
/* Public Methods */
/**
* This function flattens an alleged binary tree, throwing a new CycleException when encountering a cycle. Returns the root of the flattened tree.
**/
Node<T>* flatten (Node<T>* pRoot)
{
init(pRoot);
// Loop while there are left subtrees to process
while( findNodeWithLeftSubtree() ){
// We need to find the topmost and rightmost node of the subtree
findSubtree();
// Move the entire subtree above the current node
moveSubtree();
}
// There are no more left subtrees to process, we are finished, the tree does not contain cycles
return root;
}
protected:
/* Protected Methods */
void init (Node<T>* pRoot)
{
// Keep track of the root node so the tree is not lost
root = pRoot;
// Keep track of the parent of the current node since it is needed for insertions
parent = null;
// Keep track of the current node, obviously it is needed
current = root;
}
bool findNodeWithLeftSubtree ()
{
// Find a node with a left subtree using Floyd's cycle detection algorithm
turtle = parent;
while( current->left == null and current->right != null ){
if( current == turtle ){
throw new CycleException();
}
parent = current;
current = current->right;
if( current->right != null ){
parent = current;
current = current->right;
}
if( turtle != null ){
turtle = turtle->right;
}else{
turtle = root;
}
}
return current->left != null;
}
void findSubtree ()
{
// Find the topmost node
top = current->left;
// The topmost and rightmost nodes are the same
if( top->right == null ){
bottom = top;
return;
}
// The rightmost node is buried in the right subtree of topmost node. Find it using Floyd's cycle detection algorithm applied to right childs.
bottom = top->right;
turtle = top;
while( bottom->right != null ){
if( bottom == turtle ){
throw new CycleException();
}
bottom = bottom->right;
if( bottom->right != null ){
bottom = bottom->right;
}
turtle = turtle->right;
}
}
void moveSubtree ()
{
// Update root; if the current node is the root then the top is the new root
if( root == current ){
root = top;
}
// Add subtree below parent
if( parent != null ){
parent->right = top;
}
// Add current below subtree
bottom->right = current;
// Remove subtree from current
current->left = null;
// Update current; step up to process the top
current = top;
}
Node<T>* root;
Node<T>* parent;
Node<T>* current;
Node<T>* top;
Node<T>* bottom;
Node<T>* turtle;
private:
Flattener (Flattener&);
Flattener& operator = (Flattener&);
};
template <class T>
void traverseFlat (Node<T>* current)
{
while( current != null ){
cout << dec << current->value << " @ 0x" << hex << reinterpret_cast<int32>(current) << endl;
current = current->right;
}
}
template <class T>
Node<T>* makeCompleteBinaryTree (int32 maxNodes)
{
Node<T>* root = new Node<T>();
queue<Node<T>*> q;
q.push(root);
int32 nodes = 1;
while( nodes < maxNodes ){
Node<T>* node = q.front();
q.pop();
node->left = new Node<T>();
q.push(node->left);
nodes++;
if( nodes < maxNodes ){
node->right = new Node<T>();
q.push(node->right);
nodes++;
}
}
return root;
}
template <class T>
void inorderLabel (Node<T>* root)
{
int32 label = 0;
inorderLabel(root, label);
}
template <class T>
void inorderLabel (Node<T>* root, int32& label)
{
if( root == null ){
return;
}
inorderLabel(root->left, label);
root->value = label++;
inorderLabel(root->right, label);
}
int32 main (int32 argc, char* argv[])
{
if(argc||argv){}
typedef Node<int32> Node;
// Make binary tree and label it in-order
Node* root = makeCompleteBinaryTree<int32>(1 << 24);
inorderLabel(root);
// Try to flatten it
try{
Flattener<int32> F;
root = F.flatten(root);
}catch(CycleException*){
cout << "Oh noes, cycle detected!" << endl;
return 0;
}
// Traverse its flattened form
// traverseFlat(root);
}