数据结构之树

目录:

    二叉树的结构

    二叉树的性质

    二叉树的遍历

    二叉树的特例

    二叉树典型程序


是一种编程中常常用到的一种数据结构,它的逻辑是:除了根结点之外每个结点只有一个父结点,根结点没有父结点;除了叶结点之外所有结点都有一个或多个子结点,叶结点没有子结点。父结点和子结点之间用指针连接。

    二叉树是一种特殊的树结构,即二叉树中每个结点最多只能有两个子结点。

 

二叉树的结构:

struct BinaryTreeNode
{
    int        m_nValue;
    BinaryTreeNode*    m_pLeft;
    BinaryTreeNode*    m_pRight;
};
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
    }
}


二叉树的性质

    在二叉树的第i层上最多有2 i-1 个节点 。(i>=1)

    二叉树中如果深度为k,那么最多有2k-1个节点。(k>=1)

    n0=n2+1  n0表示度数为0的节点 n2表示度数为2的节点

    在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]+1是向下取整。

    若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:

            (1) i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;  

            (2) 2i>n,则该结点无左孩子,  否则,编号为 2i 的结点为其左孩子结点;

              (3) 2i+1>n,则该结点无右孩子结点,  否则,编号为2i+1 的结点为其右孩子结点

二叉树的遍历

    前序遍历先访问根结点,再访问左子结点,最后访问右子节点;

    中序遍历先访问左子结点,再访问根结点,最后访问右子节点;

    后序遍历先访问左子结点,再访问右子节点,最后访问根结点;

    (前三种遍历都有递归和循环两种不同的实现方式,递归都要比循环实现要简洁)

    宽度优先遍历先访问树的第一层结点,再访问树的第二层结点......一直到最后一层结点。在同一层结点中,以从左到右的循序依次访问。

二叉树的特例

1.二叉搜索树

又叫二叉查找树或者二叉排序树

左子结点总是小于或等于根结点,而右子节点总是大于或等于根结点;

搜索二叉树.png 

2.平衡二叉树

判断平衡二叉树.png


3.红黑树

    红黑树的定义:

        红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由鲁道夫·贝尔发明的,称之为"对称二叉B树",它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在O(logn)时间内做查找,插入和删除,这里的n是树中元素的数目。

    红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。这不只是使它们在时间敏感的应用如实时应用(real time application)中有价值,而且使它们有在提供最坏情况担保的其他数据结构中作为建造板块的价值;例如,在计算几何中使用的很多数据结构都可以基于红黑树。此外,红黑树还是2-3-4树的一种等同,它们的思想是一样的,只不过红黑树是2-3-4树用二叉树的形式表示的。

红黑树的性质:

    红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制的一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

    性质1. 节点是红色或黑色。

    性质2. 根是黑色。

    性质3. 所有叶子都是黑色(叶子是NIL节点)。

    性质4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)

    性质5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

下面是一个具体的红黑树的图例:

image.png


    这些约束确保了红黑树的关键特性: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

    要知道为什么这些性质确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

4.堆    

    最大堆和最小堆;在最大堆中根结点的值最大,在最小堆中根结点的值最小。(需要快速查找最大值或者最小值的问题经常用堆来解决)

5.完全二叉树

    假设一个深度为h的二叉树,除第h层其他各层结点都达到最大,第h层的结点都集中在该层最左边的若干位置上。

6.满二叉树

    满二叉树必为完全二叉树,完全二叉树不一定是满二叉树。


7.B/B-/B+树

B(B-)

image.png

image.png

image.png


B+树

image.png

image.png

以下B-树和B+树的区别参考:http://blog.csdn.net/wwh578867817/article/details/50493940

1.B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)

如下所示B-树/B+树查询节点 key 为 50 的 data。

B-树

B-树

从上图可以看出,key 为 50 的节点就在第一层,B-树只需要一次磁盘 IO 即可完成查找。所以说B-树的查询最好时间复杂度是 O(1)


