21

One of my favorite interview questions is

In O(n) time and O(1) space, determine whether a linked list contains a cycle.

This can be done using Floyd's cycle-finding algorithm.

My question is whether it's possible to get such good time and space guarantees when trying to detect whether a binary tree contains a cycle. That is, if someone gives you a struct definition along the lines of

struct node {
    node* left;
    node* right;
};

How efficiently can you verify that the given structure is indeed a binary tree and not, say, a DAG or a graph containing a cycle?

Is there an algorithm that, given the root of a binary tree, can determine whether that tree contains a cycle in O(n) time and better than O(n) space? Clearly this could be done with a standard DFS or BFS, but this requires O(n) space. Can it be done in O(√n) space? O(log n) space? Or (the holy grail) in just O(1) space? I'm curious because in the case of a linked list this can be done in O(1) space, but I've never seen a correspondingly efficient algorithm for this case.

GEOCHET
  • 21,119
  • 15
  • 74
  • 98
templatetypedef
  • 362,284
  • 104
  • 897
  • 1,065
  • 20
    Yes: `bool containsCycle(Tree t) { return false; }`. Trees cannot contain cycles by definition. Did you mean to ask if any general graph contains a cycle? – Peter Alexander Aug 21 '11 at 18:36
  • 8
    A tree cannot contain a cycle by definition. I think you mean some kind of graph. – Karl Knechtel Aug 21 '11 at 18:36
  • 2
    @Karl - unless it's a family tree: http://stackoverflow.com/questions/6163683/cycles-in-family-tree-software – Flexo Aug 21 '11 at 18:37
  • 2
    (Also if that was an interview question as the tag implies - **run**!) – Flexo Aug 21 '11 at 18:39
  • Do you mean directed or undirected cycles? – svick Aug 21 '11 at 18:40
  • @awoodland: Funny, but still doesn't have any cycles :-) Family trees are directed *acyclic* graphs. To have a cycle in a family "tree" would mean that someone's daughter was also their mother (or something like that). – Peter Alexander Aug 21 '11 at 18:40
  • @svick- Sorry, I should have been more precise in the question. I'm looking for undirected cycles. – templatetypedef Aug 21 '11 at 18:45
  • 1
    @Peter Alexander- Yes, sorry, I knew that. My question is more along the lines of "if you have a data structure that could encode a binary tree (each node has two pointers in it), does it actually describe a binary tree? Or does it encode something with a cycle in it?" Thanks for pointing that out! – templatetypedef Aug 21 '11 at 18:46
  • In topic, I think it's better to write "binary trees" than "binary search trees", even though these are not trees – sdcvvc Aug 21 '11 at 18:50
  • @sdcvvc- Whoops! Yeah, that was definitely a mistake. Man, I am on a roll here with this question... – templatetypedef Aug 21 '11 at 18:51
  • if there is no circle, but 2 routes to the same vertex [i.e. edge to a sibling], what should the algorithm yield? – amit Aug 21 '11 at 19:12
  • @amit- Since that's an undirected cycle, it should (hopefully) report a cycle. If you can get the algorithm working purely for the directed cycle case, though, that would still be pretty cool. – templatetypedef Aug 21 '11 at 19:15
  • Just a thought: if you also store what the bigger subtree is (left or right) in each node, I think you can use DFS with `O(log n)` space, similar to how you would get quicksort to use `O(log n)` space: by recursing only on the smaller subtree and iterating / using tail recursion on the larger one. – IVlad Aug 21 '11 at 21:10
  • Traverse tree top-bottom. Mark each visited node. If you meet marked node - you have cycle. Space: O(n) / Time: O(nlog(n)) – dfens Aug 23 '11 at 09:58
  • Downvoter- Can you explain what's wrong with this question? – templatetypedef Aug 23 '11 at 11:19
  • @dfens- My question is mostly about how to accomplish this in less than O(n) space. But thanks for the thought! – templatetypedef Aug 23 '11 at 11:20
  • sorry: time is O(n), not O(nlog(n)) of course – dfens Aug 23 '11 at 14:37
  • The problem here is as soon as you trade space off, time increases and visa-versa. I am not sure if it's entirely possible - although you could write an algorithm that would be space-efficient if the graph is invalid (see [my answer](http://stackoverflow.com/questions/7140215/efficient-algorithm-to-determine-if-an-alleged-binary-tree-contains-a-cycle/7163989#7163989)). – Jonathan Dickinson Aug 23 '11 at 16:25
  • O(1) for time would be storing a boolean indicating if the node is parented, and preventing insertion if it is. – Jonathan Dickinson Aug 23 '11 at 16:27
  • Do you want to detect only cycles (giving infinite descending path) or also two ways to get to a vertex? For example, x = new node(null, null); y = new node(x, x); has no infinite descending path but two ways to get from y to x. – sdcvvc Aug 26 '11 at 15:14
  • +1 Without thinking about problem statement described currectly, that's realy nice question. – Saeed Amiri Sep 12 '11 at 09:31

