前言
【从蛋壳到满天飞】JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组)、Stacks(栈)、Queues(队列)、LinkedList(链表)、Recursion(递归思想)、BinarySearchTree(二分搜索树)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(优先队列)、SegmentTree(线段树)、Trie(字典树)、UnionFind(并查集)、AVLTree(AVL 平衡树)、RedBlackTree(红黑平衡树)、HashTable(哈希表)
源代码有三个:ES6(单个单个的 class 类型的 js 文件) | JS + HTML(一个 js 配合一个 html)| JAVA (一个一个的工程)
全部源代码已上传 github,点击我吧,光看文章能够掌握两成,动手敲代码、动脑思考、画图才可以掌握八成。
本文章适合 对数据结构想了解并且感兴趣的人群,文章风格一如既往如此,就觉得手机上看起来比较方便,这样显得比较有条理,整理这些笔记加源码,时间跨度也算将近半年时间了,希望对想学习数据结构的人或者正在学习数据结构的人群有帮助。
AVL 树 平衡机制的四种处理
-
左旋转和右旋转的情况
- 当插入的元素在不平衡的节点的左侧的左侧的时候,
- 就需要向右旋转。
- 当插入的元素在不平衡的节点的右侧的右侧的时候,
- 就需要向左旋转。
- 其实对于这两种情况来说它们是对称的,
- 所以处理的思路完全是一致的。
-
其它两种情况
- 插入的元素在不平衡的节点的左侧的右侧,
- 也还是会让祖先节点不满足平衡二叉树的性质,
- 因为这个不平衡节点的高度还是+1 了,所以平衡因子还是大于 1 了,
- 所以不能单纯的简单的进行右旋转或者左旋转了。
-
不同情况的区分
-
一共分为 4 种情况,LL、RR、LR、RL,
-
LL 表示的是插入的元素在不平衡节点的左侧的左侧的时候,
-
RR 表示的是插入的元素在不平衡节点的右侧的右侧的时候,
-
LR 表示的是插入的元素在不平衡节点的左侧的右侧的时候,
-
RL 表示的是插入的元素在不平衡节点的右侧的左侧的时候。
// RR的情况 新插入的节点在Y的右侧的右侧 // (Y) // / \ // (T4) (X) // / \ // (T3) (Z) // / \ // (T1) (T2) // LL的情况 新插入的节点在Y的左侧的左侧 // (Y) // / \ // (X) (T4) // / \ // (Z) (T3) // / \ // (T1) (T2) // LR的情况 新插入的节点在Y的左侧的右侧 // (Y) // / \ // (X) (T4) // / \ // (T1) (Z) // / \ // (T2)(T3) // RL的情况 新插入的节点在Y的右侧的左侧 // (Y) // / \ // (T1) (X) // / \ // (Z) (T4) // / \ // (T2)(T3) 复制代码
-
-
LR 的处理方式
- 首先需要对节点 X 进行左旋转,
- 之前的 LL 和 RR 都是对 X 和 Y 这两个节点做改变,并没有改变节点 Z,
- 现在对节点 X 进行左旋转就会相迎的改变 X 和 Z 这两个节点,
- 旋转之后,就将 LR 转换为了 LL 的情况,
- 此时只需要继续使用 LL 的方式进行处理即可。
- LR 出现的情况是,当前节点的平衡因子大于 1 并且
- 当前节点的左子树的平衡因子小于 0,也就是当前节点的左子树的右孩子要比左孩子要高,
- 所以相应的就是小于 0 的。
- 先对当前节点的左孩子进行一个左旋转,然后重新赋值给当前节点的左孩子,
- 然后对当前节点进行一个右旋转,直接返回当前节点即可。
// LR的情况 新插入的节点在Y的左侧的右侧 // (Y) // / \ // (X) (T4) // / \ // (T1) (Z) // / \ // (T2)(T3) // 对X节点进行左旋转,就将LR转换为了LL的情况 // (Y) // / \ // (Z) (T4) // / \ // (X) (T3) // / \ // (T1) (T2) 复制代码
-
RL 的处理方式
- RL 就是新插入的节点在 Y 这个不平衡的点的右子树的左侧,
- 先右后左,先 R 后 L,这种情况就叫做 RL,
- 对于 RL 这种情况的处理方式和刚才对 LR 这种的处理方式是完全对称的,
- 首先需要对节点 X 进行右旋转,旋转之后,就将 LR 转换为了 RR 的情况,
- 此时只需要继续使用 RR 的方式进行处理即可。
- RL 出现的情况是,当前节点的平衡因子小于负一并且
- 当前节点的右子树的平衡因子大于 0,也就是当前节点的右子树的右孩子要比左孩子要高,
- 所以相应的就是大于 0 的。
- 先对当前节点的右孩子进行一个右旋转,然后重新赋值给当前节点的右孩子,
- 然后对当前节点进行一个左旋转,直接返回当前节点即可。
// RL的情况 新插入的节点在Y的右侧的左侧 // (Y) // / \ // (T1) (X) // / \ // (Z) (T4) // / \ // (T2)(T3) // 对X节点进行右旋转,就将LR转换为了RR的情况 // (Y) // / \ // (T1) (Z) // / \ // (T2) (X) // / \ // (T3)(T4) 复制代码
-
已经对 LL 和 RR 这两种情况进行了处理
- 在这个处理过程中,涉猎了左旋转和右旋转这样两个子过程,
- 在吃力 LR 和 RL 这两种情况的时候,直接复用那两个子过程,
- 就可以非常容易的处理 LR 和 RL 这两种情况,
- 这样就对一个节点所有的不平衡的可能性进行了处理,
- 处理完了之后继续向上回溯,寻找上面的节点,
- 看看是否还有不平衡的情况,整个处理过程依次类推,
- 直至根节点就好了。
-
对 LL、RR、LR、RL 这四种情况进行了处理
- 如果这四种情况都不满足的话,
- 那么说明这个节点符合平衡二叉树的性质,
- 那么直接返回这个节点,然后递归回溯的返回给了上一层节点,
- 那么在上一层继续来处理更新完这个节点相应的高度之后,
- 继续判断它的平衡因子从而看是否进行平衡的维护,
- 整个过程依次类推。
- 经过这样的处理之后这棵 AVL 树就基本实现的差不多了,
- 这棵 AVL 树的添加操作目前是满足了平衡二叉树的性质了,
- 同时它也满足了二分搜索树的性质。
-
将二分搜树改造成了 AVL 树
- 这么做是为了让整个树能够保持平衡,
- 保持平衡的目的就是因为原先二分搜索树可能退化成链表,
- 现在 AVL 树肯定不会退化成链表,
- 相应的所有的操作的时间复杂度都是
O(logn)
这个级别的, - 现在这个 AVL 树比之前的二分搜索树肯定是更加平衡的,
- 所以整体在性能上也应该更加有优势。
-
性能测试
- 在随机数据的测试中,AVLTree 比 BSTTree 要快一点,
- 如果是最坏的数据的测试的话,AVLTree 比 BSTTree 要快非常多,
- 因为 BSTTree 退化成了一个链表,时间复杂度是
O(n)
级别的了, - 而 AVLTree 是
O(logn)
这个级别的, - 这就是平衡二叉树的威力,由于有了自平衡的这种机制,
- 所以整棵树不会退化成链表,在最差的情况下这个 BST 将会表现的非常慢,
- 但是平衡二叉树 AVL 却非常的快。
代码示例
-
(class: MyBSTMap, class: AVLTree, class: PerformanceTest, class: Main)
-
MyBSTMap
// 自定义二分搜索树树映射节点 TreeMapNode class MyBinarySearchTreeMapNode { constructor(key = null, value = null, left = null, right = null) { this.key = key; this.value = value; this.left = left; this.right = right; } // @Override toString 2018-11-5-jwl toString() { return this.key.toString() + '---------->' + this.value.toString(); } } // 自定义二分搜索树映射 Map class MyBinarySearchTreeMap { constructor() { this.root = null; this.size = 0; } // 比较的功能 compare(keyA, keyB) { if (keyA === null || keyB === null) throw new Error("key is error. key can't compare."); if (keyA > keyB) return 1; else if (keyA < keyB) return -1; else return 0; } // 根据key获取节点 - getNode(node, key) { // 先解决最基本的问题 if (node === null) return null; // 开始将复杂的问题 逐渐缩小规模 // 从而求出小问题的解,最后构建出原问题的解 switch (this.compare(node.key, key)) { case 1: // 向左找 return this.getNode(node.left, key); break; case -1: // 向右找 return this.getNode(node.right, key); break; case 0: // 找到了 return node; break; default: throw new Error( 'compare result is error. compare result : 0、 1、 -1 .' ); break; } } // 添加操作 + add(key, value) { this.root = this.recursiveAdd(this.root, key, value); } // 添加操作 递归算法 - recursiveAdd(node, key, value) { // 解决最简单的问题 if (node === null) { this.size++; return new MyBinarySearchTreeMapNode(key, value); } // 将复杂的问题规模逐渐变小, // 从而求出小问题的解,从而构建出原问题的答案 if (this.compare(node.key, key) > 0) node.left = this.recursiveAdd(node.left, key, value); else if (this.compare(node.key, key) < 0) node.right = this.recursiveAdd(node.right, key, value); else node.value = value; return node; } // 删除操作 返回被删除的元素 + remove(key) { let node = this.getNode(this.root, key); if (node === null) return null; this.root = this.recursiveRemove(this.root, key); return node.value; } // 删除操作 递归算法 + recursiveRemove(node, key) { // 解决最基本的问题 if (node === null) return null; if (this.compare(node.key, key) > 0) { node.left = this.recursiveRemove(node.left, key); return node; } else if (this.compare(node.key, key) < 0) { node.right = this.recursiveRemove(node.right, key); return node; } else { // 当前节点的key 与 待删除的key的那个节点相同 // 有三种情况 // 1. 当前节点没有左子树,那么只有让当前节点的右子树直接覆盖当前节点,就表示当前节点被删除了 // 2. 当前节点没有右子树,那么只有让当前节点的左子树直接覆盖当前节点,就表示当前节点被删除了 // 3. 当前节点左右子树都有, 那么又分两种情况,使用前驱删除法或者后继删除法 // 1. 前驱删除法:使用当前节点的左子树上最大的那个节点覆盖当前节点 // 2. 后继删除法:使用当前节点的右子树上最小的那个节点覆盖当前节点 if (node.left === null) { let rightNode = node.right; node.right = null; this.size--; return rightNode; } else if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } else { let predecessor = this.maximum(node.left); node.left = this.removeMax(node.left); this.size++; // 开始嫁接 当前节点的左右子树 predecessor.left = node.left; predecessor.right = node.right; // 将当前节点从根节点剔除 node = node.left = node.right = null; this.size--; // 返回嫁接后的新节点 return predecessor; } } } // 删除操作的两个辅助函数 // 获取最大值、删除最大值 // 以前驱的方式 来辅助删除操作的函数 // 获取最大值 maximum(node) { // 再也不能往右了,说明当前节点已经是最大的了 if (node.right === null) return node; // 将复杂的问题渐渐减小规模,从而求出小问题的解,最后用小问题的解构建出原问题的答案 return this.maximum(node.right); } // 删除最大值 removeMax(node) { // 解决最基本的问题 if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } // 开始化归 node.right = this.removeMax(node.right); return node; } // 查询操作 返回查询到的元素 + get(key) { let node = this.getNode(this.root, key); if (node === null) return null; return node.value; } // 修改操作 + set(key, value) { let node = this.getNode(this.root, key); if (node === null) throw new Error(key + " doesn't exist."); node.value = value; } // 返回是否包含该key的元素的判断值 + contains(key) { return this.getNode(this.root, key) !== null; } // 返回映射中实际的元素个数 + getSize() { return this.size; } // 返回映射中是否为空的判断值 + isEmpty() { return this.size === 0; } // @Override toString() 2018-11-05-jwl toString() { let mapInfo = `MyBinarySearchTreeMap: size = ${ this.size}, data = [ `; document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [
`; // 以非递归的前序遍历 输出字符串 let stack = new MyLinkedListStack(); stack.push(this.root); if (this.root === null) stack.pop(); while (!stack.isEmpty()) { let node = stack.pop(); if (node.left !== null) stack.push(node.left); if (node.right !== null) stack.push(node.right); if (node.left === null && node.right === null) { mapInfo += ` ${node.toString()} \r\n`; document.body.innerHTML += ` ${node.toString()}
`; } else { mapInfo += ` ${node.toString()}, \r\n`; document.body.innerHTML += ` ${node.toString()},
`; } } mapInfo += ` ] \r\n`; document.body.innerHTML += ` ]
`; return mapInfo; } } 复制代码 -
AVLTree
// 自定义AVL树节点 AVLTreeNode class MyAVLTreeNode { constructor(key = null, value = null, left = null, right = null) { this.key = key; this.value = value; this.left = left; this.right = right; this.height = 1; } // @Override toString 2018-11-24-jwl toString() { return this.key + '--->' + this.value + '--->' + this.height; } } // 自定义AVL树 AVLTree class MyAVLTree { constructor() { this.root = null; this.size = 0; } // 比较的功能 compare(keyA, keyB) { if (keyA === null || keyB === null) throw new Error("key is error. key can't compare."); if (keyA > keyB) return 1; else if (keyA < keyB) return -1; else return 0; } // 获取某个节点的高度 - getHeight(node) { // 节点为空 返回0 if (!node) return 0; // 直接返回这个节点的高度 return node.height; } // 获取一个节点的平衡因子 - getBalanceFactor(node) { // 节点为空 返回0 if (!node) return 0; // 左右子树的高度值 const leftHeight = this.getHeight(node.left); const rightHeight = this.getHeight(node.right); // 左子树的高度 - 右子树高度的值 = 平衡因子 return leftHeight - rightHeight; } // 根据key获取节点 - getNode(node, key) { // 先解决最基本的问题 if (node === null) return null; // 开始将复杂的问题 逐渐缩小规模 // 从而求出小问题的解,最后构建出原问题的解 switch (this.compare(node.key, key)) { case 1: // 向左找 return this.getNode(node.left, key); break; case -1: // 向右找 return this.getNode(node.right, key); break; case 0: // 找到了 return node; break; default: throw new Error( 'compare result is error. compare result : 0、 1、 -1 .' ); break; } } // 对节点y进行向右旋转操作,返回旋转后新的根节点x // y x // / \ / \ // x T4 向右旋转 (y) z y // / \ - - - - - - - -> / \ / \ // z T3 T1 T2 T3 T4 // / \ // T1 T2 rightRotate(y) { const x = y.left; const T3 = x.right; // 向右旋转的过程 y.left = T3; x.right = y; // 更新节点的height值 只需要更新x和y的即可 y.height = 1 + Math.max(this.getHeight(y.left), this.getHeight(y.right)); x.height = 1 + Math.max(this.getHeight(x.left), this.getHeight(x.right)); // 返回 新节点 x return x; } // 对节点y进行向左旋转操作,返回旋转后新的根节点x // y x // / \ / \ // T1 x 向左旋转 (y) y z // / \ - - - - - - - -> / \ / \ // T2 z T1 T2 T3 T4 // / \ // T3 T4 leftRotate(y) { const x = y.right; const T2 = x.left; // 向左旋转的过程 y.right = T2; x.left = y; // 更新节点的height值 只需要更新x和y的即可 y.height = 1 + Math.max(this.getHeight(y.left), this.getHeight(y.right)); x.height = 1 + Math.max(this.getHeight(x.left), this.getHeight(x.right)); // 返回 新节点 x return x; } // 添加操作 + add(key, value) { this.root = this.recursiveAdd(this.root, key, value); } // 添加操作 递归算法 - recursiveAdd(node, key, value) { // 解决最简单的问题 if (node === null) { this.size++; return new MyAVLTreeNode(key, value); } // 将复杂的问题规模逐渐变小, // 从而求出小问题的解,从而构建出原问题的答案 if (this.compare(node.key, key) > 0) node.left = this.recursiveAdd(node.left, key, value); else if (this.compare(node.key, key) < 0) node.right = this.recursiveAdd(node.right, key, value); else node.value = value; // 在这里对节点的高度进行重新计算 节点本身高度为1 // 计算方式: 1 + 左右子树的height值最大的那个height值 node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right)); // 计算一个节点的平衡因子 const balanceFactor = this.getBalanceFactor(node); // 如果平衡因子的绝对值大于1 说明不满足AVL平衡二叉树的性质了 if (Math.abs(balanceFactor) > 1) { console.log( node.toString() + ' unbalanced : ' + balanceFactor + '\r\n' ); document.body.innerHTML += node.toString() + ' unbalanced : ' + balanceFactor + '
'; } // LL情况 平衡维护 右旋转操作 平衡因子为正数则表示左倾 反之为右倾 if (balanceFactor > 1 && this.getBalanceFactor(node.left) >= 0) return this.rightRotate(node); // RR情况 平衡维护 左旋转操作 平衡因子为负数则表示右倾 反之为左倾 if (balanceFactor < -1 && this.getBalanceFactor(node.right) <= 0) return this.leftRotate(node); // LR情况 平衡维护 先转换为LL情况 再处理LL情况 if (balanceFactor > 1 && this.getBalanceFactor(node.left) < 0) { node.left = this.leftRotate(node.left); return this.rightRotate(node); } // RL情况 平衡维护 先转换为RR情况 再处理RR情况 if (balanceFactor < -1 && this.getBalanceFactor(node.right) > 0) { node.right = this.rightRotate(node.right); return this.leftRotate(node); } return node; } // 删除操作 返回被删除的元素 + remove(key) { let node = this.getNode(this.root, key); if (node === null) return null; this.root = this.recursiveRemove(this.root, key); return node.value; } // 删除操作 递归算法 + recursiveRemove(node, key) { // 解决最基本的问题 if (node === null) return null; if (this.compare(node.key, key) > 0) { node.left = this.recursiveRemove(node.left, key); return node; } else if (this.compare(node.key, key) < 0) { node.right = this.recursiveRemove(node.right, key); return node; } else { // 当前节点的key 与 待删除的key的那个节点相同 // 有三种情况 // 1. 当前节点没有左子树,那么只有让当前节点的右子树直接覆盖当前节点,就表示当前节点被删除了 // 2. 当前节点没有右子树,那么只有让当前节点的左子树直接覆盖当前节点,就表示当前节点被删除了 // 3. 当前节点左右子树都有, 那么又分两种情况,使用前驱删除法或者后继删除法 // 1. 前驱删除法:使用当前节点的左子树上最大的那个节点覆盖当前节点 // 2. 后继删除法:使用当前节点的右子树上最小的那个节点覆盖当前节点 if (node.left === null) { let rightNode = node.right; node.right = null; this.size--; return rightNode; } else if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } else { let predecessor = this.maximum(node.left); node.left = this.removeMax(node.left); this.size++; // 开始嫁接 当前节点的左右子树 predecessor.left = node.left; predecessor.right = node.right; // 将当前节点从根节点剔除 node = node.left = node.right = null; this.size--; // 返回嫁接后的新节点 return predecessor; } } } // 删除操作的两个辅助函数 // 获取最大值、删除最大值 // 以前驱的方式 来辅助删除操作的函数 // 获取最大值 maximum(node) { // 再也不能往右了,说明当前节点已经是最大的了 if (node.right === null) return node; // 将复杂的问题渐渐减小规模,从而求出小问题的解,最后用小问题的解构建出原问题的答案 return this.maximum(node.right); } // 删除最大值 removeMax(node) { // 解决最基本的问题 if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } // 开始化归 node.right = this.removeMax(node.right); return node; } // 查询操作 返回查询到的元素 + get(key) { let node = this.getNode(this.root, key); if (node === null) return null; return node.value; } // 修改操作 + set(key, value) { let node = this.getNode(this.root, key); if (node === null) throw new Error(key + " doesn't exist."); node.value = value; } // 返回是否包含该key的元素的判断值 + contains(key) { return this.getNode(this.root, key) !== null; } // 返回映射中实际的元素个数 + getSize() { return this.size; } // 返回映射中是否为空的判断值 + isEmpty() { return this.size === 0; } // 判断当前这棵树是否是一棵二分搜索树,有二分搜索树顺序性 isBanarySearchTree() { // 如果节点为空 那么这就是一棵空的二分搜索树 if (!this.root) return true; // 存储二分搜索树中的key const list = new Array(); // 中序遍历后,添加到list中的值会是以从小到大升序的样子排列 this.inOrder(this.root, list); // 从前往后判断 list中的值是否是从小到大升序的排列 // 验证 当前树是否符合二分搜索树的性质 for (var i = 1; i < list.length; i++) if (list[i - 1] > list[i]) return false; return true; } // 中序遍历 辅助函数 - inOrder(node, list) { // 递归到底的情况 if (!node) return; // 中序遍历时,添加到数组中的值会是以从小到大升序的样子排列 this.inOrder(node.left, list); list.push(node.key); this.inOrder(node.right, list); } // 判断该二叉树是否一棵平衡二叉树 isBalanced() { return this.recursiveIsBalanced(this.root); } // 递归判断某一个节点是否符合平衡二叉树的定义 辅助函数 - recursiveIsBalanced(node) { // 能够递归到底,说明符合要求 // 空的节点左右孩子高度差肯定为0, // 因为空树没有左右子树,更加谈不上下面去判断它的左右子树高度差是否会超过一。 if (!node) return true; // 如果当前节点的高度差大于1 说明不符合要求 if (Math.abs(this.getBalanceFactor(node)) > 1) return false; // 递归的去判断当前节点的 左右子树是否符合要求 return ( this.recursiveIsBalanced(node.left) && this.recursiveIsBalanced(node.right) ); } // @Override toString() 2018-11-05-jwl toString() { let mapInfo = `MyBinarySearchTreeMap: size = ${ this.size}, data = [ `; document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [
`; // 以非递归的前序遍历 输出字符串 let stack = new MyLinkedListStack(); stack.push(this.root); if (this.root === null) stack.pop(); while (!stack.isEmpty()) { let node = stack.pop(); if (node.left !== null) stack.push(node.left); if (node.right !== null) stack.push(node.right); if (node.left === null && node.right === null) { mapInfo += ` ${node.toString()} \r\n`; document.body.innerHTML += ` ${node.toString()}
`; } else { mapInfo += ` ${node.toString()}, \r\n`; document.body.innerHTML += ` ${node.toString()},
`; } } mapInfo += ` ] \r\n`; document.body.innerHTML += ` ]
`; return mapInfo; } } 复制代码 -
PerformanceTest
// 性能测试 class PerformanceTest { constructor() {} // 对比队列 testQueue(queue, openCount) { let startTime = Date.now(); let random = Math.random; for (var i = 0; i < openCount; i++) { queue.enqueue(random() * openCount); } while (!queue.isEmpty()) { queue.dequeue(); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 对比栈 testStack(stack, openCount) { let startTime = Date.now(); let random = Math.random; for (var i = 0; i < openCount; i++) { stack.push(random() * openCount); } while (!stack.isEmpty()) { stack.pop(); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 对比集合 testSet(set, openCount) { let startTime = Date.now(); let random = Math.random; let arr = []; let temp = null; // 第一遍测试 for (var i = 0; i < openCount; i++) { temp = random(); // 添加重复元素,从而测试集合去重的能力 set.add(temp * openCount); set.add(temp * openCount); arr.push(temp * openCount); } for (var i = 0; i < openCount; i++) { set.remove(arr[i]); } // 第二遍测试 for (var i = 0; i < openCount; i++) { set.add(arr[i]); set.add(arr[i]); } while (!set.isEmpty()) { set.remove(arr[set.getSize() - 1]); } let endTime = Date.now(); // 求出两次测试的平均时间 let avgTime = Math.ceil((endTime - startTime) / 2); return this.calcTime(avgTime); } // 对比映射 testMap(map, openCount) { let startTime = Date.now(); let array = new MyArray(); let random = Math.random; let temp = null; let result = null; for (var i = 0; i < openCount; i++) { temp = random(); result = openCount * temp; array.add(result); array.add(result); array.add(result); array.add(result); } for (var i = 0; i < array.getSize(); i++) { result = array.get(i); if (map.contains(result)) map.add(result, map.get(result) + 1); else map.add(result, 1); } for (var i = 0; i < array.getSize(); i++) { result = array.get(i); map.remove(result); } let endTime = Date.now(); return this.calcTime(endTime - startTime); } // 对比堆 主要对比 使用heapify 与 不使用heapify时的性能 testHeap(heap, array, isHeapify) { const startTime = Date.now(); // 是否支持 heapify if (isHeapify) heap.heapify(array); else { for (const element of array) heap.add(element); } console.log('heap size:' + heap.size() + '\r\n'); document.body.innerHTML += 'heap size:' + heap.size() + '
'; // 使用数组取值 let arr = new Array(heap.size()); for (let i = 0; i < arr.length; i++) arr[i] = heap.extractMax(); console.log( 'Array size:' + arr.length + ',heap size:' + heap.size() + '\r\n' ); document.body.innerHTML += 'Array size:' + arr.length + ',heap size:' + heap.size() + '
'; // 检验一下是否符合要求 for (let i = 1; i < arr.length; i++) if (arr[i - 1] < arr[i]) throw new Error('error.'); console.log('test heap completed.' + '\r\n'); document.body.innerHTML += 'test heap completed.' + '
'; const endTime = Date.now(); return this.calcTime(endTime - startTime); } // 对比并查集 testUnionFind(unionFind, openCount, primaryArray, secondaryArray) { const size = unionFind.getSize(); const random = Math.random; return this.testCustomFn(function() { // 合并操作 for (var i = 0; i < openCount; i++) { let primaryId = primaryArray[i]; let secondaryId = secondaryArray[i]; unionFind.unionElements(primaryId, secondaryId); } // 查询连接操作 for (var i = 0; i < openCount; i++) { let primaryRandomId = Math.floor(random() * size); let secondaryRandomId = Math.floor(random() * size); unionFind.unionElements(primaryRandomId, secondaryRandomId); } }); } // 计算运行的时间,转换为 天-小时-分钟-秒-毫秒 calcTime(result) { //获取距离的天数 var day = Math.floor(result / (24 * 60 * 60 * 1000)); //获取距离的小时数 var hours = Math.floor((result / (60 * 60 * 1000)) % 24); //获取距离的分钟数 var minutes = Math.floor((result / (60 * 1000)) % 60); //获取距离的秒数 var seconds = Math.floor((result / 1000) % 60); //获取距离的毫秒数 var milliSeconds = Math.floor(result % 1000); // 计算时间 day = day < 10 ? '0' + day : day; hours = hours < 10 ? '0' + hours : hours; minutes = minutes < 10 ? '0' + minutes : minutes; seconds = seconds < 10 ? '0' + seconds : seconds; milliSeconds = milliSeconds < 100 ? milliSeconds < 10 ? '00' + milliSeconds : '0' + milliSeconds : milliSeconds; // 输出耗时字符串 result = day + '天' + hours + '小时' + minutes + '分' + seconds + '秒' + milliSeconds + '毫秒' + ' <<<<============>>>> 总毫秒数:' + result; return result; } // 自定义对比 testCustomFn(fn) { let startTime = Date.now(); fn(); let endTime = Date.now(); return this.calcTime(endTime - startTime); } } 复制代码 -
Main
// main 函数 class Main { constructor() { this.alterLine('Map Comparison Area'); const n = 2000000; // const n = 200; const myBSTMap = new MyBinarySearchTreeMap(); const myAVLTree = new MyAVLTree(); let performanceTest1 = new PerformanceTest(); const random = Math.random; let arrNumber = new Array(n); // 循环添加随机数的值 for (let i = 0; i < n; i++) arrNumber[i] = Math.floor(n * random()); this.alterLine('MyBSTMap Comparison Area'); const myBSTMapInfo = performanceTest1.testCustomFn(function() { // 添加 for (const word of arrNumber) myBSTMap.add(word, String.fromCharCode(word)); // 删除 for (const word of arrNumber) myBSTMap.remove(word); // 查找 for (const word of arrNumber) if (myBSTMap.contains(word)) throw new Error("doesn't remove ok."); }); // 总毫秒数: console.log(myBSTMapInfo); console.log(myBSTMap); this.show(myBSTMapInfo); this.alterLine('MyAVLTree Comparison Area'); const that = this; const myAVLTreeInfo = performanceTest1.testCustomFn(function() { for (const word of arrNumber) myAVLTree.add(word, String.fromCharCode(word)); // 输出当前这棵myAVLTree树是否是一个二分搜索树 that.show( 'Is Binary Search Tree : ' + myAVLTree.isBanarySearchTree() ); console.log( 'Is Binary Search Tree : ' + myAVLTree.isBanarySearchTree() ); // 输出当前这棵myAVLTree树是否是一个平衡二叉树 that.show('Is Balanced : ' + myAVLTree.isBalanced()); console.log('Is Balanced : ' + myAVLTree.isBalanced()); // 删除 for (const word of arrNumber) { myAVLTree.remove(word); } // // 查找 for (const word of arrNumber) if (myAVLTree.contains(word)) throw new Error("doesn't remove ok."); }); console.log(myAVLTree); // 总毫秒数: console.log(myAVLTreeInfo); this.show(myAVLTreeInfo); } // 将内容显示在页面上 show(content) { document.body.innerHTML += `${content}
`; } // 展示分割线 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}
`; } } // 页面加载完毕 window.onload = function() { // 执行主函数 new Main(); }; 复制代码
AVL 树的删除操作 自平衡
- 在添加节点的操作中通过左旋转和右旋转的操作维持的 AVL 树的平衡
- AVL 树添加一个节点,
- 就是相应的按照二分搜索树的思路添加到适当的位置之后,
- 往上回溯,在这个回溯的过程中,由于添加了一个新的节点,
- 所以它的父辈节点相应的就有可能不再满足平衡二叉树的性质,
- 也就是说父辈节点的平衡因子的绝对值是大于一的,
- 那么对于这样的节点就需要相应的维护一下平衡,
- 这个具体的维护方法分别就是处理 LL、RR、LR、RL 这四种情况,
- 其实 AVL 的删除操作的平衡维护也是这四种情况。
- AVL 树的删除节点的操作和 AVL 树的添加操作是非常类似的
- 在删除的过程中依然使用二分搜索树的思路把某一个节点删除掉,
- 删除掉之后相应的从删除这个节点的子树的根节点出发,向上回溯搜索,
- 对于它的父辈节点,由于整棵二分搜索树删除掉了一个元素,
- 所以就有可能破坏了平衡性,
- 那么对于这些节点怎么维护平衡和具体维护平衡的方式
- 和添加操作维护平衡的方式是一模一样的。
- 删除操作逻辑分析
- 如果当前这个 node 节点已经为空了,直接返回空,
- 因为这就意味着没有找到待删除的节点,也就是说当前二分搜索树中根本就没有。
- 如果待删除的这个节点比当前节点的值要小,
- 那么就去当前这个节点的左子树继续去删除待删除的节点,
- 如果待删除的这个节点比当前节点的值要大,
- 那么就去当前这个节点的右子树继续去删除待删除的节点,
- 在这个删除的过程中直接将待删除的节点删除之后,
- 还是将当前这个节点给返回回去了,
- 在 AVL 树中有可能当前这个节点的左子树或者右子树
- 接到了来自递归的这个 remove 新的根节点之后,
- 当前的这个 node 节点的平衡性已经被破坏了,
- 所以在后续必须找到一个机会来维护 node 的平衡,
- 那么就不能那么早的将 node 给返回回去,
- 需要将删除待删除节点后的当前节点 node 保存一下,
- 这样做完之后在后续就有机会对这个保存的节点进行一下平衡的维护。
- 如果待删除的这个节点与当前这个节点相等的话,
- 就需要执行这样的逻辑,
- 如果待删除节点的左子树为空的话,那么就保存一下待删除节点的右子树,
- 因为当前节点即将被删除掉,左子树为空,那么当前节点就要被右子树覆盖,
- 之所以保存当前节点的右子树,
- 那么是因为要对这个保存的节点进行一下平衡的维护;
- 如果待删除节点的右子树为空的话,也是要保存一下待删除节点的左子树,
- 也是要对这个保存的节点进行一下平衡的维护;
- 如果待删除的这个节点左右子树均不为空的话,
- 那么就需要将待删除节点的右子树中最小的那个节点或者左子树中最大的那个节点
- 进行一次删除操作,删除后返回的节点作为新节点来进行保存,
- 因为这个节点将会取代当前的这个旧节点,
- 取代之前需要将当前这个节点的左右子树拷贝一份给这个新节点,
- 这个新的节点也是要进行平衡的维护。
- 经过上述的一系列操作之后,得到了保存的那个节点之后,
- 在最后对这个节点进行一下判断,看看是否需要根据这个节点来维护一下平衡,
- 这个维护的过程其实和添加操作中维护平衡的方式是一模一样的,
- 更新这个节点的高度,计算这个节点的平衡因子,
- 处理这个节点不平衡时可能发生的四种情况,
- 维护好这个节点之后,最后返回这个节点给上一层的递归调用,
- 在上一层的递归调用中依然是这样的一个过程来检查相应的对于这个 node 节点
- 是否需要这个后续的这些更新节点的操作,
- 那么整体的逻辑其实和之前添加节点是一样的。
- 删除操作逻辑的小 bug
- 在将待删除节点的右子树中最小的那个节点或者左子树中最大的那个节点
- 进行一次删除操作的时候,这一次操作并没有维持节点的平衡,
- 所以在这一步是有可能要打破 AVL 树的平衡条件的,
- 在这里有两个解决方案,
- 解决方案一是为这一次操作也添加上平衡维护这样的一个过程;
- 解决方案二是 直接将这一次删除操作变更一下,
- 也就是直接将 removeMin 改变为 remove,
- 在当前节点的右子树中删除最小的那个节点,
- 也就是复用 remove 方法中维护平衡的这个过程,
- 那样你就不需要在 removeMin 中再添加这样一个过程了;
- 解决方案二相当于又递归的调用了一下 remove 这个函数,
- 而整个 remove 函数已经添加了对这个节点平衡性的处理,
- 所以在整个逻辑中所有的删除操作都对平衡性进行了维护,
- 那么此时这个删除代码就完全正确了,
- 那么就可以将 removeMin 从 AVLTree 中删除掉。
- 删除操作逻辑整理
- 最终删除节点的操作那三个判断是互斥的,
- 所以需要通过 if-elseif-else 来分隔开来,
- 不然是会出现问题的,
- 然后删除节点操作是在以 node 为根的进行节点的删除,
- 很有可能删除掉这个节点之后获得的是空,
- 例如删除的这个节点是叶子节点之后就会发生这种情况,
- 那么此时在后面更新 height 的时候就会产生空指针的异常,因为 null 没有属性,
- 所以就还有一个边界需要处理,
- 也就是这个节点为空的时候不需要再维护这个空节点的平衡了,
- 只有在这个节点不为空时才去维护这个空姐点的平衡。
- 删除操作的测试
- 每删除一个节点就判断当前 AVL 树是否符合二分搜索树及 AVL 树的性质,
- 同时删除在所有节点之后打印一下 AVL 树中的节点个数。
代码示例
-
AVLTree
// 自定义AVL树节点 AVLTreeNode class MyAVLTreeNode { constructor(key = null, value = null, left = null, right = null) { this.key = key; this.value = value; this.left = left; this.right = right; this.height = 1; } // @Override toString 2018-11-24-jwl toString() { return this.key + '--->' + this.value + '--->' + this.height; } } // 自定义AVL树 AVLTree class MyAVLTree { constructor() { this.root = null; this.size = 0; } // 比较的功能 compare(keyA, keyB) { if (keyA === null || keyB === null) throw new Error("key is error. key can't compare."); if (keyA > keyB) return 1; else if (keyA < keyB) return -1; else return 0; } // 获取某个节点的高度 - getHeight(node) { // 节点为空 返回0 if (!node) return 0; // 直接返回这个节点的高度 return node.height; } // 获取一个节点的平衡因子 - getBalanceFactor(node) { // 节点为空 返回0 if (!node) return 0; // 左右子树的高度值 const leftHeight = this.getHeight(node.left); const rightHeight = this.getHeight(node.right); // 左子树的高度 - 右子树高度的值 = 平衡因子 return leftHeight - rightHeight; } // 根据key获取节点 - getNode(node, key) { // 先解决最基本的问题 if (!node) return null; // 开始将复杂的问题 逐渐缩小规模 // 从而求出小问题的解,最后构建出原问题的解 switch (this.compare(node.key, key)) { case 1: // 向左找 return this.getNode(node.left, key); break; case -1: // 向右找 return this.getNode(node.right, key); break; case 0: // 找到了 return node; break; default: throw new Error( 'compare result is error. compare result : 0、 1、 -1 .' ); break; } } // 对节点y进行向右旋转操作,返回旋转后新的根节点x // y x // / \ / \ // x T4 向右旋转 (y) z y // / \ - - - - - - - -> / \ / \ // z T3 T1 T2 T3 T4 // / \ // T1 T2 rightRotate(y) { const x = y.left; const T3 = x.right; // 向右旋转的过程 y.left = T3; x.right = y; // 更新节点的height值 只需要更新x和y的即可 y.height = 1 + Math.max(this.getHeight(y.left), this.getHeight(y.right)); x.height = 1 + Math.max(this.getHeight(x.left), this.getHeight(x.right)); // 返回 新节点 x return x; } // 对节点y进行向左旋转操作,返回旋转后新的根节点x // y x // / \ / \ // T1 x 向左旋转 (y) y z // / \ - - - - - - - -> / \ / \ // T2 z T1 T2 T3 T4 // / \ // T3 T4 leftRotate(y) { const x = y.right; const T2 = x.left; // 向左旋转的过程 y.right = T2; x.left = y; // 更新节点的height值 只需要更新x和y的即可 y.height = 1 + Math.max(this.getHeight(y.left), this.getHeight(y.right)); x.height = 1 + Math.max(this.getHeight(x.left), this.getHeight(x.right)); // 返回 新节点 x return x; } // 添加操作 + add(key, value) { this.root = this.recursiveAdd(this.root, key, value); } // 添加操作 递归算法 - recursiveAdd(node, key, value) { // 解决最简单的问题 if (node === null) { this.size++; return new MyAVLTreeNode(key, value); } // 将复杂的问题规模逐渐变小, // 从而求出小问题的解,从而构建出原问题的答案 if (this.compare(node.key, key) > 0) node.left = this.recursiveAdd(node.left, key, value); else if (this.compare(node.key, key) < 0) node.right = this.recursiveAdd(node.right, key, value); else node.value = value; // 在这里对节点的高度进行重新计算 节点本身高度为1 // 计算方式: 1 + 左右子树的height值最大的那个height值 node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right)); // 计算一个节点的平衡因子 const balanceFactor = this.getBalanceFactor(node); // // 如果平衡因子的绝对值大于1 说明不满足AVL平衡二叉树的性质了 // if (Math.abs(balanceFactor) > 1) { // console.log(node.toString() + " unbalanced : " + balanceFactor + "\r\n"); // document.body.innerHTML += node.toString() + " unbalanced : " + balanceFactor + "
"; // } // LL情况 平衡维护 右旋转操作 平衡因子为正数则表示左倾 反之为右倾 if (balanceFactor > 1 && this.getBalanceFactor(node.left) >= 0) return this.rightRotate(node); // RR情况 平衡维护 左旋转操作 平衡因子为负数则表示右倾 反之为左倾 if (balanceFactor < -1 && this.getBalanceFactor(node.right) <= 0) return this.leftRotate(node); // LR情况 平衡维护 先转换为LL情况 再处理LL情况 if (balanceFactor > 1 && this.getBalanceFactor(node.left) < 0) { node.left = this.leftRotate(node.left); return this.rightRotate(node); } // RL情况 平衡维护 先转换为RR情况 再处理RR情况 if (balanceFactor < -1 && this.getBalanceFactor(node.right) > 0) { node.right = this.rightRotate(node.right); return this.leftRotate(node); } return node; } // 删除操作 返回被删除的元素 + remove(key) { let node = this.getNode(this.root, key); if (!node) return null; this.root = this.recursiveRemove(this.root, key); return node.value; } // 删除操作 递归算法 + recursiveRemove(node, key) { // 解决最基本的问题 if (!node) return null; // 临时存储待返回的节点,但是返回之前先对它的平衡进行一下维护。 let returnNode; if (this.compare(node.key, key) > 0) { node.left = this.recursiveRemove(node.left, key); returnNode = node; } else if (this.compare(node.key, key) < 0) { node.right = this.recursiveRemove(node.right, key); returnNode = node; } else { // 当前节点的key 与 待删除的key的那个节点相同 // 有三种情况 // 1. 当前节点没有左子树,那么只有让当前节点的右子树直接覆盖当前节点,就表示当前节点被删除了 // 2. 当前节点没有右子树,那么只有让当前节点的左子树直接覆盖当前节点,就表示当前节点被删除了 // 3. 当前节点左右子树都有, 那么又分两种情况,使用前驱删除法或者后继删除法 // 1. 前驱删除法:使用当前节点的左子树上最大的那个节点覆盖当前节点 // 2. 后继删除法:使用当前节点的右子树上最小的那个节点覆盖当前节点 if (!node.left) { let rightNode = node.right; node.right = null; this.size--; returnNode = rightNode; } else if (!node.right) { let leftNode = node.left; node.left = null; this.size--; returnNode = leftNode; } else { let predecessor = this.maximum(node.left); node.left = this.removeMax(node.left); // this.recursiveRemove(node.left, predecessor.key) this.size++; // 开始嫁接 当前节点的左右子树 predecessor.left = node.left; predecessor.right = node.right; // 将当前节点从根节点剔除 node = node.left = node.right = null; this.size--; // 返回嫁接后的新节点 returnNode = predecessor; } } // 如果原本的节点或者新的节点是空 直接返回空即可 不需要下面的平衡维护 if (!returnNode) return null; // 在这里对节点的高度进行重新计算 节点本身高度为1 // 计算方式: 1 + 左右子树的height值最大的那个height值 returnNode.height = 1 + Math.max( this.getHeight(returnNode.left), this.getHeight(returnNode.right) ); // 删除节点后进行节点的平衡维护 // 计算一个节点的平衡因子 const balanceFactor = this.getBalanceFactor(returnNode); // LL情况 平衡维护 右旋转操作 平衡因子为正数则表示左倾 反之为右倾 if (balanceFactor > 1 && this.getBalanceFactor(returnNode.left) >= 0) return this.rightRotate(returnNode); // RR情况 平衡维护 左旋转操作 平衡因子为负数则表示右倾 反之为左倾 if (balanceFactor < -1 && this.getBalanceFactor(returnNode.right) <= 0) return this.leftRotate(returnNode); // LR情况 平衡维护 先转换为LL情况 再处理LL情况 if (balanceFactor > 1 && this.getBalanceFactor(returnNode.left) < 0) { returnNode.left = this.leftRotate(returnNode.left); return this.rightRotate(returnNode); } // RL情况 平衡维护 先转换为RR情况 再处理RR情况 if ( balanceFactor < -1 && this.getBalanceFactor(returnNode.right) > 0 ) { returnNode.right = this.rightRotate(returnNode.right); return this.leftRotate(returnNode); } return returnNode; } // 删除操作的两个辅助函数 // 获取最大值、删除最大值 // 以前驱的方式 来辅助删除操作的函数 // 获取最大值 maximum(node) { // 再也不能往右了,说明当前节点已经是最大的了 if (!node.right) return node; // 将复杂的问题渐渐减小规模,从而求出小问题的解,最后用小问题的解构建出原问题的答案 return this.maximum(node.right); } // 删除最大值 removeMax(node) { // 临时存储待返回的节点,但是返回之前先对它的平衡进行一下维护。 let returnNode; // 解决最基本的问题 if (!node.right) { let leftNode = node.left; node.left = null; this.size--; returnNode = leftNode; } else { // 开始化归 node.right = this.removeMax(node.right); returnNode = node; } // 如果原本的节点或者新的节点是空 直接返回空即可 不需要下面的平衡维护 if (!returnNode) return null; // 在这里对节点的高度进行重新计算 节点本身高度为1 // 计算方式: 1 + 左右子树的height值最大的那个height值 returnNode.height = 1 + Math.max( this.getHeight(returnNode.left), this.getHeight(returnNode.right) ); // 删除节点后进行节点的平衡维护 // 计算一个节点的平衡因子 const balanceFactor = this.getBalanceFactor(returnNode); // LL情况 平衡维护 右旋转操作 平衡因子为正数则表示左倾 反之为右倾 if (balanceFactor > 1 && this.getBalanceFactor(returnNode.left) >= 0) return this.rightRotate(returnNode); // RR情况 平衡维护 左旋转操作 平衡因子为负数则表示右倾 反之为左倾 if (balanceFactor < -1 && this.getBalanceFactor(returnNode.right) <= 0) return this.leftRotate(returnNode); // LR情况 平衡维护 先转换为LL情况 再处理LL情况 if (balanceFactor > 1 && this.getBalanceFactor(returnNode.left) < 0) { returnNode.left = this.leftRotate(returnNode.left); return this.rightRotate(returnNode); } // RL情况 平衡维护 先转换为RR情况 再处理RR情况 if ( balanceFactor < -1 && this.getBalanceFactor(returnNode.right) > 0 ) { returnNode.right = this.rightRotate(returnNode.right); return this.leftRotate(returnNode); } return returnNode; } // 查询操作 返回查询到的元素 + get(key) { let node = this.getNode(this.root, key); if (!node) return null; return node.value; } // 修改操作 + set(key, value) { let node = this.getNode(this.root, key); if (!node) throw new Error(key + " doesn't exist."); node.value = value; } // 返回是否包含该key的元素的判断值 + contains(key) { return !!this.getNode(this.root, key); } // 返回映射中实际的元素个数 + getSize() { return this.size; } // 返回映射中是否为空的判断值 + isEmpty() { return this.size === 0; } // 判断当前这棵树是否是一棵二分搜索树,有二分搜索树顺序性 isBanarySearchTree() { // 如果节点为空 那么这就是一棵空的二分搜索树 if (!this.root) return true; // 存储二分搜索树中的key const list = new Array(); // 中序遍历后,添加到list中的值会是以从小到大升序的样子排列 this.inOrder(this.root, list); // 从前往后判断 list中的值是否是从小到大升序的排列 // 验证 当前树是否符合二分搜索树的性质 for (var i = 1; i < list.length; i++) if (list[i - 1] > list[i]) return false; return true; } // 中序遍历 辅助函数 - inOrder(node, list) { // 递归到底的情况 if (!node) return; // 中序遍历时,添加到数组中的值会是以从小到大升序的样子排列 this.inOrder(node.left, list); list.push(node.key); this.inOrder(node.right, list); } // 判断该二叉树是否一棵平衡二叉树 isBalanced() { return this.recursiveIsBalanced(this.root); } // 递归判断某一个节点是否符合平衡二叉树的定义 辅助函数 - recursiveIsBalanced(node) { // 能够递归到底,说明符合要求 // 空的节点左右孩子高度差肯定为0, // 因为空树没有左右子树,更加谈不上下面去判断它的左右子树高度差是否会超过一。 if (!node) return true; // 如果当前节点的高度差大于1 说明不符合要求 if (Math.abs(this.getBalanceFactor(node)) > 1) return false; // 递归的去判断当前节点的 左右子树是否符合要求 return ( this.recursiveIsBalanced(node.left) && this.recursiveIsBalanced(node.right) ); } // @Override toString() 2018-11-05-jwl toString() { let mapInfo = `MyBinarySearchTreeMap: size = ${ this.size}, data = [ `; document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [
`; // 以非递归的前序遍历 输出字符串 let stack = new MyLinkedListStack(); stack.push(this.root); if (!this.root === null) stack.pop(); while (!stack.isEmpty()) { let node = stack.pop(); if (node.left !== null) stack.push(node.left); if (node.right !== null) stack.push(node.right); if (node.left === null && node.right === null) { mapInfo += ` ${node.toString()} \r\n`; document.body.innerHTML += ` ${node.toString()}
`; } else { mapInfo += ` ${node.toString()}, \r\n`; document.body.innerHTML += ` ${node.toString()},
`; } } mapInfo += ` ] \r\n`; document.body.innerHTML += ` ]
`; return mapInfo; } } 复制代码
基于 AVL 树的集合和映射
- 已经基于二分搜索树相应的添加了自平衡的机制
- 使其成为了一棵 AVL 树,
- 在添加节点和删除节点的时候有了自平衡这样的一个处理机制,
- 使得整棵二分搜索树不会退化成为一个链表。
更多 AVL 树的相关问题
- 基于 AVL 树的 Set 和 Map
- 已经基于链表和二分搜索树这两个数据结构实现了集合和映射,
- 在这个封装中,由于之前实现的二分搜索树其中每一个节点只承载一个元素,
- 所以可以基于这个二分搜索树的代码直接封装出集合这个数据结构,
- 对于映射需要重新再写一版二分搜索树,
- 这版二分搜索树中承载了 key 和 vlaue 这样的键值数据对,从而实现了相应的映射,
- 如果直接有一个二分搜索树的结构,这个二分搜索树的底层就支持键值对的存储,
- 那么基于这样的一个二分搜索树就可以直接实现映射,
- 与此同时使用这样的一个数据结构只要忽略掉值那一项就可以封装出集合来,
- 当前实现的 AVL 树就是从底层这个树结构直接支持了键值这样的数据对,
- 那么就可以直接复用数据结构封装出基于 AVL 树的集合和映射这样的两种数据结构。
- AVL 树的优化
- 由于 AVL 树的实现由于它已经保持了自平衡,
- 所以整体它的性能已经非常好了,
- 可以做到在最差的情况下在 AVL 树中
- 无论是增删改查这些操作全都是
Olog(n)
这个级别的, - 不过对于 AVL 树还可以进行一些比较细微的优化。
- 最典型的就是
- 在维护平衡之前都需要对每一个节点的高度进行一下重新的计算,
- 如果重新计算出的节点的高度和这个节点原先的高度相等的话,
- 那么后续对于这个节点的祖先节点就不再需要维护平衡的操作了,
- 这是因为这个节点的高度和原先一样,
- 从它的父亲节点或者祖先节点的角度上来看,它的子树的高度并没有发生变化,
- 也就不需要相应的去维护平衡了,将这个优化添加到 AVL 树中后,
- 这个 AVL 树的性能已经很高了。
- AVL 树的局限性
- 虽然对 AVL 树进行优化后性能已经很高了,但是依然有另外一种平衡二叉树,
- 它的性能可以和 AVL 树相匹敌,甚至可以说在统计意义上,
- 也就是在一般的平均情况下,
- 这种平衡二叉树它的整体性能是比 AVL 树更优的一种平衡二叉树,
- 就是大名鼎鼎的红黑树,红黑树的平均性能是比 AVL 树更优的,
- 这就像平均来讲快速排序算法是比归并排序算法更加的快的,
- 不过无论是快速排序算法还是归并排序算法它们二者都是
O(nlogn)
级别的时间复杂度, - 同理对于红黑树来说,它的增删改查的操作其实也都是在
O(logn)
这个级别的, - 也就是在复杂度上和 AVL 树并没有大的差异,
- 不过在具体的操作中由于红黑树的操作相应的旋转操作会更少一些,
- 所以整体的性能比 AVL 树更优一些,
- 尽管如此,AVL 树本身由于是第一个自平衡的二分搜索树,
- 与此同时这种维护自平衡的方式是基于左旋转和右旋转的方式是一种非常经典的操作,
- 所以 AVL 树是非常值得学习的,有了 AVL 树的基础之后再看红黑树,
- 在具体的理解上也会容易很多。
代码示例
-
MyAVLTree
// 自定义AVL树节点 AVLTreeNode class MyAVLTreeNode { constructor(key = null, value = null, left = null, right = null) { this.key = key; this.value = value; this.left = left; this.right = right; this.height = 1; } // @Override toString 2018-11-24-jwl toString() { return this.key + '--->' + this.value + '--->' + this.height; } } // 自定义AVL树 AVLTree class MyAVLTree { constructor() { this.root = null; this.size = 0; } // 比较的功能 compare(keyA, keyB) { if (keyA === null || keyB === null) throw new Error("key is error. key can't compare."); if (keyA > keyB) return 1; else if (keyA < keyB) return -1; else return 0; } // 获取某个节点的高度 - getHeight(node) { // 节点为空 返回0 if (!node) return 0; // 直接返回这个节点的高度 return node.height; } // 获取一个节点的平衡因子 - getBalanceFactor(node) { // 节点为空 返回0 if (!node) return 0; // 左右子树的高度值 const leftHeight = this.getHeight(node.left); const rightHeight = this.getHeight(node.right); // 左子树的高度 - 右子树高度的值 = 平衡因子 return leftHeight - rightHeight; } // 根据key获取节点 - getNode(node, key) { // 先解决最基本的问题 if (!node) return null; // 开始将复杂的问题 逐渐缩小规模 // 从而求出小问题的解,最后构建出原问题的解 switch (this.compare(node.key, key)) { case 1: // 向左找 return this.getNode(node.left, key); break; case -1: // 向右找 return this.getNode(node.right, key); break; case 0: // 找到了 return node; break; default: throw new Error( 'compare result is error. compare result : 0、 1、 -1 .' ); break; } } // 对节点y进行向右旋转操作,返回旋转后新的根节点x // y x // / \ / \ // x T4 向右旋转 (y) z y // / \ - - - - - - - -> / \ / \ // z T3 T1 T2 T3 T4 // / \ // T1 T2 rightRotate(y) { const x = y.left; const T3 = x.right; // 向右旋转的过程 y.left = T3; x.right = y; // 更新节点的height值 只需要更新x和y的即可 y.height = 1 + Math.max(this.getHeight(y.left), this.getHeight(y.right)); x.height = 1 + Math.max(this.getHeight(x.left), this.getHeight(x.right)); // 返回 新节点 x return x; } // 对节点y进行向左旋转操作,返回旋转后新的根节点x // y x // / \ / \ // T1 x 向左旋转 (y) y z // / \ - - - - - - - -> / \ / \ // T2 z T1 T2 T3 T4 // / \ // T3 T4 leftRotate(y) { const x = y.right; const T2 = x.left; // 向左旋转的过程 y.right = T2; x.left = y; // 更新节点的height值 只需要更新x和y的即可 y.height = 1 + Math.max(this.getHeight(y.left), this.getHeight(y.right)); x.height = 1 + Math.max(this.getHeight(x.left), this.getHeight(x.right)); // 返回 新节点 x return x; } // 添加操作 + add(key, value) { this.root = this.recursiveAdd(this.root, key, value); } // 添加操作 递归算法 - recursiveAdd(node, key, value) { // 解决最简单的问题 if (node === null) { this.size++; return new MyAVLTreeNode(key, value); } // 将复杂的问题规模逐渐变小, // 从而求出小问题的解,从而构建出原问题的答案 if (this.compare(node.key, key) > 0) node.left = this.recursiveAdd(node.left, key, value); else if (this.compare(node.key, key) < 0) node.right = this.recursiveAdd(node.right, key, value); else node.value = value; // 在这里对节点的高度进行重新计算 节点本身高度为1 // 计算方式: 1 + 左右子树的height值最大的那个height值 node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right)); // 计算一个节点的平衡因子 const balanceFactor = this.getBalanceFactor(node); // // 如果平衡因子的绝对值大于1 说明不满足AVL平衡二叉树的性质了 // if (Math.abs(balanceFactor) > 1) { // console.log(node.toString() + " unbalanced : " + balanceFactor + "\r\n"); // document.body.innerHTML += node.toString() + " unbalanced : " + balanceFactor + "
"; // } // LL情况 平衡维护 右旋转操作 平衡因子为正数则表示左倾 反之为右倾 if (balanceFactor > 1 && this.getBalanceFactor(node.left) >= 0) return this.rightRotate(node); // RR情况 平衡维护 左旋转操作 平衡因子为负数则表示右倾 反之为左倾 if (balanceFactor < -1 && this.getBalanceFactor(node.right) <= 0) return this.leftRotate(node); // LR情况 平衡维护 先转换为LL情况 再处理LL情况 if (balanceFactor > 1 && this.getBalanceFactor(node.left) < 0) { node.left = this.leftRotate(node.left); return this.rightRotate(node); } // RL情况 平衡维护 先转换为RR情况 再处理RR情况 if (balanceFactor < -1 && this.getBalanceFactor(node.right) > 0) { node.right = this.rightRotate(node.right); return this.leftRotate(node); } return node; } // 删除操作 返回被删除的元素 + remove(key) { let node = this.getNode(this.root, key); if (!node) return null; this.root = this.recursiveRemove(this.root, key); return node.value; } // 删除操作 递归算法 + recursiveRemove(node, key) { // 解决最基本的问题 if (!node) return null; // 临时存储待返回的节点,但是返回之前先对它的平衡进行一下维护。 let returnNode; const originHeight = node.height; // 记录原节点的高度 if (this.compare(node.key, key) > 0) { node.left = this.recursiveRemove(node.left, key); returnNode = node; } else if (this.compare(node.key, key) < 0) { node.right = this.recursiveRemove(node.right, key); returnNode = node; } else { // 当前节点的key 与 待删除的key的那个节点相同 // 有三种情况 // 1. 当前节点没有左子树,那么只有让当前节点的右子树直接覆盖当前节点,就表示当前节点被删除了 // 2. 当前节点没有右子树,那么只有让当前节点的左子树直接覆盖当前节点,就表示当前节点被删除了 // 3. 当前节点左右子树都有, 那么又分两种情况,使用前驱删除法或者后继删除法 // 1. 前驱删除法:使用当前节点的左子树上最大的那个节点覆盖当前节点 // 2. 后继删除法:使用当前节点的右子树上最小的那个节点覆盖当前节点 if (!node.left) { let rightNode = node.right; node.right = null; this.size--; returnNode = rightNode; } else if (!node.right) { let leftNode = node.left; node.left = null; this.size--; returnNode = leftNode; } else { let predecessor = this.maximum(node.left); node.left = this.removeMax(node.left); // this.recursiveRemove(node.left, predecessor.key) this.size++; // 开始嫁接 当前节点的左右子树 predecessor.left = node.left; predecessor.right = node.right; // 将当前节点从根节点剔除 node = node.left = node.right = null; this.size--; // 返回嫁接后的新节点 returnNode = predecessor; } } // 如果原本的节点或者新的节点是空 直接返回空即可 不需要下面的平衡维护 if (!returnNode) return null; // 在这里对节点的高度进行重新计算 节点本身高度为1 // 计算方式: 1 + 左右子树的height值最大的那个height值 returnNode.height = 1 + Math.max( this.getHeight(returnNode.left), this.getHeight(returnNode.right) ); // 旧节点的高度如果和新节点的高度一致,就不需要进行节点的平衡维护了 if (originHeight !== returnNode.height) { // 删除节点后进行节点的平衡维护 // 计算一个节点的平衡因子 const balanceFactor = this.getBalanceFactor(returnNode); // LL情况 平衡维护 右旋转操作 平衡因子为正数则表示左倾 反之为右倾 if ( balanceFactor > 1 && this.getBalanceFactor(returnNode.left) >= 0 ) return this.rightRotate(returnNode); // RR情况 平衡维护 左旋转操作 平衡因子为负数则表示右倾 反之为左倾 if ( balanceFactor < -1 && this.getBalanceFactor(returnNode.right) <= 0 ) return this.leftRotate(returnNode); // LR情况 平衡维护 先转换为LL情况 再处理LL情况 if ( balanceFactor > 1 && this.getBalanceFactor(returnNode.left) < 0 ) { returnNode.left = this.leftRotate(returnNode.left); return this.rightRotate(returnNode); } // RL情况 平衡维护 先转换为RR情况 再处理RR情况 if ( balanceFactor < -1 && this.getBalanceFactor(returnNode.right) > 0 ) { returnNode.right = this.rightRotate(returnNode.right); return this.leftRotate(returnNode); } } return returnNode; } // 删除操作的两个辅助函数 // 获取最大值、删除最大值 // 以前驱的方式 来辅助删除操作的函数 // 获取最大值 maximum(node) { // 再也不能往右了,说明当前节点已经是最大的了 if (!node.right) return node; // 将复杂的问题渐渐减小规模,从而求出小问题的解,最后用小问题的解构建出原问题的答案 return this.maximum(node.right); } // 删除最大值 removeMax(node) { // 临时存储待返回的节点,但是返回之前先对它的平衡进行一下维护。 let returnNode; const originHeight = node.height; // 记录原节点的高度 // 解决最基本的问题 if (!node.right) { let leftNode = node.left; node.left = null; this.size--; returnNode = leftNode; } else { // 开始化归 node.right = this.removeMax(node.right); returnNode = node; } // 如果原本的节点或者新的节点是空 直接返回空即可 不需要下面的平衡维护 if (!returnNode) return null; // 在这里对节点的高度进行重新计算 节点本身高度为1 // 计算方式: 1 + 左右子树的height值最大的那个height值 returnNode.height = 1 + Math.max( this.getHeight(returnNode.left), this.getHeight(returnNode.right) ); // 旧节点的高度如果和新节点的高度一致,就不需要进行节点的平衡维护了 if (originHeight !== returnNode.height) { // 删除节点后进行节点的平衡维护 // 计算一个节点的平衡因子 const balanceFactor = this.getBalanceFactor(returnNode); // LL情况 平衡维护 右旋转操作 平衡因子为正数则表示左倾 反之为右倾 if ( balanceFactor > 1 && this.getBalanceFactor(returnNode.left) >= 0 ) return this.rightRotate(returnNode); // RR情况 平衡维护 左旋转操作 平衡因子为负数则表示右倾 反之为左倾 if ( balanceFactor < -1 && this.getBalanceFactor(returnNode.right) <= 0 ) return this.leftRotate(returnNode); // LR情况 平衡维护 先转换为LL情况 再处理LL情况 if ( balanceFactor > 1 && this.getBalanceFactor(returnNode.left) < 0 ) { returnNode.left = this.leftRotate(returnNode.left); return this.rightRotate(returnNode); } // RL情况 平衡维护 先转换为RR情况 再处理RR情况 if ( balanceFactor < -1 && this.getBalanceFactor(returnNode.right) > 0 ) { returnNode.right = this.rightRotate(returnNode.right); return this.leftRotate(returnNode); } } return returnNode; } // 查询操作 返回查询到的元素 + get(key) { let node = this.getNode(this.root, key); if (!node) return null; return node.value; } // 修改操作 + set(key, value) { let node = this.getNode(this.root, key); if (!node) throw new Error(key + " doesn't exist."); node.value = value; } // 返回是否包含该key的元素的判断值 + contains(key) { return !!this.getNode(this.root, key); } // 返回映射中实际的元素个数 + getSize() { return this.size; } // 返回映射中是否为空的判断值 + isEmpty() { return this.size === 0; } // 判断当前这棵树是否是一棵二分搜索树,有二分搜索树顺序性 isBanarySearchTree() { // 如果节点为空 那么这就是一棵空的二分搜索树 if (!this.root) return true; // 存储二分搜索树中的key const list = new Array(); // 中序遍历后,添加到list中的值会是以从小到大升序的样子排列 this.inOrder(this.root, list); // 从前往后判断 list中的值是否是从小到大升序的排列 // 验证 当前树是否符合二分搜索树的性质 for (var i = 1; i < list.length; i++) if (list[i - 1] > list[i]) return false; return true; } // 中序遍历 辅助函数 - inOrder(node, list) { // 递归到底的情况 if (!node) return; // 中序遍历时,添加到数组中的值会是以从小到大升序的样子排列 this.inOrder(node.left, list); list.push(node.key); this.inOrder(node.right, list); } // 判断该二叉树是否一棵平衡二叉树 isBalanced() { return this.recursiveIsBalanced(this.root); } // 递归判断某一个节点是否符合平衡二叉树的定义 辅助函数 - recursiveIsBalanced(node) { // 能够递归到底,说明符合要求 // 空的节点左右孩子高度差肯定为0, // 因为空树没有左右子树,更加谈不上下面去判断它的左右子树高度差是否会超过一。 if (!node) return true; // 如果当前节点的高度差大于1 说明不符合要求 if (Math.abs(this.getBalanceFactor(node)) > 1) return false; // 递归的去判断当前节点的 左右子树是否符合要求 return ( this.recursiveIsBalanced(node.left) && this.recursiveIsBalanced(node.right) ); } // @Override toString() 2018-11-05-jwl toString() { let mapInfo = `MyBinarySearchTreeMap: size = ${ this.size}, data = [ `; document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [
`; // 以非递归的前序遍历 输出字符串 let stack = new MyLinkedListStack(); stack.push(this.root); if (!this.root === null) stack.pop(); while (!stack.isEmpty()) { let node = stack.pop(); if (node.left !== null) stack.push(node.left); if (node.right !== null) stack.push(node.right); if (node.left === null && node.right === null) { mapInfo += ` ${node.toString()} \r\n`; document.body.innerHTML += ` ${node.toString()}
`; } else { mapInfo += ` ${node.toString()}, \r\n`; document.body.innerHTML += ` ${node.toString()},
`; } } mapInfo += ` ] \r\n`; document.body.innerHTML += ` ]
`; return mapInfo; } } 复制代码 -
MyAVLTreeMap
// 自定义AVLTree映射 AVLTreeMap class MyAVLTreeMap { constructor() { this.myAVLTree = new MyAVLTree(); } // 添加操作 add(key, value) { this.MyAVLTree.add(key, value); } // 查询操作 get(key) { return this.MyAVLTree.get(key); } // 删除操作 remove(key) { return this.MyAVLTree.remove(key); } // 查看key是否存在 contains(key) { return this.MyAVLTree.contains(key); } // 更新操作 set(key, value) { this.MyAVLTree.set(key, value); } // 获取映射Map中实际元素个数 getSize() { return this.MyAVLTree.getSize(); } // 查看映射Map中是否为空 isEmpty() { return this.MyAVLTree.isEmpty(); } } 复制代码
-
MyAVLTreeSet
// 自定义AVLTree集合 AVLTreeSet class MyAVLTreeSet { // constructor() { this.myAVLTree = new MyAVLTree(); } add(element) { this.myAVLTree.add(element, null); } remove(element) { this.myAVLTree.remove(element); } contains(element) { return this.myAVLTree.contains(element); } getSize() { return this.myAVLTree.getSize(); } isEmpty() { return this.myAVLTree.isEmpty(); } } 复制代码
-
Main
// main 函数 class Main { constructor() { this.alterLine('Map Comparison Area'); const n = 2000000; // const n = 200; const myBSTMap = new MyBinarySearchTreeMap(); const myAVLTree = new MyAVLTree(); let performanceTest1 = new PerformanceTest(); const random = Math.random; let arrNumber = new Array(n); // 循环添加随机数的值 for (let i = 0; i < n; i++) arrNumber[i] = Math.floor(n * random()); this.alterLine('MyBSTMap Comparison Area'); const myBSTMapInfo = performanceTest1.testCustomFn(function() { // 添加 for (const word of arrNumber) myBSTMap.add(word, String.fromCharCode(word)); // 删除 for (const word of arrNumber) myBSTMap.remove(word); // 查找 for (const word of arrNumber) if (myBSTMap.contains(word)) throw new Error("doesn't remove ok."); }); // 总毫秒数: console.log(myBSTMapInfo); console.log(myBSTMap); this.show(myBSTMapInfo); this.alterLine('MyAVLTree Comparison Area'); const that = this; const myAVLTreeInfo = performanceTest1.testCustomFn(function() { for (const word of arrNumber) myAVLTree.add(word, String.fromCharCode(word)); // 输出当前这棵myAVLTree树是否是一个二分搜索树 that.show( 'Is Binary Search Tree : ' + myAVLTree.isBanarySearchTree() ); console.log( 'Is Binary Search Tree : ' + myAVLTree.isBanarySearchTree() ); // 输出当前这棵myAVLTree树是否是一个平衡二叉树 that.show('Is Balanced : ' + myAVLTree.isBalanced()); console.log('Is Balanced : ' + myAVLTree.isBalanced()); // 删除 for (const word of arrNumber) { myAVLTree.remove(word); } // // 查找 for (const word of arrNumber) if (myAVLTree.contains(word)) throw new Error("doesn't remove ok."); }); console.log(myAVLTree); // 总毫秒数: console.log(myAVLTreeInfo); this.show(myAVLTreeInfo); } // 将内容显示在页面上 show(content) { document.body.innerHTML += `${content}
`; } // 展示分割线 alterLine(title) { let line = `--------------------${title}----------------------`; console.log(line); document.body.innerHTML += `${line}
`; } } // 页面加载完毕 window.onload = function() { // 执行主函数 new Main(); }; 复制代码