Java数据结构与算法笔记——234树详解

2-3-4树简介

2-3-4树的结构

2-3-4树是一种多叉树,它的每个节点最多有三个数据项和四个子节点

非叶节点数据项和其子节点数量的关系:

  1. 有一个数据项的节点总是有两个子节点
  2. 有两个数据项的节点总是有三个子节点
  3. 有三个数据项的节点总是有四个子节点

简而言之,非叶节点的子节点数总是比它含有的数据项多1
在这里插入图片描述
叶节点(上图最下面的一排)是没有子节点的,然而叶节点可能含有一个、两个或三个数据项。空节点是不会存在的。

2-3-4树数据项之间的大小关系

树结构中很重要的一点就是节点之间关键字值大小的关系。(为了方便描述2-3-4树节点关键字之间的大小关系,用从0到2的数字给数据项编号,用0到3的数字给子节点编号,如下图)
在这里插入图片描述

  1. 根是child0的子树的所有子节点的关键字值小于key0;
  2. 根是child1的子树的所有子节点的关键字值大于key0并且小于key1;
  3. 根是child2的子树的所有子节点的关键字值大于key1并且小于key2;
  4. 根是child3的子树的所有子节点的关键字值大于key2。

由于2-3-4树中一般不允许出现重复关键值,所以不用考虑比较关键值相同的情况

2-3-4树的操作

搜索2-3-4树

从根节点开始搜索,(除非查找的关键 字值就是根,)选择关键字值所在的合适范围,转向那个方向,直到找到为止。

比如对于下面这幅图,我们需要查找关键字值为64的数据项。
在这里插入图片描述
首先从根节点开始,根节点只有一个数据项50,没有找到,而且因为64比50大,那么转到根节点的子节点child1。60|70|80 也没有找到,而且60<64<70,所以我们还是找该节点的child1,62|64|66,我们发现其第二个数据项正好是64,于是找到了。

插入

新的数据项一般要插在叶节点里,在树的最底层。如果插入到有子节点的节点里,那么子节点的数量就要发生变化来维持树的结构,因为在2-3-4树中节点的子节点要比数据项多1。

插入操作有时比较简单,有时却很复杂。

  1. 当插入没有满数据项的叶节点时是很简单的,找到合适的位置,只需要把新数据项插入就可以了,插入可能会涉及到在一个节点中移动一个或其他两个数据项,这样在新的数据项插入后关键字值仍保持正确的顺序。如下图所示。
  2. 如果往下寻找插入位置的途中(包括非叶节点),有节点中的数据项是满的,节点必须分裂,以保证2-3-4树的平衡。
    在这里插入图片描述

节点分裂

在寻找插入数据位置的过程中,如果遇到了满数据项的节点,该满数据项节点就要分裂。

  1. 创建一个新的空节点,它是要分裂节点的兄弟,在要分裂节点的右边;
  2. 数据项C移到新节点中;
  3. 数据项B移到要分裂节点的父节点中;
  4. 数据项A保留在原来的位置;
  5. 最右边的两个子节点从要分裂处断开,连到新节点上。

在这里插入图片描述
一般插入只需要分裂一个节点,除非插入路径上存在不止一个满节点时,这种情况就需要多重分裂。

根的分裂

如果一开始查找插入节点时就碰到满的根节点,那么插入过程更复杂。

  1. 创建新的根节点,它是要分裂节点的父节点。
  2. 创建第二个新的节点,它是要分裂节点的兄弟节点;
  3. 数据项C移到新的兄弟节点中;
  4. 数据项B移到新的根节点中;
  5. 数据项A保留在原来的位置;
  6. 要分裂节点最右边的两个子节点断开连接,连到新的兄弟节点中。

在这里插入图片描述
分裂完成之后,整个树的高度加1。

Java实现2-3-4树

封装数据项的类

package tree234;

/**
 * 封装数据项的类
 */
public class DataItem {
    public int data;//节点中的具体的数据项的值,为了简化代码,这里用public定义数据类型
    //在这里还可以同时封装其他属性


    //构造方法
    public DataItem(int data){
        this.data = data;
    }

    //打印数据项的方法
    public void displayItem(){
        System.out.print("/" + data);
    }
}

封装节点的类

package tree234;


/**
 * 234数的节点封装起来的类
 */
