数据结构之二叉树(C#版)

倖福魔咒の 提交于 2020-01-08 02:02:16

什么是二叉树

数据结构里面的“二叉树”这种结构,听起来很高大上,但实际上,他也的确是高大上,那么什么是二叉树呢?

下面我再次用灵魂给你画一下,什么是二叉树。
二叉树的描述

人话版

可以看到,最顶端那个小伙伴最拽,为什么呢?因为他是老大(根节点),然后这位老大,左右手都提着一个小弟(左右子树),并不是每个老大左右手都能提着小弟的哦,有可能只提一个,也有可能两手空空,然后他的小弟呢,也跟他一样(如果小弟比老大能提的小弟多,那老大可能就打死这个小弟了),可能提着两个小弟,可能提着一个小弟,也可能这个小弟也是两手空空,那么如此反反复复就构成了一个二叉树。

题外话
一开始我画上面的图的时候,我将没有提小弟的那只手,是给他们砍掉的,但后面发现不对,小伙伴还是要双手都有,但提不提小弟,就看他自己咯,这里其实是对应于猿话版中的第二点

备注一下
图中红色的部分,其实在二叉树中并不存在,只是为了解释得更加生动,就把他画成比较像人(我也不敢说我画的是个人)

猿话版

1.首先二叉树是树;
2.二叉树每个元素都有只有两棵子树(左子树和右子树,其中一个或两个可以为空);
3.二叉树中每个元素子树都是有序的,分为左子树和右子树(这里的有序指的是左右的顺序)。

代码实现

树结构

//二叉树基类
public abstract class BinaryTreeBase<T>
{
        public abstract BinaryTreeNode<T> Root { get; }
}

//二叉树
public class BinaryTree<T> : BinaryTreeBase<T>
{
		//根节点
        private BinaryTreeNode<T> _root;
        
        public override BinaryTreeNode<T> Root { get { return _root; } }
        
        public BinaryTree(BinaryTreeNode<T> rootNode)
        {
            _root = rootNode;
        }

        public BinaryTree(T rootData)
        {
            _root = new BinaryTreeNode<T>(rootData);
        }
}

一棵二叉树的基本结构,可以参考上述代码,这里我为什么写了一个二叉树的基类,是为了我下一篇讲述二叉搜索树做准备的,因为代码写好了,也就懒得改了。

其实二叉树的结构,主要的就是保存一个根节点咯,其他类似计算二叉树的层数和元素个数,这里先暂时不提及了。

树节点

 public class BinaryTreeNode<T>
 {


        ///<summary>
        ///节点数据
        ///<summary>        
        private T _data;
        ///<summary>
        ///左节点
        ///<summary>
        private BinaryTreeNode<T> _leftChild;
        ///<summary>
        ///右子节点
        ///<summary>
        private BinaryTreeNode<T> _rightChild;
        

        ///<summary>
        ///节点数据
        ///<summary>
        public T Data
        {
            get { return _data; }
            set { _data = value; }
        }
        ///<summary>
        ///左子节点
        ///<summary>
        public BinaryTreeNode<T> LeftChild
        {
            get { return _leftChild; }
            set { _leftChild = value; }
        }
        ///<summary>
        ///右子节点
        ///<summary>
        public BinaryTreeNode<T> RightChild
        {
            get { return _rightChild; }
            set { _rightChild = value; }
        }

        
        public BinaryTreeNode() { }
        
        public BinaryTreeNode(T data)
        {
            _data = data;

        }
        public override string ToString() {
            return _data.ToString();
        }

}

对于树节点的话,每个元素都有左子树和右子树,当然不能少的就是保存元素自身的数据啦,我这里还重写了ToString()方法,主要是为了方便将元素的数据输出。

二叉树的遍历方法

示例树
下面就用上面这个二叉树来说明一下,不同的遍历方式。

深度优先—Depth First Search(DFS)

什么是深度优先遍历方法?还是用上面的那些小伙伴来说明一下。

有一天,大王来巡山,给老大说:“喂,我要一个一个地看看你小弟弟”,老大:“这不好吧。。。”,大王说:“我要从上往下看(深度优先)”,老大:“。。。。”,大王:“不要想歪,我在说正事”,老大:“那好吧,那你对从上往下看有什么要求吗?”,大王:“有啊,我要从左往右(先序遍历),看完中间再从左往右看两边(中序遍历),从左往右两边看完再看中间(后序遍历)。”,老大:“WDLLM。。。。”

先序遍历

先序遍历是深度优先遍历方式中的一种,其实就是先看看自己,再看左边的小弟,最后看右边的小弟(先访问树根,再前序遍历左子树,最后前序遍历右子树)

 		///<summary>
        ///前序输出树
        ///<summary>
        /// <param name="tree">需要输出的树</param>
        public void PreOrderPrint(BinaryTree<T> tree)
        {
            PreOrderPrintNode(tree.Root);
        }
        ///<summary>
        ///先序输出节点值
        ///<summary>
        /// <param name="node">节点</param>
        private void PreOrderPrintNode(BinaryTreeNode<T> node)
        {

            if (node != null)
            {
                PrintNode(node);
                PreOrderPrintNode(node.LeftChild);
                PreOrderPrintNode(node.RightChild);
            }

        }
        ///<summary>
        ///输出节点
        ///<summary>
        /// <param name="node">节点</param>
        private void PrintNode(BinaryTreeNode<T> node)
        {
            Console.Write(node.ToString());
        }

可以看到这里代码使用了递归,每次都先输出当前节点的值,然后继续递归的方式遍历左子树,再遍历右子树。
所以最终输出结果为: ABDC

中序遍历

中序遍历是深度优先遍历方式中的第二种,先看左边的小弟,然后看自己,最后看右边的小弟(中序遍历左子树,访问树根,最后中序遍历右子树)

		///<summary>
        ///中序输出树
        ///<summary>
        /// <param name="tree">需要输出的树</param>
        public void InOrderPrint(BinaryTree<T> tree)
        {
            InOrderPrintNode(tree.Root);
        }
        ///<summary>
        ///中序输出节点值
        ///<summary>
        /// <param name="node">节点</param>
        private void InOrderPrintNode(BinaryTreeNode<T> node)
        {

            if (node != null)
            {
                InOrderPrintNode(node.LeftChild);
                PrintNode(node);
                InOrderPrintNode(node.RightChild);
            }

        }

中序遍历,每次都先遍历左子树,然后输出当前节点的值,最后遍历右子树。
所以输出结果为:DBAC

后序遍历

中序遍历是深度优先遍历方式中的第三种,先看左边的小弟,然后看右边的小弟,最后看自己(后序遍历左子树,中序遍历右子树,最后访问树根)

		///<summary>
        ///后序输出树
        ///<summary>
        /// <param name="tree">需要输出的树</param>
        public void PostOrderPrint(BinaryTree<T> tree)
        {
            PostOrderPrintNode(tree.Root);
        }
        ///<summary>
        ///后序输出节点值
        ///<summary>
        /// <param name="node">节点</param>
        private void PostOrderPrintNode(BinaryTreeNode<T> node)
        {

            if (node != null)
            {
                PostOrderPrintNode(node.LeftChild);
                PostOrderPrintNode(node.RightChild);
                PrintNode(node);
            }
        }

后序遍历,每次都先遍历左子树,然后遍历右子树,最后输出当前节点的值。
所以输出结果为:DBCA

深度优先遍历总结

其实深度优先,就是往树的竖直方向去遍历,那么三种不同的深度优先遍历的方法,就是在竖直遍历的过程中,在横向方向上的三种不同顺序(左子树,树根和右子树的遍历顺序),这里貌似有点绕,可能需要花点时间去理解一下,同时参考着树的结构图和输出,应该会好理解很多。(如果你有更好的解释,可以给我提供建议哦)

广度优先—Breadth First Search(BFS)

呼,解释完深度优先,我都快炸了,不知道作为读者的你炸了没有,如果没有炸的话,那么我们继续。。。。广度优先的遍历方式。
如果炸了的话。。。那就炸了吧。
示例树
广度优先的遍历呢,其实从思想上很简单,拿上面的树结构图来解释的话,就是从左往右先输出第一层的所有元素,然后从左往右输出第二层所有的元素,一直从左往右输出第N层的所有元素。

那么,怎么才能按照上面的方式,一层一层地输出呢?这里需要用到另外一种数据结构,它叫做队列(Queue),队列就跟他的名字一样咯,就是排队咯,排第一的先出队,再到排第二的,也就是所谓的先进先出(FIFO)。

那如何用队列来实现呢,很简单,每遍历一个元素的时候,先将它的左子树丢进队列,然后将它的右子树丢进队列,接着输出当前元素的值,然后再从队列里面出列(Dequeue)一个元素,重复上述步骤,就可以啦。

话不多说了,直接上代码吧。(因为C#有现成的队列,我就偷懒了,先不自己实现队列这种结构了)

 		///<summary>
        ///广度优先遍历树
        ///<summary>
        /// <param name="tree">需要输出的树</param>
        public void BreadthFirstSearch(BinaryTree<T> tree)
        {
            Queue<BinaryTreeNode<T>> cache = new Queue<BinaryTreeNode<T>>();

            cache.Enqueue(tree.Root);

            while (cache.Count > 0)
            {
                BinaryTreeNode<T> node = cache.Dequeue();

                PrintNode(node);

                if (node.LeftChild != null)
                {
                    cache.Enqueue(node.LeftChild);
                }
                if (node.RightChild != null)
                {
                    cache.Enqueue(node.RightChild);
                }
            }
        }

最后,我还是用图来解释一下,整个过程吧。
广度优先遍历方法图解
1.将根节点入列(Enqueue);
2.对出列(Dequeue)的元素遍历,这里是元素A,输出值,将左子树入列,然后右子树入列(如果子树为空,则不入列),如图中的2所示;
3.重复步骤2,如图中的3所示;
4.重复步骤2,如图中的4所示;
5.队列为空,遍历结束。

总结

终于描述完二叉树了,讲真,2个小时的代码,解释了4个半小时。。。。。

文中可能有些许地方表述得不是非常严谨(我语文不好。。),希望大家谅解谅解。

二叉树可能看起来比较复杂,但如果能够自己亲手去敲一遍的话,敲完后,你可能会发现,哦,原来就这样,反正我觉得,解释这个东西,比我写代码要难几倍。。

另外,因为二叉搜索树,也是二叉树,所以预告一下啦,我的下一篇博客,写的就是二叉搜索树啦!

最后,希望我的博客,能对作为读者的你有那么一点点的帮助(哪怕能让你觉得编程有趣或者觉得编程也没有想象中那么难)。如果有任何建议都可以留言哦。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!