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

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

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

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

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

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

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

准备数据

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];

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

平衡二叉树的构建

js 复制代码
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);
	}
}

捋一下构建的过程:

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

首先创建一个AVLTree对象

js 复制代码
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函数完善一下

js 复制代码
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

像下面这样:

css 复制代码
          A
         / \
        B   C
           / \
          D   E
               \
                F

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

css 复制代码
          c
         / \
        A   E
       / \   \
      B   D   F      

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

整棵树依然是二叉排序树

2. RR(Right Right Rotation)

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

像下面这样:

css 复制代码
          A
         / \
        B   C
       / \
      D   E
     /
    F

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

css 复制代码
          B
         / \
        D   A
       /   / \
      F   E   C      

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

整棵树依然是二叉排序树

3. LR(Left Right Rotation)

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

像下面这样:

css 复制代码
        A
       / \
      B   C
     / \
    D   E
       / \
      F   G

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

经过了第一次L:

css 复制代码
        A
       / \
      E   C
     / \
    B   G
   / \    
  D   F    

经过了第二次R:

css 复制代码
      E
     / \
    B   A
   / \ / \
  D   FG   C

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

整棵树依然是二叉排序树

4. RL(Right Left Rotation)

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

像下面这样:

css 复制代码
        A
       / \
      B   C
         / \
        D   E
       / \
      G   F   

经过了第一次R:

css 复制代码
        A
       / \
      B   D
         / \
        G   C
           / \    
          F   E    

经过了第二次L:

css 复制代码
      D
     / \
    A   C
   / \ / \
  B   GF   E

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

整棵树依然是二叉排序树

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

左旋代码

js 复制代码
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。

右旋代码

js 复制代码
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方法了

js 复制代码
/**
* 保持节点的平衡
* @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 之间,则不需要进行平衡操作,直接返回当前节点。

完整代码

js 复制代码
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);
  }
}

测试下代码:

js 复制代码
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结构,确认是否为平衡二叉树:

yaml 复制代码
{
  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,
  },
}

没有问题

总结

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

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

相关推荐
qq_589568102 分钟前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
游是水里的游3 分钟前
【算法day19】回溯:分割与子集问题
算法
不想当程序猿_4 分钟前
【蓝桥杯每日一题】分糖果——DFS
c++·算法·蓝桥杯·深度优先
南城花随雪。23 分钟前
单片机:实现FFT快速傅里叶变换算法(附带源码)
单片机·嵌入式硬件·算法
dundunmm38 分钟前
机器学习之scikit-learn(简称 sklearn)
python·算法·机器学习·scikit-learn·sklearn·分类算法
古希腊掌管学习的神38 分钟前
[机器学习]sklearn入门指南(1)
人工智能·python·算法·机器学习·sklearn
波音彬要多做40 分钟前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法
Noah_aa1 小时前
代码随想录算法训练营第五十六天 | 图 | 拓扑排序(BFS)
数据结构
KpLn_HJL1 小时前
leetcode - 2139. Minimum Moves to Reach Target Score
java·数据结构·leetcode
暴富的Tdy2 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js