10 Answers10

7

You can't even visit each node of a real, honest-to-god, cycle-free tree in O(1) space, so what you are asking for is clearly impossible. (Tricks with modifying a tree along the way are not O(1) space).

If you are willing to consider stack-based algorithms, then a regular tree walk can be easily modified along the lines of Floyd's algorithm.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • It's a good point that you can't use O(1) space, but could you do it in (say) O(log n) space? I think you just need enough space to store the decisions you've made in the past so that you can avoid taking the same path unnecessarily many times. – templatetypedef Aug 22 '11 at 19:21
  • 3
    @templatetypedef: you can do it in O(h) where h is the height of the tree. For balanced trees this is O(log N), for arbitrary trees O(N). You store your decisions in a list, and launch a half-speed pointer down that list, just like in the Floyd's algorithm. For each two steps you go down the tree, the additional pointer goes one step down the decision path. The only difference with the original Floyd's algorithm is that you sometimes go back up, and so does the additional pointer. – n. m. could be an AI Aug 23 '11 at 05:11
  • The space will depend on the depth of the tree, so if it turns out to have a cycle the space will probably be more like O(n). – phkahler Aug 23 '11 at 16:46
  • It would easily be possible to do it in O(lg n) space if we can say that no node has more than one parent. Otherwise, we would definitely need O(n) space. – user606723 Aug 24 '11 at 15:45
  • I'm sorry but the Morris traversal does indeed work in O(1) (additional) space for mutable, legitimate binary trees. Unless there is a constraint in the problem specification about the immutability of the binary tree, you shouldn't reject any solution based on it. – Frigo Aug 25 '11 at 10:13
  • @Frigo: Morris traversal makes use of NULL child pointers, which only exist if nodes are fixed-size and indeed waste space on NULL pointers. One bit is enough to indicate whether a node has a child. Morris traversal won't work with such implementation. – n. m. could be an AI Aug 25 '11 at 12:46
  • I fail to see how could you have a binary tree implementation without explicitly storing its children, only with a bit. Granted, storing the children in a list or map makes Morris traversal an O(n) space algorithm, but I doubt many implementation stores trees that way. – Frigo Aug 25 '11 at 17:47
  • @Frigo: you need to store a child if there *is* a child. Half of the nodes have no children. – n. m. could be an AI Aug 25 '11 at 18:19
4

