树结构在程序设计中的运用

                                                                                引言

近年来,由于各种竞赛纷纷采用free-pascal,因此对于算法来说,空间效率上的要求降低了,而对时间效率却提出了更高的要求。这使得选手不仅要娴熟地掌握常规算法,而且要大胆创新,构造更高效的算法来解决问题。

在以往的程序设计中,链式结构采用得较多。的确链式结构有编程复杂度低、简单易懂等优点,但有一个致命的弱点:相邻的两个元素间的联系并不明显。而树结构却能很好的做到这一点。

竞赛中会经常遇到这样的题目:给出各个元素之间的联系,要求将这些元素分成几个集合,每个集合中的元素直接或间接有联系。在这类问题中主要涉及的是对集合的合并和查找,因此将这种集合称为并查集。

并查集

一.链结构的并查集

二.树结构的并查集

        1.链结构的并查集

链表被普通用来计算并查集:表中的每个元素设两个指针:一个指向同一集合中的下一个元素;另一个指向表首元素。采用链式存储结构,在进行集合的查找时的算法复杂度仅为O1);但合并集合时的算法复杂度却达到了On)。如果我们希望两种基本操作的时间效率都比较高的话,链式存储方式就力不从心了。

        2.树结构的并查集

采用树结构支持并查集的计算能够满足我们的要求。并查集与一般的树结构不同,每个顶点纪录的不是它的子结点,而是将它的父结点记录下来。下面我介绍一下树结构的并查集的两种运算方式

n

n⑴直接在树中查询

n⑵边查询边“路径压缩”

对应与前面的链式存储结构,树状结构的优势非常明显:编程复杂度低;时间效率高。

               (1).直接在树中查

集合的合并算法很简单,只要将两棵树的根结点相连即可,这步操作只要O(1)时间复杂度。所以算法的时间效率取决于集合查找的快慢。而集合的查找效率与树的深度呈线性关系。因此直接查询所需要的时间复杂度平均为O(logN)。但在最坏情况下,树退化成为一条链,使得每一次查询的算法复杂度为O(N)。

               (2).边询边路径压缩

其实,我们还能将集合查找的算法复杂度进一步降低:采用路径压缩算法。它的想法很简单:在集合的查找过程中顺便将树的深度降低。采用路径压缩后,每一次查询所用的时间复杂度为增长极为缓慢的ackerman函数的反函数——α(x)。对于可以想象到的n,α(n)都是在5之内的。

 

                                                                          线段树

处理涉及到图形的面积、周长等问题的时候,并不需要依赖很深的数学知识,但要提高处理此类问题的效率却又十分困难。这就需要从根本上改变算法的基础——数据结构。这里要说的就是一种特殊的数据结构——线段树

先看一道较基础的题目:给出区间上的n条线段,判断这些线段覆盖到的区间大小。通过这题我们来逐步认识线段树。

一.线段树的定义

二.在线段树中加入和删除线段

三.计算测度和连续线段数

              1.线段树的定义

线段树是一棵二叉树,将一个区间划分为一个个[i,i+1]的单元区间,每个单元区间对应线段树中的一个叶子结点。每个结点用一个变量count来记录覆盖该结点的线段条数。

区间[1,7]所对应的线段树如下图所示。区间上有一条线段[3,6]。

(  电脑弱,搞不上去,望理解)

                2.在线段树中插入和删除线段

在线段树中插入和删除线段的方法类似,都采用递归逐层向两个子结点扫描,直到线段能够盖满结点表示的整个区间为止。

经过分析可以发现:在线段树中插入、删除线段的时间复杂度均为O(logN)。

计算测度和连续线段数

结点的测度m指的是结点所表示区间中线段覆盖过的长度。

j - i                       (count>0)

m=  0                              (count=0 且结点为叶结点)

lch.m + rch.m    (count=0 且结点为内部结点)

                3.计算测度和连续线段数

连续线段数line指的是区间中互不相交的线段条数。

连续线段数并不能像测度那样将两个子结点中的连续线段数简单相加。于是我们引进了两个量lbd,rbd,分别表示区间的左右两端是否被线段覆盖。

1 (左端点被线段覆盖到)

lbd  =  0 (左端点不被线段覆盖到)

1   (右端点被线段覆盖到)

rbd  =  0 (右端点不被线段覆盖到)

line可以根据lbd,rbd定义如下:

1       (count > 0)

0    (count=0 且结点为叶结点)

Line=  lch^.Line + rch^.Line - 1    (count=0 且结点为内部结点, lch^.rbd、rch^.lbd都为1)

