关于树结构的非线性表编程在数据结构中能够说占据了半壁江山,其中涉及的知识点繁杂,但也是数据结构体现运算优化的核心所在,下面咱们将较为初步且系统得讨论数据结构中一系列有关树的表示。c++
首先咱们再次明确树的形式化概念:编程
树是n个节点的有限集合,这个集合知足如下的条件:数组
1) 有且仅有一个节点没有前件。数据结构
2) 除根外,其余的全部节点都有且仅有一个前件。优化
3) 除去根之外,其余每一个节点都经过惟一的路径链接根上。每一个节点的前件称为该节点的父节点,后件称为该节点的子节点。spa
这篇文章主要包含以下4个知识点:设计
(1) 用树的遍历求解层次性问题。code
(2) 用树结构支持并查集。blog
(3) 用树状数组同济子树权和。递归
(4) 用四叉树求解二维空间问题。
首先咱们讨论树在计算机中的表示,这不只仅在处理一些数据结构的题目中很重要,在一些树形动态规划的问题上,咱们首先要完成的也是树的表示。
方法1:双亲表示法。
在高级语言中咱们容易操做和定义的是线性结构,即数组这样的数据结构,所以咱们考虑非线性结构的程序语言表示的时候,基本原则就是将其向线性数据结构转化。给出一个树结构,根据树的定义咱们知道,除了根节点,每一个节点有且仅有一个父节点。所以咱们定义双亲表示数组f[].其中f[i]表示节点i的父节点的节点序号。
方法2:多重链表法
这里咱们经常用到c++语言stl库中的vector类。对于给定的一棵树,咱们利用”vector<int> tree[n]”这样一条语句,而后定义tree[i].push_back(j)表示j做为i的一个子节点。通俗来讲,这里咱们将树结构扩成了一个二维数组,而利用vector类则是为了更好的节约空间资源。
通常题目中,输入的树结构每每是对于给定的n个节点的树结构,输入n-1个有序节点对用于表示父子关系,这时候每每用方法2表示树是比较常见的,而每每在通常题目中,须要多种表示方法并用,由于各个树结构的表示方式都有各自的优势(访问树自身某方面的信息时比较快)。
用树的遍历求解层次性问题:
最近公共祖先问题:
给出一个n节点的树结构的n-1对有序顶点对<x , y>,表示树结构中的一条边且x是父节点,y是子节点。最后输入一对节点序号<a, b>,编写程序
最近公共祖先代码以下:
//poj 1330.
#include<cstdio> #include<cstring> #include<vector> using namespace std; const int N = 10000; vector<int > a[N]; int f[N] , r[N]; void DFS(int u , int dep)//从dep层的u节点处罚,先序遍历计算每一个节点的层次 { r[u] = dep; for(vector<int>::iterator it = a[u].begin();it != a[u].end();++it){ DFS(*it , dep + 1); } } int main(){ int casenum , num , n , i , x , y; scanf("%d" , &casenum); for(num = 0;num < casenum;num++){ scanf("%d" , &n); for(i = 0;i < n;i++) a[i].clear(); memset(f , -1 , sizeof(f)); for(i = 0;i < n - 1;i++){//n个节点的树 , n-1条边 scanf("%d %d" , &x , &y);//树种的一个边<x , y>,x是父节点,y是子节点; x--;y--; a[x].push_back(y); f[y] = x; } for(i = 0;f[i] >= 0;i++); DFS(i , 0); scanf("%d %d" , &x , &y); x--;y--; while(x != y){ if(r[x] > r[y]) x = f[x]; else y = f[y]; } printf("%d\n" , x + 1); } }
用树结构支持并查集:
在通常的数据结构的教材中,集合与图、树同样,都是群聚类的非线性表,可是集合更加侧重于包含关系而忽略一个集合中各个元素之间的先后件关系。在一些问题中,咱们须要将n个元素划分红若干组,每一个组视为一个集合,一般须要涉及集合的合并与查找,所以咱们称其为并查集。
并查集须要支持以下的操做:
(1) Make_set(x):加入单个元素x到集合S中。
(2) Join(x,y),把x、y所在的不一样集合进行合并。
(3) Set_find(x):获得x所在集合S的表明元。
下面咱们来讨论并查集的存储结构。理论上来讲,并查集有链结构和树结构两种,可是树结构在完成各个操做时的效率更加优良,咱们便直接介绍树结构。
咱们用一棵树结构表示集合S={s1,s2,s3,…,sn}.因为咱们仅仅是用树结构来表示集合,所以这棵树中的边关系,仅仅是体现了合并操做的先后顺序,边关系不一样时,所表达的集合是彻底等价的。紧接着,因为树结构依然不能直接存储,咱们还要想办法将其转化成线性存储结构。咱们如何来表征这样一个集合的特征呢?咱们选择一个集合的表明元(也就是树结构表示的并查集的根节点),咱们定义set[x]表示元素x所在集合的表明元,而若是x是集合S的表明元,则令set[x] = -1(这种表示方法能够理解为并查集 -> 树结构表示 -> 双亲表示法).那么基于这样的定义,咱们可以看到,set[x]与 set[y]的相等关系即可以做为两个元素x、y是否在同一集合的指标了。
查找过程:
对于给出的元素x,咱们想要找到x所在集合的表明元,也能够说成是x所在树结构的根节点。直接访问set[x]咱们发现存在这样一个问题,x的根节点y在某一次合并操做以后合并到了一个更大的集合,本来是根节点的y变成了树结构中的分支节点,所以咱们须要沿树结构继续向上找,咱们会遇到相同的问题,所以须要设计一个递归机制。同时为了之后访问的方便,咱们采用路径压缩的手法,来使得从x出发找到根节点r的路径通过的全部节点i的set[i]都变成r.
int set_find(int p){ if(set[p] < 0) return p; //找到根节点/集合表明元 return set[p] = set_find(set[p]); }
合并过程:
合并过程就显得很简单,假设咱们想要合并元素x、y所在集合,咱们只须要分别找到x、y所在集合的表明元p , q,使set[p] = q便可,固然yekeyi 交换p、q的位置。即从x、y所在集合的表明元中选择任意一个成为新的集合的表明元。
void join(int x , y){ p = set_find(p); q = set_find(q) if(p != q) set[q] = p; }