it is possible to test in logarithmic space if two vertices of a graph belong to the same connected component (Reingold, Omer (2008), "Undirected connectivity in log-space", Journal of the ACM 55 (4): Article 17, 24 pages, doi:10.1145/1391289.1391291). A connected component is cyclic; hence if you can find two vertices in a graph that belong to the same connected component there is a cycle in the graph. Reingold published the algorithm 26 years after the question of its existence was first posed (see http://en.wikipedia.org/wiki/Connected_component_%28graph_theory%29). Having an O(1) space algorithm sounds unlikely given that it took 25 years to find a log-space solution. Note that picking two vertices from a graph and asking if they belong to a cycle is equivalent to asking if they belong to a connected component.

This algorithm can be extended to a log-space solution for graphs with out-degree 2 (OP: "trees"), as it is enough to check for every pair of a node and one of its immediate siblings if they belong to the same connect component, and these pairs can be enumerated in O(log n) space using standard recursive tree descent.

Antti Huima
  • 25,136
  • 3
  • 52
  • 71
  • But will that algorithm prove that a graph is a tree? In O(N) time? – Hot Licks Aug 22 '11 at 03:05
  • Don't know, it might that a graph is non-cyclical but not a tree either. But that wasn't the original question, though. – Antti Huima Aug 22 '11 at 14:10
  • My point is that proving that a cycle exists (or not) between two points in the graph is nowhere near what's needed to prove the graph is a tree (or shrub or forest or whatever). Some amount of effort and storage is needed to navigate the putative tree and make sure all branches are checked. – Hot Licks Aug 22 '11 at 17:18
  • True, but you can have a special class of graphs that is "binary trees with back-pointers", i.e. classes that can be factored into (1) a binary tree and (2) arcs that point from nodes to their ancestors in the tree... and that was what I was thinking the OP had in mind. This is a set of graphs that is different from that of general directed graphs. Once you assume this set of graphs, logarithmic space is enough to scan through the putative tree. – Antti Huima Aug 25 '11 at 18:16
  • Reingold's algorithm is O(log n) for Turing machines (O(1) is clearly impossible, since constant space = regular languages), uses O(1) space in RAM machine model. It requires that given any vertex you can list all neighbours. In this question for any vertex you know address of only left and right neigbours. So I don't know how it might help. – sdcvvc Aug 26 '11 at 15:17
  • @sdcvvc If it is a "binary tree with back-pointers" (this data structure has also other names, also known as 'DFS search tree', and it is actually used in some graph planarization algorithms) then the only missing neighbor is the parent of a node; but you can keep a pointer to it when you descend to the node itself as there is no other entry pointer to it. Anyway, I guess at this point the discussion would require a deeper investigation of Reingold's algorithm. – Antti Huima Sep 12 '11 at 23:24
2

If you assume that the cycle points to a node at the same depth or smaller depth in the "tree", then you can do a BFS (iterative version) with two stacks, one for the turtle (x1) and one for the hare (x2 speed). At some point, the Hare's stack will be either empty (no cycle), or be a subset of the turtle's stack (a cycle was found). The time required is O(n k), and the space is O(lg n) where n is the number of used nodes, and k the time required to check the subset condition which can be upper bounded by lg(n). Note that the initial assumption about the cycle does not constraints the original problem, since it is assumed to be a tree but for a finite number of arcs that form a cycle with previous nodes; a link to a deeper node in the tree will not form a cycle but destroy the tree structure.

If it can be further assumed that the cycle points to an ancestor, then, the subset condition can be changed by checking that both stacks are equal, which is faster.

HeXuX
  • 31
  • 4
  • space is O(n). for a full binary tree there are n/2 nodes in the stack, when visiting the first leaf [full binary tree has n/2 leaves]. – amit Aug 25 '11 at 07:43
  • This will not work for a tree that a lower-level node points to a higher level. The higher node will be in the tortoise and hare sets, but there is no cycle. http://i.imgur.com/EYVUk1M.png – cdbitesky Jun 09 '13 at 21:40
1

Visited Aware

You would need to redefine the structure as such (I am going to leave pointers out of this):

class node {
    node left;
    node right;
    bool visited = false;
};

And use the following recursive algorithm (obviously re-working it to use a custom stack if your tree could grow big enough):

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;
}

Comments: It does technically have O(n) space; because of the extra field in the struct. The worst case for it would also be O(n+1) if all the values are on a single side of the tree and every value is in the cycle.

Depth Aware

When inserting into the tree you could keep track of the maximum depth:

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;
}

Comments: The storage space is O(n+1) because we are storing the depth on the stack (as well as the maximum depth); the time is still O(n+1). This would do better on invalid trees.

Jonathan Dickinson
  • 9,050
  • 1
  • 37
  • 60
  • the recursive call also require O(n) space [in the call stack], so it is not only the 'visited' field – amit Aug 25 '11 at 08:10
1

Managed to get it right!

  • Runtime: O(n). I suspect it goes through an edge at most a constant number of times. No formal proof.
  • Space: O(1). Only stores a few nodes. Does not create new nodes or edges, only rearranges them.
  • Destructive: Yes. It flattens the tree, every node has the inorder successor as its right child, and null as the left.

