二叉树与堆:JavaScript中的树形数据结构实战

二叉树与堆:JavaScript中的树形数据结构实战

【免费下载链接】computer-science-in-javascript Collection of classic computer science paradigms, algorithms, and approaches written in JavaScript. 【免费下载链接】computer-science-in-javascript 项目地址: https://gitcode.com/gh_mirrors/co/computer-science-in-javascript

本文深入探讨了JavaScript中二叉树与堆的实现和应用,涵盖了二叉搜索树的基本原理与操作、遍历算法、堆数据结构以及实际应用案例。通过详细的代码示例和性能分析,帮助开发者掌握这些核心数据结构在真实项目中的使用技巧和最佳实践。

二叉搜索树的基本原理与实现

二叉搜索树(Binary Search Tree,BST)是一种高效的数据结构,它结合了链表的灵活性和数组的快速查找能力。在JavaScript中实现二叉搜索树不仅能够加深对数据结构的理解,还能在实际项目中提供高效的搜索、插入和删除操作。

二叉搜索树的核心特性

二叉搜索树遵循一个简单的规则:对于任意节点,其左子树的所有节点值都小于该节点的值,而右子树的所有节点值都大于该节点的值。这个特性使得二叉搜索树能够实现快速的查找操作。

mermaid

JavaScript中的二叉搜索树实现

让我们深入分析这个项目的二叉搜索树实现。该实现采用了ES6的类语法,并使用了Symbol来保护内部属性,确保封装性。

节点类定义
class BinarySearchTreeNode {
    constructor(value) {
        this.value = value;    // 存储的数值
        this.left = null;      // 左子节点指针
        this.right = null;     // 右子节点指针
    }
}

每个节点包含三个关键属性:存储的值、指向左子节点的指针和指向右子节点的指针。这种简单的结构构成了整个树的基础。

二叉搜索树类结构
class BinarySearchTree {
    constructor() {
        this[root] = null;  // 使用Symbol保护的根节点
    }
    
    // 核心方法:添加、查找、删除
    add(value) { /* 实现 */ }
    has(value) { /* 实现 */ }
    delete(value) { /* 实现 */ }
}

插入操作的实现原理

插入操作是二叉搜索树的基础,其算法遵循特定的遍历规则:

mermaid

插入算法代码实现
add(value) {
    const newNode = new BinarySearchTreeNode(value);
    
    if (this[root] === null) {
        this[root] = newNode;  // 空树时直接设置为根节点
    } else {
        let current = this[root];
        
        while (current !== null) {
            if (value < current.value) {
                if (current.left === null) {
                    current.left = newNode;
                    break;
                } else {
                    current = current.left;
                }
            } else if (value > current.value) {
                if (current.right === null) {
                    current.right = newNode;
                    break;
                } else {
                    current = current.right;
                }
            } else {
                break;  // 重复值,直接退出
            }
        }
    }
}

查找操作的实现

查找操作利用了二叉搜索树的排序特性,平均时间复杂度为O(log n):

has(value) {
    let found = false;
    let current = this[root];
    
    while (!found && current !== null) {
        if (value < current.value) {
            current = current.left;
        } else if (value > current.value) {
            current = current.right;
        } else {
            found = true;  // 找到匹配值
        }
    }
    
    return found;
}

删除操作的复杂性

删除操作是二叉搜索树中最复杂的操作,需要处理三种不同的情况:

删除情况处理方式复杂度
叶子节点直接删除O(1)
只有一个子节点用子节点替换O(1)
有两个子节点找到前驱或后继节点替换O(log n)
删除算法实现要点
delete(value) {
    // 查找要删除的节点及其父节点
    let current = this[root], parent = null;
    let found = false;
    
    // 搜索阶段
    while (!found && current !== null) {
        if (value < current.value) {
            parent = current;
            current = current.left;
        } else if (value > current.value) {
            parent = current;
            current = current.right;
        } else {
            found = true;
        }
    }
    
    if (!found) return;  // 未找到要删除的节点
    
    const nodeToRemove = current;
    
    // 处理三种删除情况
    if (nodeToRemove.left === null && nodeToRemove.right === null) {
        // 情况1: 叶子节点
        this.#removeLeafNode(nodeToRemove, parent);
    } else if (nodeToRemove.left === null || nodeToRemove.right === null) {
        // 情况2: 只有一个子节点
        this.#removeNodeWithOneChild(nodeToRemove, parent);
    } else {
        // 情况3: 有两个子节点
        this.#removeNodeWithTwoChildren(nodeToRemove);
    }
}

