数据结构之---树

来源:互联网 发布:c语言教程 编辑:程序博客网 时间:2024/05/19 12:39

这里说得树是基本的树结构,至于高级树结构我会慢慢更新上来的。

先说我们为什么要用树??? :如果有1000条数据,你想找到“s”在哪,如果在第999的位置呢?有人可能说那倒过来,那你倒过来他要是第一个呢??效率就很低吧?所以二叉树大部分的运行时间平均是O(logN),上面说的那种是O(N),O()为大欧记法,不明白请稍微了解下。想了解二叉树的效率还要了解下二分法查找。

什么是树:把大树倒过来, 把它倒过来(你倒立也行)就是树状结构图。他得模样就是树,树根就是根节点(父亲),树梢就是边,树梢的每一个拐弯处就是子节点(儿子),有多少层就叫深度。那么图4-2从A到Q。深度就为4。

这货就是一颗一般的树了,,, 为什么要用一般。。 因为还有很多特殊的树(B树,avl,红黑等等)。

那么什么叫二叉树??顾名思义!最多两个树叉子,如下图每个节点最多只有两个节点就叫二叉树(听起来像脏话2X。。)。

你以为这就完啦?? 哈哈哈哈!!天真,还有完全二叉树跟满二叉树



满二叉树就是“丰满”的二叉树。 那就是所有子节点没有缺失的二叉树。看起来很丰满(根节点1个,二层子节点2个。三层4个,四层8个。。。)

那么完全二叉树怎么回事? 在二叉树的基础上,把最最最下面的子节点的右边删了就叫完全二叉树(删除几个都行,全部都干掉包括左边的也行。但是必须挨个删除,不能跳着删)。这样的话就变成残疾了不是么??所以大家可以理解为(从右脚开始残疾的二叉树,也许俩脚都残疾了)。

注意:有很多树形结构,但是你要从基础开始后续的高级结构都是初级结构的变体。

记忆比喻:这么形象。。。就是树。。


树怎么存?:我们都知道物理结构上只有表跟链。物理结构是没有树跟图结构的,那只能吧他转成表结构去存储了,顺序存储只适用于满二叉树,完全二叉树。如一图4-2如果用数组来存,先吧空元素填满。因为数组是连续的内存,不填空得话怎么确保下一个元素在准确的位置,然后从上到下从左到右的存。顺序是1-2-3-4...i,第i个位置的下标就是i-1。这样就很浪费内存,因为有很多空。

链式存储:因为链有双指针域(指针地址),一个指向左孩子,一个指右孩子。第二种是,可以增加一个指针域存储他得父母。还有上一个兄弟/下一个父母方式,按照下图的箭头存储,一个存上一个兄弟一个存下一个孩子。这样可以吧任意一个树转换成二叉树。

这样我们是不是吧一棵树转成二叉树了呢,为什么要用二叉树,因为他拥有很多特性。还有很多比较方便的算法。

操作方式

树结构都是递归的分为三种,如下图顺序

先根遍历:先访问根节点然后遍历左子树,最后遍历右子树。先遍历跟节点-然后是以+为根节点的子树+然后是a然后是右子树X然后是b..形成-  +a*b-cd/ef

中根遍历:中序遍历左子树,访问根节点,最有中序右子树。先遍历左子树a然后以+为根节点的根+,遍历以X为根节点的左子树b..形成 a+b*c-d  -  e/f

后根遍历:后序遍历左子树,后序右子树,最后访问根节点。先遍历左子树a然后以X为根节点的左子树b,然后是C和D,访问跟节点-,然后x,+形成 abcd-*+ef/  -

上面加大得字体就是代码实现的递归顺序,下面是递归代码。

