二叉树

使用二叉树原因:

有序数组 VS 链表

 

有序数组

链表

比较

插入

O[N]

O[1]

链表插入删除快

查找

O[log(2)N]

O[N]

有序数组查找快

删除

O[N]

O[1]

链表插入删除快

 

在有序数组中插入数据项太慢,在链表中查找太慢.

要是能有一种数据结构,既能像链表那样快速的插入和删除.又能像有序数组那样快速查找.

树实现了这些特点.成为最有意思的数据结构之一.

几种二叉树:

非平衡树:

概念:大部分的节点在根的一边或者另外一边

造成非平衡的原因:

数据项插入顺序造成的.如果关键字是随机插入的.则树或多或少更平衡一些.

如果插入顺序是升序<11 18 33 42 65>或者降序.

则所有的值都是右节点<升序>或者左节点<降序>.这样树就不平衡了.

 

 

 

平衡树:

 

完全二叉树:

 

 

二叉树:

    若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

    若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

它的左、右子树也分别为二叉查找树。

 

 

 

 

是否为空:  

判断根节点是否为空,就可以判断整棵树是否为空:

 

查找结点:

效率O[log2N]:

取决于层数,节点在哪一层就有多少次比较.

分析:

图解:

 

 

 

public Node find(int name);

输入参数为目标数据项

Current 指针指向的节点为当前数据项

1 将current 指针指向root 根节点

2 进入循环.循环条件: 当前指针所指向的节点数据项不等于目标数据项

         2.1 如果目标数据项小于当前数据项

         2.2 指针移动向左节点

         2.3如果目标数据项大于当前数据项

2.4 指针移动向右节点

2.5 当前节点为空 抛出找不到数据项的异常.

3 返回当前数据项

 

 

插入结点 :

分析:首先要定位插入位置.该过程类似于要找到一个不存在节点的过程.

图解:

 

假设要插入关键字为45的节点.第一步找到该节点的插入位置.45比60小但是比40大.所以turn right转向50.

45小于50,所以turn left,但是50 没有左子节点.它的leftchild字段等于null.找到null后 ,就等于找到新节点的位置了.为45建立一个新的节点.并把它连接到50的左子节点处.

 

1 新建节点

2 检查树是否为空树

   将根节点赋予新值

3 如果树不为空

   3.1 用当前指针搜索整个树.并保持与parentNode的关系

     3.1.1 新数据项小于当前节点.当前节点指向左子节点

       如果左子节点为空.把新节点接到parentNode的左子节点处

    返回函数

     3.1.2  新数据项大于当前节点.当前节点指向右子节点

       如果右子节点为空.把新节点接到parentNode的左右节点处

           返回函数

          3.1.3 新数据项等于当前数据项

                   抛出数据重复异常

 二叉树插入顺序与树形结构

执行如下代码:

                   BinaryTree theTree = new BinaryTree();

                   theTree.insert(20, "E");

                   theTree.insert(50, "B");

                   theTree.insert(40, "G");

                   theTree.insert(10, "A");

                   theTree.insert(20, "C");

                   theTree.insert(60, "F");

                   theTree.insert(70, "H");

二叉树结构以及插入顺序

 

遍历树:

有三种方式遍历整棵树:中序遍历,前序遍历,后序遍历

 

如下图所示 :

不同的遍历方式,显示节点顺序也不同.

 

中序遍历inorder <最常用>

特点:所有节点按关键字值升序被访问到.

遍历节点可以应用于任何二叉树.而不只是二叉搜索树.

关键点是该节点是否有子节点.与节点上面的值无关.

应用:二叉树创建有序数列

算法:递归.

用一个节点作为参数.初始化时这个节点是根节点.

1 调用自身来遍历节点的左子树

2 访问这个节点<访问这个节点可代表显示节点,把节点写入文件等>

3 调用自身来遍历节点的右子树.

 

代码:

Private void inOrder(node localRoot){

         //当前节点不为0的情况下

         If(localRoot!=null){

                   //递归遍历左子节点

                   inOrder(localRoot.leftChild);

       //打印当前节点

                   System.out.print(localRoot.iData+” “);

            //递归遍历右子节点

                   inOrder(localRoot.rightChild);

 }

}

具体步骤:

遍历一颗只有三个节点的树 :

下图为,节点作为输入参数的顺序.

 

图形结构:

 

前序遍历preorder

 

代码:

 

输出结果以及分析:

 

后序遍历postorder

 

 

前序遍历后序遍历的应用:

编写程序来解析或者分析代数表达式.

根节点:用于保存运算符号.其他节点或者保存变量名,或者保存运算符号,每一棵子树都是一个合法的代数表达式.

 

编写程序根据后序表达式建立树.

