C# 递归的应用 TreeView递归绑定数据

递归在WinForm中的应用

最近做项目经常用到递归,刚开始很久没用,不太熟悉,现在研究了下,并写下了学习笔记及开发经验总结。

递归热身

一个算法调用自己来完成它的部分工作,在解决某些问题时,一个算法需要调用自身。如果一个算法直接调用自己或间接地调用自己,就称这个算法是递归的(Recursive)。根据调用方式的不同,它分为直接递归(Direct Recursion)和间接递归(Indirect Recursion) 比如,在收看电视节目时,如果演播室中也有一台电视机播放的是与当前相同的节目,观众就会发现屏幕里的电视套有一层层的电视画面。这种现象类似于直接递归。 

如果把两面镜子面对面摆放,便可从任意一面镜子里看到两面镜子无数个影像,这类似于间接递归。 

一个递归算法必须有两个部分:初始部分(Base Case)和递归部分(Recursion Case)。初始部分只处理可以直接解决而不需要再次递归调用的简单输入。递归部分包含对算法的一次或多次递归调用,每一次的调用参数都在某种程度上比原始调用参数更接近初始情况。 

函数的递归调用可以理解为:通过一系列的自身调用,达到某一终止条件后,再按照调用路线逐步返回。递归是程序设计中强有力的工具,有很多数学函数是以递归来定义的。 

如大家熟悉的阶乘函数,我们可以对n!作如下定义:f(n)= 

(n=1)

n*f(n-1)  (n>=2)

      一个算法具有的特性之一就是有穷性(Finity):一个算法总是在执行有穷步之后结束,即算法的执行时间是有限的。递归算法当然也是算法,也满足算法的特性,因此递归不可能无限递归下去,总有一个终止条件。对该示例,递归的终止条件是n=1. n=1是,返回1,不在调用自己本身,递归结束。

class Program

    {

        static void Main(string[] args)

        {

            long result = function(20);

            Console.WriteLine(result);

            Console.ReadLine();

        }

        static long function(long n)

        {

            if (n == 1)  //算法终止条件

            {

                return 1;

            }

            return n * function(n - 1);

        }

}

递归算法通常不是解决问题最有效的计算机程序,因为递归包含函数调用,函数调用需要时空开销。所以,递归比其他替代选择诸如while循环等,所花费的代价更大。但是,递归通常提供了一种能合理有效地解决某些问题的算法。 

递归示例():遍历二叉树

二叉树是一种典型的树形结构,常用到递归算法来遍历。遍历按照根节点的相对顺序可分为前序遍历(DLR)、中序遍历(LDR)、后序遍历(RDL)

对二叉树节点,有数据域存放数据,左孩子和右孩子为引用域存放孩子的引用:

左孩子  LChhild

数据域  data

右孩子 RChild

    /// 

    /// 二叉树节点

    /// 

    /// 

   public class Node<T>

    {

       private T data;//数据域

       private Node<TlChild;//左孩子

       private Node<TrChild;//右孩子

       public Node()

       {

           data = default(T);

           lChild = null;

           rChild = null;

       }

       public Node(T dataNode<TlChildNode<TrChild)

       {

           this.data = data;

           this.lChild = lChild;

           this.rChild = rChild;

       }

       public Node(Node<TlChildNode<TrChild)

       {

           data = default(T);

           this.lChild = lChild;

           this.rChild = rChild;

       }

       public Node(T data)

           : this(datanullnull)

       {

           this.data = data;

       }

       /// 

       /// 数据域

       /// 

       public T Data

       {

           get { return data; }

           set { this.data = value; }

       }

       /// 

       /// 左孩子

       /// 

       public Node<TLChild

       {

           get { return lChild; }

           set { lChild = value; }

       }

       /// 

       /// 右孩子

       /// 

       public Node<TRChild

       {

           get { return rChild; }

           set { rChild = value; }

       }

}

先假设有以下结构的二叉树:

先在构造函数中简单构造下对应的数据:

public Node<stringA;

public 遍历二叉树()

        {

            A = new Node<string>("A");

            Node<stringB = new Node<string>("B");

            Node<stringC = new Node<string>("C");

            Node<stringD = new Node<string>("D");

            Node<stringE = new Node<string>("E");

            Node<stringF = new Node<string>("F");

            Node<stringG = new Node<string>("G");

            Node<stringH = new Node<string>("H");

            Node<stringI = new Node<string>("I");

            Node<stringJ = new Node<string>("J");

            D.LChild = H;

            D.RChild = I;

            E.LChild = J;

            B.LChild = D;

            B.RChild = E;

            C.LChild = F;

            C.RChild = G;

            A.LChild = B;

            A.RChild = C;

        }