lch^.Line + rch^.Line        (count=0且结点为内部结点,      lch^.rbd,rch^.Lbd不同时为1)

                                                           

                                     

                                                                          树状数组

IOI2001中的MOBILE难倒了很多选手。虽然该题的题意十分简单:在一个矩阵中,通过更新元素值修改矩阵状态,并计算某子矩阵的数字和,但难点在于数据的规模极大。下面,我来介绍一种新的数据结构树状数组

1、建立树状数组C

2、更新元素值

3、子序列求和

利用树状数组,编程的复杂度提高了,但程序的时间效率也大幅地提高。这正是利用了树结构能够减少搜索范围,将信息集中起来的优点,让更新数组和求和运算牵连尽量少的变量。

        1.建立树状数组C

先将问题简化,考察一维子序列求和的算法。设该序列为a[1]a[2]……a[n]

w

算法1:直接在原序列中计算。显然更新元素值的时间复杂度为O(1);在最坏情况下,子序列求和的时间复杂度为O(n)。

算法2:增加数组b,其中b[i]a[1]+a[2]+……+a[i]。由于a[i]的更改影响b[i]┅b[n],因此在最坏情况下更新元素值的算法复杂度为O(n);而子序列求和的算法复杂度仅为O(1)。

以上两种算法,要么在更新元素值上耗费时间过长(算法1),要么在子序列求和上无法避免大量运算(算法2)。有没有更好的方法呢?

算法三:增加数组C,其中C[i]=a[i-2k+1]+……+a[i](k为i在二进制形式下末尾0的个数)。由c数组的定义可以得出

c[1]=a[1]

c[2]=a[1]+a[2]=c[1]+a[2]

c[3]=a[3]

c[4]=a[1]+a[2]+a[3]+a[4]=c[2]+c[3]+a[4]

c[5]=a[5]

c[6]=a[5]+a[6]=c[5]+a[6]

………………

数组的结构对应一棵树,因此称之为树状数组。

在统计更新元素值和子序列求和的算法复杂度后,会发现两种操作的时间复杂度均为O(logN),大大提高了算法效率。

        2.更新元素值

定理

若a[k]所牵动的序列为C[p1],C[p2]……C[pm]。

则p1=k,而p i+1=pi+2li(li为pi在二进制中末尾0的个数)。

由此得出更改元素值的方法:若将x添加到a[k],则c数组中c[p1]、c[p2]、c[pm](pm≤n<pm+1)受其影响,亦应该添加x。

例如a[1]……a[9]中,a[3] 添加x;

p1=k=3          p2=3+20=4

p3=4+22=8       p4=8+23=16>9

由此得出,c[3]、c[4]、c[8]亦应该添加x。

 

            3.子序列求和

子序列求和可以转化为求由a[1]开始的序列a[1]……a[k]的和S。

而在树状数组中求S十分简单:

       根据c[k]=a[k-2l+1]+ +a[k] (l为k在二进制数中末尾0的个数)

我们从k1=k出发,按照

ki+1=ki-2lki(lki为ki在二进制数中末尾0的个数)   公式一

递推k2,k3,km (km+1=0)。由此得出

S=c[k1]+c[k2]+c[k3] + + c[km]

例如,计算a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]

k1=7

k2= k1-2l1=7-20=6

k3= k2-2l2=6-21=4

k4= k3-2l3=4-22=0

即a[1]+a[2]+ a[3]+a[4]+ a[5]+a[6]+ a[7]=c[7]+c[6]+c[4]

推广到二维,同样也只需要计算由(1,1)开始的矩阵中的数字和,然后通过加减来计算子矩阵中的数字和。

 

                                                                             小结

在上面的例子中我们可以看出采用树结构处理问题时,往往能够缩小搜索范围。这样在每次的查找过程中都能正确地选择由根开始的一条路径,不会出现无用的搜索

由于通常都是选取一条由根开始的路径,所以遍历树的快慢很大程度上取决于树的深度。如何降低树的高度就成了算法的关键。

相对于传统意义上的树,树状数组就其形式并不能称为“树”,但其核心思想却与树结构如出一辙。

<Marvolo原创,严禁转载>

时间: 2024-07-31 14:29:06

树结构在程序设计中的运用的相关文章

第04章 程序设计中的流程控制

