当前位置:首页 > 通信资讯 > 正文

先前我们讲的都是“线性结构”,他的特征就是“一个节点最多有一个”前驱“和一个”后继“。那么我们今天讲的树会是怎样的呢?

我们可以对”线性结构“改造一下,变为”一个节点最多有一个"前驱“和”多个后继“。哈哈,这就是我们今天说的”树“。

一: 树

我们思维中的”树“就是一种枝繁叶茂的形象,那么数据结构中的”树“该是怎么样呢?对的,他是一种现实中倒立的树。

算法系列15天速成 第十一天 树操作(上)(算法系列15天速成 第十一天 树操作(上))

1:术语

其实树中有很多术语的,这个是我们学习树形结构必须掌握的。

<1> 父节点,子节点,兄弟节点

这个就比较简单了,b和c的父节点就是a,反过来说就是b和c是a的子节点。b和c就是兄弟节点。

<2> 结点的度

其实”度“就是”分支数“,比如a的分支数有两个“b和c",那么a的度为2。

<3> 树的度

看似比较莫名其妙吧,他和”结点的度“的区别就是,树的度讲究大局观,乃树中最大的结点度,其实也就是2。

<4> 叶结点,分支结点

叶结点就是既没有左孩子也没有右孩子结点,也就是结点度为0。分支节点也就是if的else的条件咯。

<5> 结点的层数

这个很简单,也就是树有几层。

<6> 有序树,无序树

有序树我们先前也用过,比如“堆”和“二叉排序树”,说明这种树是按照一定的规则进行排序的,else条件就是无序树。

<7> 森林

现实中,很多的树形成了森林,那在数据结构中,我们把上图的“a”节点砍掉,那么b,c子树合一起就是森林咯。

2: 树的表示

树这个结构的表示其实有很多种,常用的也就是“括号”表示法。
比如上面的树就可以表示为:(a(b(d),(e)),(c(f),(g)))

二: 二叉树

在我们项目开发中,很多地方都会用到树,但是多叉树的处理还是比较纠结的,所以俺们本着“大事化小,小事化了“的原则

把”多叉树“转化为”二叉树“,那么问题就简化了很多。

1: ”二叉树“和”树“有什么差异呢?

第一点: 树的度没有限制,而“二叉树”最多只能有两个,不然也就不叫二叉树了,哈哈。
第二点:树中的子树没有左右划分,很简单啊,找不到参照点,二叉树就有参照物咯。

2: 二叉树的类型

二叉树中有两种比较完美的类型,“完全二叉树”和“满二叉树”。

<1> 满二叉树

除叶子节点外,所有节点的度都为2,文章开头处的树就是这里的“满二叉树”。

<2> 完全二叉树

必须要满足两个条件就即可: 干掉最后一层,二叉树变为“满二叉树”。

最后一层的叶节点必须是“从左到右”依次排开。

我们干掉文章开头处的节点“f和”g",此时还是“完全二叉树”,但已经不是“满二叉树”了,你懂的。

3: 二叉树的性质

二叉树中有5点性质非常重要,也是俺们必须要记住的。

<1> 二叉树中,第i层的节点最多有2(i-1)个。

<2> 深度为k的二叉树最多有2k-1个节点。

<3> 二叉树中,叶子节点树为n1个,度为2的节点有n2个,那么n1=n2+1。

<4> 具有n个结点的二叉树深度为(log2 n)+1层。

<5> n个结点的完全二叉树如何用顺序存储,对于其中的一个结点i,存在以下关系,

2*i是结点i的父结点。

i/2是结点i的左孩子。

(i/2)+1是结点i的右孩子。

4: 二叉树的顺序存储

同样的存储方式也有两种,“顺序存储”和“链式存储”。

<1> 顺序存储

说实话,树的存储用顺序结构比较少,因为从性质定理中我们都可以看出只限定为“完全二叉树”,那么如果二叉树不是

“完全二叉树”,那我们就麻烦了,必须将其转化为“完全二叉树”,将空的节点可以用“#”代替,图中也可看出,为了维护

性质定理5的要求,我们牺牲了两个”资源“的空间。

算法系列15天速成 第十一天 树操作(上)(算法系列15天速成 第十一天 树操作(上))

<2> 链式存储

上面也说了,顺序存储会造成资源的浪费,所以嘛,我们开发中用的比较多的还是“链式存储”,同样“链式存储”

也非常的形象,非常的合理。

一个结点存放着一个“左指针”和一个“右指针”,这就是二叉链表。

