2-3-4树是如何解决二叉树中非平衡问题的?

简介: 2-3-4树是如何解决二叉树中非平衡问题的?

我之前写过一篇文章:下次面试若再被问到二叉树,希望你能对答如流!没错,二叉搜索树是个很好的数据结构,可以快速地找到一个给定关键字的数据项,并且可以快速地插入和删除数据项。

但是二叉搜索树有个很麻烦的问题,如果树中插入的是随机数据,则执行效果很好,但如果插入的是有序或者逆序的数据,那么二叉搜索树的执行速度就变得很慢。因为当插入数值有序时,二叉树就是非平衡的了,它的快速查找、插入和删除指定数据项的能力就丧失了。

为了解决这个问题,我们可以使用多叉树。

2-3-4树就是一个多叉树,它的每个节点最多有四个子节点和三个数据项。2-3-4树和红-黑树一样,也是平衡树,它的效率比红-黑树稍差,但是编程容易。2-3-4树名字中的2、3、4的含义是指一个节点可能含有的子节点的个数。对非叶节点有三种可能的情况:

  • 有一个数据项的节点总是有两个子节点;
  • 有两个数据项的节点总是有三个子节点;
  • 有三个数据项的节点总是有四个字节点。


简而言之,非叶节点的子节点总是比它含有的数据项多1。如下图所示:

00d94e1025569f68e1b4eaafe0f8276d_640_wxfrom=5&wx_lazy=1&wx_co=1.jpg


为了方便起见,用0到2给数据项编号,用0到3给子节点链编号。树的结构中很重要的一点就是它的链与自己数据项的关键字值之间的关系。二叉树所有关键字值比某个节点值小的都在这个节点的左子节点为根的子树上,所有关键字值比某个及诶的那值大的节点都在这个节点右子节点为根的子树上。2-3-4树中规则是一样的,还加上了以下几点:

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


这种关系如下图所示,2-3-4树中一般不允许出现重复关键字值,所以不用考虑比较相同的关键字值的情况。

d4acf405cfdcd43fb6fef760ac60e5c2_640_wxfrom=5&wx_lazy=1&wx_co=1.png

2-3-4树中插入节点有时比较简单,有时比较复杂。当没有碰到满节点时插入很简单,找到合适的叶节点后,只要把新数据项插入进去即可,插入可能会涉及到在一个节点中移动一个或两个其他的数据项,这样在新的数据项插入后关键字值仍保持正确的顺序。如下图:

e6ebb06fa8b81bfe27e42096c844fa00_640_wxfrom=5&wx_lazy=1&wx_co=1.jpg


如果往下寻找要插入的位置的路途中,节点已经满了,插入就变得复杂了。这种情况下,节点必须分裂。正是这种分裂过程保证了树的平衡。设要分裂节点中的数据项为A、B、C,下面是分裂时的情况(假设分裂的节点不是根节点):

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


下图显示了一个节点分裂的过程。另一种描述节点分裂的方法是说4-节点变成了两个2-节点。

4f3f76e0b47e455a5fa7277b06670ed3_640_wxfrom=5&wx_lazy=1&wx_co=1.jpg


如果一开始查找插入点时就碰到满根时,插入过程就更复杂一点:

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


下图是根分裂的过程。过程中创建新的根,比旧的高一层,因此整个树的高度就增加了一层。

00f5d8e25c8612472707ddb44a671be7_640_wxfrom=5&wx_lazy=1&wx_co=1.jpg


下面是2-3-4树的代码:

