Redis研究-3.3数据结构之树与查找、排序等

1.树相关的内容
  1.1 Tree概念
      树是n(n>=0)个节点的有限集。n=0的时候,咱们把它叫作空树。在任何一非空树中知足两个特色:(1) 有且只有一个叫作根的节点。(2)n>1时,其他节点可分为m(m>0)个 互不相交的有限集T1,T2,...其中每个结合自己也是一棵树。
     上面的这概念用到了递归的定义。

    树的相关概念:
    节点的度:是指这个节点的子树的个数。
    树的度:是指树的节点中拥有最大多数量的节点的度。
   节点的关系:节点的子树的根叫作该节点的 孩子,相应的,该节点成为孩子的 双亲(父母同体)。同一个双亲的孩子之间叫作 兄弟,节点的祖先是从根节点到该节点所经历的分支上的全部的节点。以某节点为根的子树中的任一节点都是该节点的 子孙
    节点的层次:节点的层次从根开始定义,根为第一层,根的孩子为第二层,以此类推。双亲在同一层的互为堂兄弟。
    树的深度:树中节点的最大层次叫作树的深度或者高度。
咱们用一颗树来讲清楚上面的概念。
 
其中,节点D的度是3,由于他有3颗子树,整棵树的度是4,由于整棵树中全部的节点的最大的度是G,他有4颗子树。节点D、E叫作兄弟。
D、E、F是堂兄弟,并且树的深度为4.
1.2 树的结构和存储
   经过上面的描述,咱们知道,一棵树是由节点来构成的,所以树节点是最基本的组成单位,那么,一棵树的节点有怎么表示或者说是怎么构成的呢?咱们知道,一棵树的节点他不多是从石头里面蹦出来的(除了跟元素),所以,他必须存在双亲,同时,他有可能存在子树(这里指子节点)。那么,咱们就能够获得基本的节点结构以下:
typedef struct TreeNode{
    struct TreeNode *parent;//双亲
    struct TreeNode **childs;//孩子数组
    int childCount;//孩子的数量
    void *data;//节点的数据
}TreeNode;



可是,咱们单从节点自己来讲,上述的表示方法,存在了不少的冗余信息,所以,咱们从最平凡的节点信息来看,也就是说,对于节点,咱们只考虑他的双亲和自身的数据,所以,能够表示为下面的结构:
typedef struct SNode{
    struct SNode *parent;//双亲
    void *data;//节点的数据
}SNode;



上述的代码的双亲,咱们是用指针来表示的,为了简化咱们的表述,咱们能够把上述的双亲的指针表述方式换成一个整数型的位置数据来表示(根节点的位置数据为-1)。
 
typedef struct NNode{
   int parentPosition;//双亲
    void *data;//节点的数据
    int32_t curPosition;//当前节点的坐标
}NNode;
 那么,针对咱们这章节最开始提到的那颗树,咱们能够表示为下面的表格:
curpositon data parent
0 A -1
1 B 0
2 C 0
3 D 1
4 E 1
5 F 2
6 G 2
7 H 3
8 I 3
9 J 3
10 K 6
11 L 6
12 M 6
13 N 6
 
 这种表示方法对咱们找到某节点的双亲是很方便的,可是你想想,要想找到某节点的孩子,是否是很费劲,所以,咱们须要作一下改进,也就是说,咱们要考虑节点的度的问题,改进后,能够获得下面的节点结构定义:
typedef struct CPNode{
    int32_t parentPosition;//双亲
    void *data;//节点的数据
    int32_t curPosition;//当前节点的坐标
    int32_t *childs;//节点的孩子位置坐标数组
    int32_t degree;//节点的度
}CPNode;



经过此次的改进,咱们在某节点上,能很快的找到双亲和子树。为此,咱们可以获得最终的树的结构定义:
typedef struct tree{
  int32_t tail;//树的根节点的标号
  int32_t nodeCount;//树的节点数量
  struct CPNode *nodes;//节点数组
}tree;



好啦,咱们把这种拓展到通常的状况,获得常规的树节点表示方式和树的表示方式:



