二叉树是最复杂的数据结构之一。二叉树之所以如此复杂,原因之一是很难用简单的方式将其形象化。

在本教程中,我将向您展示如何创建自己的 BinaryTreeViewer,以便您在运行时查看树。

您可以在此处查看该项目的源代码: https://github.com/giladbarilan/binary-tree-viewer

什么是二叉树?

二叉树是一种非常常用的基于节点的数据结构。树的每个节点由三个元素组成:

  • 节点的值,
  • 对左孩子的引用(如果没有左孩子,则可以为空),
  • 对右孩子的引用(如果没有右孩子,则可以为空)。

例如,假设我们有一个节点,其值为 1,其左侧子节点的值为 3,其右侧子节点的值为 2。下面是我们绘制树的图的方式:

image-53

一个节点最多可以有两个子节点,但也可以有一个或没有子节点。当我们想要遍历二叉树中的元素时,我们通常使用递归方法(下面显示了一个示例)。

现在我们知道了二叉树数据结构的工作原理,让我们学习如何在 C# 中实现二叉树结构。

namespace BinaryTreeViewer
{
    /// 
    /// Represents a Binary Tree class used for the BinaryTreeViewer.
    /// 
    /// The tree node's type.
    public partial class BinaryTree
    {
        private BinaryTree? rightNode; // right node of the binary tree.
        private BinaryTree? leftNode; // left node of the binary tree.
        public T value { get; set; } // the value of the current node.

        public BinaryTree(T value)
        {
            this.value = value;
            this.rightNode = null;
            this.leftNode = null;
        }

        public BinaryTree(T value, BinaryTree? left, BinaryTree? right) : this(value)
        {
            this.rightNode = right;
            this.leftNode = left;
        }

        public void SetLeftNode(BinaryTree node)
        {
            this.leftNode = node;
        }

        public void SetRightNode(BinaryTree node)
        {
            this.rightNode = node;
        }

        public BinaryTree? GetRightNode() => this.rightNode;
        public BinaryTree? GetLeftNode() => this.leftNode;

        public override string? ToString() => this.value?.ToString();
    }
}
binary_tree_implementation.cs

在上面的代码中,我们用我们讨论过的 3 个元素构建了二叉树结构:值、右孩子和左孩子。问号表示它们是可空的。

让我们做一个简单的例子,演示如何打印二叉树上的所有元素。

//Builds the tree.

BinaryTree tree2 = new BinaryTree(1);
tree2.SetRightNode(new BinaryTree(2));
tree2.GetRightNode().SetLeftNode(new BinaryTree(4));
tree2.SetLeftNode(new BinaryTree(3));
tree2.GetLeftNode().SetRightNode(new BinaryTree(5));
PrintTree(tree2);

//Prints out the tree
public static void PrintTree(BinaryTree tree_)
{
    if (tree_.GetLeftNode() != null) //if he has a child from his left.
     PrintTree(tree_.GetLeftNode()); //go to his left child family.

    if (tree_.GetRightNode() != null) //if he has a child from his right.
     PrintTree(tree_.GetRightNode()); //go to his right child family.

    Console.WriteLine(tree_.value); //print the current value.
}

基本二叉树算法

在我们开始实施之前,最好先了解一下算法。

让我们回到树的简单例子:

image-36

从这个小例子中,我们实际上可以了解如何正确构建树的基础知识。

首先,我们无法预测需要给父节点多少偏移量(以便有足够的空间来绘制树的最左边的节点)。因此,我们需要先找到树的最左边的节点。

一旦我们找到它,我们就会明白我们需要与树的父级有多少偏移。

在这个例子中,我们不能先写父节点,因为我们不知道从左边开始会有多少个节点。如果我们在写节点 1 时在 x 轴上没有任何偏移,那么绘制节点 2 可能会有问题。

二叉树算法的问题

当我们绘制树时,我们与父节点之间的距离是恒定的。在上面的例子中,父节点与节点 3 之间的距离等于父节点与节点 2 之间的距离。因此,该算法可能会遇到菱形图等问题。

什么是钻石图?

image-52

假设父节点有左子节点(节点 2)和右子节点(节点 3)。右子节点(节点 3)有左子节点(节点 4),左子节点(节点 2)有右子节点(节点 5)。

