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

本文详细介绍了递归在WinForm开发中的应用,包括递归的原理、使用场景、常见算法实现(如阶乘、二叉树遍历)及在TreeView控件中的绑定实例。重点阐述了递归绑定Treeview的两大步骤以及如何动态加载节点以提高性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

递归在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

 

 

     ///   <summary>

     ///  二叉树节点

     ///   </summary>

     ///   <typeparam name="T"></typeparam>

    public   class   Node < T >

    {

        private   T   data ; //数据域

        private   Node < T lChild ; //左孩子

        private   Node < T rChild ; //右孩子

 

        public   Node ()

       {

            data  =  default ( T );

            lChild  =  null ;

            rChild  =  null ;

       }

 

        public   Node ( T   data Node < T lChild Node < T rChild )

       {

            this . data  =  data ;

            this . lChild  =  lChild ;

            this . rChild  =  rChild ;

       }

 

        public   Node ( Node < T lChild Node < T rChild )

       {

            data  =  default ( T );

            this . lChild  =  lChild ;

            this . rChild  =  rChild ;

       }

 

        public   Node ( T   data )

           :  this ( data null null )

       {

            this . data  =  data ;

       }

 

        ///   <summary>

        ///  数据域

        ///   </summary>

        public   T   Data

       {

            get  {  return   data ; }

            set  {  this . data  =  value ; }

       }

 

        ///   <summary>

        ///  左孩子

        ///   </summary>

        public   Node < T LChild

       {

            get  {  return   lChild ; }

            set  {  lChild  =  value ; }

       }

 

        ///   <summary>

        ///  右孩子

        ///   </summary>

        public   Node < T RChild

       {

            get  {  return   rChild ; }

            set  {  rChild  =  value ; }

       }

 

}

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

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

public   Node < string A ;

public   遍历二叉树 ()

        {

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

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

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

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

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

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

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

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

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

             Node < string J  =  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 ,然后分别访问左子树和右子树,把 B B 的子孙看作一个结点处理, C C 的子孙看作一个结点处理,访问 B 时,把 B 当作根结点处理, B 的左子树及左子树的子孙看作一个结点处理 …… 可见,顺序依次是顶点- 左孩子 - 右孩子( DLR ),直到结点为叶子 ( 即不包含子结点的结点 ) ,即为递归的终止条件。对任意结点,只要结点确定,其左孩子和右孩子就确定,因此递归算法方法参数将结点传入即可。

///   <summary>

         ///  前序遍历--DLR

         ///   </summary>

         ///   <param name="root"></param>

         public   void   PreOrder ( Node < T root )

        {

             if  ( root  ==  null )

            {

                 return ;

            }

 

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

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

             PreOrder ( root . LChild );

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

             PreOrder ( root . RChild );

 

        }

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

///   <summary>

         ///  中序遍历 LDR

         ///   </summary>

         ///   <param name="node"></param>

         public   void   InOrder ( Node < T node )

        {

             //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 );

            }

        }

 

         ///   <summary>

         ///  后序遍历--LRD

         ///   </summary>

         ///   <param name="node"></param>

         public   void   PostOrder ( Node < T node )

        {

             if  ( node  ==  null )

            {

                 return ;

            }

 

             PostOrder ( node . LChild );

             PostOrder ( node . RChild );

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

        }

 

         ///   <summary>

         ///  层序遍历

         ///   </summary>

         ///   <param name="node"></param>

         public   void   LevelOrder ( Node < T node )

        {

             if  ( node  ==  null )

            {

                 return ;

            }

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

             //根结点入队

             sq . Enqueue ( node );

 

             while  ( sq . Count  != 0)

            {

                 Node < T tmp  =  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 )

        {

             遍历二叉树 < string t  =  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 ();

        }

运行结果为

 

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

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   sender EventArgs   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 );

            }

        }

}

运行截图:

 

 

 

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

 

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

思路分析:

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

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

  public   partial   class   MainForm  :  Form

    {

       

         public   MainForm ()

        {

             InitializeComponent ();    

        }

         private   void   MainForm_Load ( object   sender EventArgs   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 时,发现加载要很多时间。针对这个问题,请看下一篇:动态加载结点。

 

 

 

 

 

 

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

 

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

实现要点:

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

   public   partial   class   MainForm2  :  Form

    {

         public   MainForm2 ()

        {

             InitializeComponent ();

             this . SetStyle ( ControlStyles . OptimizedDoubleBuffer true );

        }

 

         private   void   MainForm2_Load ( object   sender EventArgs   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   sender TreeViewEventArgs   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 结点,这里就不介绍了。

//设定生成树的原始数据 void getdatable() { tblDatas.Columns.Add("groupid", Type.GetType("System.String")); tblDatas.Columns.Add("groupname", Type.GetType("System.String")); tblDatas.Columns.Add("parentid", Type.GetType("System.String")); tblDatas.Rows.Add(new object[] { "1", "机关", "0" }); tblDatas.Rows.Add(new object[] { "2", "学院", "0" }); tblDatas.Rows.Add(new object[] { "3", "教学管理中心", "1" }); tblDatas.Rows.Add(new object[] { "4", "校园管理中心", "1" }); tblDatas.Rows.Add(new object[] { "5", "数据中心", "3" }); tblDatas.Rows.Add(new object[] { "6", "信息中心", "3" }); tblDatas.Rows.Add(new object[] { "7", "一卡通", "4" }); tblDatas.Rows.Add(new object[] { "8", "保卫处", "4" }); tblDatas.Rows.Add(new object[] { "9", "信工系", "2" }); tblDatas.Rows.Add(new object[] { "10", "艺术系", "2" }); dataGridView1.DataSource = tblDatas; } //递归生成树函数 public void AddTree(int ParentID, TreeNode pNode) { DataTable dt = new DataTable(); dt = tblDatas; DataView dvTree = new DataView(dt); //过滤ParentID,得到当前的所有子节点 dvTree.RowFilter = "parentid = " + ParentID; foreach (DataRowView Row in dvTree) { TreeNode Node = new TreeNode(); if (pNode == null) { //添加根节点 Node.Text = Row["groupname"].ToString(); treeView1.Nodes.Add(Node); AddTree(Int32.Parse(Row["groupid"].ToString()), Node); //再次递归 } else { //添加当前节点的子节点 Node.Text = Row["groupname"].ToString(); pNode.Nodes.Add(Node); AddTree(Int32.Parse(Row["groupid"].ToString()), Node); //再次递归 } } } //调用递归函数在treeView1里面显示给出数据的树形图 private void button1_Click(object sender, EventArgs e) { treeView1.Nodes.Clear(); AddTree(0, (TreeNode)null); }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值