Detailed traversal algorithm for binary trees and graphs

Before formally introducing the contents of this article, review the classification of data structures to better understand the traversal of trees and graphs.

Data structures are logically linear and can be classified into linear and non-linear tables.

Linear tables refer to data that conforms to one state after another during storage. Drawing it on paper is like a segment with a linear relationship.Pure linear structures include arrays and chained lists, on which stacks and queues are defined, but they remain essentially linear.

The non-linear table structure mainly includes trees and graphs. The tree structure consists of several nodes hanging under one node and exists in the form of up-down node relationship between parent and child. In addition to parent-child relationship, there is generally no relationship between sibling nodes.The graph can be thought of as a tree-based one in which peer nodes are related, even between any two nodes, regardless of their proximity, so it is more like a network structure with close relationships.In addition, the tree structure rests on the nodes, which contain all the data such as data and parent/child pointers, and the edge information is very important in graph structure besides nodes.

In addition to these two large classifications, there are other derived structures, such as a heap structure that can be viewed as a binary tree implemented by an array or a list of chains, a set that can be viewed as a tree containing a collection of nodes, and so on.

Thus, a linear table can be viewed as a one-to-one relationship, a tree as a one-to-many relationship, and a graph as the most complex one-to-many relationship.The reason why trees and graphs are explained together is that they all have one-to-many characteristics, so to some extent, the traversal process can be carried out in the form of width and depth preference.

That's the end of the pre-knowledge, so let's formally begin our protagonist: the traversal algorithm for trees and graphs.

1. Traversal algorithm for binary trees

1. Binary Tree Depth-first Traversal

In binary trees, the so-called depth-first traversal refers to the familiar sequential, middle-order and post-order traversal, which starts from the root node and pricks down to the depth of the tree to do traversal operations. The implementation of these traversals is essentially the result of recursive calls, so these traversals can be achieved by printing at different times in recursive order.
First, the node structure of the binary tree:

	// binary tree node structure
	public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }

(1) Recursive order

Recursive order, as the name implies, is that all nodes in a tree are recorded if they are traversed during a recursive process, and the final recursive process ends the sequence presented by the traversal of each node. This sequence can show the trace of the entire recursive process, that is, through recursive order,You can see how the recursive process is done step by step between nodes.In recursive order, each node occurs three times, corresponding to the following code:
1 Location: The recursive function has just started traversing the tree. If the node currently passing into the tree is the head node (not empty), then the recursive process traverses it for the first time.
Position 2: After the left child of the head node is called, the recursion returns to the current head node.
Location 3: Once the right child of the head node is invoked, the recursive process returns to the current node again.
After three encounters, the recursive process fully traverses the head node.

	// recursive process
	public static void f(Node head) {
        if (head == null) {
            return;
        }
        // 1
        f(head.left);
        // 2
        f(head.right);
        // 3
    }

Here, the process is recursive, not print.If you print 1, 2, 3 places, you can see the traversal results of the entire recursive order explicitly.
Examples:

(2) Sequential traversal

Pre-order traversal is based on recursive order, and when it comes to the position of the current node as the first node, it does the printing operation. The final result is: head-left-right.

	// pre-order recursive process
	public static void pre(Node head) {
        if (head == null) {
            return;
        }
        // Print the first time you traverse to the current node
        System.out.println(head.value);
        pre(head.left);
        pre(head.right);
    }

We know that for any recursive behavior, non-recursive operations can be used instead, so here's also the non-recursive version of the precedence traversal below for reference:

	// pre-order non-recursive process
	public static void pre2(Node head) {
        if(head != null){
        	// If the incoming node is not empty, create a stack to stack the header node
        	Stack<Node> stack = new Stack<>();
        	stack.push(head);
        	// As long as the stack is not empty, it will always bounce, print, stack
        	while(!stack.isEmpty){
        		// Pop current top stack element out
				Node cur = stack.pop();
				// Print on pop-up
				System.out.print(cur.value + " ");
				// Stack the right and left children of the current pop-up node in turn
				// This is because the stack will bounce in reverse order, so to ensure that the pop-up order is left-right, the stack order must be right-left
				if(cur.right != null){
					stack.push(cur.right);
				}
				if(cur.left != null){
					stack.push(cur.left);
				}
			}
        }
        System.out.println();
    }

(3) Intermediate traversal

On the basis of recursive order, the middle-order traversal does not print until the left child of the current node has been invoked. The final result is left-head-right.

	// in-order recursive process
	public static void in(Node head) {
        if (head == null) {
            return;
        }
        in(head.left);
        // Second traverse to current node to print
        System.out.println(head.value);
        in(head.right);
    }