B+树

B+树

由于B+树所有的 data 域都在根节点,所以查询 key 为 50的节点必须从根节点索引到叶节点,时间复杂度固定为 O(log n)


2.B+树叶节点两两相连可大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找

B+树

根据空间局部性原理:如果一个存储器的某个位置被访问,那么将它附近的位置也会被访问。

B+树可以很好的利用局部性原理,若我们访问节点 key为 50,则 key 为 55、60、62 的节点将来也可能被访问,我们可以利用磁盘预读原理提前将这些数据读入内存,减少了磁盘 IO 的次数。 
当然B+树也能够很好的完成范围查询。比如查询 key 值在 50-70 之间的节点。


3.B+树更适合外部存储。由于内节点无 data 域,每个节点能索引的范围更大更精确

这个很好理解,由于B-树节点内部每个 key 都带着 data 域,而B+树节点只存储 key 的副本,真实的 key 和 data 域都在叶子节点存储。前面说过磁盘是分 block 的,一次磁盘 IO 会读取若干个 block,具体和操作系统有关,那么由于磁盘 IO 数据大小是固定的,在一次 IO 中,单个元素越小,量就越大。这就意味着B+树单次磁盘 IO 的信息量大于B-树,从这点来看B+树相对B-树磁盘 IO 次数少。

B/B+树

从上图可以看出相同大小的区域,B-树仅有 2 个 key,而B+树有 3 个 key。

为什么 MongoDB 使用B-树

MongoDB 是一种 nosql,也存储在磁盘上,被设计用在 数据模型简单,性能要求高的场合。性能要求高,看看B/B+树的区别第一点:

B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)

我们说过,尽可能少的磁盘 IO 是提高性能的有效手段。MongoDB 是聚合型数据库,而 B-树恰好 key 和 data 域聚合在一起


为什么 Mysql 使用B+树

Mysql 是一种关系型数据库,区间访问是常见的一种情况,而 B-树并不支持区间访问(可参见上图),而B+树由于数据全部存储在叶子节点,并且通过指针串在一起,这样就很容易的进行区间遍历甚至全部遍历。见B/B+树的区别第二点:

B+树叶节点两两相连可大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找

其次B+树的查询效率更加稳定,数据全部存储在叶子节点,查询时间复杂度固定为 O(log n)

最后第三点:

B+树更适合外部存储。由于内节点无 data 域,每个节点能索引的范围更大更精确




案例程序:

1.哈夫曼树