在这种情况下,我们将与子节点(节点 4 和节点 5)发生碰撞,因为它们将被放置在相同的位置。

目前,解决这个问题主要有两种方法:

  • 进行预先构建的计算,将树绘制为非对称树,而不必与父级保持恒定的距离。
  • 将来自左侧的节点涂成一种颜色,将来自右侧的节点涂成不同的颜色。

第一种实现方式的问题在于,我们使用 BinaryTreeViewer 是为了节省时间。即使使用这种实现方式比使用颜色更整洁、更美观,程序也会太慢,并且会损害我们代码的性能。

因此在本教程中,我们将坚持采用第二种方法来解决菱形图问题(着色)。

image-40
How the coloring algorithm output looks like.

如何实现算法

现在我们已经讨论了二叉树是什么、它们的问题是什么以及我们将使用什么算法来解决这些问题——现在是时候实际实现该算法了。

笔记 : a 部分类 is a class that can be written in separated files, and will be combined on compilation.

让我们从最简单的实现开始,即 BinaryTree 类。

namespace BinaryTreeViewer
{
    /// 
    /// Represents a Binary Tree class used for the BinaryTreeViewer.
    /// 
    /// The tree node's type.
    public partial class BinaryTree
    {
        private BinaryTree? rightNode; // right node of the binary tree.
        private BinaryTree? leftNode; // left node of the binary tree.
        public T value { get; set; } // the value of the current node.

        public BinaryTree(T value)
        {
            this.value = value;
            this.rightNode = null;
            this.leftNode = null;
        }

        public BinaryTree(T value, BinaryTree? left, BinaryTree? right) : this(value)
        {
            this.rightNode = right;
            this.leftNode = left;
        }

        public void SetLeftNode(BinaryTree node)
        {
            this.leftNode = node;
        }

        public void SetRightNode(BinaryTree node)
        {
            this.rightNode = node;
        }

        public BinaryTree? GetRightNode() => this.rightNode;
        public BinaryTree? GetLeftNode() => this.leftNode;

        public override string? ToString() => this.value?.ToString();
    }
}

在部分类的其他部分,我们还有一些方法可用于打印树。

public partial class BinaryTree
    {
        private BinaryTree? max_left_node;

        /// 
        /// Finds the max left offset from the starting node.
        /// 
        /// 
        /// The beginning of the tree we want to draw.
        /// 
        /// 
        /// 

        internal (BinaryTree?, int max_offset) GetMaxLeft()
        {
            int max_offset = 0;

            GetMaxLeft(this, 0, ref max_offset);
            return (max_left_node, max_offset);
        }

        private void GetMaxLeft(BinaryTree head, int left_offset, ref int max_offset)
        {
            if (head.GetLeftNode() != null)
            {
                left_offset += 1;

                if(left_offset > max_offset)
                {
                    max_left_node = head.GetLeftNode();
                }

                GetMaxLeft(head.GetLeftNode(), left_offset, ref max_offset);
            }
            if (head.GetRightNode() != null)
            {
                left_offset -= 1;
                GetMaxLeft(head.GetRightNode(), left_offset, ref max_offset);
            }

            if(left_offset > max_offset)
            {
                max_offset = left_offset;
            }
        }
    }

获取最大剩余值

GetMaxLeft 应该为我们提供实现所需的两个细节:

  • 哪个节点是树的最左边的节点,以及
  • 它与父节点的左偏移量是多少(稍后将乘以一个常数值)。
image-38
Return Value Visualization

我们将两者作为一个元组返回。

GetMaxLeft 的工作原理

GetMaxLeft 返回树的最左节点相对于父节点的偏移量。它是如何工作的?

每次移动到右侧节点时,我们都会减少左侧偏移值(因为我们正在远离文档的左侧)。每次移动到左侧节点时,我们都会增加偏移值(因为我们正在靠近文档的左侧)。

我们保留从已到达的头部开始的最长偏移量,并通过引用 \' max_offset \' 参数来设置它的值,该参数在非递归 GetMaxLeft 方法中返回。

BTViewer 类

现在我们已经了解了基础知识,是时候面对我们要解决的问题了,那就是如何绘制树。在 BTViewer 类中,我们管理所有树构建过程和临时文件。

