数据结构之二叉树(C#版)
什么是二叉树
数据结构里面的“二叉树”这种结构,听起来很高大上,但实际上,他也的确是高大上,那么什么是二叉树呢?
下面我再次用灵魂给你画一下,什么是二叉树。
人话版
可以看到,最顶端那个小伙伴最拽,为什么呢?因为他是老大(根节点),然后这位老大,左右手都提着一个小弟(左右子树),并不是每个老大左右手都能提着小弟的哦,有可能只提一个,也有可能两手空空,然后他的小弟呢,也跟他一样(如果小弟比老大能提的小弟多,那老大可能就打死这个小弟了),可能提着两个小弟,可能提着一个小弟,也可能这个小弟也是两手空空,那么如此反反复复就构成了一个二叉树。
题外话:
一开始我画上面的图的时候,我将没有提小弟的那只手,是给他们砍掉的,但后面发现不对,小伙伴还是要双手都有,但提不提小弟,就看他自己咯,这里其实是对应于猿话版中的第二点
备注一下:
图中红色的部分,其实在二叉树中并不存在,只是为了解释得更加生动,就把他画成比较像人(我也不敢说我画的是个人)
猿话版
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个半小时。。。。。
文中可能有些许地方表述得不是非常严谨(我语文不好。。),希望大家谅解谅解。
二叉树可能看起来比较复杂,但如果能够自己亲手去敲一遍的话,敲完后,你可能会发现,哦,原来就这样,反正我觉得,解释这个东西,比我写代码要难几倍。。
另外,因为二叉搜索树,也是二叉树,所以预告一下啦,我的下一篇博客,写的就是二叉搜索树啦!
最后,希望我的博客,能对作为读者的你有那么一点点的帮助(哪怕能让你觉得编程有趣或者觉得编程也没有想象中那么难)。如果有任何建议都可以留言哦。
来源:CSDN
作者:爱码星人
链接:https://blog.csdn.net/jmelzc/article/details/103844841