Status PreOrderTraverse(BiTree T,Status(* Visit)(TElem Type)){if(T){Visit(T->data)//根节点PreOrderTraverse(T->lchild,Visit)//左孩子PreOrderTraverse(T->rchild,Visit);//右孩子}    else return OK;}


接下来是线索二叉树:他就非常简单,地址域变成4个就可以了,分别存储做孩子右孩子和左孩子右孩子的线索,比如是否为空还是存在,为左右存储节点提供线索。


二叉查找树/二叉搜索树,这个是最适合搜索的树类结构之一,以根节点为中心,左边的子树集合永远比根节点小,右边的子树集合永远比根节点大,下图只有左边的是二叉查找树。因为根为6那左边的图的左节点是没有比6大得。但右图的最后有一个左侧的根节点为7大于6,所以他就不是二叉查找树,7改成5二叉查找树依然成立。

如上图结构要查找key(数字只是key,并不是实际值)就相当容易。平均深度为O(logN)。 

创建一棵树:

MakeEmpty(SearchTree T){   if(T != NULL){MakeEmpty(T->Left)//先建立左树MakeEmpty(T->Right);        //右树      free(T);   }     return NULL;}
查找:

查找就相对简单,跟结构规则一样的。如果比要查找的X大就往左节点查找,小就右节点

Find(ElementType X,SearchTree T){<span style="white-space:pre"></span>if(T == null){<span style="white-space:pre"></span>return null;<span style="white-space:pre"></span>if(X<T ->Element)<span style="white-space:pre"></span>return Find(X,T->Left);<span style="white-space:pre"></span>//X大于T元素,左查找递归<span style="white-space:pre"></span>else<span style="white-space:pre"></span>if(X> T->Element)        //如果X大于T元素,右查找递归

else return T;

}}

那么如何查找最大跟最小的???:

如上图查找,如果左节点的下一个元素为空,那他就是最后一个元素,我们根据二叉查找树的规则可以判断,那就是最小的,最大的就是最右边为空的元素。

插入也是按照规则,如果比他大就插入右边,相反就左边。

删除是最麻烦的,首先要判断如果他没有子元素就可以直接删除。那么如果他有子元素怎么办。特别是左右都有孩子怎么办?

如果他有单个子元素:

要删除的为X上一个元素为Y,下一个元素为Z。由于X下面有子孩子Z所以想删除X必须让Y元素跨过X直接连接到Z,然后把中间的元素X删掉。

左右都有树的话,找到要删除节点右子树的最小的节点,因为他不可能有左孩子,所他跟要删除的对象互换位置然后用删除单个孩子的方法删除。


AVL树


如图4-28,这是一颗不好的二叉树,他的查找效率是直线型的O(n),就是最坏得树形结构,所以我们推荐使用平衡二叉树(AVL)。

以下内容转载自:http://blog.csdn.net/gabriel1026/article/details/6311339

感谢这位朋友  讲的确实很容易理解。


平衡二叉树在进行插入操作的时候可能出现不平衡的情况,AVL树即是一种自平衡的二叉树,它通过旋转不平衡的节点来使二叉树重新保持平衡,并且查找、插入和删除操作在平均和最坏情况下时间复杂度都是O(log n)

 

      AVL树的旋转一共有四种情形,注意所有旋转情况都是围绕着使得二叉树不平衡的第一个节点展开的。

 

1. LL型

    平衡二叉树某一节点的左孩子的左子树上插入一个新的节点,使得该节点不再平衡。这时只需要把树向右旋转一次即可,如图所示,原A的左孩子B变为父结点,A变为其右孩子,而原B的右子树变为A的左子树,注意旋转之后Brh是A的左子树(图上忘在A于Brh之间标实线)

 

 


2. RR型

    平衡二叉树某一节点的右孩子的右子树上插入一个新的节点,使得该节点不再平衡。这时只需要把树向左旋转一次即可,如图所示,原A右孩子B变为父结点,A变为其左孩子,而原B的左子树Blh将变为A的右子树。

 

 

3. LR型

      平衡二叉树某一节点的左孩子的右子树上插入一个新的节点,使得该节点不再平衡。这时需要旋转两次,仅一次的旋转是不能够使二叉树再次平衡。如图所示,在B节点按照RR型向左旋转一次之后,二叉树在A节点仍然不能保持平衡,这时还需要再向右旋转一次。

 

 

4. RL型

      平衡二叉树某一节点的右孩子的左子树上插入一个新的节点,使得该节点不再平衡。同样,这时需要旋转两次,旋转方向刚好同LR型相反。

经过上面的讲述我相信很多人都能理解单双旋转了。


最后一个是哈弗曼树,也就是最优二叉树,效率最高。下图就是一个完美树




构造哈弗曼树

引自:http://blog.csdn.net/shuangde800/article/details/7341289

下面演示了用Huffman算法构造一棵Huffman树的过程:
 
 
 

三、哈夫曼树的在编码中的应用

 

在电文传输中,需要将电文中出现的每个字符进行二进制编码。在设计编码时需要遵守两个原则:
(1)发送方传输的二进制编码,到接收方解码后必须具有唯一性,即解码结果与发送方发送的电文完全一样;
(2)发送的二进制编码尽可能地短。下面我们介绍两种编码的方式。
 
1. 等长编码 
            这种编码方式的特点是每个字符的编码长度相同(编码长度就是每个编码所含的二进制位数)。假设字符集只含有4个字符A,B,C,D,用二进制两位表示的编码分别为00,01,10,11。若现在有一段电文为:ABACCDA,则应发送二进制序列:00010010101100,总长度为14位。当接收方接收到这段电文后,将按两位一段进行译码。这种编码的特点是译码简单且具有唯一性,但编码长度并不是最短的。
 
2. 不等长编码 
            在传送电文时,为了使其二进制位数尽可能地少,可以将每个字符的编码设计为不等长的,使用频度较高的字符分配一个相对比较短的编码,使用频度较低的字符分配一个比较长的编码。例如,可以为A,B,C,D四个字符分别分配0,00,1,01,并可将上述电文用二进制序列:000011010发送,其长度只有9个二进制位,但随之带来了一个问题,接收方接到这段电文后无法进行译码,因为无法断定前面4个0是4个A,1个B、2个A,还是2个B,即译码不唯一,因此这种编码方法不可使用。
 
因此,为了设计长短不等的编码,以便减少电文的总长,还必须考虑编码的唯一性,即在建立不等长编码时必须使任何一个字符的编码都不是另一个字符的前缀,这宗编码称为前缀编码(prefix  code)
 
 
 
(1)利用字符集中每个字符的使用频率作为权值构造一个哈夫曼树;
(2)从根结点开始,为到每个叶子结点路径上的左分支赋予0,右分支赋予1,并从根到叶子方向形成该叶子结点的编码
 
 
例题:
假设一个文本文件TFile中只包含7个字符{A,B,C,D,E,F,G},这7个字符在文本中出现的次数为{5,24,7,17,34,5,13}
利用哈夫曼树可以为文件TFile构造出符合前缀编码要求的不等长编码
 
具体做法:
 
1. 将TFile中7个字符都作为叶子结点,每个字符出现次数作为该叶子结点的权值
2. 规定哈夫曼树中所有左分支表示字符0,所有右分支表示字符1,将依次从根结点到每个叶子结点所经过的分支的二进制位的序列作为该
     结点对应的字符编码
3. 由于从根结点到任何一个叶子结点都不可能经过其他叶子,这种编码一定是前缀编码,哈夫曼树的带权路径长度正好是文件TFile编码
    的总长度
 
通过哈夫曼树来构造的编码称为哈弗曼编码(huffman code)
 
 
 
 
 
 
 
[cpp] view plain copy
  1. #include<iostream>  
  2. #include<cstdio>  
  3. #include<cstring>  
  4. using namespace std;  
  5.   
  6. #define N 10         // 带编码字符的个数,即树中叶结点的最大个数  
  7. #define M (2*N-1)    // 树中总的结点数目  
  8.   
  9. class HTNode{        // 树中结点的结构  
  10. public:   
  11.     unsigned int weight;  
  12.     unsigned int parent,lchild,rchild;  
  13. };                      
  14.   
  15. class HTCode{  
  16. public:  
  17.     char data;      // 待编码的字符  
  18.     int weight;     // 字符的权值  
  19.     char code[N];   // 字符的编码  
  20. };  
  21.   
  22. void Init(HTCode hc[], int *n){  
  23. // 初始化,读入待编码字符的个数n,从键盘输入n个字符和n个权值  
  24.     int i;  
  25.     printf("input n = ");  
  26.     scanf("%d",&(*n));  
  27.   
  28.     printf("\ninput %d character\n",*n);  
  29.       
  30.     fflush(stdin);  
  31.     for(i=1; i<=*n; ++i)  
  32.         scanf("%c",&hc[i].data);  
  33.   
  34.     printf("\ninput %d weight\n",*n);  
  35.       
  36.     for(i=1; i<=*n; ++i)  
  37.         scanf("%d",&(hc[i].weight) );  
  38.     fflush(stdin);  
  39. }//  
  40.   
  41. void Select(HTNode ht[], int k, int *s1, int *s2){  
  42. // ht[1...k]中选择parent为0,并且weight最小的两个结点,其序号由指针变量s1,s2指示  
  43.     int i;  
  44.     for(i=1; i<=k && ht[i].parent != 0; ++i){   
  45.         ; ;  
  46.     }  
  47.     *s1 = i;  
  48.   
  49.     for(i=1; i<=k; ++i){  
  50.         if(ht[i].parent==0 && ht[i].weight<ht[*s1].weight)  
  51.             *s1 = i;  
  52.     }  
  53.   
  54.     for(i=1; i<=k; ++i){  
  55.         if(ht[i].parent==0 && i!=*s1)  
  56.             break;  
  57.     }  
  58.     *s2 = i;  
  59.   
  60.     for(i=1; i<=k; ++i){  
  61.         if(ht[i].parent==0 && i!=*s1 && ht[i].weight<ht[*s2].weight)  
  62.             *s2 = i;  
  63.     }  
  64. }  
  65.   
  66. void HuffmanCoding(HTNode ht[],HTCode hc[],int n){  
  67. // 构造Huffman树ht,并求出n个字符的编码  
  68.     char cd[N];  
  69.     int i,j,m,c,f,s1,s2,start;  
  70.     m = 2*n-1;  
  71.       
  72.     for(i=1; i<=m; ++i){  
  73.         if(i <= n)  
  74.             ht[i].weight = hc[i].weight;  
  75.         else  
  76.             ht[i].parent = 0;  
  77.         ht[i].parent = ht[i].lchild = ht[i].rchild = 0;  
  78.     }  
  79.   
  80.     for(i=n+1; i<=m; ++i){  
  81.         Select(ht, i-1, &s1, &s2);  
  82.         ht[s1].parent = i;  
  83.         ht[s2].parent = i;  
  84.         ht[i].lchild = s1;  
  85.         ht[i].rchild = s2;  
  86.         ht[i].weight = ht[s1].weight+ht[s2].weight;  
  87.     }  
  88.   
  89.     cd[n-1] = '\0';  
  90.   
  91.     for(i=1; i<=n; ++i){  
  92.         start = n-1;  
  93.         for(c=i,f=ht[i].parent; f; c=f,f=ht[f].parent){  
  94.             if(ht[f].lchild == c)  
  95.                 cd[--start] = '0';  
  96.             else  
  97.                 cd[--start] = '1';  
  98.         }  
  99.         strcpy(hc[i].code, &cd[start]);  
  100.     }  
  101. }  
  102.   
  103.   
  104. int main()  
  105. {  
  106.     int i,m,n,w[N+1];  
  107.     HTNode ht[M+1];  
  108.     HTCode hc[N+1];  
  109.     Init(hc, &n);     // 初始化  
  110.     HuffmanCoding(ht,hc,n);   // 构造Huffman树,并形成字符的编码  
  111.   
  112.     for(i=1; i<=n; ++i)    
  113.         printf("\n%c---%s",hc[i].data,hc[i].code);    
  114.     printf("\n");  
  115.   
  116.     return 0;  
  117. }  






0 0
原创粉丝点击