public class Tree234 {
    private Node2 root = new Node2();
    public int find(long key) {
        Node2 currentNode = root;
        int childNumber;
        while(true) {
            if((childNumber = currentNode.findItem(key)) != -1) {
                return childNumber;
            }
            else if(currentNode.isLeaf()) {
                return -1;
            }
            else {
                currentNode = getNextChild(currentNode, key);
            }
        }
    }
    //insert a DataItem
    public void insert(long data) {
        Node2 currentNode = root;
        DataItem tempItem = new DataItem(data);
        while(true) {
            if(currentNode.isFull()) {
                split(currentNode); //if node is full, split it
                currentNode = currentNode.getParent();  //back up
                currentNode = getNextChild(currentNode, data);  //search once
            }
            else if(currentNode.isLeaf()) { //if node if leaf
                break;  //go insert
            }
            else {
                currentNode = getNextChild(currentNode, data);
            }
        }
        currentNode.insertItem(tempItem);
    }
    //display tree
    public void displayTree() {
        recDisplayTree(root, 0, 0);
    }
    public Node2 getNextChild(Node2 currentNode, long key) {
        int j;
        //assumes node is not empty, not full and not leaf
        int numItems = currentNode.getNumItems();
        for(j = 0; j < numItems; j++) {
            if(key < currentNode.getItem(j).dData) {
                return currentNode.getChild(j);
            }
        }
        return currentNode.getChild(j);
    }
    public void split(Node2 currentNode) {
        //assumes node is full
        DataItem itemB, itemC;  //存储要分裂节点的后两个DataItem
        Node2 parent, child2, child3;   //存储要分裂节点的父节点和后两个child
        int itemIndex;
        itemC = currentNode.removeItem();
        itemB = currentNode.removeItem();   //remove items from this node
        child2 = currentNode.disconnectChild(2);
        child3 = currentNode.disconnectChild(3); //remove children from this node
        Node2 newRight = new Node2(); //make a new node
        if(currentNode == root) {
            root = new Node2(); //make a new root
            parent = root;  //root is our parent
            root.connectChild(0, currentNode);//connect currentNode to parent
        }
        else {
            parent = currentNode.getParent();
        }
        //deal with parent
        itemIndex = parent.insertItem(itemB);   //insert B to parent
        int n = parent.getNumItems();   //total items
        for(int j = n-1; j > itemIndex; j--) {
            Node2 temp = parent.disconnectChild(j);
            parent.connectChild(j+1, temp);
        }
        parent.connectChild(itemIndex+1, newRight);
        //deal with newRight
        newRight.insertItem(itemC);
        newRight.connectChild(0, child2);
        newRight.connectChild(1, child3);
    }
    public void recDisplayTree(Node2 thisNode, int level, int childNumber) {
        System.out.print("level = " + level + " child = " + childNumber + " ");
        thisNode.displayNode();
        //call ourselves for each child of this node
        int numItems = thisNode.getNumItems();
        for(int j = 0; j < numItems+1; j++) {
            Node2 nextNode = thisNode.getChild(j);
            if(nextNode != null) {
                recDisplayTree(nextNode, level+1, j);
            }
            else 
                continue;
        }
    }
}
//数据项
class DataItem {
    public long dData;
    public DataItem(long data) {
        dData = data;
    }
    public void displayItem() {
        System.out.print("/" + dData);
    }
}
//节点
class Node2 {
    private static final int ORDER = 4;
    private int numItems; //表示该节点存有多少个数据项
    private Node2 parent;
    private Node2 childArray[] = new Node2[ORDER]; //存储子节点的数组,最多四个子节点
    private DataItem itemArray[] = new DataItem[ORDER-1];//该节点中存放数据项的数组,每个节点最多存放三个数据项
    //连接子节点
    public void connectChild(int childNum, Node2 child) {
        childArray[childNum] = child;
        if(child != null) {
            child.parent = this;
        }
    }
    //断开与子节点的连接,并返回该子节点
    public Node2 disconnectChild(int childNum) {
        Node2 tempNode = childArray[childNum];
        childArray[childNum] = null;
        return tempNode;
    }
    public Node2 getChild(int childNum) {
        return childArray[childNum];
    }
    public Node2 getParent() {
        return parent;
    }
    public boolean isLeaf() {
        return (childArray[0] == null);
    }
    public int getNumItems() {
        return numItems;
    }
    public DataItem getItem(int index) {
        return itemArray[index];
    }
    public boolean isFull() {
        return (numItems == ORDER-1);
    }
    public int findItem(long key) {
        for(int j = 0; j < ORDER-1; j++) {
            if(itemArray[j] == null) {
                break;
            }
            else if(itemArray[j].dData == key) {
                return j;
            }
        }
        return -1;
    }
    public int insertItem(DataItem newItem) {
        //assumes node is not full
        numItems++;
        long newKey = newItem.dData;
        for(int j = ORDER-2; j >= 0; j--) {     //start on right
            if(itemArray[j] == null) {      //item is null
                continue;                   //get left one cell
            }
            else {                          //not null
                long itsKey = itemArray[j].dData;   //get its key
                if(newKey < itsKey) {               //if it's bigger
                    itemArray[j+1] = itemArray[j];  //shift it right
                }
                else {
                    itemArray[j+1] = newItem;       //insert new item
                    return j+1;                     //return index to new item
                }
            }
        }
        itemArray[0] = newItem;
        return 0;
    }
    public DataItem removeItem() {
        //assumes node not empty
        DataItem tempItem = itemArray[numItems-1];  //save item
        itemArray[numItems-1] = null;               //disconnect it
        numItems--;
        return tempItem;
    }
    public void displayNode() {
        for(int i = 0; i < numItems; i++) {
            itemArray[i].displayItem();
        }
        System.out.println("/");
    }
}