前序遍历:先访问根结点A,然后分别访问左子树和右子树,把BB的子孙看作一个结点处理,CC的子孙看作一个结点处理,访问B时,把B当作根结点处理,B的左子树及左子树的子孙看作一个结点处理……可见,顺序依次是顶点-左孩子-右孩子(DLR),直到结点为叶子(即不包含子结点的结点),即为递归的终止条件。对任意结点,只要结点确定,其左孩子和右孩子就确定,因此递归算法方法参数将结点传入即可。

/// 

        /// 前序遍历--DLR

        /// 

        /// 

        public void PreOrder(Node<Troot)

        {

            if (root == null)

            {

                return;

            }

            Console.Write("{0} ",root.Data);

           //当节点无左孩子时,传入参数为null,下次调用即返回,终止

            PreOrder(root.LChild);

            //当节点无右孩子时,传入参数为null,下次调用即返回,终止

            PreOrder(root.RChild);

        }

同理,中序遍历和后序遍历如下:

/// 

        /// 中序遍历 LDR

        /// 

        /// 

        public void InOrder(Node<Tnode)

        {

            //if (node == null)

            //{

            //    return;

            //}

            //InOrder(node.LChild);

            //Console.Write("{0} ",node.Data);

            //InOrder(node.RChild);

           

            //另外一种写法

            if (node.LChild!=null)  

            {

                InOrder(node.LChild);

            }

            Console.Write("{0} "node.Data);

            if (node.RChild != null)

            {

                InOrder(node.RChild);

            }

        }

        /// 

        /// 后序遍历--LRD

        /// 

        /// 

        public void PostOrder(Node<Tnode)

        {

            if (node == null)

            {

                return;

            }

            PostOrder(node.LChild);

            PostOrder(node.RChild);

            Console.Write("{0} ",node.Data);

        }

        /// 

        /// 层序遍历

        /// 

        /// 

        public void LevelOrder(Node<Tnode)

        {

            if (node == null)

            {

                return;

            }

            Queue<Node<T>> sq = new Queue<Node<T>>();

            //根结点入队

            sq.Enqueue(node);

            while (sq.Count != 0)

            {

                Node<Ttmp = sq.Dequeue(); //出队

                Console.Write("{0} ",tmp.Data);

                if (tmp.LChild != null)

                {

                    sq.Enqueue(tmp.LChild);

                }

                if (tmp.RChild != null)

                {

                    sq.Enqueue(tmp.RChild);

                }

            }

        }

其中,另外一种写法就是在递归前判断下,满足递归条件才调用自己,这也是处理递归终止的一种方法。

   static void Main(string[] args)

        {

            遍历二叉树<stringt = new 遍历二叉树<string>();

            Console.Write("前序遍历:");

            t.PreOrder(t.A);

            Console.WriteLine();

            Console.Write("中序遍历:");

            t.InOrder(t.A);

            Console.WriteLine();

            Console.Write("后序遍历:");

            t.PostOrder(t.A);

            Console.WriteLine();

            Console.Write("层序遍历:");

            t.LevelOrder(t.A);

            Console.ReadLine();

        }

运行结果为

递归示例()WinFormTreeView的应用绑定区域树

C#中的树很多。比如,Windows Form程序设计和Web程序设计中都有一种被称为TreeView的控件。TreeView控件是一个显示树形结构的控件,此树形结构与Windows资源管理器中的树形结构非常类似。不同的是,TreeView可以由任意多个节点对象组成。每个节点对象都可以关联文本和图像。另外,Web程序设计中的TreeView的节点还可以显示为超链接并与某个URL相关联。每个节点还可以包括任意多个子节点对象。包含节点及其子节点的层次结构构成了TreeView控件所呈现的树形结构。

下面是很典型的一个例子,就是用TreeView绑定数据。数据一般符合树形结构,如行政区域之间的关系、公司部门与部门员工之间关系、磁盘目录文件之间的关系等。

父级与子级之间满足一对多的关系,因此在数据库设计中常用一字段来做本表主键的外键,代表父级区域ID。当然,如果要方便求子孙的算法(例如列举武汉所有子区域)可以另加一字段,记录从根结点到当前结点所经历的结点ID