可以相当容易地根据输入的后缀表达式建立一颗像图8.11那样的树.

方法类似于计算后缀表达式.方法类似于计算后缀表达式.

不过这里面不是在栈中储存操作数,而是储存整个子树.

顺序的读取后缀字符串:

遇到操作数时候的操作步骤:

1 建立一棵树.它只有一个保存操作数的节点

2 这棵树入栈

遇到操作符时候的操作步骤

1 两个操作数的树B和C出栈

2 建立一棵新的树A 操作符是根

3 把B接到A的右子节点处

4 把C接到A的左子节点处

5 把得到的新树压入节点

这样,在分解完成后缀表达式后,把栈中所剩的唯一的一个数据项出栈。奇妙的是这个数据项是一颗完整的树,

表示整个算术表达式.

 

 

 

interface Tree {

         Node root = null;

         //根据目标数据项 查找指定节点

         public Node find(String value);

         public void insert(int iData, String name);

         public boolean delete(String name);

            //找到目标结点的中继节点

            private Node getSuccessor(Node delNode);

         //根据输入选项判断执行前序中序后序哪种遍历方式

         public void traverse(int i);

//      前序遍历 中序遍历 后序遍历

          private void preOrder(Node localNode);

          private void inOrder(Node localNode);

          private void postOrder(Node localNode);

          //显示树形结构

          public void displayTree();

         //查找最小子节点

     Public Node minimum();

     //查找最大子节点

          Public Node maxmum();

}

 

二叉树的方法:

 

interface Tree {

         Node root = null;

         //根据目标数据项 查找指定节点

         public Node find(String value);

         public void insert(int iData, String name);

         public boolean delete(String name);

            //找到目标结点的中继节点

            private Node getSuccessor(Node delNode);

         //根据输入选项判断执行前序中序后序哪种遍历方式

         public void traverse(int i);

//      前序遍历 中序遍历 后序遍历

          private void preOrder(Node localNode);

          private void inOrder(Node localNode);

          private void postOrder(Node localNode);

          //显示树形结构

          public void displayTree();

         //查找最小子节点

     Public Node minimum();

     //查找最大子节点

          Public Node maxmum();

}

 

 

可以用递归替代的是 :

//find node with given name 递归的查找方法

         public Node findRec(Node localRoot ,String target)

//递归方法查找最小的子节点

            public Node minimumRecs(Node localNode)

//递归方法查找最大节点的

            public Node maxmumRecs(Node localNode)

查找最大值和最小值:

查找最小值:

首先走到根的左子节点处,接着走到那个节点的左子节点.以此类推.直到找到一个没有左子节点的节点.这个节点就是最小值的节点.

查找最大值:

一直向右,直到找到没有右子节点的节点.这个节点就是最大值的节点.

删除节点:

 

该节点是叶节点 <没有子节点>.

1 找到该节点

2 判断该目标结点是否真的没有子节点的

   2.1 如果该节点是根节点.将树的根节点置为空.整个树即使空

   2.2 判断该节点时父节点的左子节点或者右子节点.

       2.2.1 将该节点与其父节点disconnect

删除有一个子节点的节点:

这个节点只有两个连接.:连接向父节点.和连向他唯一的子节点.

需要从这个序列中剪断这个节点.把它的子节点直接练到它的父节点上.

这个过程要求改变父节点适当的引用.是目标结点的父节点指向它的子节点.

 

注意; 引用使得移动整棵子树非常容易.只要断开连向子树的旧的引用.建立新的引用连到别处去就可以了.虽然子树种可能有很多节点.但不必要一个一个移动它们.实际上.它们的移动只是概念上移动到其他位置.和其他节点关联.

 

 

四种情况:

 

目标结点有左子节点

目标结点有右子节点

目标是父节点的左子节点

current.rightChild=null; isLeftChild=true;  

current.leftChild=null; isLeftChild=true;

目标是父节点的右子节点

current.rightChild=null;    isLeftChild=false;

current.leftChild=null; isLeftChild=false;

 

1 找到该节点

2 判断该目标结点是否真的没有子节点的

   2.1 如果该节点是根节点.将树的根节点置为空.整个树即使空

   2.2 判断该节点时父节点的左子节点或者右子节点.

       2.2.1 将该节点与其父节点disconnect

3 确认目标结点只有左子节点

         3.1 目标结点是根节点

3.2 目标结点是父节点的左子节点

3.3 目标结点是父节点的右子节点

4 确认目标结点只有右子节点

         4.1 目标结点是根节点

4.2 目标结点是父节点的左子节点

4.3 目标结点是父节点的右子节点

删除有两个子节点的节点:

窍门:

删除有两个子节点的节点用它的中继后续来替代该节点.