哈夫曼树
#include <stdio.h>
#include<stdlib.h>
#define MAXBIT      99
#define MAXVALUE  9999
#define MAXLEAF     30
#define MAXNODE    MAXLEAF*2 -1
typedef struct 
{
    int bit[MAXBIT];
    int start;
} HCodeType;        /* 编码结构体 */
typedef struct
{
    int weight;
    int parent;
    int lchild;
    int rchild;
    int value;
} HNodeType;        /* 结点结构体 */
/* 构造一颗哈夫曼树 */
void HuffmanTree (HNodeType HuffNode[MAXNODE],  int n)
{ 
    /* i、j: 循环变量,m1、m2:构造哈夫曼树不同过程中两个最小权值结点的权值,
        x1、x2:构造哈夫曼树不同过程中两个最小权值结点在数组中的序号。*/
    int i, j, m1, m2, x1, x2;
    /* 初始化存放哈夫曼树数组 HuffNode[] 中的结点 */
    for (i=0; i<2*n-1; i++)
    {
        HuffNode[i].weight = 0;//权值 
        HuffNode[i].parent =-1;
        HuffNode[i].lchild =-1;
        HuffNode[i].rchild =-1;
        HuffNode[i].value=i; //实际值,可根据情况替换为字母  
    } 
    /* 输入 n 个叶子结点的权值 */
    for (i=0; i<n; i++)
    {
        printf ("Please input weight of leaf node %d: \n", i);
        scanf ("%d", &HuffNode[i].weight);
    } 
    /* 循环构造 Huffman 树 */
    for (i=0; i<n-1; i++)
    {
        m1=m2=MAXVALUE;     /* m1、m2中存放两个无父结点且结点权值最小的两个结点 */
        x1=x2=0;
        /* 找出所有结点中权值最小、无父结点的两个结点,并合并之为一颗二叉树 */
        for (j=0; j<n+i; j++)
        {
            if (HuffNode[j].weight < m1 && HuffNode[j].parent==-1)
            {
                m2=m1; 
                x2=x1; 
                m1=HuffNode[j].weight;
                x1=j;
            }
            else if (HuffNode[j].weight < m2 && HuffNode[j].parent==-1)
            {
                m2=HuffNode[j].weight;
                x2=j;
            }
        } /* end for */
            /* 设置找到的两个子结点 x1、x2 的父结点信息 */
        HuffNode[x1].parent  = n+i;
        HuffNode[x2].parent  = n+i;
        HuffNode[n+i].weight = HuffNode[x1].weight + HuffNode[x2].weight;
        HuffNode[n+i].lchild = x1;
        HuffNode[n+i].rchild = x2;
  
        printf ("x1.weight and x2.weight in round %d: %d, %d\n", i+1, HuffNode[x1].weight, HuffNode[x2].weight);  /* 用于测试 */
        printf ("\n");
    } /* end for */
} /* end HuffmanTree */
  
//解码 
void decodeing(char string[],HNodeType Buf[],int Num)
{
  int i,tmp=0;//,code[1024];
  int m=2*Num-1;
  char *nump;
  char num[1024];
  for(i=0;i<sizeof(string)/sizeof(string[0]);i++)
  {
   if(string[i]=='0')
  num[i]=0;        
  else
  num[i]=1;                    
  } 
  i=0;
  nump=&num[0];
   
 while(nump<(&num[sizeof(string)/sizeof(string[0])]))
 {   
      tmp=m-1;
      while((Buf[tmp].lchild!=-1)&&(Buf[tmp].rchild!=-1))
      {
       if(*nump==0)
       {
         tmp=Buf[tmp].lchild ;          
       } 
       else tmp=Buf[tmp].rchild;
       nump++;
  } 
  printf("%d",Buf[tmp].value);                                  
 }
}
int main(void)
{
    HNodeType HuffNode[MAXNODE];            /* 定义一个结点结构体数组 */
    HCodeType HuffCode[MAXLEAF],cd;       /* 定义一个编码结构体数组, 同时定义一个临时变量来存放求解编码时的信息 */
    int i, j, c, p, n;
    char pp[100];
    printf ("Please input n:\n");
    scanf ("%d", &n);
    HuffmanTree (HuffNode, n);
    for (i=0; i < n; i++)
    {
        cd.start = n-1;
        c = i;
        p = HuffNode[c].parent;
        while (p != -1)   /* 父结点存在 */
        {
            if (HuffNode[p].lchild == c)
                cd.bit[cd.start] = 0;
            else
                cd.bit[cd.start] = 1;
            cd.start--;        /* 求编码的低一位 */
            c=p;                    
            p=HuffNode[c].parent;    /* 设置下一循环条件 */
        } /* end while */
         
        /* 保存求出的每个叶结点的哈夫曼编码和编码的起始位 */
        for (j=cd.start+1; j<n; j++)
        { HuffCode[i].bit[j] = cd.bit[j];}
        HuffCode[i].start = cd.start;
    } /* end for */
     
    /* 输出已保存好的所有存在编码的哈夫曼编码 */
    for (i=0; i<n; i++)
    {
        printf ("%d 's Huffman code is: ", i);
        for (j=HuffCode[i].start+1; j < n; j++)
        {
            printf ("%d", HuffCode[i].bit[j]);
        }
        printf(" start:%d",HuffCode[i].start);
        
        printf ("\n");
         
    }
    printf("Decoding?Please Enter code:\n");
    scanf("%s",&pp);
    decodeing(pp,HuffNode,n);
    //getch();
    return 0;
}