思路分析:

1. 获取表Area中的所有数据,存放到DataTable中。

2. 获取根结点的数据并添加到根节点。根结点的处理常与子结点的递归处理不一样,例如根结点的添加是在treeView1.Nodes.Add里面,而子结点递归是在父结点上添加,因此经常要分开处理。获取根结点数据可用DataTable.Select(fAreaId=-1)来获取。绑定结点时,将Node.Text设为区域的名字,Node.Tag设为区域对应的数据行DataRow或者区域的ID,这样遍历子区域就知道父结点区域信息,也方便应用程序获取选中的结点对应的数据。

3. 递归遍历子区域并添加到TreeView控件中。递归方法参数为Node,由父级Node.Tag就能获取父级区域数据信息,进而获取其子区域,获取子区域可用    

DataRow[] rows=DataTable.Select(fAreaId=+父级区域ID)。获取子区域后将其获取的信息绑定到新建的Node对象,方法同第二步,然后递归调用自己。当区域不包含任何子区域时,递归终止,rows.Length==0.

代码如下:

public partial class BindAreaForm : Form

    {

        private DataTable dt = null;

        public BindAreaForm()

        {

            InitializeComponent();

            InitDataTable();

        }

        //获取Area所用数据

        private void InitDataTable()

        {

            SqlConnection conn = new SqlConnection("Data Source=.;Initial Catalog=Test;Integrated Security=True");

            SqlCommand cmd = new SqlCommand("SELECT * FROM Area"conn);

            SqlDataAdapter ada = new SqlDataAdapter(cmd);

            dt = new DataTable();

            ada.Fill(dt);

        }

        private void BindAreaForm_Load(object senderEventArgs e)

        {

            BindRoot();

        }

        //绑定根节点

        private void BindRoot()

        {

            DataRow[] rows = dt.Select("fAreaId=-1");//取根

            foreach (DataRow dRow in rows)

            {

                TreeNode rootNode = new TreeNode();

                rootNode.Tag = dRow;

                rootNode.Text = dRow["AreaName"].ToString();

                treeView1.Nodes.Add(rootNode);

                BindChildAreas(rootNode);

            }

            

        }

        //递归绑定子区域

        private void BindChildAreas(TreeNode fNode)

        {

            DataRow dr = (DataRow)fNode.Tag;//父节点数据关联的数据行

            int fAreaId = (int)dr["id"]; //父节点ID

            DataRow[] rows = dt.Select("fAreaId="+fAreaId);//子区域

            if (rows.Length == 0)  //递归终止,区域不包含子区域时

            {

                return;

            }

            foreach (DataRow dRow in rows)

            {

                TreeNode node = new TreeNode();

                node.Tag = dRow;

                node.Text = dRow["AreaName"].ToString();

                //添加子节点

                fNode.Nodes.Add(node);

                //递归

                BindChildAreas(node);

            }

        }

}

运行截图:

递归示例()WinFormTreeView的应用绑定磁盘目录()

磁盘文件系统结构符合树形结构,可以把“我的电脑”或者驱动器看做是树的根(多个驱动器看做多个根吧,做多课树处理),文件夹下面可以包含文件夹或文件,文件则是树的叶子,不能再分,显然,这也是递归的终止条件。

思路分析:

1. 获取要绑定的目录,此目录为treeView控件的根。将结点的Tag设置成觉对路径,以便子节点获取父结点信息。