The algorithm tries to flatten the binary tree by moving the whole left subtree of the current node to above it, making it the rightmost node of the subtree, then updating the current node to find further left subtrees in the newly discovered nodes. If we know both the left child and the predecessor of the current node, we can move the entire subtree in a few operations, in a similar manner to inserting a list into another. Such a move preserves the in-order sequence of the tree and it invariably makes the tree more right slanted.

There are three cases depending on the local configuration of nodes around the current one: left child is the same as the predecessor, left child is different from the predecessor, or no left subtree. The first case is trivial. The second case requires finding the predecessor, the third case requires finding a node on the right with a left subtree. Graphical representation helps in understanding them.

In the latter two cases we can run into cycles. Since we only traverse a list of the right childs, we can use Floyd's cycle detection algorithm to find and report loops. Sooner or later every cycle will be rotated into such a form.

#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);

}
Frigo
  • 1,709
  • 1
  • 14
  • 32
  • Is there any advantage to Floyd's algorithm over Brent's? The latter wouldn't require marching two pointers through the tree. Also, I would think it should be feasible, and not too difficult (especially when using Brent's algorithm instead of Floyd's) to have the algorithm leave a legitimate tree in the same condition as it was before it started. Not sure about an algorithm to back out changes to a tree with cycles. – supercat Dec 06 '11 at 01:35
  • Since I only check lists in this algorithm, yeah, Brent's is good as well. I also had some thoughts about leaving clues in the transformed tree to restore it (storing information by putting subtree in left or right child; or using self-loops), but did not investigate any further. If you have any success let me know ;) – Frigo Dec 06 '11 at 21:21
0

As said by Karl by definition a "Tree" is free of cycles. But still i get the spirit in which the question is asked. Why do you need fancy algorithms for detecting cycle in any graph. You can simply run a BFS or DFS and if you visit a node which is visited already it implies a cycle. This will run in O(n) time but the space complexity is also O(n), dont know if this can be reduced.

FUD
  • 5,114
  • 7
  • 39
  • 61
  • I think you're missing the point of the question. The goal isn't to detect cycles in an arbitrary graph; just those where each node has valence at most two. Also, I'm specifically interested in algorithms that don't use O(n) space even though I know they exist. – templatetypedef Aug 21 '11 at 18:49
  • As the author mentioned, this question is similar to finding cycle in a Linked List and though a Node is said to have Left and Right children, each pointing to a Node not visitable by any other path, violation means, a structure like a family tree. – Shamim Hafiz - MSFT Aug 21 '11 at 18:53
  • Look if a tree is binary it isnt very special compared to a say 1-ary tree( i.e. a list ) or a 3-ary tree or etc... So my point is i fear if you will find an algorithm which is highly optimized for binary tree. As this is a generalized polynomial problem not a np problem where you can expect efficeint algorithms for small cases like ( set cover problem for set size two ). Anyways we will look for efficient answrs. – FUD Aug 21 '11 at 18:55
  • if the tree is balanced, it will be O(logn) [with DFS]. BFS will not do the trick, you if there is an edge to a sibling [no circle], the algorithm yield the wrong answer. – amit Aug 21 '11 at 19:09
  • @amit: No, it's O(n). You have to check the whole tree. – hammar Aug 21 '11 at 19:15
  • @hammar: O(logn) space, not time. you save only the route to this vertex, which of size O(logn) in balanced trees. – amit Aug 21 '11 at 19:16
  • @hammar, reading the OP's comment regarding undirected cycles [and not directed], I revert my comment, you will have to save every vertex. – amit Aug 21 '11 at 19:19
  • @amit: Ah I see. Well, that would work for finding cycles, but it would not detect if the graph is a DAG. – hammar Aug 21 '11 at 19:20
  • @amit please refer here http://en.wikipedia.org/wiki/Depth-first_search for runtime of an DFS, And BFS is nothing but simulation of DFS without recursion which will work just fine too. – FUD Aug 21 '11 at 19:21
  • @ChingPing: BFS is NOT simulation of DFS without recursion, it (BFS) uses a **queue** while DFS uses a **stack** [the call stack]. if the OP wanted directed cycle, BFS might return wrong answer, when there is an edge between 2 sibling vertices. – amit Aug 21 '11 at 19:23
0

As mentioned by ChingPing, a simple DFS should do the trick. You would need to mark each node as visited(need to do some mapping from Reference of Node to Integer) and if an reentry is attempted on an already visited Node, that means there is a cycle.