image.png

image.png

2.二叉树的深度

题目描述 

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

思路

第一种方法是使用递归方法执行,第二种方法是使用循环(里面使用了队列)

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
     /**
 * 递归写法
 * @return
 */
    public int TreeDepth(TreeNode root) {
                if(root==null){
            return 0;
        }
           
        int nLeft=TreeDepth(root.left);
        int nRight=TreeDepth(root.right);
         
        return nLeft>nRight?(nLeft+1):(nRight+1);
    }
    /**
 * 非递归写法:层次遍历
 * @return
 */
    public int TreeDepth2(TreeNode pRoot)
    {
        if(pRoot == null){
            return 0;
        }
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.add(pRoot);
        int depth = 0, count = 0, nextCount = 1;
        while(queue.size()!=0){
            TreeNode top = queue.poll();
            count++;
            if(top.left != null){
                queue.add(top.left);
            }
            if(top.right != null){
                queue.add(top.right);
            }
            if(count == nextCount){
                nextCount = queue.size();
                count = 0;
                depth++;
            }
        }
        return depth;
    }
}


3.判断平衡二叉树

题目描述

输入一棵二叉树,判断该二叉树是否是平衡二叉树。

public class Solution {
    public boolean IsBalanced_Solution(TreeNode root) {
        if(root == null)
            return true;
        
        if(Math.abs(getHign(root.left) - getHign(root.right)) > 1)
            return false;
        
        return IsBalanced_Solution(root.left) && IsBalanced_Solution(root.right);
    }
    
    private int getHign(TreeNode root){
        if(root == null)
            return 0;
        return max(getHign(root.left), getHign(root.right)) + 1;
    }
    
    private int max(int a, int b){
        return (a > b) ? a : b ;
    }
}

4.重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