和红-黑树一样,2-3-4树同样要访问每层的一个节点,但2-3-4树有比相同数据项的红-黑树短(层数少)。更特别的是,2-3-4树中每个节点最多可以有4个子节点,如果每个节点都是满的,树的高度应该和log4N成正比。以2为底的对数和以4为底的对数底数相差2,因此,在所有节点都满的情况下,2-3-4树的高度大致是红-黑树的一般。不过它们不可能都是满的,2-3-4树的高度就大致在log2(N+1) 和 log2(N+1)/2 之间。


另一方面,每个节点要查看的数据项就更多了,这会增加查找时间。因为节点中用线性搜索来查看数据项,使查找时间增加的倍数和M成正比,即每个节点数据项的平均数量。总的查找时间和Mlog4N成正比。有些节点有1个数据项,有些有2个,有些有3个,如果按照平均两个来计算,查找时间和2log4N成正比。


因此,2-3-4树中增加每个节点的数据项数量可以抵偿树的高度的减少。2-3-4树中的查找时间与平衡二叉树(如红-黑树)大致相等,都是O(logN)。


本文建议收藏,在等班车的时候、吃饭排队的时候可以拿出来看看。利用碎片化时间来学习!


人之所以能,是相信能!这世上没有天才,你若对得起时间,时间便对得起你。关注我们,每天进步一点点~


相关文章
|
存储 C语言 C++
问题 D: DS查找—二叉树平衡因子(不一样的新做法哦)
问题 D: DS查找—二叉树平衡因子(不一样的新做法哦)
|
25天前
|
应用服务中间件 Linux 调度
二叉树与红黑树重制版(二)
二叉树与红黑树重制版(二)
32 0
|
25天前
|
算法 API
二叉树与红黑树重制版(一)
二叉树与红黑树重制版(一)
23 0
|
5月前
数据结构学习记录——平衡二叉树的调整(基本介绍、右单旋、左单旋、左右双旋、右左双旋、平衡因子的计算)
数据结构学习记录——平衡二叉树的调整(基本介绍、右单旋、左单旋、左右双旋、右左双旋、平衡因子的计算)
59 1
|
5月前
|
算法
数据结构和算法学习记录——平衡二叉树(基本介绍、平衡因子、平衡二叉树的定义、平衡二叉树的高度)
数据结构和算法学习记录——平衡二叉树(基本介绍、平衡因子、平衡二叉树的定义、平衡二叉树的高度)
47 0
|
6月前
|
机器学习/深度学习 算法 测试技术
【深度优先搜索】【树】【C++算法】2003. 每棵子树内缺失的最小基因值
【深度优先搜索】【树】【C++算法】2003. 每棵子树内缺失的最小基因值
|
存储 算法
【数据结构oj】树的度(树和二叉树的相互转化)
【数据结构oj】树的度(树和二叉树的相互转化)
60 0
从前序与中序遍历序列构造二叉树(中等难度)
从前序与中序遍历序列构造二叉树(中等难度)
98 0
从前序与中序遍历序列构造二叉树(中等难度)
|
测试技术
#### [623. 在二叉树中增加一行](https://leetcode.cn/problems/add-one-row-to-tree/)
#### [623. 在二叉树中增加一行](https://leetcode.cn/problems/add-one-row-to-tree/)