性能特征分析

二叉搜索树的性能高度依赖于树的平衡性。以下是不同情况下的时间复杂度对比:

操作平均情况最坏情况空间复杂度
查找O(log n)O(n)O(1)
插入O(log n)O(n)O(1)
删除O(log n)O(n)O(1)

实际应用场景

二叉搜索树在JavaScript开发中有多种应用场景:

  1. 数据库索引:快速查找和范围查询
  2. 自动补全:前缀搜索和字典实现
  3. 游戏开发:空间划分和碰撞检测
  4. 文件系统:目录结构的组织
  5. 网络路由:IP地址的路由查找

实现的最佳实践

在实现二叉搜索树时,有几个重要的最佳实践:

  1. 使用Symbol保护内部状态:防止外部直接修改内部指针
  2. 充分的错误处理:处理边界情况和异常输入
  3. 详细的注释:便于理解和维护
  4. 迭代器实现:支持ES6的迭代协议
  5. 内存管理:及时清理不再使用的节点

通过这个完整的二叉搜索树实现,我们不仅掌握了数据结构的基本原理,还学习了如何在JavaScript中高效地实现复杂的数据结构。这种实现方式既保持了代码的可读性,又确保了性能的最优化。

二叉树遍历算法的JavaScript实现

二叉树遍历是树形数据结构中最基础且重要的操作之一,它决定了我们如何访问和处理树中的每个节点。在JavaScript中实现二叉树遍历算法,不仅能够帮助我们深入理解递归和迭代的编程思想,还能为后续的树形结构操作打下坚实基础。

遍历算法的基本类型

二叉树遍历主要分为四种基本类型,每种类型都有其特定的访问顺序和应用场景:

遍历类型访问顺序应用场景
前序遍历根 → 左 → 右复制树结构、前缀表达式
中序遍历左 → 根 → 右二叉搜索树排序输出
后序遍历左 → 右 → 根删除树节点、后缀表达式
层序遍历按层次从上到下广度优先搜索、最短路径

递归实现的核心代码

在computer-science-in-javascript项目中,二叉搜索树的遍历实现采用了经典的递归方法。让我们深入分析其中的关键代码:

// 中序遍历的递归实现
function traverse(node) {
    if (node !== null) {
        // 遍历左子树
        if (node.left !== null) {
            traverse(node.left);
        }
        
        // 访问当前节点
        console.log(node.value);
        
        // 遍历右子树
        if (node.right !== null) {
            traverse(node.right);
        }
    }
}

这种递归实现虽然简洁,但存在栈溢出的风险。对于深度较大的树,我们需要考虑迭代实现。

迭代遍历算法的实现

迭代遍历使用显式的栈来模拟递归调用,避免了递归深度限制的问题:

// 中序遍历的迭代实现
function inorderTraversalIterative(root) {
    const stack = [];
    let current = root;
    const result = [];
    
    while (current !== null || stack.length > 0) {
        // 遍历到最左边的节点
        while (current !== null) {
            stack.push(current);
            current = current.left;
        }
        
        // 访问节点
        current = stack.pop();
        result.push(current.value);
        
        // 转向右子树
        current = current.right;
    }
    
    return result;
}

前序遍历与后序遍历的实现差异

不同遍历类型的实现主要区别在于节点访问的时机:

// 前序遍历迭代实现
function preorderTraversalIterative(root) {
    if (root === null) return [];
    
    const stack = [root];
    const result = [];
    
    while (stack.length > 0) {
        const node = stack.pop();
        result.push(node.value);
        
        // 先右后左,保证左子树先被访问
        if (node.right !== null) stack.push(node.right);
        if (node.left !== null) stack.push(node.left);
    }
    
    return result;
}

层序遍历(广度优先搜索)