This is O(n) in memory though.

Shamim Hafiz - MSFT
  • 21,454
  • 43
  • 116
  • 176
0

At first glance you can see that this problem would be solved by a non-deterministic application of Floyd's algorithm. So what happens if we apply Floyd's in a split-and-branch way?

Well we can use Floyd's from the base node, and then add an additional Floyd's at each branch. So for each terminal path we have an instance of Floyd's algorithm which ends there. And if a cycle ever arises, there is a turtle which MUST have a corresponding hare chasing it. So the algorithm finishes. (and as a side effect, each terminal node is only reached by one hare/turtle pair so there are O(n) visits and thus O(n) time. (store the nodes which have been branched from, this doesn't increase the order of magnitude of memory and prevents memory blowouts in the case of cycles) Furthermore, doing this ensures the memory footprint is the same as the number of terminal nodes. The number of terminal nodes is O(log n) but O(n) in worst case.

TL;DR: apply Floyd's and branch at each time you have a choice to make:
time: O(n)
space: O(log n)

Jean-Bernard Pellerin
  • 12,556
  • 10
  • 57
  • 79
  • This is a cool idea, but I'm not convinced that it runs in O(n) time or uses O(lg n) space. If you need to branch the algorithm every time that you encounter a choice, for each independent Floyd's algorithm to work correctly, wouldn't you need to store which choices you made at each branch so that the tortoise and hare are walking on the same path through the tree? Similarly, if you may end up running the algorithm in parallel for each choice point, how can you guarantee that it runs in O(n) time, when for each of the (possibly) O(n) paths of length O(lg n) you need O(lg n) time each? – templatetypedef Aug 21 '11 at 20:28
  • the number of terminal nodes for a perfect binary tree is `n/2`. also, if you 'split' from each internal node, you have n/2 of those, which results in O(n) space [at least O(n) pointers], assuming I understood the suggested algorithm correctly. – amit Aug 22 '11 at 06:48
  • You're right, I went too fast and assumed log n. This method doesn't work. – Jean-Bernard Pellerin Aug 22 '11 at 14:21
  • This will not work for a tree that a lower-level node points to a higher level. The higher node will be in the tortoise and hare sets, but there is no cycle. http://i.imgur.com/EYVUk1M.png – cdbitesky Jun 09 '13 at 21:37
0

I don't believe there exists an algorithm for walking a tree with less than O(N) space. And, for a (purported) binary tree, it takes no more space/time (in "order" terms) to detect cycles than it does to walk the tree. I believe that DFS will walk a tree in O(N) time, so probably O(N) is the limit in both measures.

Hot Licks
  • 47,103
  • 17
  • 93
  • 151
  • You say "I don't believe there exists an algorithm for walking a tree with less than O(N) space." I don't believe it either... but I'd like to see a proof. Prior to seeing Floyd's algorithm, I might have also said "I don't believe there exists an algorithm for walking a linked list with less than O(N) space". And yet, there it is. – Don Hatch Feb 04 '19 at 22:57
  • @DonHatch - To detect a cycle you need at least one bit per node, to store the "previously visited" status. – Hot Licks Feb 26 '19 at 23:23
  • you say 'To detect a cycle you need at least one bit per node, to store the "previously visited" status.' Yes, this seems obvious, but, as I said above, it also seems obvious for a linked list, and yet it isn't true for a linked list: see Floyd's algorithm. If you still think it's true for trees anyway, please explain the crucial difference between a linked list and a tree that makes the storing-of-one-bit-per-node necessary for a tree but not for a linked list. – Don Hatch Feb 27 '19 at 08:55
0

Okay after further thought I believe I found a way, provided you

  • know the number of nodes in advance
  • can make modifications to the binary tree

The basic idea is to traverse the tree with Morris inorder tree traversal, and count the number of visited nodes, both in the visiting phase and individual predecessor finding phases. If any of these exceeds the number of nodes, you definitely have a cycle. If you don't have a cycle, then it is equivalent to the plain Morris traversal, and your binary tree will be restored.

I'm not sure if it is possible without knowing the number of nodes in advance though. Will think about it more.

Frigo
  • 1,709
  • 1
  • 14
  • 32