Non-recursive version implementation:

	// in-order non-recursive process
	public static void in2(Node head) {
        if(head != null){
        	// If the incoming node is not empty, create a stack to stack the header node
        	Stack<Node> stack = new Stack<>();
        	stack.push(head);
        	// Continue the loop as long as the current node has a left child or the stack is not empty
        	while(head.left != null || !stack.isEmpty){
        		// If the left child of the current node exists, first stack the left child, then head to the left child, and then re-enter the outer loop
        		// That is, if the head node has many left children, the process will always push all left children in
				if(head.left != null){
					stack.push(head.left);
					head = head.left;
				}else{
					//If you enter the current condition, it means that there is no left child for the current node head er, pop up the top element of the stack and print it, then see if cur has a right child
					Node cur = stack.pop();
					// Print on pop-up
					System.out.print(cur.value + " ");
					// If the cur node has a right child, let the head point to the right child, and the head will re-enter the outer loop to see if the right child it refers to has a left child
					// If so, do the same process of pressing the left child out of the print and determining if there is a right child
					head = cur.right;
				}
			}
        }
        System.out.println();
    }

(4) Post-order traversal

Post-order traversal is based on recursive order. It does not print until the left child and post-child of the current node have been invoked. The final result is left-right-head.

	// pos-order recursive process
	public static void pos(Node head) {
        if (head == null) {
            return;
        }
        pos(head.left);
        pos(head.right);
        // Third traverse to current node to print
        System.out.println(head.value);
    }

The result of sequential traversal is left-right-head, and we observed that its reverse order is head-right-left, which is very similar to the non-recursive version of sequential traversal, so in the non-recursive version of sequential traversal, you only need to change the order of the stacking process in the non-recursive version by first traversing the left child, then the right child, and you will get the head-right-left traversal result.Then push the result into a new stack and pop it out, you will get the left-right-head result of the post-order traversal.
Non-recursive version implementation:

	// pos-order non-recursive process
	public static void pos2(Node head) {
        if(head != null){
        	// If the incoming node is not empty, create a stack to stack the header node
        	Stack<Node> stack = new Stack<>();
        	stack.push(head);
        	// Loop on as long as the stack is not empty
        	while(!stack.isEmpty){
        		// Pop current top stack element out
				Node cur = stack.pop();
				// Instead of printing, the pop-up will press the result into a new stack, res, and then print the elements in the res in turn after the entire process is finished.
				Stack<Node> res = new Stack<>();
				res.push(cur);
				// Stack the left and right children of the current pop-up node in turn
				// Here we change the stacking order in the sequence traversal, first pressing the left child, then the right child, because you can get the pop-up order right-left
				if(cur.left != null){
					stack.push(cur.left);
				}
				if(cur.right != null){
					stack.push(cur.right);
				}
			}
        }
        // The code runs here, indicating that the results have been put into res in the order of head-right-left, and then the elements in res are printed as pop-ups
        while(!res.isEmpty()){
        	System.out.print(res.pop().value + " ");
        }
        System.out.println();
    }

2. Binary Tree Width First Traversal

The so-called width-first traversal of a binary tree refers to the hierarchical traversal process of a binary tree.Unlike the stacking operations of the previous depth-first traversal methods, hierarchical traversal is a layer-by-layer scan that prints a node as long as it is scanned first, so for hierarchies, the structure of the queue is usually used.

// level traversal binary tree
public static void level(Node head){
	if(head == null){
		return;
	}
	Queue<Node> queue = new Queue<>(); // Create a queue
	queue.add(head); // Enter incoming head nodes into the team
	while(!queue.isEmpty){ // Pop up a node as long as the queue is not empty
		Node cur = queue.poll(); 
		System.out.print(cur.value + " "); // Print out of line
		if(cur.left != null){
			queue.add(cur.left); // If left child is not empty, let left child join the team
		}
		if(cur.right != null){
			queue.add(cur.right); // If the right child is not empty, let the right child join the team
		}
	}
	System.out.println();
}

As you can see, the process code for sequential traversal is very similar to that for sequential traversal, which prints when a node is ejected, as well as pushing the left and right children in turn.The difference is that the stack structure is used to traverse first, and because the stack comes in and out, the right child is pressed first, then the left child is pressed.Sequence traversal uses a queue structure, so it presses the left child first, then the right child.

2. Traversal algorithm of graph

Before traversing a graph, you first need to understand the overall structure of a graph, which is created here Omnipotent graph structure The traversal process described below is based on this diagram structure.

The Node class Node related structure is also put here:

// Node class
public class Node {
    public int value; // Value of node
    public int in; // Initiation
    public int out; // Outbound
    public ArrayList<Node> nexts; // Store all your immediate neighbors
    public ArrayList<Edge> edges; // Store all edges from this node down