层序遍历使用队列数据结构,按层次访问节点:

function levelOrderTraversal(root) {
    if (root === null) return [];
    
    const queue = [root];
    const result = [];
    
    while (queue.length > 0) {
        const levelSize = queue.length;
        const currentLevel = [];
        
        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();
            currentLevel.push(node.value);
            
            if (node.left !== null) queue.push(node.left);
            if (node.right !== null) queue.push(node.right);
        }
        
        result.push(currentLevel);
    }
    
    return result;
}

遍历算法的性能分析

为了帮助开发者选择合适的遍历算法,我们通过表格对比各种实现的性能特征:

算法类型时间复杂度空间复杂度适用场景
递归遍历O(n)O(h)树深度较小的情况
迭代遍历O(n)O(h)避免栈溢出
莫里斯遍历O(n)O(1)需要常数空间
层序遍历O(n)O(w)广度优先需求

其中h表示树的高度,w表示树的最大宽度,n表示节点总数。

实际应用示例

让我们通过一个具体的例子来演示遍历算法的应用:

// 构建一个示例二叉树
const tree = new BinarySearchTree();
tree.add(5);
tree.add(3);
tree.add(7);
tree.add(2);
tree.add(4);
tree.add(6);
tree.add(8);

// 中序遍历得到排序结果
const sorted = inorderTraversalIterative(tree.root);
console.log('中序遍历结果:', sorted); // [2, 3, 4, 5, 6, 7, 8]

// 前序遍历结果
const preorder = preorderTraversalIterative(tree.root);
console.log('前序遍历结果:', preorder); // [5, 3, 2, 4, 7, 6, 8]

// 层序遍历结果
const levelOrder = levelOrderTraversal(tree.root);
console.log('层序遍历结果:', levelOrder); // [[5], [3, 7], [2, 4, 6, 8]]

遍历算法的优化技巧

在实际开发中,我们可以采用一些优化技巧来提升遍历算法的性能:

  1. 尾递归优化:对于支持尾递归的JavaScript引擎,可以优化递归实现
  2. 迭代器模式:使用生成器函数实现惰性求值
  3. 内存优化:复用数据结构减少内存分配
// 使用生成器实现遍历迭代器
function* inorderIterator(root) {
    const stack = [];
    let current = root;
    
    while (current !== null || stack.length > 0) {
        while (current !== null) {
            stack.push(current);
            current = current.left;
        }
        
        current = stack.pop();
        yield current.value;
        current = current.right;
    }
}

// 使用示例
for (const value of inorderIterator(tree.root)) {
    console.log(value);
}

常见问题与解决方案

在实现二叉树遍历时,开发者经常会遇到一些典型问题:

  1. 栈溢出:对于深度很大的树,递归实现可能导致栈溢出

    • 解决方案:使用迭代实现或尾递归优化
  2. 内存消耗:迭代实现需要显式的栈结构

    • 解决方案:使用莫里斯遍历算法实现O(1)空间复杂度
  3. 性能瓶颈:频繁的函数调用和内存分配

    • 解决方案:优化数据结构和算法选择

通过深入理解各种遍历算法的实现原理和性能特征,开发者可以根据具体需求选择最合适的遍历策略,从而编写出高效、可靠的树形结构处理代码。

二叉堆的数据结构与操作

二叉堆是一种特殊的完全二叉树数据结构,它满足堆属性:对于最小堆,每个节点的值都小于或等于其子节点的值;对于最大堆,每个节点的值都大于或等于其子节点的值。这种数据结构在优先队列、堆排序和Dijkstra算法等场景中有着广泛的应用。

二叉堆的核心结构

二叉堆通常使用数组来实现,这种实现方式既节省空间又便于操作。数组中的元素按照层次遍历的顺序存储,具有以下重要特性:

  • 父节点索引计算parentIndex = Math.floor((index - 1) / 2)
  • 左子节点索引计算leftChildIndex = (index * 2) + 1
  • 右子节点索引计算rightChildIndex = (index * 2) + 2

mermaid

核心操作方法详解

1. 插入操作 (add)

插入操作是二叉堆的基本操作之一,时间复杂度为O(log n)。具体步骤如下:

add(data) {
    this[array].push(data);          // 将新元素添加到数组末尾
    heapifyUp(this[array], this[compare]);  // 执行向上堆化操作
}

向上堆化(heapifyUp)的过程确保新插入的元素能够找到其在堆中的正确位置:

function heapifyUp(array, compare) {
    let currentIndex = array.length - 1;
    
    while (currentIndex > 0) {
        let parentIndex = getParentIndex(currentIndex);
        
        // 如果父节点应该排在当前节点之后,则交换位置
        if (compare(array[parentIndex], array[currentIndex]) > 0) {
            swap(array, parentIndex, currentIndex);
            currentIndex = parentIndex;
        } else {
            break;  // 堆属性已满足,退出循环
        }
    }
}
2. 删除堆顶操作 (poll)

删除堆顶元素是二叉堆的另一个关键操作,同样具有O(log n)的时间复杂度:

poll() {
    if (this.isEmpty()) {
        throw new Error("Heap is empty.");
    }

    if (this[array].length > 1) {
        const topValue = this[array][0];
        const replacementValue = this[array].pop();
        this[array][0] = replacementValue;
        heapifyDown(this[array], this[compare]);
        return topValue;
    }
    
    return this[array].pop();
}

向下堆化(heapifyDown)过程确保在移除堆顶元素后,堆属性得以维持:

function heapifyDown(array, compare) {
    let currentIndex = 0;
    
    while (hasLeftChild(array, currentIndex)) {
        let smallerChildIndex = getLeftChildIndex(currentIndex);
        
        // 检查右子节点是否存在且是否更小
        if (hasRightChild(array, currentIndex)) {
            let rightChildIndex = getRightChildIndex(currentIndex);
            if (compare(array[smallerChildIndex], array[rightChildIndex]) > 0) {
                smallerChildIndex = rightChildIndex;
            }
        }
        
        // 如果当前节点应该排在较小子节点之后,则交换
        if (compare(array[currentIndex], array[smallerChildIndex]) > 0) {
            swap(array, currentIndex, smallerChildIndex);
            currentIndex = smallerChildIndex;
        } else {
            break;  // 堆属性已满足,退出循环
        }
    }
}

堆操作的时间复杂度分析

操作时间复杂度描述
插入(add)O(log n)最坏情况下需要向上遍历整个树的高度
删除堆顶(poll)O(log n)需要向下调整整个堆结构
查看堆顶(peek)O(1)直接访问数组第一个元素
构建堆O(n)通过Floyd算法批量构建

比较器函数的灵活性

二叉堆的实现支持自定义比较器,这使得它可以灵活地处理各种数据类型和排序需求:

// 最小堆(默认)
const minHeap = new BinaryHeap((a, b) => a - b);

// 最大堆
const maxHeap = new BinaryHeap((a, b) => b - a);

// 对象堆(按优先级排序)
const priorityHeap = new BinaryHeap((a, b) => a.priority - b.priority);

// 字符串堆(按字母顺序)
const stringHeap = new BinaryHeap((a, b) => a.localeCompare(b));

实际应用示例

二叉堆在现实世界中有许多重要应用,以下是一些典型场景:

优先队列实现

class PriorityQueue {
    constructor() {
        this.heap = new BinaryHeap((a, b) => a.priority - b.priority);
    }
    
    enqueue(item, priority) {
        this.heap.add({ item, priority });
    }
    
    dequeue() {
        return this.heap.poll().item;
    }
    
    get isEmpty() {
        return this.heap.isEmpty();
    }
}

Top K 问题解决方案

function findTopK(arr, k, comparator = (a, b) => a - b) {
    const heap = new BinaryHeap(comparator);
    
    for (const item of arr) {
        heap.add(item);
        if (heap.size > k) {
            heap.poll();
        }
    }
    
    return [...heap.values()].sort(comparator);
}

二叉堆的这种实现方式不仅高效,而且具有良好的扩展性和灵活性,使其成为处理优先级相关问题的理想选择。通过合理的抽象和封装,开发者可以轻松地在各种应用场景中利用二叉堆的强大功能。

树形结构的实际应用案例

树形数据结构在现代软件开发中扮演着至关重要的角色,它们不仅仅是理论上的概念,更是解决实际问题的强大工具。让我们深入探讨二叉树和堆在现实世界中的具体应用场景。

文件系统与目录结构

文件系统是树形结构最直观的应用之一。每个目录都可以看作是一个节点,子目录和文件则是其子节点。这种层次化的组织方式使得文件的查找、遍历和管理变得高效。

// 模拟文件系统的树形结构
class FileSystemNode {
    constructor(name, isDirectory = false) {
        this.name = name;
        this.isDirectory = isDirectory;
        this.children = [];
        this.parent = null;
    }
    
    addChild(node) {
        node.parent = this;
        this.children.push(node);
    }
    
    // 查找文件或目录
    find(path) {
        const parts = path.split('/').filter(part => part !== '');
        let current = this;
        
        for (const part of parts) {
            const child = current.children.find(child => child.name === part);
            if (!child) return null;
            current = child;
        }
        return current;
    }
}

// 创建文件系统示例
const root = new FileSystemNode('', true);
const home = new FileSystemNode('home', true);
const user = new FileSystemNode('user', true);
const documents = new FileSystemNode('documents', true);
const file1 = new FileSystemNode('document.txt', false);

root.addChild(home);
home.addChild(user);
user.addChild(documents);
documents.addChild(file1);

数据库索引优化

二叉搜索树(BST)在数据库系统中被广泛用于索引的实现。通过BST,数据库可以快速定位记录,将查询时间复杂度从O(n)降低到O(log n)。

mermaid

优先队列与任务调度

二叉堆是实现优先队列的理想数据结构,广泛应用于任务调度系统中。操作系统进程调度、网络请求优先级处理等都依赖于堆结构。

// 使用二叉堆实现任务调度器
class TaskScheduler {
    constructor() {
        this.heap = new BinaryHeap((a, b) => a.priority - b.priority);
    }
    
    addTask(task, priority) {
        this.heap.add({ task, priority });
    }
    
    executeNext() {
        if (this.heap.isEmpty()) return null;
        const nextTask = this.heap.poll();
        return nextTask.task();
    }
    
    getPendingTasks() {
        return this.heap.size();
    }
}

// 示例使用
const scheduler = new TaskScheduler();
scheduler.addTask(() => console.log('紧急任务'), 1);
scheduler.addTask(() => console.log('普通任务'), 3);
scheduler.addTask(() => console.log('高优先级任务'), 2);

// 按优先级顺序执行任务
while (scheduler.getPendingTasks() > 0) {
    scheduler.executeNext();
}

编译器与语法分析

在编译原理中,抽象语法树(AST)是源代码的树形表示。编译器使用二叉树结构来解析和优化代码。