/**第四章 程序设计中的流程控制 @选择语句 形式一:if(条件表达式) 单条语句; 形式二:if(条件表达式){ 语句体;} 形式三:if(条件表达式){ 语句体;}else{ 语句体;} 形式四:if(条件表达式){ 语句体;}else if{ 语句体;} 形式五:if(条件表达式){ 语句体;}else if{ 语句体;}else{ 语句体;}=========================================================================

程序设计中的命名

[程序设计中的命名] 在设计过程中好的命名不一定但更大可能会带来好的设计,但是如果坏的命名那一定不会给你带来好的设计.在设计过程,如果你发现你很难命名某一个模块,某个方法时,可能你真正遇到的问题不是难命名的问题,而是这个设计是否真的合理,你或许应该花更多的时间来重新设计一下你的模块. 1.名字应该尽量采用名词 Bad:           Happy Good:          Happiness 2.不要使用类似名字空间的前缀 Bad:           SystemOnlineMessa

[转]在C#程序设计中使用Win32类库

http://blog.163.com/j_yd168/blog/static/496797282008611326218/ C# 用户经常提出两个问题:"我为什么要另外编写代码来使用内置于 Windows 中的功能?在框架中为什么没有相应的内容可以为我完成这一任务?"当框架小组构建他们的 .NET 部分时,他们评估了为使 .NET 程序员可以使用 Win32 而需要完成的工作,结果发现 Win32 API 集非常庞大.他们没有足够的资源为所有 Win32 API 编写托管接口.加以测

程序设计中的一些感悟

这篇感悟写的不错,特此转贴 1)学习应该从基础打起,不要一开始就尝试最高深的技术. 2)每看一本书,不要说这章我以前学习过了,也掌握的很好,因此我可以跳过这一章看更重要的了. 3)对于作业,遇到不会的尽量不要立刻向别人请教.如果实在解决不了的问题,可以先完成你会的,然后把一些特别的难点提炼出来,向高手请教. 3)不要指望书本和行家能帮你解决一切问题,因为并不是所有问题都能由别人教给你. 4)向别人请教问题应该把问题说明白.对于错误提示信息应该原样提供出来,不要按自己理解的信息提供.因为既然你自己

状态机思路在程序设计中的应用

状态机思路在程序设计中的应用 作者: 张俊  发布时间: 2015-09-13 12:20  阅读: 1314 次  推荐: 3   [收藏] 状态机的概念 状态机是软件编程中的一个重要概念,比这个概念更重要的是对它的灵活应用.在一个思路清晰而且高效的程序中,必然有状态机的身影浮现. 比如说一个按键命令解析程序,就可以被看做状态机:本来在A状态下,触发一个按键后切换到了B状态,再触发另一个键后切换到C状态,或者返回到A状态.这就是最简单的按键状态机例子.实际的按键解析程序会比这更复杂些,但这不影

程序设计中关于异常机制的思考

程序的运行过程从来都不是一帆风顺的,运行期间会遇到各式各样的突发状况,如文件打不开.内存分配错误.数据库连不上等等.作为一个进阶过程中的编程人员,思考和处理例外状况极为重要.因为它在很大程度保证了程序的连贯性和稳定性,并为问题的发现提供支撑. 下面就本人在编程过程中有关异常的编程范式做一下总结. 一.面向过程形式 面向过程式的范式将异常的传递都交于函数的参数与返回值来处理,如: bool func ( const InType& input, OutType& output, string

Qt多线程程序设计中,可使用信号和槽进行线程通信

Qt多线程程序设计中,可使用信号和槽进行线程通信.下面是一个简单的示例. 该程序实现了线程中自定义一个信号和槽,定时1秒发送信号,槽响应后打印一条信息. [cpp] view plain copy  #include <QtCore/QCoreApplication> #include <QThread> #include <stdio.h> class MyThread:public QThread { Q_OBJECT public: MyThread(); voi

浅谈“回调”在程序设计中的好处

1:回调还是返回(return) 在写代码的时候,我们经常碰到这样的场景:调用一个函数或者方法时需要返回多个值给上级调用者,如示例: void methodA(){    Wrap w = methodB();     w.one; //use    w.two; } Wrap methodB(){     do something;     return Wrap; } class Wrap{    Type one;    Type two; } 上面是我刚开始写代码时候常用的方式,在多个类

学号20175313 《程序设计中临时变量的使用》第八周

目录 程序设计中临时变量的使用 一.题目要求 二.运行结果截图 三.遇到的问题及其解决方法 四.代码链接 五.心得体会 程序设计中临时变量的使用 一.题目要求 //定义一个数组,比如 int arr[] = {1,2,3,4,5,6,7,8}; //打印原始数组的值 for(int i:arr){ System.out.print(i + " "); } System.out.println(); // 添加代码删除上面数组中的5 ... //打印出 1 2 3 4 6 7 8 0 f