Tree traversal without recursion: the tree as a state machine (2007)
plasmasturm.org
plasmasturm.org
Anyways, I did some digging into TAOCP. In section 2.3.1 Knuth mentions threaded binary trees[1]. They are trees that you can traverse without a stack and do not use more memory than a regular binary tree. This is compared to the method used in the post which requires you to store the parent pointers. The trick is that in a binary tree, lots of pointers are going to be null. Instead of storing null there, you can store a special value that provides information on how to traverse the tree. Indicating the difference between a regular pointer and one of these special values requires only a single bit. You can often store that bit within the pointer itself, requiring no additional space over a regular binary tree.
For a threaded binary tree, if the left child is null, you instead store the predecessor node. If the right child is null, you store the successor node. Finding the successor now becomes:
1) Check if the right child is null. If so the right child pointer points to the successor. We are done.
2) Follow the right child pointer. Then keep following the left child pointer until the left child is null. This gives us the smallest value greater than the current value.
One small advantage of threaded binary trees is you traverse the tree slightly less. If the right child is null, instead of traversing back up the tree as in the post, you can jump straight to the successor node.Yep, the lower 1-3 bits in many pointers will always be zero due to how C run-time allocators and structure alignment works out of the box.
―
¹ I think, but to be honest it's been some time since I really knew the arcane C rules and I'm not confident anymore
http://www.folklore.org/StoryView.py?project=Macintosh&story...
Sadly the original Mozilla link is broken. https://wingolog.org/archives/2011/05/18/value-representatio...
Alternatively you can follow what java did with compressed ops and use 4bytes per pointer on 64bit archs (instead of 8) and shift the pointers prior to dereference - effectively enabling 32GB heaps with 4bytes pointers.
Consider a typical binary-tree with a 64-bit integer as its value. You use 24-bytes per node (value, left child, right child) if you use 64-bit pointers, but only use 16-bytes per node if you use 32-bit pointers (or array indexes).
Now, a 32-bit pointer can have at most 4billion nodes across the whole tree. But 4billion is more than usable for many programs.
EDIT: Consider that 4billion nodes of size 16 bytes (where each node is the value + left child + right child struct discussed earlier) will take up 64GB of RAM.
EDIT2: And half the time, my brain gets short-circuited and I end up recreating some terrible form of segmented memory before having to slap myself for thinking up such a horrible idea. In any case, a surprising amount of memory is used up as pointers in almost all the code I write. If you care about fitting as much data into L1 cache (64kB on modern machines), you will absolutely want to minimize your data usage.
Im wondering though, this isnt totally free, is it ? I guess there’s an extra addition/subtraction calculation everytime you go from one cell to the next since instead of reading the next/prev pointer you now need to read a value and calculate the pointer from it, am i understanding this correctly ? (Not a computer scientist, so ive never had a formal algorithm class).
Very very very neat nonetheless, thank you very much ;)
The article used parent pointers in node to avoid using a stack (native or custom made). One downside of a parent pointer is that your identical subtrees can no longer share structure.
First, the depth (the amount of storage for traversal) typically is on the order of log2(n) and so this gets amortized very quickly when traversing nodes.
What if the tree is unbalanced?
If the tree is unbalanced you will have all sorts of other problems like problems with inserting or searching. Since trees are typically used for efficient searching and traversal is done more efficiently with just plain array (or list) it is really hard to find any real-world trees that are not kept balanced somehow.
This is super relevant to me right now, thanks!
[actualTreeNodeData1,child1Idx,child2Idx][actualTreeNodeData2,child1Idx,child2Idx][...]
instead of just [actualTreeNodeData1][actualTreeNodeData2][...]
Vary this to include whatever you actually need, could also be links to parents instead of or in addition to links to child nodes.Given we have indices along the serialisation for child nodes I would have thought not having nodes start at the consistent intervals doesn't hurt us for traversal at least.
I'd quite like to use this idea for a file backed immutable tree. The array of children would have variable size with bit field to index then nth child in the sparse array.
I suppose you could (should) write your own allocator to ensure nodes are allocated in a contiguous piece of memory, and have a separate array of indexes / pointers that tells you where the n-th node starts. That is another array that'll have to be kept in cache though.
If you need per-traversal state per node (such as visited status or intermediate dataflow), it's very handy to allocate another flat array for that and use the node indices.
I've looked into this in a compiler setting, transforming ASTs and intermediate representations.
You can actually in-order traverse a binary tree without any memory overhead at all (no parent pointer nor an implicit or explicit stack) using Joe Morris' traversal.
I found that this can be done to be surprising but effectively it uses the NULL slots as temporary storage.
Here's one explanation: https://yuyuan.org/MorrisAlgorithm/ - although there seem to be a few youtube videos on it also.
> ...to erase elements from some point in my model to some other point in my model, what I do is grab those two locations on the string, and I pick the whole thing up, and the beads that fall off are the ones that get erased ... (48:41)
preorder(nil). preorder([Node, FG, FD]) :- format('~w ', [Node]), preorder(FG), preorder(FD).
inorder(nil). inorder([Node, FG, FD]) :-inorder(FG), format('~w ', [Node]), inorder(FD).
postorder(nil). postorder([Node, FG, FD]) :-postorder(FG), postorder(FD),format('~w ', [Node]).
While this sounds innocent, truly consider the ramifications of this decision.
Your node has now grown to be +8 bytes (the size of a 64-bit pointer). If you have 1-million nodes in your binary tree, you are using +8MB of memory.
-------
Lets consider the alternative: lets say you have 1-million nodes, and you have a red-black tree (imperfect balance: instead of a perfectly balanced 20-depth tree you have some branches which are 40-depth).
Traversing to 40-depth requires 40x pointers on the stack, or 320 bytes.
----------
In effect, you're spending 8-bytes per node (which could be millions, or even billions of nodes), to save log2(depth) space off of your stack frame.
While the state-machine is very "clean", it seems like a very bad tradeoff from an algorithmic / memory space point of view. I'd rather spend log2(depth) space on the stack, rather than O(+8 bytes * number nodes) in the heap.
I think there's something to be said about the clarity and cleanliness of the state-machine design. Its quite possible that some algorithms are easier to write with this "parent pointer in node" methodology. However, anyone seeking the highest performance per unit-memory will have to see that the +8 bytes per node is a terrible, terrible tradeoff.
-------------
> Update: Todd Lehman pointed out that given node-level locks, this algorithm allows concurrent traversal and update of the tree. Any atomic operation other than detaching a non-leaf node is safe.
Hmm... the parallel-angle I haven't thought about. But it does seem to be a potential building block as maybe even a lock-free data-structure.
Or just use locks, since locks are easier to think about.
Insertion and deletion into the tree seems possible, but "rebalance" is very difficult for me to think about (since it possibly requires modifying many, many nodes). Locking all nodes involved would be a heavy approach and probably work.
Red-black colors are kept track of for self-balancing purposes. Maybe more colors are needed to make the methodology "clean" for multi-threaded / lock free purposes.
How can it be a state machine if you aren't mutating it?
In fact, you can traverse a tree without recursion (or any equivalent stack-like structure) and without parent pointers. And then you actually use the tree as a state machine: you stash the reverse path pointer in the tree itself by overwriting the downward links temporarily and then putting them back.
This is useful for garbage collection; you can have a garbage collector that is guaranteed never to blow the stack in the marking phase.
You are confusing a state machine (which is an abstract model of computation) with the behavior of a program implementing it. The set of possible states and the conditions of transition from one state to the next never change.
These conditions can depend on mutable state, as is the case described by the author (in this case, mutable state is kept in the current and previous node pointers).
https://i.imgur.com/YM3dIcG.png
Surely that's not considered a great design?
EDIT: ok, was fault of a chrome extension "NightModePro" :)
Generally, trees are poor for performance as you zig-zag around memory and stall on cache misses. But in situations where you have to do tree traversal and you have to be as fast as possible, trading the memory for the parent pointer (which probably fits within the same cache line as the rest of the node anyhow, so in practice basically free) vs recursion or an explicit stack which stress memory in the tight loop, it is probably a winner! Definitely worth profiling in those situations.
Assume you have 64GB of memory devoted to binary tree (of just integers), assuming the tree is balanced.
Your node is two pointers + integer (24 bytes). That gives 2666666667 nodes. Balanced tree will have 32 levels (log2(2666666667) ~= 31). When doing recursion you just need to keep pointer to the level, so this is 32 times pointer or just about 256 bytes of your cache.
Also, even if we were storing data in memory (and not in cache) this still is orders of magnitude less effort than accessing the memory for the large data structure.
There are methods for optimising the use of various tiers of cache, trying to pack multiple nodes into one 64 byte line, arranging nodes such that left and right children are usually adjacent in memory so prediction losses are minimised, and other tricks.
What language do you use where a recursive function invocation only requires a call frame large enough to hold a single parameter? By what magic does the function know where to return? And what are you doing in that function that doesn't require any temporaries whatsoever?
At best you're looking at a couple of kilobytes. For all but the most constrained environments (toaster, a 4Kb kernel thread stack, etc), this isn't really worth worrying about. And compared to the pointer chasing, all those pushes and pops aren't much of a problem, either.
However, in a language like C without function closures, non-recursive traversal can keep code much more concise and clear. This is the real win, IMO.
In any event, you always need a stack of some sort. Recursion is one way to accomplish that, but saying that you're simulating recursion by explicitly building a stack reverses the categories.
I love the discussion that it sparked here too. It's wonderful and educational to see so many smart people breaking down the problem space, discussing de/merits of this particular technique, presenting alternative approaches with analysis on memory use, performance..
It does generalize, though, to ordered-tree nodes that aren't represented as cons chains. You just have to walk through the n child pointers when you're moving back up the tree to search for the one you're returning from, adding an extra runtime cost factor proportional to n but no extra memory. Moreover, if you devote three or four registers to an amnesic stack, you can avoid that cost for the bottom two or three levels of the tree, which will necessarily contain the vast majority of tree nodes.
How about a depth-first search?
Data structures are a very specific subject of IT Engineering, every person studying subject should take the whole curriculum, including recursivity scanning data structures and how to transform these recursive procedures into iterative code (or FSM as the article says).
There is no point of comparison in five dedicated years of highly academic study versus 6 months of touching the techs du jour. Will the boot campers have also graph theory background? And compilers and assembly or just webpack preprocessors? Database design or just picking stacks? and compilers and network protocols?
Are you sure you are not comparing a 35 year old responsible person who made a boot camp to sustain his family or an entitled post-grad IT engineer who is still a bit high from the graduation party? It would be hypocrite not to admit that a hungry serious battle tested individual will have the right attitude to build a role rather than the not serious entitled individual who just wants to see what he can get from there and does not make any effort in continuously improve.
Once in a plane I met a CEO who told me he would take any day an engineer for the decision making role or business related roles. Was he crazy or it was just his previous experience conditioning him? you can take whoever you want for your company, even someone who took gender studies and ended up taking a code camp hoping to pay its former debt and ends up making high impact blog posts about thyr experience in the digital IT world as person of XYZ gender.
Well, yes and no.
If you're an 18 year old who wants to program computers and you read Turing and Dijkstra for fun, then go study math or physics, which you won't have much time to do later. Or pick a subject in the humanities if you like to read and write. Is it a great way to become a programmer? Maybe not, it would be better to just go and get an internship or a job, but (1) smart companies are hard to find and (2) you'd be missing out on a chance to get a university education at the age when most people do so.
On the other hand, if you're picking coworkers, go with the self-motivated one who had to learn everything they learned because they were interested in it, over the one who had to learn it to get a degree in a field that everyone knows is a ticket to a high-paying job.
> Once in a plane I met a CEO who told me he would take any day an engineer for the decision making role or business related roles. Was he crazy
Sure, this is the same point: if the only thing you're good at is your specialization, then you're probably not going to be very good at it. Unless it's something like chess that really has minimal relation with anything else in society.
And yeah, signalling. Because starting at a job now is hard, getting fired is risky. The English major has the maturity, the goal and the life skills. He already has less risk with his degree. The bootcamp kid? Not so much. The bootcamp history major? Good!
The English major (assuming a good liberal education) is well-placed to understand his or her role as an engineer. He or she probably well knows how much there is to learn!
Of course, as you point out, a "large number" won't apply themselves, and won't become engineers, but they'll still have a well-rounded education. So if I was advising a smart youngster on how to get a job writing software in 6-8 years, I'd advise them to study English or Math or Physics or History before computer science. Pick a general concentration in the humanities or sciences that interests you and learn everything you can about the world through the lens of that field. Then go and pick up some programming languages, do an online masters program in the hot tech du jour, and write some code and get an internship or a job. Whatever you do, don't waste your chance at a university education studying computer science if that's what you want to do. The curriculum will bore you, it will move too slowly in all the wrong directions, and you'll pick up a bunch of obsolete preconceptions that you'll be poorly equipped to discard later.
Of course this has implications for what CS programs should be doing and mostly aren't, but that's neither here nor there.
Universities are win-win-win for people, bosses and government. Especially for the majority, the average students who can't retrain or self-study as easily. And the minorities, parents without degrees, middle and lower class or even outside the family business.
Aside, I was that smart youngster, I chose math, I wouldn't gave graduated. English would have been much worse. I was bored but at least not burned out. And I have used every course practically, 6 years of education and almost nothing that can be reliably scrapped to create a bootcamp.
My question is why do we think it's better to put all the learning for a particular career at the front of your life when you have no experience, and don't even know why you're going to need to know any of what you're learning? It's wasteful to learn something you think you're going to use and then find yourself in a career you prepared for and don't like. The average 18 year old doesn't know much about adult life (because we insulate them from it more and more) and has no idea what a professional in most fields actually does every day, so why do we expect them to pick an occupation before having any experience in it and then commit themselves to it for an intensive course of study?
I'm not arguing against university education, if it is truly general. But if it is just a passport to a particular career path, which is increasingly the case, then (a) it's too early to pick a career, and (b) if you want a career, and know what you want, it makes more sense to start working first and then learn what you need according to the demands of the work. That's why I'm in favor of continuing education and apprenticeships, and in general having more diverse life paths rather than trying to fit everyone to the same pattern, which is what seems to be happening now with college being expected for everyone.
Obviously there are careers like medicine that are going to require additional intensive training, and that needs to be front-loaded for obvious reasons. However, most people are working longer and changing careers more than in the past, and the ability to learn as you go should be more valuable to society than expecting that everyone get a university education while at the same time turning universities into trade schools.
I'm proud of my university education, but it decidedly did NOT cover the method discussed in this blog post. (Nor other popular subjects, such as Robin Hood hashing. My professor did cover skip lists and cuckoo hashing however)
There's so much to learn about trees out there, that I doubt any university class can really cover them all (even with 3-months per semester and maybe 3 or 4 different classes discussing the subject).
Even IF you did go to a university, there's no guarantee you've learned everything. And even if you were taught it at a university, there's no guarantee you'd actually remember it all. I'm sure I've forgotten plenty of things from my classes (or in the case of "Dynamic programming", I never really learned it the first time around...)
I think it came about because a couple of us were determined to prove trees could be used without recursion, as at the time it was a daunting subject for most of us and the teacher wasn't very good at explaining it (how does it stop??/etc). The idea is based on applying a doubly-linked list - something that was part of the class work - to trees.