/**
 * Definition for binary tree
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
         TreeNode root=reConstructBinaryTree(pre,0,pre.length-1,in,0,in.length-1);
        return root;
    }
    private TreeNode reConstructBinaryTree(int [] pre,int startPre,int endPre,int [] in,int startIn,int endIn) {
        
        if(startPre>endPre||startIn>endIn)
            return null;
        TreeNode root=new TreeNode(pre[startPre]);
        
        for(int i=startIn;i<=endIn;i++)
            if(in[i]==pre[startPre]){
                root.left=reConstructBinaryTree(pre,startPre+1,startPre+i-startIn,in,startIn,i-1);
                root.right=reConstructBinaryTree(pre,i-startIn+startPre+1,endPre,in,i+1,endIn);
            }
        
        return root;
    }
}

5.树的子结构

输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
    public boolean HasSubtree(TreeNode root1,TreeNode root2) {
        boolean result = false;
        if(root1!=null&&root2!=null){
            if(root1.val==root2.val){
                result = isSubtree(root1,root2);
            }
            if(!result)
                result = HasSubtree(root1.left,root2);
            if(!result)
                result = HasSubtree(root1.right,root2);
        }
        return result;
    }
    
    public boolean isSubtree(TreeNode root1, TreeNode root2)
    {
       if(root2 == null)
           return true;
        if(root1 == null)
            return false;
        if(root1.val!=root2.val)
            return false;
        return isSubtree(root1.left,root2.left)&&isSubtree(root1.right,root2.right);
    }
}

6.二叉树的镜像

题目描述

操作给定的二叉树,将其变换为源二叉树的镜像。

输入描述:

二叉树的镜像定义:源二叉树

         8

        /  \

       6   10

      / \  / \

     5  7 9 11

     镜像二叉树

         8

        /  \

       10   6

      / \  / \

     11 9 7  5

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
    public void Mirror(TreeNode root) {
        if(root==null)
            return;
        TreeNode tmpnode = root.left;
        root.left = root.right;
        root.right = tmpnode;
        
        Mirror(root.left);
        Mirror(root.right);
    }
}

7.从上往下打印二叉树

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
    public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
       ArrayList<Integer> arrayList = new ArrayList<Integer>();
        if (root==null){
            return arrayList;
        }
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.add(root);
        while (!queue.isEmpty()){
            TreeNode node= queue.poll();
            arrayList.add(node.val);
            if (node.left!=null){
                queue.add(node.left);
            }
            if (node.right!=null){
                queue.add(node.right);
            }
        }
        return arrayList;
    }
}

8.二搜索叉树的后序遍历序列

BST的后序序列的合法序列是,对于一个序列S,最后一个元素是x (也就是根),如果去掉最后一个元素的序列为T,那么T满足:T可以分成两段,前一段(左子树)小于x,后一段(右子树)大于x,且这两段(子树)都是合法的后序序列。完美的递归定义 : )

public class Solution {
    public boolean VerifySquenceOfBST(int [] sequence) {
        if(sequence.length == 0) return false;
        return IsTreeBST(sequence, 0, sequence.length-1);
    }
    public boolean IsTreeBST(int [] sequence,int start,int end ){
        if(end <= start) return true;
        int i = start;
        for (; i < end; i++) {
            if(sequence[i] > sequence[end]) break;
        }
        for (int j = i; j < end; j++) {
            if(sequence[j] < sequence[end]) return false;
        }
        return IsTreeBST(sequence, start, i-1) && IsTreeBST(sequence, i, end-1);
    }
}

9.二叉树中和为某一值的路径

输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。

import java.util.ArrayList;
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
        ArrayList<ArrayList<Integer>> arr = new ArrayList<ArrayList<Integer>>();
        if (root == null)
        return arr;
        ArrayList<Integer> a1 = new ArrayList<Integer>();
        int sum = 0;
        pa(root, target, arr, a1, sum);
        return arr;
    }
     
    public void pa(TreeNode root,int target, ArrayList<ArrayList<Integer>> arr, ArrayList<Integer> a1, int sum) {
        if (root == null)
        return;
        sum += root.val;
         
        if (root.left == null && root.right == null) {
            if (sum == target) {
            a1.add(root.val);
            arr.add(new ArrayList<Integer>(a1));
            a1.remove(a1.size() - 1);
         
            }
            return;
     
        }
     
        a1.add(root.val);
        pa(root.left, target, arr, a1, sum);
        pa(root.right, target, arr, a1, sum);
        a1.remove(a1.size() - 1);
         
    }
}

10.最小的k个数

题目描述

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,88个数字,则最小的4个数字是1,2,3,4,

image.png

image.png

我这里采用堆的方式解决:

import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Comparator;
public class Solution {
   public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
       ArrayList<Integer> result = new ArrayList<Integer>();
       int length = input.length;
       if(k > length || k == 0){
           return result;
       }
        PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(k, new Comparator<Integer>() {
 
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        for (int i = 0; i < length; i++) {
            if (maxHeap.size() != k) {
                maxHeap.offer(input[i]);
            } else if (maxHeap.peek() > input[i]) {
                Integer temp = maxHeap.poll();
                temp = null;
                maxHeap.offer(input[i]);
            }
        }
        for (Integer integer : maxHeap) {
            result.add(integer);
        }
        return result;
    }
}

注:这里使用了priorityQueue:PriorityQueue也叫优先队列,所谓优先队列指的就是每次从优先队列中取出来的元素要么是最大值(最大堆),要么是最小值(最小堆)。我们知道,队列是一种先进先出的数据结构,每次从队头出队(移走一个元素),从队尾插入一个元素(入队)。参考:http://blog.csdn.net/u011116672/article/details/50997622