如何方便的查找到该结点的父结点,可以采用三叉链表。

5: 常用操作

一般也就是“添加结点“,“查找节点”,“计算深度”,“遍历结点”,“清空结点”

<1> 这里我们就用二叉链表来定义链式存储模型

复制代码 代码如下:


#region 二叉链表存储结构
/// <summary>
/// 二叉链表存储结构
/// </summary>
/// <typeparam name="t"></typeparam>
public class chaintree<t>
{
public t data;

public chaintree<t> left;

public chaintree<t> right;
}
#endregion

<2> 添加结点

要添加结点,我们就要找到添加结点的父结点,并且根据指示插入到父结点中指定左结点或者右结点。

复制代码 代码如下:


#region 将指定节点插入到二叉树中
/// <summary>
/// 将指定节点插入到二叉树中
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="tree"></param>
/// <param name="node"></param>
/// <param name="direction">插入做左是右</param>
/// <returns></returns>
public chaintree<t> bintreeaddnode<t>(chaintree<t> tree, chaintree<t> node, t data, direction direction)
{
if (tree == null)
return null;

if (tree.data.equals(data))
{
switch (direction)
{
case direction.left:
if (tree.left != null)
throw new exception("树的左节点不为空,不能插入");
else
tree.left = node;

break;
case direction.right:
if (tree.right != null)
throw new exception("树的右节点不为空,不能插入");
else
tree.right = node;

break;
}
}

bintreeaddnode(tree.left, node, data, direction);
bintreeaddnode(tree.right, node, data, direction);

return tree;
}
#endregion

<3> 查找节点

二叉树中到处都散发着递归思想,很能锻炼一下我们对递归的认识,同样查找也是用到了递归思想。

复制代码 代码如下:


#region 在二叉树中查找指定的key
/// <summary>
///在二叉树中查找指定的key
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="tree"></param>
/// <param name="data"></param>
/// <returns></returns>
public chaintree<t> bintreefind<t>(chaintree<t> tree, t data)
{
if (tree == null)
return null;

if (tree.data.equals(data))
return tree;

return bintreefind(tree, data);
}
#endregion

<4> 计算深度

这个问题纠结了我二个多小时,原因在于没有深刻的体会到递归,其实主要思想就是递归左子树和右子树,然后得出较大的一个。

复制代码 代码如下:


#region 获取二叉树的深度
/// <summary>
/// 获取二叉树的深度
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="tree"></param>
/// <returns></returns>
public int bintreelen<t>(chaintree<t> tree)
{
int leftlength;
int rightlength;

if (tree == null)
return 0;

//递归左子树的深度
leftlength = bintreelen(tree.left);

//递归右子书的深度
rightlength = bintreelen(tree.right);

if (leftlength > rightlength)
return leftlength + 1;
else
return rightlength + 1;
}
#endregion

<5> 遍历结点

二叉树中遍历节点的方法还是比较多的,有“先序”,“中序”,“后序”,“按层”,其实这些东西只可意会,不可言传,真的很难在口头

上说清楚,需要反复的体会递归思想。

先序:先访问根,然后递归访问左子树,最后递归右子树。(dlr模式)

中序:先递归访问左子树,在访问根,最后递归右子树。(ldr模式)

后序:先递归访问左子树,然后递归访问右子树,最后访问根。(lrd模式)

按层:这个比较简单,从上到下,从左到右的遍历节点。

复制代码 代码如下:


#region 二叉树的先序遍历
/// <summary>
/// 二叉树的先序遍历
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="tree"></param>
public void bintree_dlr<t>(chaintree<t> tree)
{
if (tree == null)
return;

//先输出根元素
console.write(tree.data + "\t");

//然后遍历左子树
bintree_dlr(tree.left);

//最后遍历右子树
bintree_dlr(tree.right);
}
#endregion

#region 二叉树的中序遍历
/// <summary>
/// 二叉树的中序遍历
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="tree"></param>
public void bintree_ldr<t>(chaintree<t> tree)
{
if (tree == null)
return;

//优先遍历左子树
bintree_ldr(tree.left);

//然后输出节点
console.write(tree.data + "\t");

//最后遍历右子树
bintree_ldr(tree.right);
}
#endregion

#region 二叉树的后序遍历
/// <summary>
/// 二叉树的后序遍历
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="tree"></param>
public void bintree_lrd<t>(chaintree<t> tree)
{
if (tree == null)
return;

//优先遍历左子树
bintree_lrd(tree.left);

//然后遍历右子树
bintree_lrd(tree.right);

//最后输出节点元素
console.write(tree.data + "\t");
}
#endregion