public class Node {
    //Node节点里的基本属性值
    //把最大的节点数,做成一个常数
    private static final int ORDER = 4;//最多4个子节点
    private int numItems;//保存节点中含有的数据项的个数
    private Node parent;//指向当前节点的父节点
    private Node[] childArray = new Node[ORDER];//指向当前节点的子节点的指针集
    private DataItem[] itemArray = new DataItem[ORDER-1];//封装节点中的数据

    //返回节点中的数据项的数组
    public DataItem[] getItemArray(){
        return itemArray;
    }

    //连接子节点。
    //将child节点作为当前节点的第childNum个子节点
    public void connectChild(int childNum, Node child){
        if (childNum>=0 && childNum<ORDER){
            childArray[childNum] = child;
            if (child != null){
                child.parent = this;
            }
        }else {
            System.out.println("2-3-4树节点不允许索引为"+childNum+"的子节点");
        }

    }

    //断开子节点,并返回子节点
    public Node disconnectChild(int childNum){
        if (childNum>=0 && childNum<ORDER) {
            Node childNode = childArray[childNum];
            childArray[childNum] = null;//断开
            return childNode;
        }else {
            System.out.println("2-3-4树节点不会存在索引为"+childNum+"的子节点");
            return null;
        }
    }

    //拿到当前节点的某个指定的子节点
    public Node getChild(int childNum){
        if (childNum>=0 && childNum<ORDER) {
            return childArray[childNum];
        }else {
            System.out.println("2-3-4树节点不会存在索引为"+childNum+"的子节点");
            return null;
        }
    }

    //获取当前节点的父节点
    public Node getParent(){
        return parent;
    }

    //判断当前节点是不是叶节点
    public boolean isLeaf(){
        //是叶节点返回true,否则返回false
        return childArray[0] == null;//如果子节点指针列表的第一个元素是空,那就说明没有子节点
    }

    //拿到当前节点包含的数据项个数
    public int getNumItems(){
        return numItems;
    }

    //判断当前节点是不是满节点
    public boolean isFull(){
        return numItems == ORDER-1;
    }

    //找到指定的数据项在节点中的位置
    public int findItem(int key){
        for(int i=0;i<numItems;i++){
            if (key == itemArray[i].data){
                return i;
            }
        }
        return -1;//表示节点中没有要找的数据
    }

    //实现数据项插入到此节点中对应的位置,并返回插入的位置
    public int insertItem(DataItem newDataItem){
        if (numItems<ORDER-1){
            //节点不满,可以插入新数据
            numItems++;
            int newData = newDataItem.data;
            for (int i=ORDER-2;i>=0;i--){
                if (itemArray[i] == null){
                    continue;
                }else {
                    int itemArrayData = itemArray[i].data;
                    if (newData < itemArrayData){
                        //新数据项比当前的数据项小
                        itemArray[i+1] = itemArray[i];//把数据项往后面移动
                    }else {
                        //新数据项比当前的数据项大,那么新数据项应该插入到当前数据项的后面
                        itemArray[i+1] = newDataItem;//把新的数据项插入到当前数据项的后面
                        return i+1;
                    }
                }
            }
            //如果循环完了,还没有结束,就证明数组是空的,直接插入到第0个位置
            itemArray[0] = newDataItem;
            return 0;
        }else {
            //节点已满
            System.out.println("节点已满");
            return 3;//表示节点已满
        }

    }

    //实现移除当前节点中的最后一个数据项
    public DataItem removeLastDataItem(){
        DataItem delData = itemArray[numItems-1];
        itemArray[numItems-1] =null;
        numItems--;
        return delData;
    }

    //把当前节点中的所有数据项都打印出来
    public void displayNode(){
        for (int i = 0; i<numItems;i++){
            itemArray[i].displayItem();
        }
        System.out.print("/"+"\n");
    }
}

封装2-3-4树的类

package tree234;

/**
 * 实现234树的类
 */
public class Tree234 {
    private Node root;

    //构造防范。初始的一个空的234树,有一个空节点
    public Tree234(){
        root = new Node();
    }

    //实现搜索234树
    //找到了指定的数据,就返回数据在节点数据项数组中的索引,没找到返回-1
    public int find(int key){
        Node current = root;
        int childNumber;//存放返回值的变量
        while (true){
            if ((childNumber=current.findItem(key)) != -1){
                //找到了
                return childNumber;
            }else if (current.isLeaf()){
                //树中没有要找的数据项
                return -1;
            }else {
                //current指针向正确的子节点移动
                current = getNextNode(current,key);
            }
        }
    }