using System.Diagnostics;
using System.Text.RegularExpressions;

namespace BinaryTreeViewer
{
    /// 
    /// Shows in an HTML document a graph of the tree.
    /// RECOMMENDATION: Use break-point on the line of the BinaryTreeViewer.View .
    /// 
    public static class BTViewer
    {
        private static int StartingTempCount = 1; //the starting temp count so we know how many
        //trees we've created.
        private static int tempCount = 1; // the number of temporary files we've created.
        private static readonly string BINTREE_CSS_FILENAME = "BINTREEINITIALIZER.css";
        private static string fileName => $"BINTREE{tempCount}.html"; //name structure of BINTREE files.

        /// 
        /// Sets the value of tempCount according to the previous saved_trees.
        /// 
        static BTViewer()
        {
            string directory = Directory.GetCurrentDirectory();

            if(!Directory.GetFiles(directory).Contains(BINTREE_CSS_FILENAME))
            {
                File.WriteAllText(BINTREE_CSS_FILENAME, @"#circle{
		border-radius: 50%;
		display: inline-block;
		border: 1px solid black;
	}
	.a{
		padding: 50px;
	}
	.b{
		width: 70px;
		height: 70px;
	}
	 .line{
width: 150px;
height: 150px;
border-bottom: 1px solid black;
position: absolute;
}");
            }

            Regex reg = new Regex(@"BINTREE\d+\.html"); //we check what is the latest binary tree file.

            List fileNames = Directory.GetFiles(directory).ToList();
            fileNames = reg.Matches(string.Join(" ", fileNames)).Select(x => x.Value).ToList(); //Get the BINTREE files on the directory.

            if (fileNames.Count > 0)
            {
                //we find the next fileName as -> the latest file name count (BINTREE*Number*) + 1
                tempCount = fileNames.Select(x => int.Parse(new Regex(@"\d+").Match(x).Value)).Max() + 1; //the next tree to draw.
            }
            else
                tempCount = 1;

            StartingTempCount = tempCount;
        }

        /// 
        /// Writes the full tree into a file by the head.
        /// 
        /// 
        /// The starting of the tree.
        public static void View(BinaryTree tree)
        {
            // in case they entered invalid tree.
            if (tree == null)
                return;

            // in case there is only one node on the tree (only the head).
            if(tree.GetRightNode() == null && tree.GetLeftNode() == null)
            {
                InitializeFileStructure(); // we initialize the file structure.
                DrawElement(tree, (0, 0), false);      
                File.AppendAllText(fileName, "");
                RunTree();

                tempCount++;
                return;
            }

            //how much left we take from the beginning (max value). -> max_left_offset
            int max_left_offset = tree.GetMaxLeft(); // the max left node.

            // we start by finding the position of the head of the tree.
            (int x, int y) head_position = (0, 50);
            head_position.x = max_left_offset * (100 + 50); //the size of every circle + offset between circles.

            InitializeFileStructure();
            DrawTree(tree, head_position, false);

            File.AppendAllText(fileName, ""); //finishes the document.

            RunTree();

            tempCount++;
        }

        /// 
        /// Deletes the trees we want to clear.
        /// 
        public static void ClearTrees(TreesToClear treesToClear)
        {
            string directory = Directory.GetCurrentDirectory();
            Regex reg = new Regex(@"BINTREE\d+\.html"); //the structure of a BINTREE runtime file.
            Regex findCount = new Regex(@"\d+");

            List fileNames = Directory.GetFiles(directory).ToList();
            fileNames = reg.Matches(string.Join(" ", fileNames)).Select(x => x.Value).ToList();

            if (((int)treesToClear & 0b1) != 0) //current run.
            {
                fileNames.Where(x => int.Parse(findCount.Match(x).Value) >= StartingTempCount)
                         .ToList().ForEach(x => File.Delete(x));
            }

            if(((int)treesToClear & 0b10) != 0) //other runs.
            {
                fileNames.Where(x => int.Parse(findCount.Match(x).Value) < StartingTempCount)
                         .ToList().ForEach(x => File.Delete(x));
            }
        }