中继后续:

定义:

对于每一个节点来说,比该节点关键字值次高的节点就是他的中继后续.可以简称为该节点的后继。

寻找中继后续过程:

1 找到目标的节点的的右子节点.<它的关键字的值一定比初始节点大>

2 然后转向,目标结点的右子节点的左子节点那里.

3 第二步继续类推,继续找左子节点的左子节点.这个路径上的最后一个左子节点就是初始节点的后继.

4 如果初始节点的右子节点没有左子节点.那么这个右子节点本身就是后继.

 

1 起始:

         

2 首先找到中继节点:

             

3 之后:处理中继节点的子节点

4 代码:

  

  // 找到中继节点 处理中继节点子节点和新的右子节点 
public Node getSuccessor(Node delNode) {

         //找到中继节点

            //中继节点父节点

        Node successorParent = delNode;

            //中继节点

        Node successor = delNode;

            //当前节点

        Node current = delNode.getRightNode();

        while(current!=null){

            successorParent = successor;

            successor = current;

            current = current.getLeftNode();

        }

        //如果中继节点不是右子节点

        if(successor!=delNode.getRightNode()){

            //设置中继节点的右节点 将其交给它的父节点

            successorParent.setLeftNode(successor.getRightNode());

            //设置中继节点新的右子节点

            successor.setRightNode(delNode.getRightNode());

        }

        return successor;

    }

用中继节点替代要删除的节点:

// 如果左右子节点都存有 用中继节点替代该节点 处理中继节点和待删除节点的父节点和中继节点新的右子节点 
        else {
             //找到 中继节点 <同时右子节点已经设置完毕>
            Node successor = getSuccessor(current);
            //如果待删除节点是跟节点 
            if(root.compareTo(successor)==0 ){
                root = successor;
            }
            //如果是左子节点
            else if(isLeftChild){
                parent.setLeftNode(successor);
            }
            //如果是右子节点
            else{
                parent.setRightNode(successor);
            }
            //设置中继节点的左子节点 <右子节点已经设置完毕> 
                successor.setLeftNode(current.getLeftNode());
}

 

二叉树效率:

层数L 与节点个数N的关系:

 

 

哈夫曼huffman编码:

应用:

使用二叉树以令人惊讶的方式来压缩数据.

规则:

1 编码时候,出现次数最多的字符所占的位数应该最少.

2 每个代码都不能是其他代码的前缀.

步骤:

1 创建一段信息的频率表

 

SUSIE SAYS IT IS EASY

 

2 制定哈夫曼编码[A1] 

最终哈夫曼编码表为:

 

2.1  首先确定了如下两个字符的编码: [A2] 

S    10

空格 00

2.2 分析其他字符的哈夫曼编码

三位组合有8 <2^3>种可能性.

000 001 010 011 100 101 110 和 111

2.3 排除10和00开头的数字组合.只剩如下四种组合

010 011 110 111   

2.4

011  U和换行符代码的开始

111 用于E和Y的开始

A 010

I 110

2.5

整个消息编码后为:

 

用哈夫曼树解码:

哈夫曼树 :

特点:

1 字符在消息中出现的频率越高,在树中的位置就越高.

2 每个圆圈外面的数字就是频率.非叶节点外面的数字就是他子节点的频率的和.

 

3 创建哈夫曼树

http://www.java3z.com/cwbwebhome/article/article5/51057.html

 

对已经生成的哈夫曼树进行编码访问的节点顺序

 

 

4 解码

 

从根开始,遇到0就向左走到下一个节点.遇到1就向右.

例如A字符是010.

 

代码:

 E:\WorkspaceAlgorithm\Algorithm\review\com\cici\tree\exercise

 

分析代码

HuffmanTree.setHuffmanCoderString()

Exp 1 :

 

 

 

Exp 2:

 

 

 

代码结构1 :

1 接口 combinable.该接口继承comparable接口,包含combinate()

         方法.用于合并两个元素

2哈夫曼树类.

该类的结构:

 

 

该类的方法

l  public int compareTo(HuffmanTree<T> o) ß比较两个哈夫曼树

l  private void setHuffmanCoderString() ß为每个节点设置 0 1 编码

l  public void printHuffmanCoderString() ß只打印哈夫曼树的叶节点

3 生成哈夫曼树的工厂类.

 

 

4 UnitClass

自定一个用于测试的单元类.

 

用数组表示树

用引用的方式表达树:

数据模型

 

代码原型:

 

 

 

用数组表达树:

原理:

节点存放在数组中,而非引用相连.节点在数组中的位置,对应它在引用中的位置.

树中的每个位置,无论是否存在节点.都对应数组中的一个位置.意味着要在数组的响应位置插入一个数据项.

