Redis研究-3.3数据结构之树与查找、排序等
Redis研究-3.3数据结构之树与查找、排序等
会飞的杨先生 发表于2年前
Redis研究-3.3数据结构之树与查找、排序等
  • 发表于 2年前
  • 阅读 1404
  • 收藏 5
  • 点赞 0
  • 评论 0

腾讯云 新注册用户 域名抢购1元起>>>   

摘要: 通过上一章节的学习,我们了解了一些关于hash的实现相关方面的东西,但是里面还涉及到几个方面的知识,我们是还不太清楚的,比如说是查找,搜索(查找是搜索的特例)以及排序。因此为了能了解更多的底层知识,我觉得这一章节有必要先说说有关树的概念、排序、查找等方面的内容,目的是为了总结一下上一章节里面的提到的开放式寻址(在解决键冲突时候用到的方法)的原因以及为下一章节来要说道的跳跃表做个知识铺垫。
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 中序遍历

      中序遍历的规则就是从根节点开始,如果二叉树为空,则直接返回,否则从 根节点开始(并不是先访问根节点),中序遍历根节点的左子树,然后访问根节点,最后访问根节点的右子树。
最终的结果是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探讨
共有 人打赏支持
粉丝 10
博文 14
码字总数 30689
×
会飞的杨先生
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: