为二进制搜索树添加新元素
这将介绍树形数据结构。树是计算机科学中一种重要的、通用的数据结构。当然,它们的名字来自于这样一个事实:当它们被可视化时,看起来很像我们在自然界中熟悉的树木。树形数据结构从一个节点开始,通常被称为根,然后从这里分支到其他节点,每个节点可能有更多的子节点,如此循环往复。该数据结构通常以根节点为顶点进行可视化;你可以把它想象成一棵自然的树,倒过来。
请注意,树本身就是递归数据结构。也就是说,一个节点的任何子节点都是其自己子树的父节点,以此类推。在为常见的树操作设计算法时,了解树的递归性质是很重要的。
首先,我们将讨论树的一个特殊类型,即二叉树。事实上,我们实际上将讨论一种特殊的二叉树,即二叉搜索树。虽然树形数据结构在一个节点上可以有任何数量的分支,但二叉树每个节点只能有两个分支。此外,二进制搜索树相对于子子树来说是有序的,这样,左子树中每个节点的值都小于或等于父节点的值,右子树中每个节点的值都大于或等于父节点的值。为了更好地理解这种关系,将这种关系可视化是非常有帮助的。
现在这种有序的关系非常容易看到。请注意,根节点8左边的每一个值都小于8,右边的每一个值都大于8,同时注意这种关系也适用于每一个子树。例如,左边的第一个孩子是一个子树。3是父节点,它正好有两个子节点--根据二进制搜索树的规则,我们甚至不用看就知道这个节点的左子(以及它的任何子)将小于3,右子(以及它的任何子)将大于3(但也小于结构的根值),等等。
二进制搜索树是非常常见和有用的数据结构,因为它们在平均情况下为几种常见的操作如查找、插入和删除提供对数时间。
我们将从简单的开始。我们在这里定义了一个二进制搜索树结构的骨架,此外还有一个为我们的树创建节点的函数。
请注意,每个节点可以有一个左值和右值。这些将被分配给子子树,如果它们存在的话。
在我们的二进制搜索树中,你将创建一个方法来向我们的二进制搜索树添加新的值。这个方法应该被称为add,它应该接受一个整数值来添加到树中。**注意保持二进制搜索树的不变性:每个左边子节点的值应该小于或等于父节点的值,而每个右边子节点的值应该大于或等于父节点的值。**这里,让我们使我们的树不能容纳重复的值。如果我们试图添加一个已经存在的值,该方法应该返回null。否则,如果添加成功,应该返回undefined。
提示:树是天然的递归数据结构!
var displayTree = tree => console.log(JSON.stringify(tree, null, 2));
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
};
function BinarySearchTree() {
this.root = null;
this.add = function(value) {
const newNode = new Node(value);
if (!this.root) {
this.root = newNode;
return undefined;
}
let currentNode = this.root;
while (currentNode) {
if (currentNode.value === newNode.value) {
return null; //添加失败,去重
}
const direction = currentNode.value > newNode.value ? "left" : "right";
if (!currentNode[direction]) {
currentNode[direction] = newNode;
return undefined; //添加成功
}
currentNode = currentNode[direction];
}
}
}
寻找二进制搜索树中的最小值和最大值
你将定义两个方法,findMin 和 findMax。这些方法应该返回二进制搜索树中持有的最小值和最大值。如果你被卡住了,反思一下二元搜索树必须要有的不变性:每个左子树小于或等于它的父树,每个右子树大于或等于它的父树。我们还可以说,我们的树只能存储整数值。如果树是空的,任何一个方法都应该返回空。
var displayTree = tree => console.log(JSON.stringify(tree, null, 2));
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}
function BinarySearchTree() {
this.root = null;
this.findMin = function() {
if (!this.root) return null;
let curr = this.root;
while (curr.left) { curr = curr.left; }
return curr.value;
}
this.findMax = function() {
if (!this.root) return null;
let curr = this.root;
while (curr.right) { curr = curr.right; }
return curr.value;
}
检查一个元素是否存在于二进制搜索树中
现在我们对二进制搜索树有了一个大致的了解,让我们更详细地谈一谈它。二进制搜索树在平均情况下为查找、插入和删除等常见操作提供对数时间,在最坏情况下提供线性时间。这是为什么呢?这些基本操作中的每一项都要求我们在树中找到一个项目(或者在插入的情况下找到它应该去的地方),由于树的结构在每个父节点,我们都在向左或向右分支,有效地排除了剩余树的一半大小。这使得搜索与树中节点数的对数成正比,在平均情况下,这些操作会产生对数时间。好的,但是最坏的情况呢?好吧,考虑从下列数值中构建一棵树,从左到右添加:10,12,17,25。按照二进制搜索树的规则,我们将在10的右边加上12,在17的右边加上这个,在25的右边加上这个。现在我们的树类似于一个链接列表,遍历它以找到25将需要我们以线性方式遍历所有项目。因此,在最坏的情况下是线性时间。这里的问题是,这棵树是不平衡的。我们将在下面的挑战中进一步研究这意味着什么。
在这个挑战中,我们将为我们的树创建一个工具。编写一个方法isPresent,该方法接收一个整数值作为输入,并返回一个布尔值,表示该值在二进制搜索树中是否存在。
var displayTree = tree => console.log(JSON.stringify(tree, null, 2));
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}
function BinarySearchTree() {
this.root = null;
this.isPresent = function(value) {
if (!this.root) return false;
let curr = this.root;
while (curr && curr.value !== value) {
if (curr.value > value) {
curr = curr.left;
} else {
curr = curr.right;
}
}
return !!curr;
}
}
检查树是否是二进制搜索树
二进制搜索树的主要区别是,节点是有组织的排序。节点最多有2个子节点(放在右边和/或左边),根据子节点的值是否大于或等于(右边)或小于(左边)父节点。
你将为你的树创建一个工具。编写一个 JavaScript 方法 isBinarySearchTree,该方法接收一棵树作为输入,并返回一个布尔值,表示该树是否为二进制搜索树。尽可能地使用递归。
var displayTree = (tree) => console.log(JSON.stringify(tree, null, 2));
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}
function BinarySearchTree() {
this.root = null;
}
function isBinarySearchTree(tree) {
if (!tree.root) return true;
function isBadDirection(node, direction) {
if (!node[direction]) return false;
// direction is bad if
// 1) node values are out of order
return (direction === "left"
? (node.value <= node.left.value)
: (node.value >= node.right.value)) ||
// OR 2) the sub-tree in that direction is bad
!isGoodTree(node[direction])
}
function isGoodTree(node) {
if (isBadDirection(node, "left")) return false;
if (isBadDirection(node, "right")) return false;
return true;
}
return isGoodTree(tree.root);
}
寻找二进制搜索树的最小和最大高度
在上一个挑战中,我们描述了一个树可能变得不平衡的情况。为了理解平衡的概念,我们来看看另一个树的属性:高度。树的高度表示从根节点到任何给定叶节点的距离。在一个高度分支的树结构中,不同的路径可能有不同的高度,但对于一个给定的树来说,会有一个最小和最大的高度。如果该树是平衡的,这些值最多相差一个。这意味着在一棵平衡的树中,所有的叶子节点都存在于同一层次内,或者如果它们不在同一层次内,它们最多相差一个层次。
平衡属性对树很重要,因为它决定了树操作的效率。正如我们在上一个挑战中所解释的,对于严重不平衡的树,我们面临最坏的时间复杂度。在具有动态数据集的树中,通常使用自平衡树来说明这个问题。常见的例子包括AVL树、红黑树和B型树。这些树都包含额外的内部逻辑,当插入或删除产生不平衡的状态时,会重新平衡树。
注意:与高度相似的属性是深度,它指的是一个给定的节点离根节点有多远。
为我们的二叉树写两个方法:findMinHeight 和 findMaxHeight。这些方法应该分别返回给定二叉树内最小和最大高度的整数值。如果节点是空的,让我们给它分配一个高度为-1(这是基本情况)。最后,添加第三个方法isBalanced,根据树是否平衡,返回真或假。你可以使用你刚才写的前两个方法来确定这一点。
var displayTree = tree => console.log(JSON.stringify(tree, null, 2));
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}
function BinarySearchTree() {
this.root = null;
this.findMinHeight = function() {
if (!this.root) return -1;
function findNodeMinHeight(node) {
if (!node) return 0;
return 1 + Math.min(
findNodeMinHeight(node.left),
findNodeMinHeight(node.right)
);
}
return findNodeMinHeight(this.root) - 1;
}
this.findMaxHeight = function() {
if (!this.root) return -1;
function findNodeMaxHeight(node) {
if (!node) return 0;
return 1 + Math.max(
findNodeMaxHeight(node.left),
findNodeMaxHeight(node.right)
);
}
return findNodeMaxHeight(this.root) - 1;
}
this.isBalanced = function() {
return this.findMinHeight() == this.findMaxHeight();
}
}
在二进制搜索树中使用深度优先搜索
我们知道如何在二进制搜索树中搜索一个特定的值。但是如果我们只是想探索整个树呢?或者如果我们没有一个有序的树,而我们只需要搜索一个值呢?这里我们将介绍一些树的遍历方法,这些方法可以用来探索树形数据结构。首先是深度优先搜索。在深度优先搜索中,在继续搜索另一个子树之前,要尽可能深入地探索一个给定的子树。有三种方法可以做到这一点。依次进行。从最左边的节点开始搜索,在最右边的节点结束。预先顺序。在叶子之前探索所有的根。顺序后。在根部之前探索所有的叶子。正如你所猜测的那样,你可以根据你的树所存储的数据类型和你所寻找的内容来选择不同的搜索方法。对于二进制搜索树来说,无序遍历会按排序的顺序返回节点。
这里我们将在我们的二进制搜索树上创建这三种搜索方法。深度优先搜索是一个固有的递归操作,只要有子节点存在,就会继续探索更多的子树。一旦你理解了这个基本概念,你可以简单地重新安排探索节点和子树的顺序,以产生上述三种搜索中的任何一种。例如,在后序搜索中,我们希望在开始返回任何节点本身之前一直递归到叶子节点,而在前序搜索中,我们希望首先返回节点,然后继续向下递归树。在我们的树上定义inorder、preorder和postorder方法。这些方法中的每一个都应该返回一个代表树的遍历的项目数组。要确保返回数组中每个节点的整数值,而不是节点本身。最后,如果树是空的,则返回null。
var displayTree = tree => console.log(JSON.stringify(tree, null, 2));
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}
function BinarySearchTree() {
this.root = null;
// In-order traversal
this.inorder = function() {
if (!this.root) return null;
function traverse(node) {
if (!node) return [];
const nodes = traverse(node.left); // Left
nodes.push(node.value); // Root (in-order)
nodes.push(...traverse(node.right)); // Right
return nodes;
}
return traverse(this.root);
}
// Pre-order traversal
this.preorder = function() {
if (!this.root) return null;
function traverse(node) {
if (!node) return [];
const nodes = [node.value]; // Root (pre-order)
nodes.push(...traverse(node.left)); // Left
nodes.push(...traverse(node.right)); // Right
return nodes;
}
return traverse(this.root);
}
// Post-order traversal
this.postorder = function() {
if (!this.root) return null;
function traverse(node) {
if (!node) return [];
const nodes = traverse(node.left); // Left
nodes.push(...traverse(node.right)); // Right
nodes.push(node.value); // Root (post-order)
return nodes;
}
return traverse(this.root);
}
}
var displayTree = tree => console.log(JSON.stringify(tree, null, 2));
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}
function BinarySearchTree() {
this.root = null;
this.remove = function(value) {
if (!this.root) return null;
// find the node
let parent;
let target = this.root;
while (target && target.value !== value) {
parent = target;
if (target.value > value) {
target = target.left;
} else {
target = target.right;
}
}
if (!target) return null;
// remove the node
// -- root node
if (!parent) {
this.root = null;
} else {
// -- other node
const direction = parent.left === target ? "left" : "right";
parent[direction] = null;
}
}
}
- 让我们来看看第二种情况:删除有一个孩子的节点。 在这种情况下,假设我们有一棵树,有以下节点1-2-3,其中1是根。要删除2,我们只需要让1的右侧引用指向3。更普遍的是,要删除一个只有一个孩子的节点,我们要让该节点的父节点引用树上的下一个节点。
我们找到要删除的目标和它的父节点,并定义目标节点的子节点数量。让我们在这里为只有一个孩子的目标节点添加下一个案例。在这里,我们要确定这个单一的孩子是树中的左分支还是右分支,然后在父节点中设置正确的引用,以指向这个节点。此外,让我们考虑一下目标是根节点的情况(这意味着父节点将是空的).
var displayTree = tree => console.log(JSON.stringify(tree, null, 2));
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}
function BinarySearchTree() {
this.root = null;
this.remove = function(value) {
if (!this.root) return null;
// find the node
let parent;
let target = this.root;
while (target && target.value !== value) {
parent = target;
if (target.value > value) {
target = target.left;
} else {
target = target.right;
}
}
if (!target) return null;
// remove the node
// -- root node
const replacement = target.right ? target.right : target.left;
if (!parent) {
this.root = replacement;
} else {
// -- other node
const direction = parent.left === target ? "left" : "right";
parent[direction] = replacement;
}
}
}
- 在二进制搜索树中删除一个有两个子节点的节点 删除有两个孩子的节点是最难实现的情况。删除这样的节点会产生两个子树,它们不再与原来的树结构相连。我们怎样才能重新连接它们呢?一种方法是在目标节点的右边子树中找到最小的值,然后用这个值替换目标节点。以这种方式选择替换,可以确保它大于它成为新父节点的左子树中的每个节点,但也小于它成为新父节点的右子树中的每个节点。一旦进行了这种替换,就必须将替换节点从右子树中删除。即使这个操作也很棘手,因为替换节点可能是一片叶子,也可能本身就是右子树的父节点。如果它是一个叶子,我们必须删除它的父节点对它的引用。否则,它必须是目标的右子。在这种情况下,我们必须用替换值替换目标值,并使目标引用成为替换的右子。
让我们通过处理第三种情况来完成我们的删除方法。我们已经为前两种情况再次提供了一些代码。现在添加一些代码来处理有两个子节点的目标节点。有什么需要注意的边缘情况吗?如果树上只有三个节点怎么办?一旦你完成了这将完成我们对二进制搜索树的删除操作。这是个相当难的问题!
var displayTree = tree => console.log(JSON.stringify(tree, null, 2));
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}
function BinarySearchTree() {
this.root = null;
this.remove = function(value) {
if (!this.root) return null;
// find the node
let parent;
let target = this.root;
while (target && target.value !== value) {
parent = target;
if (target.value > value) {
target = target.left;
} else {
target = target.right;
}
}
if (!target) return null;
// remove the node
// -- zero or one children
if (!(target.left && target.right)) {
// ---- root node
const replacement = target.right ? target.right : target.left;
if (!parent) {
this.root = replacement;
} else {
// ---- other node
const direction = parent.left === target ? "left" : "right";
parent[direction] = replacement;
}
} else {
// -- two children
// ---- replace current value with smallest child
const newChildValue = this.findMin(target.right);
this.remove(newChildValue);
target.value = newChildValue;
}
}
this.findMin = function(node = this.root) {
if (!node) return null;
return node.left ? this.findMin(node.left) : node.value;
}
this.findMax = function(node = this.root) {
if (!node) return null;
return node.right ? this.findMax(node.right) : node.value;
}
}
反转二叉树
这里我们将创建一个函数来反转二叉树。给定一棵二叉树,我们想产生一棵新的树,相当于这棵树的镜像。在倒置的树上运行无序遍历,与原树的无序遍历相比,将以相反的顺序探索节点。在我们的二叉树上写一个名为invert的方法来完成这个任务。调用这个方法应该反转当前的树结构。理想情况下,我们希望能在线性时间内就地完成这个任务。也就是说,我们只访问每个节点一次,并且在我们进行时修改现有的树结构,不使用任何额外的内存。
Try to use recursion and think of a base case.
var displayTree = (tree) => console.log(JSON.stringify(tree, null, 2));
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}
function BinarySearchTree() {
this.root = null;
this.invert = function(node = this.root) {
if (!node) return null;
[node.left, node.right] = [node.right, node.left];
this.invert(node.left);
this.invert(node.right);
}
}
创建一个三联体搜索树
在这里,我们将从二进制搜索树转移到另一种类型的树结构,即三叶树。trie是一种有序的搜索树,通常用来保存字符串,或者更一般的关联数组或动态数据集,其中的键是字符串。当许多键有重叠的前缀时,它们在存储数据集方面非常出色,例如,字典中的所有单词。与二叉树不同的是,节点不与实际值相关。相反,通往一个节点的路径代表一个特定的键。例如,如果我们想在三叉树中存储字符串代码,我们将有四个节点,每个字母一个:C-O-D-E,然后沿着这个路径穿过所有这些节点将创建作为字符串的代码--这个路径就是我们存储的键。然后,如果我们想添加字符串的编码,它将共享前三个节点的代码,然后在d之后分支离开。 通过这种方式,大型数据集可以被非常紧凑地存储。此外,搜索可以非常快,因为它实际上被限制在你所存储的字符串的长度上。此外,与二叉树不同,一个节点可以存储任何数量的子节点。正如你可能从上面的例子中猜到的那样,一些元数据通常被存储在持有一个键的结尾的节点上,以便在以后的遍历中仍然可以检索到该键。例如,如果我们在上面的例子中添加了代码,我们就需要某种方式来知道代码中的e代表之前输入的一个键的结束。否则,当我们添加代码时,这些信息就会有效地丢失。
让我们创建一个trie来存储单词。它将通过一个添加方法接受单词,并将这些单词存储在一个 trie 数据结构中。它还将允许我们用isWord方法查询一个给定的字符串是否是一个单词,并用print方法检索所有输入到trie中的单词。isWord应该返回一个布尔值,print应该返回一个所有这些单词的字符串值的数组。为了让我们验证这个数据结构的实现是否正确,我们为树中的每个节点提供了一个Node结构。每个节点将是一个对象,它的keys属性是一个JavaScript Map对象。这将保存每个节点的有效键的单个字母。我们还在节点上创建了一个end属性,如果该节点代表一个词的终止,则可以将其设置为true。
var displayTree = tree => console.log(JSON.stringify(tree, null, 2));
var Node = function() {
this.keys = new Map();
this.end = false;
this.setEnd = function() {
this.end = true;
};
this.isEnd = function() {
return this.end;
};
};
var Trie = function() {
this.root = new Node();
this.add = (wordParam) => {
console.log(wordParam);
function addWord(word, root) {
if (word) {
// console.log(Object.keys(root.keys));
if (Object.keys(root.keys).includes(word[0])) {
let letter = word[0];
addWord(word.substring(1), root.keys[letter]);
}
else {
const node = new Node();
let letter = word[0];
root.keys[letter] = node;
// console.log("\nroot after adding the key", word[0], ":", root, "\n");
if (word.length === 1) {
node.setEnd();
}
addWord(word.substring(1), root.keys[letter]);
}
}
}
addWord(wordParam, this.root);
// console.log("Root, finally:\n", this.root);
};
this.isWord = word => {
let root = this.root;
while (word) {
let firstLetter = word[0];
if (Object.keys(root.keys).includes(firstLetter)) {
if (word.length === 1) {
if (!root.keys[firstLetter].isEnd()) {
return false;
}
}
word = word.substring(1);
}
else {
return false;
}
root = root.keys[firstLetter];
}
return true;
};
this.print = () => {
const words = [];
function reTRIEve(root, word) {
// console.log(Object.keys(root.keys).length);
if (Object.keys(root.keys).length != 0) {
for (let letter of Object.keys(root.keys)) {
reTRIEve(root.keys[letter], word.concat(letter));
}
if (root.isEnd()) {
words.push(word);
}
}
else {
word.length > 0 ? words.push(word) : undefined;
return;
}
}
reTRIEve(this.root, "");
console.log(words);
return words;
};
};
在最大堆中插入一个元素
现在我们将转向另一种树状数据结构,即二进制堆。二进制堆是一个部分有序的二进制树,它满足堆的属性。堆属性指定了父节点和子节点之间的关系。你可以有一个最大堆,其中所有的父节点都大于或等于它们的子节点,或者一个最小堆,其中反之亦然。二进制堆也是完整的二进制树。这意味着树的所有层次都被完全填满,如果最后一层被部分填满,则从左到右填满。
虽然二进制堆可以作为树状结构来实现,其节点包含左和右的引用,但根据堆的属性进行部分排序,我们可以用数组来表示堆。父子关系是我们所感兴趣的,通过简单的算术,我们可以计算出任何父节点的子节点和任何子节点的父节点。
例如,考虑这个二进制迷你堆的数组表示。
[ 6, 22, 30, 37, 63, 48, 42, 76 ] 根节点是第一个元素6,其子节点是22和30。如果我们看一下这些数值的数组索引之间的关系,对于索引i的子元素是2i+1和2i+2。同样地,索引为0的元素是索引为1和2的这两个子代的父代。更一般地说,我们可以通过以下方法找到任何索引的节点的父节点。Math.floor((i - 1) / 2)。当二叉树增长到任何大小时,这些模式都会成立。最后,我们可以稍作调整,跳过数组中的第一个元素,使这个算术更容易。这样做对给定索引i处的任何元素都会产生以下关系。
例子数组表示。
[ null, 6, 22, 30, 37, 63, 48, 42, 76 ] 一个元素的左子:i * 2
一个元素的右边的孩子:i * 2 + 1
一个元素的父元素。Math.floor(i / 2)
一旦你理解了数学,使用数组表示是非常有用的,因为节点的位置可以通过这种算术快速确定,而且内存的使用也会减少,因为你不需要维护对子节点的引用。
指示。这里我们将创建一个最大堆。首先,只需创建一个插入方法,将元素添加到我们的堆中。在插入过程中,始终保持堆的属性是很重要的。对于一个最大堆来说,这意味着根元素在树上应该总是有最大的值,所有的父节点应该大于它们的子节点。对于一个数组实现的堆来说,这通常是通过三个步骤完成的。
将新元素添加到数组的末端。 如果这个元素比它的父节点大,就切换它们。 继续切换,直到新元素比它的父元素小或者你到达树的根部。 最后,添加一个打印方法,返回所有被添加到堆中的项目的数组。
var MaxHeap = function() {
this.heap = [null];
this.print = () => [...this.heap];
let leftChildIndex = (i) => 2 * i;
let rightChildIndex = (i) => 2 * i + 1;
let parentIndex = (i) => Math.floor(i / 2);
this.insert = (item) => {
this.heap.push(item);
let index = this.heap.length - 1;
while (index > 1 && item > this.heap[parentIndex(index)]) {
this.heap[index] = this.heap[parentIndex(index)];
this.heap[parentIndex(index)] = item;
index = parentIndex(index);
}
}
};
从Max Heap中删除一个元素
现在我们可以向我们的堆添加元素了,让我们看看如何删除元素。移除和插入元素都需要类似的逻辑。在一个最大堆中,你通常想移除最大的值,所以这涉及到从我们的树的根部简单地提取它。这将破坏我们树的堆属性,所以我们必须以某种方式重新建立它。通常,对于一个最大的堆来说,这是以下列方式进行的。
将堆中的最后一个元素移到根的位置。 如果根的任何一个子元素比它大,就把根与价值更大的子元素交换。 继续交换,直到父元素大于两个子元素或达到树的最后一层。 指示:给我们的最大堆添加一个方法,叫做remove。这个方法应该返回已经添加到最大堆中的最大值,并将其从堆中移除。它还应该重新排列堆的顺序,以便保持堆的属性。在移除一个元素之后,堆中剩下的下一个最大的元素应该成为根。
var MaxHeap = function() {
this.heap = [null];
this.insert = (ele) => {
var index = this.heap.length;
var arr = [...this.heap];
arr.push(ele);
while (ele > arr[Math.floor(index / 2)] && index > 1) {
arr[index] = arr[Math.floor(index / 2)];
arr[Math.floor(index / 2)] = ele;
index = arr[Math.floor(index / 2)];
}
this.heap = arr;
}
this.print = () => {
return this.heap.slice(1);
}
this.remove = () => {
this.heap = [...this.heap];
let max = this.heap[1];
let last = this.heap.pop();
this.heap[1] = last;
this.heapify(1);
return max;
}
this.heapify = (i) => {
let large = i;
let l = 2 * i + 0;
let r = 2 * i + 1;
let length = this.heap.length;
if (l < length && this.heap[l] > this.heap[large]) {
large = l;
}
if (r < length && this.heap[r] > this.heap[large]) {
large = r;
}
if (large != i) {
let temp = this.heap[i];
this.heap[i] = this.heap[large];
this.heap[large] = temp;
this.heapify(large);
}
}
};
用最小堆实现堆排序
现在我们可以添加和删除元素了,让我们看看堆的一些应用。堆通常被用来实现优先级队列,因为它们总是将价值最大或最小的项目存储在第一位置。此外,它们还被用来实现一种叫做堆排序的分类算法。我们将在这里看到如何做到这一点。堆排序使用一个最小堆,与最大堆相反。一个最小堆总是将最小值的元素存储在根位置。
堆排序的工作原理是:取一个未排序的数组,将数组中的每个项目添加到一个最小堆中,然后从最小堆中提取每个项目到一个新的数组中。最小堆结构保证了新数组将按照从少到多的顺序包含原始项目。这是最有效的排序算法之一,其平均和最坏情况下的性能为O(nlog(n))。
让我们用一个最小堆来实现堆排序。请随意在这里改编你的最大堆代码。创建一个具有插入、移除和排序方法的对象MinHeap。排序方法应该返回一个由最小堆中的所有元素组成的数组,从最小到最大排序。
function isSorted(a){
for (let i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
return false;
}
}
return true;
}
// Generate a randomly filled array
function createRandomArray(size = 5){
let a = new Array(size);
for (let i = 0; i < size; i++) {
a[i] = Math.floor(Math.random() * 100);
}
return a;
}
const array = createRandomArray(25);
var MinHeap = function() {
this.heap = [null];
// Insert
this.insert = (element) => {
this.heap.push(element);
let heap = this.heap;
function maxHeap(index) {
let parent = Math.floor(index/2);
if (element < heap[parent] && index > 1) {
[heap[index], heap[parent]] = [heap[parent], heap[index]];
maxHeap(parent);
}
}
maxHeap(this.heap.length-1);
}
// Remove
this.remove = () => {
let arr = [...this.heap];
let max = arr.splice(1, 1);
this.heap = [null];
for (let i = 1; i < arr.length; i++) {
this.insert(arr[i]);
}
return max[0];
}
// Sort
this.sort = (heap = this.heap) => {
let arr = [];
for (let i = 0; i < heap.length; i++) {
arr.push(this.remove());
}
return arr;
}
};