        /// 
        /// Draws the full tree to the file.
        /// 
        /// 
        /// The head of the tree.
        /// The starting position to draw the tree.
        private static void DrawTree(BinaryTree head, (int x, int y) position, bool right)
        {
            DrawElement(head, position, right);

            if(head.GetRightNode() != null)
            {
                DrawLine(position, (position.x + 150, position.y + 150));
                DrawTree(head.GetRightNode(), (position.x + 150, position.y + 150), true);
            }

            if(head.GetLeftNode() != null)
            {
                DrawLine(position, (position.x - 150, position.y + 150));
                DrawTree(head.GetLeftNode(), (position.x - 150, position.y + 150), false);
            }
        }

        /// 
        /// Draws line between the nodes.
        /// 
        /// 
        /// 
        private static void DrawLine((int x, int y) p1, (int x, int y) p2)
        {
            double left;

            if (p2.x < p1.x)
            {
                left = Math.Min(p1.x, p2.x);
            }
            else
            {
                left = p2.x - 75;
            }

            string line = $"\n
"; File.AppendAllText(fileName, line); } /// /// Draws a node. /// /// /// /// private static void DrawElement(BinaryTree node, (double x, double y) position, bool right) { //DIAMOND COLLISION MEANING -> /* ( ) / \ ( ) ( ) \ / ( ) -> collision (two different nodes placed on the same place in the graph because the distance between each node to his father is equal). */ //In a case of diamond collision one node might override the other node on a graph. //Because we want to see both nodes on the graph then we color nodes that //comes from right and nodes that comes from left //with two different colors -> Red & Blue -> so we'll be able to see the differences. string color = "red"; //Red -> left side node, Blue -> right side node. if (right) color = "blue"; File.AppendAllText(fileName, $"\n
"); File.AppendAllText(fileName, $"\n
{node.value}
"); } //Opens the tree. private static void RunTree() { //shows the tree to the user. (opens the HTML file on browser). Process run_process = Process.Start(@"cmd.exe", "/c " + fileName); run_process.WaitForExit(); } /// /// Creating a BINTREE file structre. /// /// private static string InitializeFileStructure() { //The basic content of a BINTREE file. string content = @$""; File.WriteAllText(fileName, content); return fileName; } } } public enum TreesToClear { CurrentRun = 0b1, PreviousRuns = 0b10 }
btviewer.cs

如您所见,BTViewer 类很长,因此我们将其分成几个部分。

2. 如何画树

现在我们将介绍如何打印树。

BTViewer.View 方法

public static void View(BinaryTree tree)
        {
            // in case they entered invalid tree.
            if (tree == null)
                return;

            // in case there is only one node on the tree (only the head).
            if(tree.GetRightNode() == null && tree.GetLeftNode() == null)
            {
                InitializeFileStructure(); // we initialize the file structure.
                DrawElement(tree, (0, 0), false);      
                File.AppendAllText(fileName, "");
                RunTree();

                tempCount++;
                return;
            }

            //how much left we take from the beginning (max value). -> max_left_offset
            int max_left_offset = tree.GetMaxLeft(); // the max left node.

            // we start by finding the position of the head of the tree.
            (int x, int y) head_position = (0, 50);
            head_position.x = max_left_offset * (100 + 50); //the size of every circle + offset between circles.

            InitializeFileStructure();
            DrawTree(tree, head_position, false);

            File.AppendAllText(fileName, ""); //finishes the document.

            RunTree();

            tempCount++;
        }
view.cs

此方法对于用户来说是公开的,他们使用此方法来打印树。

在该方法中,我们处理两种不同的情况:

我们在树中只有一个节点 ——我们只需将节点树写入 HTML 文件,更新下一个文件编号(稍后将解释),打开文件进行预览,然后退出该方法。

树上有多个节点 ——在本例中,我们首先使用之前解释的方法获取树最左侧节点的数据。收集到最左侧节点的数据后,我们就可以计算出树的头部在文档中的位置。

找到树头的起始位置后,我们就可以从头部开始写树了。

我们如何画树?

现在是时候深入研究绘图实现。我们使用的第一个方法是 InitializeFileStructure ,它基本上为我们提供了绘制树木圆圈所需的 CSS 属性。

#circle{
		border-radius: 50%;
		display: inline-block;
		border: 1px solid black;
	}
	.a{
		padding: 50px;
	}
	.b{
		width: 70px;
		height: 70px;
	}
	.line{
        width: 150px;
        height: 150px;
        border-bottom: 1px solid black;
        position: absolute;
	}

故意创建一个 HTML 文件, InitializeFileStructure 因为 </HTML> 我们想稍后添加更多标签来构建树的结构。

DrawTree 方法

private static void DrawTree(BinaryTree head, (int x, int y) position, bool right)
        {
            DrawElement(head, position, right);

            if(head.GetRightNode() != null)
            {
                DrawLine(position, (position.x + 150, position.y + 150));
                DrawTree(head.GetRightNode(), (position.x + 150, position.y + 150), true);
            }

            if(head.GetLeftNode() != null)
            {
                DrawLine(position, (position.x - 150, position.y + 150));
                DrawTree(head.GetLeftNode(), (position.x - 150, position.y + 150), false);
            }
        }
DrawTree.cs

DrawTree 用于打印树的所有线和圆。

