likes
comments
collection
share

🥳每日一练-平衡二叉树的构建-JS简易版

作者站长头像
站长
· 阅读数 19

上篇文章介绍了排序二叉树的内容,这篇文章介绍平衡二叉树的创建。

排序二叉树虽说查找的复杂度是O(logn),但这也是平均情况,极端情况可能就是O(n)了.比如一个没有分叉的排序二叉树,这时候树的高度就和节点的数量一致了。

平衡二叉树就是解决这个问题的,平衡二叉树(Balanced Binary Tree)是指一个二叉树中,任一节点的左右子树的高度差不超过1。平衡二叉树的性质保证了二叉搜索树(BST)的查找、插入和删除操作的时间复杂度为O(log n)。

平衡二叉树的维护需要通过旋转操作来保证树的平衡。旋转操作包括左旋(Left Rotation)和右旋(Right Rotation),通过旋转操作,可以改变节点在树中的位置,从而使树重新平衡。

🥳每日一练-平衡二叉树的构建-JS简易版

像上图,左边的二叉树是非平衡二叉树,对于节点4,左子树和右子树的高度差值为2;经过处理,右边就是平衡二叉树了,即任一节点的左右子树的高度差不超过1

下面就用JS代码构建一个平衡二叉树,超级简单

准备数据

class Node {
	constructor(value) {
		this.value = value;
		this.left = null;
		this.right = null;
		this.height = 1;
	}
}

let arr = [4, 2, 7, 1, 3, 6, 9];

准备了一个节点对象,对象含有四个属性。还准备了一个数组,下面就会将这个数组转成一个平衡二叉树

平衡二叉树的构建

捋一下构建的过程:

  1. 如果插入的节点的值大于当前节点,就往当前节点的右子树上插;否则就往左子树上插
  2. 插入之后,需要更新当前节点的高度
  3. 然后判断当前节点是否需要平衡一下,即左右子树的高度之差大于2
  4. 平衡之后插入结束

首先创建一个AVLTree对象

class AVLTree {

    updateHeight(){

    }

    balance(){

    }
    
    insert(value) {
            this.root = this._insert(this.root, value);
    }
    
    _insert(node, value) {
        if (!node) return new Node(value);

        if (value < node.value) {
                node.left = this._insert(node.left, value);
        } else if (value > node.value) {
                node.right = this._insert(node.right, value);
        } else {
                return node;
        }

        this.updateHeight(node);
        return this.balance(node);
    }
}

逻辑很简单,大致过程就按照之前描述的那样。至于为什么要设计成两个insert函数,后面会讲。

然后把updateHeight函数完善一下

getHeight(node) {
    if (!node) return 0;
    return node.height;
}

updateHeight(node) {
    node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
}

完善balance需要补充一点知识

旋转操作

如果插入的节点使得某个子树不平衡,需要进行旋转操作。不同的平衡情况,需要不同的旋转操作。

1. LL(Left Left Rotation)

当前节点的balance的值为-2,即右子树的高度比左子树高2。然后对于右子树的根节点来说,右子树的balance为-1,即右子树的高度比左子树高1

像下面这样:

          A
         / \
        B   C
           / \
          D   E
               \
                F

A的balance为-2,C的balance为-1,此时需要LL,将该树重新变成平衡二叉树

          c
         / \
        A   E
       / \   \
      B   D   F      

就好像整棵树围绕着C向左旋转。A变成C的左子树,C的左子树变成A的右子树。

整棵树依然是二叉排序树

2. RR(Right Right Rotation)

当前节点的balance的值为2,即左子树的高度比右子树高2。然后对于左子树的根节点来说,左子树的balance为1,即左子树的高度比右子树高1

像下面这样:

          A
         / \
        B   C
       / \
      D   E
     /
    F

A的balance为2,B的balance为-1,此时需要RR,将该树重新变成平衡二叉树

          B
         / \
        D   A
       /   / \
      F   E   C      

就好像整棵树围绕着B向右旋转。A变成B的右子树,B的右子树变成A的左子树。

整棵树依然是二叉排序树

3. LR(Left Right Rotation)