    //查找过程中,确定下一个要查找的节点
    public Node getNextNode(Node theNode, int theValue){
        int numItems = theNode.getNumItems();//获取当前节点含有的数据项个数
        int i;
        for (i=0; i<numItems;i++){
            if (theValue<theNode.getItemArray()[i].data){
                return theNode.getChild(i);
            }
        }
        //循环完毕后,都没有返回,证明这个值比节点中的所有数据项都大,返回最后一个子节点
        return theNode.getChild(i);
    }

    //实现新的数据项插入的234树
    public void insert(int insValue){
        //封装数据
        DataItem insDataItem = new DataItem(insValue);
        //找到数据值应该被插入的位置
        Node current = root;
        while (true){
            if (current.isFull()){
                //分裂
                split(current);//独立出一个方法来专门实现复杂的分裂操作
                current = current.getParent();//分裂结束后,回到父节点重新操作
                current = getNextNode(current,insValue);//重新寻找下一个节点
            }else if(current.isLeaf()){
                //此时节点不是满节点,且是叶子节点
                //找到了要插入的节点
                break;
            }else {
                //不是满节点。也不是叶子节点,继续循环
                current = getNextNode(current,insValue);
            }
        }
        current.insertItem(insDataItem);//往目标节点中插入数据项
    }

    //实现节点分裂
    public void split(Node theNode){
        Node parent;//当前节点的父节点

        DataItem itemB;
        DataItem itemC;
        Node child2;//第三个子节点,索引值2
        Node child3;//第四个子节点,索引值3

        //把上面的四个单位分离出来
        itemC = theNode.removeLastDataItem();
        itemB = theNode.removeLastDataItem();
        child3 = theNode.disconnectChild(3);
        child2 = theNode.disconnectChild(2);

        //在右边新建一个新的节点
        Node rightNewNode = new Node();

        if (theNode == root){
            //根节点的分裂
            root = new Node();
            parent = root;
            root.connectChild(0,theNode);
        }else {
            //不是根节点分裂
            parent = theNode.getParent();
        }

        //到这里,不管是根节点分裂还是非根节点分裂,都拿到了要分裂的节点的父节点

        //将分裂节点的B数据项插入父节点中
        int itemBinParentIndex = parent.insertItem(itemB);
        //考虑父节点原本两个数据项的情况,要改变原本子节点对应的索引值
        int n = parent.getNumItems();//B数据项插入到父节点后,父节点的新的数据项个数
        Node tmp;
        for (int i =n;i>itemBinParentIndex;i--){
            tmp = parent.disconnectChild(i);
            parent.connectChild(i+1,tmp);
        }
        //处理右边新的节点
        //itemC先放到新节点
        rightNewNode.insertItem(itemC);
        //新节点连接到父节点
        parent.connectChild(itemBinParentIndex+1,rightNewNode);
        //右边两个子节点连接到新节点
        rightNewNode.connectChild(0,child2);
        rightNewNode.connectChild(1,child3);
    }

    //用递归打印输出234树所有节点的值
    public void recursionDisplayTree(Node thisNode, int level, int childNum){
        //节点,节点所在深度,当前节点是父节点的第几个子节点
        System.out.println("level="+level+"child:"+childNum);
        thisNode.displayNode();
        int numItems = thisNode.getNumItems();
        for (int i=0; i<numItems+1;i++){
            Node childNode = thisNode.getChild(i);
            //递归
            if (childNode == null){
                //边界
                return;
            }else {
                recursionDisplayTree(childNode,level+1,i);
            }
        }
    }

    //打印输出所有的节点的值
    public void displayTree(){
        recursionDisplayTree(root, 0, 0);
    }
}

测试2-3-4树的类

package tree234;