树中没有节点的位置在数组中的对应位置用0 或null来表示.

设节点索引值为index.则节点的左子节点是:

2*index+1

它的左子节点是:2*index+1

它的右子节点是:2*index+2

他的父节点是: (index-1)/2

 

 

 

数据模型:

 

 

 

 

 

 

方法总结:

 

 

         //初始化 ArrayNode[] cArr 数组容器

//数组长度为 size

public ArrNodeHigherArray(int size);

 

//直接将参数数组赋值给ArrayNode[] cArr数组容器

public ArrNodeHigherArray(ArrayNode[] cArr);

 

//得到自身持有的 ArrayNode数组容器

public ArrayNode[] getCArr();

 

public void setNElems(int nElems);

public int getNElems();

 

// 得到当前数组的迭代器

public ArrNodeHigherArrayIterator getIterator();

 

 

//扩展持有的ArrayNode数组容器 容器长度扩大一倍

public void expand();

 

//判断ArrayNode容器是否还有ArrayNode 数据项存在

public boolean isEmpty();

 

//返回ArrayNode 数组的长度 : 从第一个元素到 最后一个不为空的元素

public int size();

 

//判断ArrayNode容器 中的数据项是否已经占满整个容器

public boolean isFull();

 

//线性查找 效率O[N]

//返回-1则代表没有找到目标数据项

public int find(ArrayNode tree);

}

//无序数组的插入 数组末尾插入

public void insert(ArrayNode value);

 

//在指定数据 node1 前 插入新数据 node2

//返回新数据项 在ArrayNode 容器中的下标

public int  insertBefore(ArrayNode node1,ArrayNode node2);

 

//在指定数据 node1 后插入新数据 node2

//返回新数据项 在ArrayNode 容器中的下标

public int  insertAfter(ArrayNode node1,ArrayNode node2);

 

//删除指定元素

//remove the target element and also move higher element down

public ArrayNode delete1(Character value);

 

// 删除指定元素  return the target element

//remove the target element and leave the position to be null

public ArrayNode delete2(Character value);

 

//ArrayNode容器中 数据项依次显示显示  包含数组容器中的null值

public void display();

对比普通无序数组

二叉树的数组实现中的数组允许容器中null值的存在.而普通的无序数组只能依次存入数据 .

 

 

练习:

1

将书中的tree.java.修改成用户输入字母的字符串建立的二叉树.<如A,B等等>

建立树. 每个字母在各自的叶节点中显示.父节点可以有非字母标志<’+’>.

保证每个父节点都恰好有两个字节点.树可以不是平衡树.没有快速的方法来查找节点.

最后结果可以如下:

 

 

1 的解法:

建立一个树的数组.树的节点数据域为用户输入的字母.

把每个节点作为一棵树,节点就是根.将这些只有根节点的树放到数组中.

下面建立一棵以’+’为根的树.两个单节点树为它的子节点.依次把数组中的单节点树加到这颗大树中.

...................................................... ......................................................

                                +                                                             

                +                              +                             

        +              +              +              g             

    a      b      c      d      e      f      --      --     

...................................................... ......................................................

2 扩展编程作业8.1中的功能来创建平衡树.

思路;

保证节点尽可能出现在最底层.

依次将数组容器中的树中的节点变成树的底层叶节点.

通过递归二分查找发实现.遍历到数组容器的每个元素.并将这些元素赋值给树的左右节点.

 

核心思路;

先看一小段核心核心思路的代码:

 

 

public class Test {

                   private char[] cArr;

                   public Test(char[] c){

                            cArr = c;

                   }

                   public static void main(String[] args) {

                            char [] cArr ={'a','b','c','d','e','f'};

                            Test t = new Test(cArr);

                             t.method(cArr,0,cArr.length-1);

                   }

                   public void method(char[] c,int a,int b){

//当数组被割据的只剩下一个数据项的时候 即打印出来

                            if(a==b){

                                     System.out.print(c[a]+" ");

                            }

                            else{

//对数组的前一半不断的割据

                                      method(c,a,(a+b)/2);

//对数组的后一半不断的割据

                                      method(c,(a+b)/2+1,b);

                            }

                   }

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

输出结果:

a b c d e f

执行过程:

 

 

 

关键递归方法分析:

 

主程序:

 

执行到createTree(Tree[] array,int left,int right);方法第11行:

最多压入三栈:

          

第一栈:

 

第二栈:

 

第三栈:

 

边界条件的栈

 

返回array[0];到Line 43

 

输出结果的分析:

 


 [A1]原则还没完全搞清楚

 [A2]具体原因不知道

posted @ 2012-12-10 19:30  王超_cc  阅读(298)  评论(0编辑  收藏  举报