#region 二叉树的按层遍历
/// <summary>
/// 二叉树的按层遍历
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="tree"></param>
public void bintree_level<t>(chaintree<t> tree)
{
if (tree == null)
return;

//申请保存空间
chaintree<t>[] treelist = new chaintree<t>[length];

int head = 0;
int tail = 0;

//存放数组
treelist[tail] = tree;

//循环链中计算tail位置
tail = (tail + 1) % length;

while (head != tail)
{
var tempnode = treelist[head];

head = (head + 1) % length;

//输出节点
console.write(tempnode.data + "\t");

//如果左子树不为空,则将左子树存于数组的tail位置
if (tempnode.left != null)
{
treelist[tail] = tempnode.left;

tail = (tail + 1) % length;
}

//如果右子树不为空,则将右子树存于数组的tail位置
if (tempnode.right != null)
{
treelist[tail] = tempnode.right;

tail = (tail + 1) % length;
}
}
}
#endregion

<6> 清空二叉树

虽然c#里面有gc,但是我们能自己释放的就不麻烦gc了,同样清空二叉树节点,我们用到了递归,说实话,这次练习让我喜欢

上的递归,虽然xxx的情况下,递归的不是很好,但是递归还是很强大的。

复制代码 代码如下:


#region 清空二叉树
/// <summary>
/// 清空二叉树
/// </summary>
/// <typeparam name="t"></typeparam>
/// <param name="tree"></param>
public void bintreeclear<t>(chaintree<t> tree)
{
//递的结束点,归的起始点
if (tree == null)
return;

bintreeclear(tree.left);
bintreeclear(tree.right);

//在归的过程中,释放当前节点的数据空间
tree = null;
}
#endregion

