递归在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结点,这里就不介绍了。

杨盛超

2011年3月31日

C# 递归的应用 TreeView递归绑定数据相关推荐

  1. php 递归展现城市信息,PHP 递归兑现层级树状展现数据

    PHP 递归实现层级树状展现数据 ? $arr['id'], 'fid' => $arr['fid'], 'name' => $arr['name'], ); } // 将数据按照缩进简单 ...

  2. Vue2 MVVM 双向绑定(数据劫持+发布者-订阅者模式)

    参考文献:https://www.cnblogs.com/libin-1/p/6893712.html https://juejin.im/post/5b2f0769e51d45589f46949e ...

  3. (数据结构与算法)递归及一些经典递归问题

    文章目录 1. 递归的概念 2. 递归的调用机制 3. 递归需要遵守的重要规则 4. 递归 - 迷宫问题 5. 递归-八皇后问题 1. 递归的概念 递归就是方法自己调用自己,每次调用时传入不同的变量. ...

  4. 排序算法:快速排序算法实现及分析(递归形式和非递归形式)

    快速排序算法介绍 从名字上就可以看出快速排序算法很嚣张,直接以快速命名.确实快速排序 的确很快速,被列为20世纪十大算法之一.程序员难道不应该掌握么.快速排序(Quick Sort)的基本思想是:通过 ...

  5. 数据结构之栈与递归的实现及应用(斐波拉契数列递归解法和strlen递归解法)

    栈与递归 程序中的"函数调用栈"是栈数据结构的一种应用. 函数调用栈一般是从高地址向低地址增长的,栈底为内存的高地址处,栈顶为内存的低地址处. 函数调用栈中存储的数据为活动记录.活 ...

  6. 【C语言】函数 ---- 函数的嵌套调用和链式访问、函数的声明和定义、变量的声明和定义、函数递归与迭代、递归时的栈溢出问题

    函数 一.函数的嵌套调用和链式访问 1.嵌套调用 2.链式访问 2.1strlen()函数 2.2printf()函数 二.函数的声明和定义 1.函数声明和定义的介绍 2.函数声明和定义的使用 三.变 ...

  7. python 树结构 sql_sqlserver 树结构递归(向上递归和向下递归)

    --获取当前及以下部门 Create proc GetCurrentAndUnderOrg @orgId int as begin WITH cte AS ( SELECT * ,0 AS level ...

  8. 【数据结构】--- 二叉树的递归遍历和非递归遍历【C语言实现】

    目录 1. 创建一颗二叉树 2.递归前序遍历二叉树 3.递归中序遍历二叉树 4.递归后序遍历二叉树 5. 测试递归打印二叉树代码 6. 非-递归前序遍历二叉树 7. 非-递归实现中序遍历二叉树 8.  ...

  9. 二叉树创建,递归遍历,非递归遍历

    二叉树 博主是一个大一刚刚放暑假的大学生,大学我们只学习了c语言,现在这么卷只学c语言肯定不够,所以博主打算从零开始恶补c++顺便写文章记录一下,另外博主这个暑假还想记录一些算法基础内容欢迎关注哦.这 ...

最新文章

  1. 讲讲Git如何合并分支(一)
  2. Oracle数据库链Database links
  3. POJ 3784.Running Median
  4. 桌面开发者的界面故事,该醒醒了
  5. Java学习之javassist
  6. 图解设计模式:抽象工厂
  7. dll侧加载_win7系统开机提示xxxdll模块已加载但找不到入口点的解决方法
  8. GreenDao 工具类 --- 使用 Json 快速生成 Bean、表及其结构,炒鸡快!
  9. dpdk对虚拟化的支持
  10. 如何使用Ghost备份与还原操作系统
  11. ISO 9001 质量标准体系 免费下载
  12. 深圳-上海-呼伦贝尔-漠河-哈尔滨环行手记
  13. 详解:Java的重载方法与示例
  14. 【视频行为识别4】I3D:Two-Stream Inflated 3D ConvNets —CVPR2017
  15. MATLAB--数字图像处理 PSNR
  16. 计算机硬件大型作业报告,计算机硬件技术大作业报告 (2).doc
  17. 图标(Icon)和图标按钮(IconButton)
  18. (软件测试工程师)一线大厂新人工作指南!
  19. 远程网络监视(rmon)与简单网络管理协议(snmp)之间是什么关系
  20. ISO20000信息技术服务管理体系认证条件

热门文章

  1. 西电计算机学院有保研清华的吗,北邮西电保送清华人数进前10,不输双一流,哪些大学学生能读清华...
  2. 词云python_词-经典诗词宋词语大全
  3. 03 - OAI接入网搭建过程 - 研0
  4. python中random模块
  5. mysql时间12小时制_mysql 12小时和24小时转换
  6. 不定积分知识结构图_大一上学期《高等数学》知识整理-第四章 不定积分
  7. 对等网络限制计算机数量吗,把网络连接起来——对等网的组建
  8. c/c++结构体如何判断相等而且是否能相互赋值
  9. ASP第一章IIS发布站点
  10. 滴滴青桔单车跨端技术方案和业务技术架构,及框架设计和性能提升实践