上了三年半的大学,学的计算机,我却一直不知道二叉树是干什么使的。最近在看《Visual C# 2005 从入门到精通》,书中说.NET类库里缺少二叉树这个类,于是就想自己写一个,没想到一边写着一边思考,竟也把二叉树的用处以及其优势劣势搞明白了。
二叉树有什么用?
二叉树本身没有什么用,其主要作用在于有序二叉树(下文简称“有序二叉树”为“二叉树”)。二叉树在插入和查找的时候具有高效率。为什么会具有高效率?因为二叉树保持总保持一个有序状态:即左子树小于根节点,根节点小于右子树。这样的结构使得二叉树在查找上具有与二分搜索同样的效率。当然,排好序的的一维数组也可以进行二分搜索。那二叉树的优势在哪呢?就在于其插入的效率比数组高,二叉树不用为了给新添加的元素腾出地方而移动大量元素。二叉树插入的效率与搜索的效率是相等的。
对比如下:(lgN是以2为底N的对数)
二叉树:搜索效率=插入效率=lgN
有序数组:搜索效率=lgN,插入效率=lgN + N/2
由此不难得到二叉树的适用场合:需要经常插入和删除元素且对速度要求较高的场合。
比如要举办一个大型网络会议,有成千上万的人参加,并且随着时间的推移会不断有新的人加入进来或离开。这种场合就很适合用二叉树。
有序性是二叉树最重要特点,编程的时候决不应该破坏有序性。为了保持有序性,在删除节点的时候就会遇到一些麻烦。以前在《数据结构》课本上见过删除节点的算法,很复杂而且很难读懂。于是我就干脆自己写一个删除节点的算法,费了好一番周折。开始我打算从物理上删除,即把要删除的节点真的从树中拿掉。这种做法带来的问题就是,被删节点的左右两棵子树不知该如何安放。为了解决这个问题,我在类中添加了SearchParent方法,甚至还想添加一个parent字段以提高效率,后来越做越觉得麻烦。最后我想出一个好的解决办法,给每个节点添加一个removed字段,用来标识节点是否已被删除。这样之前为了删除节点而写的那一堆辅助方法就都用不着了!可见优秀的设计比优秀的算法更有效!有了removed字段,删除节点的效率就与插入和搜索的效率一样,都是lgN。
对二叉树来说,有序性第一重要,位列第二的就是平衡性了。一个极不平衡的二叉树的效率是很低的。为了保持高效率,我添加了Optimize方法来使二叉树变得平衡。只有在平衡结构下,效率才能达到上面所说的lgN。《数据结构》课本上也介绍过平衡二叉树的算法,但相当繁琐。我自己写了一个,可能效率不是最高,但算法简单易理解。
我写的这个二叉树类有很多方法用到了递归。虽然递归在速度上不是很快,但能大大简化算法,避免了很多因算法设计不当而引入的bug。我信奉的编程思想是:首先保证简单,然后才追求速度。
我目前还是学生,编程肯定不够专业,在此将源代码奉上,望您提出宝贵意见!新年到了,愿2007是您丰收的一年!
using System;
using System.Collections.Generic;
using System.Text;
namespace OrderedBinaryTree
{
/**//// <summary>
/// 有序二叉树
/// 作者: dc10101
/// 完成日期:2007/1/1
/// </summary>
/// <typeparam name="T"></typeparam>
public class Tree<T> where T:IComparable<T>
{
fields#region fields
private T data; // 节点的值
private Tree<T> left; // 左子树
private Tree<T> right; // 右子树
private bool removed = false; // 标记该节点是否已被删除。true:已被删除;false: 未被删除
#endregion
properties#region properties
/**//// <summary>
/// 当前节点的值
/// readonly,随意更改节点的值将有可能破坏二叉树的有序结构
/// </summary>
public T Data
{
get { return this.data; }
}// Data
/**//// <summary>
/// 以当前节点为根的树中的节点总数
/// 不包括已被标记为deleted的节点
/// readonly,此属性是一个纯粹的方法,根本就不存在相应的字段,因此无法set
/// 递归方法
/// </summary>
public int NodeCount
{
get
{
int leftCount = 0;
int rightCount = 0;
if (this.left != null)
{
leftCount = this.left.NodeCount;
}
if (this.right != null)
{
rightCount = this.right.NodeCount;
}
// 左右子树都为null,递归出口
if (this.removed)
{
return leftCount + rightCount;
}
else
{
return leftCount + rightCount + 1;
}
}
}// NodeCount
/**//// <summary>
/// 以当前节点为根的二叉树的层数
/// readonly
/// 递归方法
/// </summary>
public int LevelCount
{
get
{
int leftLevelCount;
int rightLevelCount;
if (this.left != null)
{
leftLevelCount = this.left.LevelCount;
}
else
{
leftLevelCount = 0;
}
if (this.right != null)
{
rightLevelCount = this.right.LevelCount;
}
else
{
rightLevelCount = 0;
}
int max = (leftLevelCount > rightLevelCount) ? leftLevelCount : rightLevelCount;
return max + 1;
}
}// LevelCount
/**//// <summary>
/// 将此数组中的元素从头到尾依次插入到二叉树中,会得到最佳结构的平衡二叉树
/// readonly,此属性是一个纯粹的方法,根本就不存在相应的字段,因此无法set
/// </summary>
public T[] OptimizedArray
{
get
{
T[] sortedArray = this.SortedArray;
int arrayLength = this.NodeCount;
T[] newArray = new T[arrayLength];
int optIndex = 0;
FillOptimizedArray(sortedArray, 0, arrayLength - 1, newArray, ref optIndex);//FillOptimizedArray是递归函数
return newArray;
}
}// OptimizedArray
/**//// <summary>
/// 中序遍历二叉树而得到的有序数组
/// readonly,此属性是一个纯粹的方法,根本就不存在相应的字段,因此无法set
/// </summary>
public T[] SortedArray
{
get
{
T[] orderedArray = new T[this.NodeCount];
int index = 0;
FillSortedArray(orderedArray, ref index);// FillSortedArray是个递归函数
return orderedArray;
}
}// SortedArray
#endregion
methods#region methods
/**//// <summary>
/// 将指定节点插入到树中
/// 与当前节点相比,小于插到左子树,大于插到右子树,相等不作任何操作
/// 递归方法
/// </summary>
/// <param name="newData">要插入的元素的值</param>
public void Add(T newData)
{
if (this.data.CompareTo(newData) > 0) // currentData > newData,放到左子树上
{
if (null == this.left)// 递归出口1/2
{
this.left = new Tree<T>(newData);
}
else
{
this.left.Add(newData);
}
}
if (this.data.CompareTo(newData) < 0) // // currentData < newData,放到右子树上
{
if (null == this.right)// 递归出口2/2
{
this.right = new Tree<T>(newData);
}
else
{
this.right.Add(newData);
}
}
}// Add
/**//// <summary>
/// 将sortedArray里正中间的元素添加到optimizedArray的尾部
/// 递归重复此过程,最终得到数组optimizedArray,此数组最适合构造具有高效结构的平衡二叉树
/// 内部私有方法,是属性OptimizedArray调用的子过程
/// 递归方法
/// </summary>
/// <param name="sortedArray">按由小到大排好序的数组</param>
/// <param name="begin">已经在sortedArray中锁定了一个范围,这个范围的起始位置</param>
/// <param name="end">已经在sortedArray中锁定了一个范围,这个范围的末尾位置</param>
/// <param name="optimizedArray">要填充的“优化数组”</param>
/// <param name="optIndex">“优化数组”中下一个要被填充的位置</param>
private void FillOptimizedArray(T[] sortedArray, int begin, int end, T[] optimizedArray, ref int optIndex)
{
if (end - begin < 2)// 递归出口
{
if (end == begin)
{
optimizedArray[optIndex++] = sortedArray[begin];
}
else
{
optimizedArray[optIndex++] = sortedArray[begin];
optimizedArray[optIndex++] = sortedArray[end];
}
}
else
{
int middle = (begin + end) / 2;
optimizedArray[optIndex++] = sortedArray[middle];
FillOptimizedArray(sortedArray, begin, middle - 1, optimizedArray, ref optIndex);
FillOptimizedArray(sortedArray, middle + 1, end, optimizedArray, ref optIndex);
}
}// FillOptimizedArray
/**//// <summary>
/// 从数组的指定下标起往后面添加有序序列
/// 内部私有方法,是属性SortedArray调用的子过程
/// 递归方法
/// </summary>
/// <param name="array">需要填充的数组</param>
/// <param name="index">所添加序列在数组中的起始下标</param>
private void FillSortedArray(T[] array, ref int index)
{
if (null != this.left)
{
this.left.FillSortedArray(array, ref index);
}
if (false == this.removed)// 递归出口
{
array[index++] = this.data;
}
if (null != this.right)
{
this.right.FillSortedArray(array, ref index);
}
}// FillSortedArray
/**//// <summary>
/// 优化以当前节点为根节点的二叉树,使其成为具有高效结构的平衡二叉树
/// </summary>
public void Optimize()
{
T[] tempArray = this.OptimizedArray;
// 如果this不是只读的,我才不会写下面这三行别扭的代码!!
this.data = tempArray[0];//根节点必然是首先要插入的节点,即OptimizedArray数组的首元素
this.left = null;
this.right = null;
foreach (T element in tempArray)// 依次插入每个节点
{
this.Add(element);
}
}// Optimize
/**//// <summary>
/// 删除指定的节点
/// 并非把该节点从树中拿掉,而是将其removed字段标记为true
/// 找到则删除,找不到则什么都不做
/// </summary>
/// <param name="removeMe">指定节点的data值</param>
public void Remove(T removeMe)
{
Tree<T> deleteMe = this.Search(removeMe);
if (deleteMe != null)
{
deleteMe.removed = true;
}
}// Remove
/**//// <summary>
/// 寻找并返回某个指定的节点
/// 如果找不到,返回null
/// 递归方法
/// </summary>
/// <param name="searchMe">指定的节点的值</param>
/// <returns>要找的节点</returns>
public Tree<T> Search(T searchMe)
{
if (false == this.removed && 0 == this.data.CompareTo(searchMe)) // findMe == this.data,递归出口
{
return this;
}
else if (this.left != null && this.data.CompareTo(searchMe) > 0) // findMe < this.data
{
return this.left.Search(searchMe);
}
else if (this.right != null && this.data.CompareTo(searchMe) < 0) // findMe > this.data
{
return this.right.Search(searchMe);
}
else// 没找到。范围逐渐缩小到,缩小到左右子树均为null了还没有找到,则宣布没有找到。
{
return null;
}
}// Search
/**//// <summary>
/// 在控制台上按从小到大的顺序输出,每行一个元素
/// 调试专用
/// 递归方法
/// </summary>// Traverse
public void Traverse()
{
if (null != this.left)
{
this.left.Traverse();
}
if (false == this.removed) // 递归出口
{
Console.WriteLine(this.data.ToString());
}
if (null != this.right)
{
this.right.Traverse();
}
}// Traverse
/**//// <summary>
/// 接收一个参数的构造函数
/// </summary>
/// <param name="data">根节点的值</param>
public Tree( T data )
{
this.data = data;
this.left = null;
this.right = null;
}// Tree
/**//// <summary>
/// 接受多个参数或一个数组参数的构造函数
/// </summary>
/// <param name="array">包含所有节点的值的数组</param>
public Tree(params T[] array)
{
if (array.Length > 0)
{
this.data = array[0];
for (int i = 1; i < array.Length; i++)
{
this.Add(array[i]);
}
//人们用来构造二叉树的数组往往是有序的,
//但这恰恰会让构造出的二叉树具有最低效的结构,
//因此用数组构造完二叉树应该立即优化一下
this.Optimize();
}
else
{
throw new Exception("cannot build a binary tree from an empty array");
}
}// Tree
#endregion
}// Tree<T>
}// namespace