当前节点的balance的值为2,即左子树的高度比右子树高2。然后对于左子树的根节点来说,左子树的balance为-1,即左子树的高度比右子树小1。这种不平衡的情况,需要旋转两次。

像下面这样:

        A
       / \
      B   C
     / \
    D   E
       / \
      F   G

A的balance为2,B的balance为-1,此时需要RR,将该树重新变成平衡二叉树

经过了第一次L:

        A
       / \
      E   C
     / \
    B   G
   / \    
  D   F    

经过了第二次R:

      E
     / \
    B   A
   / \ / \
  D   FG   C

旋转逻辑,以及子树的分配和上面的LL,RR都是一致的

整棵树依然是二叉排序树

4. RL(Right Left Rotation)

当前节点的balance的值为-2,即右子树的高度比左子树高2。然后对右子树的根节点来说,右子树的balance为1,即左子树的高度比右子树高1。这种不平衡的情况,需要旋转两次,先向右旋转,然后向左旋转

像下面这样:

        A
       / \
      B   C
         / \
        D   E
       / \
      G   F   

经过了第一次R:

        A
       / \
      B   D
         / \
        G   C
           / \    
          F   E    

经过了第二次L:

      D
     / \
    A   C
   / \ / \
  B   GF   E

旋转逻辑,以及子树的分配和上面的LL,RR都是一致的

整棵树依然是二叉排序树

虽然是四种情况,但归根结底,还是两种变换,一个是左旋,还有一个是右旋

左旋代码

leftRotate(node) {
    let newRoot = node.right;
    node.right = newRoot.left;
    newRoot.left = node;

    this.updateHeight(node);
    this.updateHeight(newRoot);

    return newRoot;
  }

左旋操作是将一个节点的右子节点的左子节点旋转上来,以维持节点的平衡。具体来说,左旋操作会交换节点 node 和其右子节点 newRoot 的左右子节点,把node当作newRoot的左节点,并且把newRoot 的左子树变成node的右节点。

最后更新节点 node 和右子节点 newRoot 的高度。最后返回新的根节点 newRoot。

右旋代码

rightRotate(node) {
    let newRoot = node.left;
    node.left = newRoot.right;
    newRoot.right = node;

    this.updateHeight(node);
    this.updateHeight(newRoot);

    return newRoot;
  }

右旋操作是将一个节点的左子节点的右子节点旋转上来,以维持节点的平衡。具体来说,右旋操作会交换节点 node 和其左子节点 newRoot 的左右子节点,把node当作newRoot的右节点,并且把newRoot 的右子树变成node的左节点。

最后更新节点 node 和左子节点 newRoot 的高度。最后返回新的根节点 newRoot。

好了现在知道了必要的知识,可以完善balance方法了

getBalance(node) {
    return this.getHeight(node.left) - this.getHeight(node.right);
}
        
/**
* 保持节点的平衡
* @param {AVLNode} node - 需要保持平衡的节点
* @returns {AVLNode} - 返回保持平衡后的节点
*/
balance(node) {
    // 如果当前节点的平衡因子 > 1
    if (this.getBalance(node) > 1) {
        // 如果当前节点的左子节点的平衡因子 < 0
        if (this.getBalance(node.left) < 0) {
            // 则对当前节点的左子节点进行左旋操作
            node.left = this.leftRotate(node.left);
        }
        // 对当前节点进行右旋操作
        return this.rightRotate(node);
    } else if (this.getBalance(node) < -1) {
        // 如果当前节点的右子节点的平衡因子 > 0
        if (this.getBalance(node.right) > 0) {
            // 则对当前节点的右子节点进行右旋操作
            node.right = this.rightRotate(node.right);
        }
        // 对当前节点进行左旋操作
        return this.leftRotate(node);
    }
    // 如果当前节点的平衡因子在 -1 和 1 之间,则不需要进行旋转操作,直接返回当前节点
    return node;
}