typedef struct gTreeNode{
void *data;//节点数据
int32_t degree;//节点的度
struct gTreeNode *parent;//双亲,向前指针
struct gTreeNode *next;//使用链表来存储兄弟节点,向后指针
}gTreeNode;



//树结构定义
typedef struct g{
struct gTreeNode *tail;
int32_t nodes;//树节点数量
}gTreeNode;



 
OK,树的结构已经基本完成了,下面说说一些常规的树的定义(不管他有多么特殊,均可以使用上述的树的相关表示)
 
2.二叉树
二叉树就是在通常树的定义上加了一个限制条件,这个限制条件就是一个节点的度不能超过2.这就是二叉树。
 
在二叉树里面也存在集中特殊的二叉树:
  2.1 斜树:是指二叉树的节点要么只有左子树,要么就只有右子树的状况,就叫作斜树;
  
  2.2满二叉树:是指在二叉树中,若是全部的分支节点都存在左子树和右子树,而且,全部的叶子节点都在同一层上。
 
  2.3 彻底二叉树:这个特列是相较于满二叉树来定义的,意思是说,对一个具备n个节点的二叉树,若是编号为i的节点与一样深度的满二叉树中编号为i的节点的二叉树中位置彻底相同,那么,这就是二叉树啦,换句话说,彻底二叉树是满二叉树的一部分,且全部叶子节点都是在最下面两层,并且最下层的叶子节点,必定是从最左边连续开始的,若是倒数第二层有叶子节点,那么,这些节点就应该是在右部的连续位置,若是节点的度为1,那么这个节点只能有一个左孩子。
那么,针对二叉树,到底有哪些性质呢?
性质1:二叉树的第i层上最多有个节点。
性质2:深度为K的二叉树,最多有-1个节点。
        一层:2^0=1=2^0=2^1-1
        二层:1+2^(2-1)=1+2^1=2^2-1
        三层:2^2-1+2^(3-1)=2^3-1
性质3:若是一颗二叉树的终端节点为m,度为2的节点数量为n,那么,m=n+1;
    这个性质是怎么获得的呢?咱们知道,对一颗二叉树来讲,树的节点分为三种状况:度为1的节点n0、叶子节点m、度为2的节点n,那么树的节点数量T=n0+m+n.
  同时,咱们知道,全部二叉树节点间的连线的数量时等于总节点数减1的。同时,咱们也知道,对于一个度为2的节点,他的出线应该是2条,所以,度为2的节点对应的连线应该=2n.而对于终端节点是没有出线的,对于度为1的节点,他的出线只有1,所以,从连线的角度来看节点的数量T=2n+n0+1,所以,能够获得n0+m+n=2n+n0+1,从而获得m=n+1;
      性质4:具备n个节点的彻底二叉树的深度为 +1.
       怎么算出来的?咱们知道,一个满二叉树的节点数量n= -1,其中k为这个二叉树的度,那么,这里的k= .
      根据彻底二叉树的定义,他的节点数量最可能是能够等于一样深度的满二叉树节点数量 -1的,可是必定是大于 .也就是说 ,由于n必须是整数,能够取对数,就能够获得上面的结论。
 
 
2.4 二叉树的结构和存储
   经过上面的学习,咱们知道,二叉树中的节点有应该具有四个属性:数据域、左孩子、右孩子、双亲,所以,对于二叉树来讲,咱们能够从新定义上面提到的树的节点结构的定义:
typedef struct BiTNode{
    void *data;//数据域
    struct BiTNode *left;//左孩子
    struct BiTNode *right;//右孩子
    struct BiTNode *parent;//双亲
}BiTNode;



2.5 遍历二叉树
   学习到这里,树以及二叉树相关的概念和定义已经搞定,那么,针对一个树来讲,咱们要遍历其中的节点,怎么办呢?我记得我在读本科的时候,有次考试就是考了这种题目,NND,当时不是很清楚,结果可想而知。
 在遍历二叉树的时候,咱们采用的方法取决于咱们选择的方向。
咱们先看一颗二叉树,而后再一个方法一个方法的来看是怎么实现的
 