public class Tree234Test {
    public static void main(String[] args) {
        Tree234 tree234 = new Tree234();
        tree234.insert(20);
        tree234.insert(22);
        tree234.insert(15);
        tree234.insert(98);
        tree234.insert(45);
        tree234.insert(6);

        tree234.displayTree();

        System.out.println(tree234.find(45));

    }
}

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
无限级Java递归) 2007-02-08 10:26 这几天,用java写了一个无限极的,递归写的,可能代码不够简洁,性能不够好,不过也算是练习,这几天再不断改进。前面几个小图标的判断,搞死我了。 package com.nickol.servlet; import java.io.IOException; import java.io.PrintWriter; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.nickol.utility.DB; public class category extends HttpServlet { /** * The doGet method of the servlet. * * This method is called when a form has its tag value method equals to get. * * @param request the request send by the client to the server * @param response the response send by the server to the client * @throws ServletException if an error occurred * @throws IOException if an error occurred */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setCharacterEncoding("utf-8"); response.setContentType("text/html"); PrintWriter out = response.getWriter(); out .println(""); out.println(""); out.println(" Category" + "" + "body{font-size:12px;}" + "" + "" + ""); out.println(" "); out.println(showCategory(0,0,new ArrayList(),"0")); out.println(" "); out.println(""); out.flush(); out.close(); } public String showCategory(int i,int n,ArrayList frontIcon,String countCurrent){ int countChild = 0; n++; String webContent = new String(); ArrayList temp = new ArrayList(); try{ Connection conn = DB.GetConn(); PreparedStatement ps = DB.GetPs("select * from category where pid = ?", conn); ps.setInt(1, i); ResultSet rs = DB.GetRs(ps); if(n==1){ if(rs.next()){ webContent += "";//插入结尾的减号 temp.add(new Integer(0)); } webContent += " ";//插入站点图标 webContent += rs.getString("cname"); webContent += "\n"; webContent += showCategory(Integer.parseInt(rs.getString("cid")),n,temp,"0"); } if(n==2){ webContent += "\n"; }else{ webContent += "\n"; } while(rs.next()){ for(int k=0;k<frontIcon.size();k++){ int iconStatic = ((Integer)frontIcon.get(k)).intValue(); if(iconStatic == 0){ webContent += "";//插入空白 }else if(iconStatic == 1){ webContent += "";//插入竖线 } } if(rs.isLast()){ if(checkChild(Integer.parseInt(rs.getString("cid")))){ webContent += "";//插入结尾的减号 temp = (ArrayList)frontIcon.clone(); temp.add(new Integer(0)); }else{ webContent += "";//插入结尾的直角 } }else{ if(checkChild(Integer.parseInt(rs.getString("cid")))){ webContent += "";//插入未结尾的减号 temp = (ArrayList)frontIcon.clone(); temp.add(new Integer(1)); }else{ webContent += "";//插入三叉线 } } if(checkChild(Integer.parseInt(rs.getString("cid")))){ webContent += " ";//插入文件夹图标 }else{ webContent += " ";//插入文件图标 } webContent += rs.getString("cname"); webContent += "\n"; webContent += showCategory(Integer.parseInt(rs.getString("cid")),n,temp,countCurrent+countChild); countChild++; } webContent += "\n"; DB.CloseRs(rs); DB.ClosePs(ps); DB.CloseConn(conn); }catch(Exception e){ e.printStackTrace(); } return webContent; } public boolean checkChild(int i){ boolean child = false; try{ Connection conn = DB.GetConn(); PreparedStatement ps = DB.GetPs("select * from category where pid = ?", conn); ps.setInt(1, i); ResultSet rs = DB.GetRs(ps); if(rs.next()){ child = true; } DB.CloseRs(rs); DB.ClosePs(ps); DB.CloseConn(conn); }catch(Exception e){ e.printStackTrace(); } return child; } } --------------------------------------------------------------------- tree.js文件 function changeState(countCurrent,countChild){ var object = document.getElementById("level" + countCurrent + countChild); if(object.style.display=='none'){ object.style.display='block'; }else{ object.style.display='none'; } var cursor = document.getElementById("cursor" + countCurrent + countChild); if(cursor.src.indexOf("images/tree_minus.gif")>=0) {cursor.src="images/tree_plus.gif";} else if(cursor.src.indexOf("images/tree_minusbottom.gif")>=0) {cursor.src="images/tree_plusbottom.gif";} else if(cursor.src.indexOf("images/tree_plus.gif")>=0) {cursor.src="images/tree_minus.gif";} else {cursor.src="images/tree_minusbottom.gif";} var folder = document.getElementById("folder" + countCurrent + countChild); if(folder.src.indexOf("images/icon_folder_channel_normal.gif")>=0){ folder.src = "images/icon_folder_channel_open.gif"; }else{ folder.src = "images/icon_folder_channel_normal.gif"; }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值