首先判断当前节点的平衡因子是否大于 1,如果是,则需要进行平衡操作。平衡操作分为两种情况:

  1. 如果当前节点的左子节点的平衡因子小于 0,则对当前节点的左子节点进行左旋操作。(LR)
  2. 对当前节点进行右旋操作,使得当前节点的左子节点成为新的根节点,从而保持树的平衡。(RR/LR)

这里可以看到,LR也就是比RR多了一个左旋,没有什么特别的

如果当前节点的平衡因子小于 -1,则需要进行平衡操作。平衡操作也分为两种情况:

  1. 如果当前节点的右子节点的平衡因子大于 0,则对当前节点的右子节点进行右旋操作(RL)
  2. 对当前节点进行左旋操作,使得当前节点的右子节点成为新的根节点,从而保持树的平衡。(LL/RL)

RL也就是比LL多了一个右旋,没有什么特别的

如果当前节点的平衡因子在 -1 和 1 之间,则不需要进行平衡操作,直接返回当前节点。

完整代码

class AVLTree {
  constructor() {
    this.root = null;
  }

  getHeight(node) {
    if (!node) return 0;
    return node.height;
  }

  updateHeight(node) {
    node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
  }

  getBalance(node) {
    return this.getHeight(node.left) - this.getHeight(node.right);
  }

  leftRotate(node) {
    let newRoot = node.right;
    node.right = newRoot.left;
    newRoot.left = node;

    this.updateHeight(node);
    this.updateHeight(newRoot);

    return newRoot;
  }

  rightRotate(node) {
    let newRoot = node.left;
    node.left = newRoot.right;
    newRoot.right = node;

    this.updateHeight(node);
    this.updateHeight(newRoot);

    return newRoot;
  }

  balance(node) {
    if (this.getBalance(node) > 1) {
      if (this.getBalance(node.left) < 0) {
        node.left = this.leftRotate(node.left);
      }
      return this.rightRotate(node);
    } else if (this.getBalance(node) < -1) {
      if (this.getBalance(node.right) > 0) {
        node.right = this.rightRotate(node.right);
      }
      return this.leftRotate(node);
    }
    return node;
  }

  insert(value) {
    this.root = this._insert(this.root, value);
  }

  _insert(node, value) {
    if (!node) return new Node(value);

    if (value < node.value) {
      node.left = this._insert(node.left, value);
    } else if (value > node.value) {
      node.right = this._insert(node.right, value);
    } else {
      return node;
    }

    this.updateHeight(node);
    return this.balance(node);
  }
}

测试下代码:

const printNode = (tree) => {
  if (!tree) return null;
  printNode(tree.left);
  console.log(tree.value);
  printNode(tree.right);
};

let tree = new AVLTree();
let arr = [4, 2, 7, 1, 3, 6, 9];
arr.forEach((value) => tree.insert(value));

printNode(tree.root);
//1
//2
//3
//4
//6
//7
//9

打印结果没有问题,确实是一个升序序列。再看看实际的json结构,确认是否为平衡二叉树:

{
  root: {
    value: 4,
    left: {
      value: 2,
      left: {
        value: 1,
        left: null,
        right: null,
        height: 1,
      },
      right: {
        value: 3,
        left: null,
        right: null,
        height: 1,
      },
      height: 2,
    },
    right: {
      value: 7,
      left: {
        value: 6,
        left: null,
        right: null,
        height: 1,
      },
      right: {
        value: 9,
        left: null,
        right: null,
        height: 1,
      },
      height: 2,
    },
    height: 3,
  },
}

json结构看着不太明显,修改下printNode函数,不仅打印出节点的值,还打印出node的balance的值

const root = tree;
const printNode = (tree) => {
	if (!tree) return null;
	printNode(tree.left);
	console.log(tree.value, root.getBalance(tree));
	printNode(tree.right);
};

printNode(tree.root);
//1 0
//2 0
//3 0
//4 0
//6 0
//7 0
//9 0

没有问题

总结

这篇文章分享了平衡二叉树的创建,体会了一把面向对象的鬼斧神工般的代码。要我自己想肯定是想不出来

你觉得这篇文章怎么样?喜欢就点赞+关注吧

转载自:https://juejin.cn/post/7293118779781038130
评论
请登录