//二叉树、节点定义
typedef struct BiTNode{
   void *data;//数据域
    struct BiTNode *left;//左孩子
    struct BiTNode *right;//右孩子
    struct BiTNode *parent;//双亲
}BiTNode,*BiTree;
2.5.1 前序遍历
    前序遍历的规则就是从根节点开始,若是二叉树为空,则直接返回,不然 先访问根节点,而后前序遍历左子树,再前序遍历右子树。
  
获得的结果是:ABDHIEJCFG
//前序遍历二叉树
void preOrderTraverse(BiTree t){
    if(NULL==t||NULL==t->data) return ;
    //操做每一个节点,好比说是打印节点
    printf("%s",t->data);
    preOrderTraverse(t->left);
    preOrderTraverse(t->right);
}


2.5.2 中序遍历 node

      中序遍历的规则就是从根节点开始,若是二叉树为空,则直接返回,不然从 根节点开始(并非先访问根节点),中序遍历根节点的左子树,而后访问根节点,最后访问根节点的右子树。
最终的结果是HDIBJEAFCG
 
//中序遍历二叉树
void inOrderTraverse(BiTree t){
    if(NULL==t||NULL==t->data) return ;
    inOrderTraverse(t->left);
    //操做每一个节点,好比说是打印节点
    printf("%s",t->data);
    inOrderTraverse(t->right);
}



2.5.3 后序遍历
    后序遍历的规则是,若是树为空,则直接返回,不然从左到右先叶子后节点的方式来访问左右子树,最后访问根节点。
最终的访问结果是:HIDJEBFGCA
//后序遍历二叉树
void inOrderTraverse(BiTree t){
    if(NULL==t||NULL==t->data) return ;
    inOrderTraverse(t->left);
    inOrderTraverse(t->right);
        //操做每一个节点,好比说是打印节点
    printf("%s",t->data);
}



 
2.5.4 层序遍历
  层序遍历的规则是,若果树为空,则直接返回,不然先从根的第一层开始,在访问同一层的时候,先左后右。
 
 
 
咱们回到先前定义的二叉树节点,你会发现会存在一个很严重的问题,是什么问题呢?
 咱们定义的节点有三个域,一个是左孩子指针域,一个是右孩子指针域,另外是数据域。可是,咱们看一个二叉树的例子,就能清楚的发现这种作法,有一个很大的弊端。
 
(注意:上图我是忽略了每一个节点的双亲指针域的状况了的展示状况)咱们看到上面的不少节点的指针域是没有充分利用起来的,怎么办呢?咱们知道,对于一个有n个节点的二叉树,一共有2n个指针域,同时又n-1条连线,也就是说存在2n-(n-1)=n+1个空指针域,上面的树节点的空指针域有11个空指针域。
  咱们用中序遍从来遍历上面的二叉树,获得的结果是HDIBJEAFCG,问题来了,你能很容易的知道任意节点的前驱和后继么?若是想要知道,咱们必须得先遍历一次才知道,所以,咱们能够想象一下,在创建节点的时候,是否是能够明确的标注呢?咱们所以把这种具备前驱和后继的树称做线索二叉树。
改进后,咱们获得的节点的结构定义就变成了下面的定义:
/**
* Link=0 表示左右孩子指针标示
*Thread=1 表示前驱或后继指针标示
*/
typedef enum {Link,Thread} Tag;
typedef struct tagBiTreeNode{
    struct tagBiTreeNode *left;
    struct tagBiTreeNode *right;
    void *data;
    Tag LTag;
    Tag RTag;
}tagBiTreeNode,*tagBiTree;



 
好啦,二叉树的内容基本搞定了,可是你发现没有,咱们针对的实现基本都是基于二叉树的,可是,咱们平时生活中的又有多少是知足二叉树结构的呢?咱们可否把常规的树转化为 二叉树呢?
 
3.树、森林、二叉树的转换
   我把后面的内容放在这个章节的后续章节来讲吧,貌似这章节的内容太多了
 
 
不少东西可能会没有说清楚,欢迎发表意见,同时能够加QQ:359311095探讨
相关文章
相关标签/搜索