// 简单的表达式解析树示例
class ASTNode {
    constructor(type, value = null) {
        this.type = type;
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

// 构建表达式 3 + 4 * 5 的AST
const multiply = new ASTNode('*');
multiply.left = new ASTNode('number', 4);
multiply.right = new ASTNode('number', 5);

const add = new ASTNode('+');
add.left = new ASTNode('number', 3);
add.right = multiply;

// 表达式求值函数
function evaluateAST(node) {
    if (node.type === 'number') return node.value;
    
    const leftVal = evaluateAST(node.left);
    const rightVal = evaluateAST(node.right);
    
    switch (node.type) {
        case '+': return leftVal + rightVal;
        case '-': return leftVal - rightVal;
        case '*': return leftVal * rightVal;
        case '/': return leftVal / rightVal;
        default: throw new Error('未知操作符');
    }
}

console.log(evaluateAST(add)); // 输出: 23

网络路由与决策树

在网络路由协议中,决策树用于确定数据包的最佳传输路径。二叉树结构帮助路由器快速做出转发决策。

mermaid

游戏开发中的空间分区

在游戏开发中,二叉树用于空间分区算法,如四叉树和八叉树,用于高效处理碰撞检测和可见性判断。

// 简单的四叉树实现
class QuadTree {
    constructor(boundary, capacity = 4) {
        this.boundary = boundary; // {x, y, width, height}
        this.capacity = capacity;
        this.points = [];
        this.divided = false;
        this.northwest = null;
        this.northeast = null;
        this.southwest = null;
        this.southeast = null;
    }
    
    insert(point) {
        if (!this.contains(point)) return false;
        
        if (this.points.length < this.capacity) {
            this.points.push(point);
            return true;
        }
        
        if (!this.divided) this.subdivide();
        
        return this.northwest.insert(point) ||
               this.northeast.insert(point) ||
               this.southwest.insert(point) ||
               this.southeast.insert(point);
    }
    
    subdivide() {
        const { x, y, width, height } = this.boundary;
        const halfWidth = width / 2;
        const halfHeight = height / 2;
        
        this.northwest = new QuadTree({
            x: x, y: y, width: halfWidth, height: halfHeight
        }, this.capacity);
        
        this.northeast = new QuadTree({
            x: x + halfWidth, y: y, width: halfWidth, height: halfHeight
        }, this.capacity);
        
        this.southwest = new QuadTree({
            x: x, y: y + halfHeight, width: halfWidth, height: halfHeight
        }, this.capacity);
        
        this.southeast = new QuadTree({
            x: x + halfWidth, y: y + halfHeight, width: halfWidth, height: halfHeight
        }, this.capacity);
        
        this.divided = true;
    }
    
    contains(point) {
        return point.x >= this.boundary.x &&
               point.x <= this.boundary.x + this.boundary.width &&
               point.y >= this.boundary.y &&
               point.y <= this.boundary.y + this.boundary.height;
    }
    
    query(range, found = []) {
        if (!this.intersects(range)) return found;
        
        for (const point of this.points) {
            if (this.contains(point) && 
                point.x >= range.x && point.x <= range.x + range.width &&
                point.y >= range.y && point.y <= range.y + range.height) {
                found.push(point);
            }
        }
        
        if (this.divided) {
            this.northwest.query(range, found);
            this.northeast.query(range, found);
            this.southwest.query(range, found);
            this.southeast.query(range, found);
        }
        
        return found;
    }
    
    intersects(range) {
        return !(range.x > this.boundary.x + this.boundary.width ||
                range.x + range.width < this.boundary.x ||
                range.y > this.boundary.y + this.boundary.height ||
                range.y + range.height < this.boundary.y);
    }
}

机器学习与决策树算法

在机器学习领域,决策树算法使用二叉树结构来进行分类和回归分析。每个内部节点代表一个特征测试,每个叶节点代表一个类别或数值。

算法类型应用场景二叉树角色优势
分类树垃圾邮件识别特征分割可解释性强
回归树房价预测数值预测处理非线性关系
随机森林综合预测多树集成减少过拟合
梯度提升树排序算法顺序优化高预测精度
// 简单的决策树分类器示例
class DecisionTreeNode {
    constructor(featureIndex = null, threshold = null, value = null) {
        this.featureIndex = featureIndex; // 用于分割的特征索引
        this.threshold = threshold;       // 分割阈值
        this.value = value;               // 叶节点的预测值
        this.left = null;                 // 左子树
        this.right = null;                // 右子树
    }
    
    isLeaf() {
        return this.value !== null;
    }
}

class DecisionTree {
    constructor(maxDepth = 10) {
        this.maxDepth = maxDepth;
        this.root = null;
    }
    
    fit(X, y) {
        this.root = this._growTree(X, y);
    }
    
    predict(X) {
        return X.map(sample => this._predictSample(sample, this.root));
    }
    
    _growTree(X, y, depth = 0) {
        const numSamples = X.length;
        const numFeatures = X[0].length;
        
        // 终止条件:达到最大深度或所有样本属于同一类别
        if (depth >= this.maxDepth || this._allSame(y)) {
            const leafValue = this._mostCommon(y);
            return new DecisionTreeNode(null, null, leafValue);
        }
        
        // 寻找最佳分割
        let bestFeature = null;
        let bestThreshold = null;
        let bestGain = -1;
        
        for (let featureIndex = 0; featureIndex < numFeatures; featureIndex++) {
            const featureValues = X.map(sample => sample[featureIndex]);
            const thresholds = [...new Set(featureValues)].sort((a, b) => a - b);
            
            for (const threshold of thresholds) {
                const gain = this._informationGain(X, y, featureIndex, threshold);
                if (gain > bestGain) {
                    bestGain = gain;
                    bestFeature = featureIndex;
                    bestThreshold = threshold;
                }
            }
        }
        
        // 如果无法找到有效分割,创建叶节点
        if (bestGain === 0) {
            const leafValue = this._mostCommon(y);
            return new DecisionTreeNode(null, null, leafValue);
        }
        
        // 根据最佳分割创建子节点
        const leftIndices = [];
        const rightIndices = [];
        
        for (let i = 0; i < numSamples; i++) {
            if (X[i][bestFeature] <= bestThreshold) {
                leftIndices.push(i);
            } else {
                rightIndices.push(i);
            }
        }
        
        const leftX = leftIndices.map(i => X[i]);
        const leftY = leftIndices.map(i => y[i]);
        const rightX = rightIndices.map(i => X[i]);
        const rightY = rightIndices.map(i => y[i]);
        
        const node = new DecisionTreeNode(bestFeature, bestThreshold);
        node.left = this._growTree(leftX, leftY, depth + 1);
        node.right = this._growTree(rightX, rightY, depth + 1);
        
        return node;
    }
    
    _predictSample(sample, node) {
        if (node.isLeaf()) {
            return node.value;
        }
        
        if (sample[node.featureIndex] <= node.threshold) {
            return this._predictSample(sample, node.left);
        } else {
            return this._predictSample(sample, node.right);
        }
    }
    
    _informationGain(X, y, featureIndex, threshold) {
        // 计算信息增益的简化实现
        const parentEntropy = this._entropy(y);
        
        const leftIndices = [];
        const rightIndices = [];
        
        for (let i = 0; i < X.length; i++) {
            if (X[i][featureIndex] <= threshold) {
                leftIndices.push(i);
            } else {
                rightIndices.push(i);
            }
        }
        
        if (leftIndices.length === 0 || rightIndices.length === 0) {
            return 0;
        }
        
        const leftY = leftIndices.map(i => y[i]);
        const rightY = rightIndices.map(i => y[i]);
        
        const leftWeight = leftIndices.length / X.length;
        const rightWeight = rightIndices.length / X.length;
        
        const childEntropy = leftWeight * this._entropy(leftY) + rightWeight * this._entropy(rightY);
        
        return parentEntropy - childEntropy;
    }
    
    _entropy(y) {
        // 计算熵的简化实现
        const counts = {};
        for (const label of y) {
            counts[label] = (counts[label] || 0) + 1;
        }
        
        let entropy = 0;
        const total = y.length;
        
        for (const count of Object.values(counts)) {
            const probability = count / total;
            entropy -= probability * Math.log2(probability);
        }
        
        return entropy;
    }
    
    _allSame(arr) {
        return new Set(arr).size === 1;
    }
    
    _mostCommon(arr) {
        const counts = {};
        let maxCount = 0;
        let mostCommon = null;
        
        for (const item of arr) {
            counts[item] = (counts[item] || 0) + 1;
            if (counts[item] > maxCount) {
                maxCount = counts[item];
                mostCommon = item;
            }
        }
        
        return mostCommon;
    }
}

这些实际应用案例展示了树形数据结构在现代软件开发中的广泛适用性。从底层的系统编程到高级的人工智能应用,二叉树和堆结构都发挥着不可替代的作用。掌握这些数据结构的原理和实现,将极大地提升你解决复杂问题的能力。

总结

树形数据结构在现代软件开发中扮演着至关重要的角色,从文件系统、数据库索引到机器学习算法,二叉树和堆都提供了高效的解决方案。通过本文的学习,我们不仅掌握了这些数据结构的核心原理和JavaScript实现,还了解了它们在实际项目中的广泛应用。这些知识将帮助开发者编写更高效、可靠的代码,解决复杂的计算问题。

【免费下载链接】computer-science-in-javascript Collection of classic computer science paradigms, algorithms, and approaches written in JavaScript. 【免费下载链接】computer-science-in-javascript 项目地址: https://gitcode.com/gh_mirrors/co/computer-science-in-javascript

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值