  • 每次进入时,我们都使用 DrawElement 方法打印当前元素。
  • 然后我们遍历二叉树上的所有节点,并通过从前一个节点增加/减少一个常数值来设置每个节点的位置。
  • 我们还使用 DrawLine 方法从每个节点到其子节点画一条线。

DrawElement 方法

private static void DrawElement(BinaryTree node, (double x, double y) position, bool right)
        {
            //DIAMOND COLLISION MEANING -> 
            /* 
                    ( )
                    / \
                  ( ) ( )
                    \ /
                    ( ) -> collision (two different nodes placed on the same place in the graph
                                      because the distance between each node to his father is
                                      equal).
             */
            //In a case of diamond collision one node might override the other node on a graph.
            //Because we want to see both nodes on the graph then we color nodes that
            //comes from right and nodes that comes from left
            //with two different colors -> Red & Blue -> so we'll be able to see the differences.
            string color = "red"; //Red -> left side node, Blue -> right side node.

            if (right)
                color = "blue";

            File.AppendAllText(fileName, $"\n
"); File.AppendAllText(fileName, $"\n
{node.value}
"); }
DrawElement.cs

DrawElement 方法做了几件事:

  • 确定颜色 -> 该方法检查节点来自哪个节点。如果节点来自右侧,我们将其涂成蓝色,但如果来自左侧,我们将其涂成红色。
  • 利用颜色和位置我们还可以添加一个圆圈来包裹节点的值。
  • 然后我们添加圆内节点的值。

DrawLine 方法

private static void DrawLine((int x, int y) p1, (int x, int y) p2)
        {
            double left;

            if (p2.x < p1.x)
            {
                left = Math.Min(p1.x, p2.x);
            }
            else
            {
                left = p2.x - 75;
            }

            string line = $"\n
"; File.AppendAllText(fileName, line); }
DrawLine.cs

方法 DrawLine 只是获取两个节点并在它们之间画一条线。为了拟合这条线,我们还进行了一些数学计算

构建完树后,我们添加 </HTML> 以完成文档。然后我们调用方法 RunTree() 在运行时打开树。我们还将增加 tempCount 1,这样我们就知道下一个文件的名称是什么。

BINTREE 文件

什么是 BINTREE 文件?

该项目的最后一部分是管理我们创建的 HTML 文件。每个 HTML 文件都将遵循名称格式,该格式由名称 BINTREE 组成。然后我们将添加按时间顺序排列的 ID,然后添加文件扩展名。

例如:

BINTREE1.html
BINTREE2.html
...
BINTREE143.html
BINTREE144.html

我们将要创建的下一个 HTML 文件的 ID 将等于最后一个文件名 ID 号 + 1。

  • ID 从 1 开始 -> 我们将创建的第一个 BINTREE 将是 \'BINTREE1.html\'。

如何管理 BINTREE 文件

BTViewer 的静态构造函数

static BTViewer()
        {
            string directory = Directory.GetCurrentDirectory();

            if(!Directory.GetFiles(directory).Contains(BINTREE_CSS_FILENAME))
            {
                File.WriteAllText(BINTREE_CSS_FILENAME, @"#circle{
		border-radius: 50%;
		display: inline-block;
		border: 1px solid black;
	}
	.a{
		padding: 50px;
	}
	.b{
		width: 70px;
		height: 70px;
	}
	 .line{
width: 150px;
height: 150px;
border-bottom: 1px solid black;
position: absolute;
}");
            }

            Regex reg = new Regex(@"BINTREE\d+\.html"); //we check what is the latest binary tree file.

            List fileNames = Directory.GetFiles(directory).ToList();
            fileNames = reg.Matches(string.Join(" ", fileNames)).Select(x => x.Value).ToList(); //Get the BINTREE files on the directory.

            if (fileNames.Count > 0)
            {
                //we find the next fileName as -> the latest file name count (BINTREE*Number*) + 1
                tempCount = fileNames.Select(x => int.Parse(new Regex(@"\d+").Match(x).Value)).Max() + 1; //the next tree to draw.
            }
            else
                tempCount = 1;

            StartingTempCount = tempCount;
        }
static_constructor.cs

因为我们按时间顺序命名它们,所以我们不能随机命名。这可能会覆盖已经创建的其他 BINTREE 文件。这就是我们使用正则表达式来查找所有 BINTREE 文件的原因。

然后我们取其中能找到的最大 ID,并将其加一。新值将成为本次运行中 BINTREE 文件 ID 的开头。

例如,如果我们运行该程序两次,并且在第一次运行时我们创建了 5 个 BINTREE 文件,这意味着下次运行时的 tempCount 将从 6 开始,并且我们将创建的下一个文件将是 BINTREE6.html。

静态构造函数的另一个任务是检查我们是否有一个 BINTREEINITIALIZER.css 文件。如果没有,那么我们需要重新创建它。( BINTREEINITIALIZER.css 是我们用于二叉树样式的 CSS 文件。)

我们还将 tempCount 保存在不同的字段中,这样 StartingTempCount 我们就会知道使用哪个 ID 开始编写树(我们稍后会使用此功能)。

ClearTrees 方法

方法允许我们删除在 ClearTrees 当前运行和之前运行期间创建的临时 BINTREE 文件。指示我们要删除的内容。该方法有 3 个选项可供接受:

BTViewer.ClearTrees(TreesToClear.CurrentRun); //CLEAR ONLY CURRENT RUN
BTViewer.ClearTrees(TreesToClear.PreviousRuns); //CLEAR ONLY PREVIOUS RUNS
BTViewer.ClearTrees(TreesToClear.PreviousRuns | TreesToClear.CurrentRun); //CLEAR ALL
public static void ClearTrees(TreesToClear treesToClear)
        {
            string directory = Directory.GetCurrentDirectory();
            Regex reg = new Regex(@"BINTREE\d+\.html"); //the structure of a BINTREE runtime file.
            Regex findCount = new Regex(@"\d+");

            List fileNames = Directory.GetFiles(directory).ToList();
            fileNames = reg.Matches(string.Join(" ", fileNames)).Select(x => x.Value).ToList();

            if (((int)treesToClear & 0b1) != 0) //current run.
            {
                fileNames.Where(x => int.Parse(findCount.Match(x).Value) >= StartingTempCount)
                         .ToList().ForEach(x => File.Delete(x));
            }

            if(((int)treesToClear & 0b10) != 0) //other runs.
            {
                fileNames.Where(x => int.Parse(findCount.Match(x).Value) < StartingTempCount)
                         .ToList().ForEach(x => File.Delete(x));
            }
        }
ClearTrees.cs

该方法使用正则表达式获取我们拥有的所有 BINTREE 文件。它还使用另一个正则表达式从我们拥有的每个 BINTREE 文件中获取 ID。

对于当前运行->我们删除所有大于或等于 StartingTempCount 我们在程序当前运行期间创建的第一个文件的 ID 的文件。

对于以前的运行-> 我们执行与当前运行相反的操作:我们删除所有 ID 低于我们在本次运行中创建的第一个文件的 ID 的文件。

包起来

恭喜!您现在可以创建自己的 BinaryTreeViewer 系统了。我希望本文能帮助您更清楚地了解二叉树。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信小程序

微信扫一扫体验

立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部