    public Node(int value) {
        this.value = value;
        in = 0;
        out = 0;
        nexts = new ArrayList<>();
        edges = new ArrayList<>();
    }
}

1. Depth-first traversal of Graphs

Depth-first traversal of a graph is often referred to as Depth First Search, or DFS.Since the graph has multiple connectivity paths, the depth-first traversal of the graph is specified as follows: Go along a path of the root node until there are no more nodes beneath it or until there are duplicate nodes, and then go back to see if the previous node of the current node has another path, and if so, go through the path to the end.If not, go back to the previous node to see it.The traversal process does not stop until all the paths of the nodes below the root node have been traversed once.For depth-first traversal, stacks are usually used. To prevent duplicate traversal, a set registry is also needed. Only unregistered nodes can be on the stack during traversal. If the node already exists in the table, it will not be processed.The specific implementation is as follows:

// DFS
public void dfs(Node start){
	if(start == null){
		return;
	}
	Stack<Node> stack = new Stack<>(); // Depth-first traversal using stacks
	Set<Node> set=new HashSet<>(); // Use a set table to indicate whether the current node has been registered
	stack.push(start);
	set.push(start); // When a new node is pushed into the stack, it is synchronously pushed into the registry
	System.out.println(start.value); // Print as you press
	while(!stack.isEmpty){
		Node cur = stack.pop();
		for(Node next : cur.nexts){ // next nodes traversing the currently ejected node
			// If these next nodes are not registered, execute the following program
			// If registered, do not execute
			if(!set.contains(next)){ 
				stack.push(cur); // Push the pop-up node back
				stack.push(next); // Stack next Nodes
				set.push(next); // Register the next node
				System.out.println(next.value); // Register (press in) print
				//Here, as long as one next node stacks that other next node, it no longer traverses back to the top of the loop to pop up the node
				break;
			}
		}
	}
}

It is important to note that in DFS, a new node is printed as soon as it is pushed in, and its parent node needs to be pushed back into the stack before it is pushed in, which is equivalent to keeping the current traversal path in the stack at all times.In addition, traversing one of the immediate neighbors of the current node stops immediately after it is stacked, and then pops up the currently pressed node to see if it can find a deeper node along with it. This process is characteristic of deep-first traversal.

2. Width-first traversal of the graph

The width-first traversal algorithm of a graph is also called the breadth-first search algorithm, or BFS for short.Width-first traversal is the process of traversal in which all the immediate neighbors of a node are traversed as long as they have immediate neighbors, and then the direct neighbors of those immediate neighbors are traversed in turn, similar to the hierarchical traversal of a binary tree.When traversing width first, a queue structure is usually needed to ensure that the first traversed pop-up prints are available, and, like depth first traversal, a registry set is still needed to ensure that there are no duplicate traversals.The specific process is as follows:

// BFS
public void bfs(Node start){
	if(start == null){
		return;
	}
	Queue<Node> queue = new Queue<>(); // Prepare a queue
	HashSet<Node> set = new HashSet<>(); // Prepare a registry
	queue.add(start);
	set.add(start);
	while(!queue.isEmpty){
		Node cur = queue.poll(); // Bounce out as long as the queue is not empty
		System.out.println(cur.value); // Print on pop-up
		for(Node next : cur.nexts){ // Traverse the immediate neighbors of the current pop-up node
			if(!set.contains(next)){ // If a direct neighbor is not registered
				queue.add(next); // Queue your neighbors
				set.add(next); // Register in set after queuing
			}
		}
	}
}

Compared with depth-first traversal of a graph, width-first traversal also relies on a registry to prevent duplicate traversal, which is caused by the same attributes that connect the nodes of the graph structure itself.The difference is that first the width preference is not a stack structure, but a queue. Second, it is not just a direct neighbor traversing the current node that stops and then goes down to find a deeper node.Conversely, width takes precedence over finding all the immediate neighbors of the current node before moving down.

In addition, compared to the binary tree hierarchical traversal, the width preference of the graph basically corresponds to the tree hierarchy, which is achieved by queues, and the same level of nodes are processed before the next level of nodes are processed.The difference between the two is that the binary tree has only two forks, and the nodes at the same level do not form a connection relationship. The graph is equivalent to a multifork tree and the nodes at the same level or even at any level can connect with each other, so the tree only needs to focus on the left and right children, while the graph needs to focus on all the immediate neighbors.In addition, in order to traverse nodes without duplication, the graph also needs a registry to filter duplicate nodes.

Tags: Algorithm data structure Binary tree

Posted on Thu, 02 Sep 2021 02:15:39 -0400 by somethingorothe