2.递归遍历子目录和文件,当绝对路径对应的DirectoryInfo为文件时,递归终止。这里要提一下,网上很多判断文件时文件夹还是文件都用后缀来判断,无后缀则为文件夹,这样是不正确的,例如host文件就没后缀,但它是文件而不是文件夹,还有很多软件的缓存文件也没后缀的,把它们当文件夹来处理遍历访问子目录显然有异常。正确的方法是用FileSystemInfo类的GetType()方法。

 public partial class MainForm : Form

    {

       

        public MainForm()

        {

            InitializeComponent();    

        }

        private void MainForm_Load(object senderEventArgs e)

        {

          

            TreeNode root = new TreeNode();

            root.Text = @"战国无双2";

            root.Tag = @"E:/战国无双2";

            treeView1.Nodes.Add(root);

            BindChild(root);

        }

      

        private void BindChild(TreeNode fNode)

        {

            string path = fNode.Tag.ToString();

            

            //父目录

            DirectoryInfo fDir = new DirectoryInfo(path);

            FileSystemInfo[] finfos = fDir.GetFileSystemInfos();

         

            foreach (FileSystemInfo f in finfos)

            {

               string type = f.GetType().ToString();

                TreeNode node = new TreeNode();

                node.Text = f.Name;

                node.Tag = f.FullName;

                fNode.Nodes.Add(node);

                if ("System.IO.DirectoryInfo" == type//是文件夹时才递归调用自己

                {

                    BindChild(node);

                } 

            }

        }      

运行截图如下:

总结:

TreeView递归绑定一般分两大步,第一步对根结点操作及输入绑定,并将结点关联数据传入递归;第二步主要是递归终止的控制,控制终止一般有两种方法:一是在递归方法开始判断是否满足递归终止条件,是则显式return返回,否则继续调用自己;另外一种方法是在调用自己前判断是否满足递归的条件,满足条件才调用自己。两种方法具体看程序。

当把上面的目录改为比较大的目录例如C:/Windows时,发现加载要很多时间。针对这个问题,请看下一篇:动态加载结点。

递归示例()WinFormTreeView的应用绑定磁盘目录()

当具有树形结构的数据的结点很多而且树的深度比较大时,直接用递归遍历明显能发现性能很低。因此,不要一次全部加载,而是当用户点击展开时才加载此结点下的子结点。

实现要点:

每加载添加一个结点时,判断该结点是否为叶子(即不含子结点),若包含子结点,先添加一个空的子节点,这样做主要是让用户在界面能看到“+”表示结点能展开。当用户点击“+”时触发treeView_AfterExpand事件,在该事件中处理添加子结点数据,添加之前,清理删除掉以前的结点。

  public partial class MainForm2 : Form

    {

        public MainForm2()

        {

            InitializeComponent();

            this.SetStyle(ControlStyles.OptimizedDoubleBuffertrue);

        }

        private void MainForm2_Load(object senderEventArgs e)

        {

            BindDrives();

        }

        private void BindDrives()

        {

            DriveInfo[] drvs = DriveInfo.GetDrives();

            foreach (DriveInfo drv in drvs)

            {

                TreeNode root = new TreeNode();

                root.Text = drv.Name;

                root.Tag = drv.RootDirectory.ToString();

                // root.Nodes.Add("");

                treeView1.Nodes.Add(root);

                if (Directory.Exists(drv.RootDirectory.ToString()))

                {

                    DirectoryInfo dInfo = new DirectoryInfo(drv.RootDirectory.ToString());

                    FileSystemInfo[] files = dInfo.GetFileSystemInfos();

                    if (files.Length > 0) //有子节点,先添加一个空节点

                    {

                        root.Nodes.Add("emptyNode"string.Empty);

                    }

                }

            }

        }

        //展开节点,移除以前的空节点,加载子节点

        private void treeView1_AfterExpand(object senderTreeViewEventArgs e)

        {

            TreeNode parentNode = e.Node;

            // parentNode.Nodes.RemoveByKey("emptyNode");//移除空节点

            parentNode.Nodes.Clear();

            string path = parentNode.Tag.ToString();

            if (Directory.Exists(path))

            {

                DirectoryInfo dir = new DirectoryInfo(path);

                FileSystemInfo[] files = dir.GetFileSystemInfos();

                foreach (FileSystemInfo f in files)

                {

                    TreeNode node = new TreeNode();

                    node.Text = f.Name;

                    node.Tag = f.FullName;

                    parentNode.Nodes.Add(node);  //加载子节点

                    if (Directory.Exists(node.Tag.ToString()))

                    {

                        DirectoryInfo subDir = new DirectoryInfo(node.Tag.ToString());

                        if (subDir.Attributes != (FileAttributes.System | FileAttributes.Hidden | FileAttributes.Directory))

                        {

                            FileSystemInfo[] subFiles = subDir.GetFileSystemInfos();

                            if (subFiles.Length > 0)   //有子节点,先添加一个空节点

                            {

                                node.Nodes.Add("emptyNode"string.Empty);

                            }

                        }

                    }

                }

            }

运行结果如图:

这样,只加载用户要展开的结点,而且每次只加载当前结点的下一代,性能明显能提升,当然还能用多线程技术改善性能、用WindowsAPI获取文件图标并关联TreeView结点,这里就不介绍了。

        杨盛超

2011年3月31日

你可能感兴趣的:(C# 递归的应用 TreeView递归绑定数据)