最后上一下总的代码

  1. usingsystem;
  2. usingsystem.collections.generic;
  3. usingsystem.linq;
  4. usingsystem.text;
  5. namespacechaintree
  6. {
  7. publicclassprogram
  8. {
  9. staticvoidmain(string[]args)
  10. {
  11. chaintreemanagermanager=newchaintreemanager();
  12. //插入节点操作
  13. chaintree<string>tree=createroot();
  14. //插入节点数据
  15. addnode(tree);
  16. //先序遍历
  17. console.writeline("\n先序结果为:\n");
  18. manager.bintree_dlr(tree);
  19. //中序遍历
  20. console.writeline("\n中序结果为:\n");
  21. manager.bintree_ldr(tree);
  22. //后序遍历
  23. console.writeline("\n后序结果为:\n");
  24. manager.bintree_lrd(tree);
  25. //层次遍历
  26. console.writeline("\n层次结果为:\n");
  27. manager.length=100;
  28. manager.bintree_level(tree);
  29. console.writeline("\n树的深度为:"+manager.bintreelen(tree)+"\n");
  30. console.readline();
  31. }
  32. #region生成根节点
  33. ///<summary>
  34. ///生成根节点
  35. ///</summary>
  36. ///<returns></returns>
  37. staticchaintree<string>createroot()
  38. {
  39. chaintree<string>tree=newchaintree<string>();
  40. console.writeline("请输入根节点,方便我们生成树\n");
  41. tree.data=console.readline();
  42. console.writeline("根节点生成已经生成\n");
  43. returntree;
  44. }
  45. #endregion
  46. #region插入节点操作
  47. ///<summary>
  48. ///插入节点操作
  49. ///</summary>
  50. ///<paramname="tree"></param>
  51. staticchaintree<string>addnode(chaintree<string>tree)
  52. {
  53. chaintreemanagermananger=newchaintreemanager();
  54. while(true)
  55. {
  56. chaintree<string>node=newchaintree<string>();
  57. console.writeline("请输入要插入节点的数据:\n");
  58. node.data=console.readline();
  59. console.writeline("请输入要查找的父节点数据:\n");
  60. varparentdata=console.readline();
  61. if(tree==null)
  62. {
  63. console.writeline("未找到您输入的父节点,请重新输入。");
  64. continue;
  65. }
  66. console.writeline("请确定要插入到父节点的:1左侧,2右侧");
  67. directiondirection=(direction)enum.parse(typeof(direction),console.readline());
  68. tree=mananger.bintreeaddnode(tree,node,parentdata,direction);
  69. console.writeline("插入成功,是否继续?1继续,2退出");
  70. if(int.parse(console.readline())==1)
  71. continue;
  72. else
  73. break;
  74. }
  75. returntree;
  76. }
  77. #endregion
  78. }
  79. #region插入左节点或者右节点
  80. ///<summary>
  81. ///插入左节点或者右节点
  82. ///</summary>
  83. publicenumdirection{left=1,right=2}
  84. #endregion
  85. #region二叉链表存储结构
  86. ///<summary>
  87. ///二叉链表存储结构
  88. ///</summary>
  89. ///<typeparamname="t"></typeparam>
  90. publicclasschaintree<t>
  91. {
  92. publictdata;
  93. publicchaintree<t>left;
  94. publicchaintree<t>right;
  95. }
  96. #endregion
  97. ///<summary>
  98. ///二叉树的操作帮助类
  99. ///</summary>
  100. publicclasschaintreemanager
  101. {
  102. #region按层遍历的length空间存储
  103. ///<summary>
  104. ///按层遍历的length空间存储
  105. ///</summary>
  106. publicintlength{get;set;}
  107. #endregion
  108. #region将指定节点插入到二叉树中
  109. ///<summary>
  110. ///将指定节点插入到二叉树中
  111. ///</summary>
  112. ///<typeparamname="t"></typeparam>
  113. ///<paramname="tree"></param>
  114. ///<paramname="node"></param>
  115. ///<paramname="direction">插入做左是右</param>
  116. ///<returns></returns>
  117. publicchaintree<t>bintreeaddnode<t>(chaintree<t>tree,chaintree<t>node,tdata,directiondirection)
  118. {
  119. if(tree==null)
  120. returnnull;
  121. if(tree.data.equals(data))
  122. {
  123. switch(direction)
  124. {
  125. casedirection.left:
  126. if(tree.left!=null)
  127. thrownewexception("树的左节点不为空,不能插入");
  128. else
  129. tree.left=node;
  130. break;
  131. casedirection.right:
  132. if(tree.right!=null)
  133. thrownewexception("树的右节点不为空,不能插入");
  134. else
  135. tree.right=node;
  136. break;
  137. }
  138. }
  139. bintreeaddnode(tree.left,node,data,direction);
  140. bintreeaddnode(tree.right,node,data,direction);
  141. returntree;
  142. }
  143. #endregion
  144. #region获取二叉树指定孩子的状态
  145. ///<summary>
  146. ///获取二叉树指定孩子的状态
  147. ///</summary>
  148. ///<typeparamname="t"></typeparam>
  149. ///<paramname="tree"></param>
  150. ///<paramname="direction"></param>
  151. ///<returns></returns>
  152. publicchaintree<t>bintreechild<t>(chaintree<t>tree,directiondirection)
  153. {
  154. chaintree<t>childnode=null;
  155. if(tree==null)
  156. thrownewexception("二叉树为空");
  157. switch(direction)
  158. {
  159. casedirection.left:
  160. childnode=tree.left;
  161. break;
  162. casedirection.right:
  163. childnode=tree.right;
  164. break;
  165. }
  166. returnchildnode;
  167. }
  168. #endregion
  169. #region获取二叉树的深度
  170. ///<summary>
  171. ///获取二叉树的深度
  172. ///</summary>
  173. ///<typeparamname="t"></typeparam>
  174. ///<paramname="tree"></param>
  175. ///<returns></returns>
  176. publicintbintreelen<t>(chaintree<t>tree)
  177. {
  178. intleftlength;
  179. intrightlength;
  180. if(tree==null)
  181. return0;
  182. //递归左子树的深度
  183. leftlength=bintreelen(tree.left);
  184. //递归右子书的深度
  185. rightlength=bintreelen(tree.right);
  186. if(leftlength>rightlength)
  187. returnleftlength+1;
  188. else
  189. returnrightlength+1;
  190. }
  191. #endregion
  192. #region判断二叉树是否为空
  193. ///<summary>
  194. ///判断二叉树是否为空
  195. ///</summary>
  196. ///<typeparamname="t"></typeparam>
  197. ///<paramname="tree"></param>
  198. ///<returns></returns>
  199. publicboolbintreeisempty<t>(chaintree<t>tree)
  200. {
  201. returntree==null?true:false;
  202. }
  203. #endregion
  204. #region在二叉树中查找指定的key
  205. ///<summary>
  206. ///在二叉树中查找指定的key
  207. ///</summary>
  208. ///<typeparamname="t"></typeparam>
  209. ///<paramname="tree"></param>
  210. ///<paramname="data"></param>
  211. ///<returns></returns>
  212. publicchaintree<t>bintreefind<t>(chaintree<t>tree,tdata)
  213. {
  214. if(tree==null)
  215. returnnull;
  216. if(tree.data.equals(data))
  217. returntree;
  218. returnbintreefind(tree,data);
  219. }
  220. #endregion
  221. #region清空二叉树
  222. ///<summary>
  223. ///清空二叉树
  224. ///</summary>
  225. ///<typeparamname="t"></typeparam>
  226. ///<paramname="tree"></param>
  227. publicvoidbintreeclear<t>(chaintree<t>tree)
  228. {
  229. //递的结束点,归的起始点
  230. if(tree==null)
  231. return;
  232. bintreeclear(tree.left);
  233. bintreeclear(tree.right);
  234. //在归的过程中,释放当前节点的数据空间
  235. tree=null;
  236. }
  237. #endregion
  238. #region二叉树的先序遍历
  239. ///<summary>
  240. ///二叉树的先序遍历
  241. ///</summary>
  242. ///<typeparamname="t"></typeparam>
  243. ///<paramname="tree"></param>
  244. publicvoidbintree_dlr<t>(chaintree<t>tree)
  245. {
  246. if(tree==null)
  247. return;
  248. //先输出根元素
  249. console.write(tree.data+"\t");
  250. //然后遍历左子树
  251. bintree_dlr(tree.left);
  252. //最后遍历右子树
  253. bintree_dlr(tree.right);
  254. }
  255. #endregion
  256. #region二叉树的中序遍历
  257. ///<summary>
  258. ///二叉树的中序遍历
  259. ///</summary>
  260. ///<typeparamname="t"></typeparam>
  261. ///<paramname="tree"></param>
  262. publicvoidbintree_ldr<t>(chaintree<t>tree)
  263. {
  264. if(tree==null)
  265. return;
  266. //优先遍历左子树
  267. bintree_ldr(tree.left);
  268. //然后输出节点
  269. console.write(tree.data+"\t");
  270. //最后遍历右子树
  271. bintree_ldr(tree.right);
  272. }
  273. #endregion
  274. #region二叉树的后序遍历
  275. ///<summary>
  276. ///二叉树的后序遍历
  277. ///</summary>
  278. ///<typeparamname="t"></typeparam>
  279. ///<paramname="tree"></param>
  280. publicvoidbintree_lrd<t>(chaintree<t>tree)
  281. {
  282. if(tree==null)
  283. return;
  284. //优先遍历左子树
  285. bintree_lrd(tree.left);
  286. //然后遍历右子树
  287. bintree_lrd(tree.right);
  288. //最后输出节点元素
  289. console.write(tree.data+"\t");
  290. }
  291. #endregion
  292. #region二叉树的按层遍历
  293. ///<summary>
  294. ///二叉树的按层遍历
  295. ///</summary>
  296. ///<typeparamname="t"></typeparam>
  297. ///<paramname="tree"></param>
  298. publicvoidbintree_level<t>(chaintree<t>tree)
  299. {
  300. if(tree==null)
  301. return;
  302. //申请保存空间
  303. chaintree<t>[]treelist=newchaintree<t>[length];
  304. inthead=0;
  305. inttail=0;
  306. //存放数组
  307. treelist[tail]=tree;
  308. //循环链中计算tail位置
  309. tail=(tail+1)%length;
  310. while(head!=tail)
  311. {
  312. vartempnode=treelist[head];
  313. head=(head+1)%length;
  314. //输出节点
  315. console.write(tempnode.data+"\t");
  316. //如果左子树不为空,则将左子树存于数组的tail位置
  317. if(tempnode.left!=null)
  318. {
  319. treelist[tail]=tempnode.left;
  320. tail=(tail+1)%length;
  321. }
  322. //如果右子树不为空,则将右子树存于数组的tail位置
  323. if(tempnode.right!=null)
  324. {
  325. treelist[tail]=tempnode.right;
  326. tail=(tail+1)%length;
  327. }
  328. }
  329. }
  330. #endregion
  331. }
  332. }

我们把文章开头的“二叉树”的节点输入到我们的结构中,看看遍历效果咋样。

算法系列15天速成 第十一天 树操作(上)(算法系列15天速成 第十一天 树操作(上))

如果您对该产品感兴趣,请填写办理(客服微信:xiaoxiongyidong)

为您推荐:

发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。