初级程序员必备的十大技能之基础数据结构与算法(四)

简介: 教程来源 https://yvyus.cn/ 哈希表(散列表)是高效键值存储结构,平均查找、插入、删除均为O(1)。核心是哈希函数将键映射至数组索引;冲突常用链地址法解决;负载因子超阈值时自动扩容。广泛应用于缓存、去重、统计等场景。

六、哈希表:键值存储的王者

哈希表(Hash Table)也叫做散列表,是效率最高的数据结构之一,平均情况下查找、插入、删除都是 O(1)。

6.1 哈希表的本质
哈希表的原理是:通过哈希函数将键(Key)映射到数组的某个位置,直接存取。

// 最简单的哈希表实现(理解原理)
class SimpleHashTable {
    constructor(size = 10) {
        this.table = new Array(size);
        this.size = size;
    }

    // 简单的哈希函数
    _hash(key) {
        let hash = 0;
        for (let i = 0; i < key.length; i++) {
            hash = (hash + key.charCodeAt(i)) % this.size;
        }
        return hash;
    }

    set(key, value) {
        const index = this._hash(key);
        this.table[index] = value;
    }

    get(key) {
        const index = this._hash(key);
        return this.table[index];
    }
}

6.2 哈希冲突与解决
当两个不同的键映射到同一个数组位置时,就发生了哈希冲突。常用解决方法是链地址法(每个位置存一个链表)。

// 使用链地址法解决冲突的哈希表
class HashTable {
    constructor(size = 50) {
        this.table = new Array(size);
        this.size = size;
    }

    // 更好的哈希函数
    _hash(key) {
        let hash = 0;
        for (let i = 0; i < key.length; i++) {
            hash = (hash * 31 + key.charCodeAt(i)) % this.size;
        }
        return hash;
    }

    set(key, value) {
        const index = this._hash(key);

        // 如果该位置为空,创建一个新的桶(链表)
        if (this.table[index] === undefined) {
            this.table[index] = [];
        }

        // 检查键是否已存在,存在则更新
        for (let i = 0; i < this.table[index].length; i++) {
            if (this.table[index][i][0] === key) {
                this.table[index][i][1] = value;
                return;
            }
        }

        // 不存在则添加
        this.table[index].push([key, value]);
    }

    get(key) {
        const index = this._hash(key);

        if (this.table[index] === undefined) {
            return undefined;
        }

        for (let i = 0; i < this.table[index].length; i++) {
            if (this.table[index][i][0] === key) {
                return this.table[index][i][1];
            }
        }

        return undefined;
    }

    delete(key) {
        const index = this._hash(key);

        if (this.table[index] === undefined) {
            return false;
        }

        for (let i = 0; i < this.table[index].length; i++) {
            if (this.table[index][i][0] === key) {
                this.table[index].splice(i, 1);
                return true;
            }
        }

        return false;
    }

    // 打印哈希表状态
    print() {
        for (let i = 0; i < this.size; i++) {
            if (this.table[i]) {
                console.log(`${i}: ${JSON.stringify(this.table[i])}`);
            }
        }
    }
}

// 使用示例
const ht = new HashTable(10);
ht.set("name", "张三");
ht.set("age", "25");
ht.set("city", "北京");
ht.set("name", "李四");  // 更新

console.log(ht.get("name"));  // 李四
console.log(ht.get("age"));   // 25
ht.delete("city");
console.log(ht.get("city"));  // undefined

6.3 哈希表的负载因子与扩容
负载因子 = 元素个数 / 数组长度。当负载因子超过阈值(通常 0.75),需要扩容(rehashing)。

// 带自动扩容的哈希表
class ResizableHashTable {
    constructor(initialSize = 16) {
        this.table = new Array(initialSize);
        this.size = initialSize;
        this.count = 0;           // 存储的元素个数
        this.loadFactor = 0.75;   // 负载因子阈值
    }

    _hash(key) {
        let hash = 0;
        for (let i = 0; i < key.length; i++) {
            hash = (hash * 31 + key.charCodeAt(i)) % this.size;
        }
        return hash;
    }

    // 检查是否需要扩容
    _checkResize() {
        if (this.count / this.size > this.loadFactor) {
            this._resize(this.size * 2);
        }
    }

    // 扩容(重新哈希所有元素)
    _resize(newSize) {
        const oldTable = this.table;
        this.table = new Array(newSize);
        this.size = newSize;
        this.count = 0;

        // 重新插入所有旧元素
        for (let i = 0; i < oldTable.length; i++) {
            if (oldTable[i]) {
                for (const [key, value] of oldTable[i]) {
                    this.set(key, value);
                }
            }
        }
    }

    set(key, value) {
        const index = this._hash(key);

        if (this.table[index] === undefined) {
            this.table[index] = [];
        }

        // 更新已存在的键
        for (let i = 0; i < this.table[index].length; i++) {
            if (this.table[index][i][0] === key) {
                this.table[index][i][1] = value;
                return;
            }
        }

        // 新增
        this.table[index].push([key, value]);
        this.count++;
        this._checkResize();  // 检查是否需要扩容
    }

    get(key) {
        const index = this._hash(key);

        if (this.table[index] === undefined) {
            return undefined;
        }

        for (let i = 0; i < this.table[index].length; i++) {
            if (this.table[index][i][0] === key) {
                return this.table[index][i][1];
            }
        }

        return undefined;
    }
}

6.4 哈希表的实战应用

// 1. 统计字符出现次数
function charCount(str) {
    const map = new Map();  // JavaScript 内置哈希表

    for (const char of str) {
        map.set(char, (map.get(char) || 0) + 1);
    }

    return map;
}

console.log(charCount("hello world"));
// Map(7) { 'h' => 1, 'e' => 1, 'l' => 3, 'o' => 2, ' ' => 1, 'w' => 1, 'r' => 1, 'd' => 1 }

// 2. 找出数组中出现次数超过一半的元素(摩尔投票算法)
function majorityElement(nums) {
    let candidate = null;
    let count = 0;

    for (const num of nums) {
        if (count === 0) {
            candidate = num;
            count = 1;
        } else if (num === candidate) {
            count++;
        } else {
            count--;
        }
    }

    // 验证 candidate 是否真的是多数元素
    let actualCount = 0;
    for (const num of nums) {
        if (num === candidate) actualCount++;
    }

    return actualCount > nums.length / 2 ? candidate : null;
}

console.log(majorityElement([3, 2, 3]));        // 3
console.log(majorityElement([2, 2, 1, 1, 2])); // 2

// 3. 判断两个数组是否有交集
function intersection(arr1, arr2) {
    const set1 = new Set(arr1);
    const result = [];

    for (const num of arr2) {
        if (set1.has(num)) {
            result.push(num);
            set1.delete(num);  // 去重
        }
    }

    return result;
}

console.log(intersection([1, 2, 2, 3], [2, 2, 4]));  // [2]

七、树:层次化的数据结构

树是一种非线性数据结构,用于表示层次关系(如文件系统、HTML DOM、组织架构)。

7.1 树的基本概念

        根节点(Root)
        /    \
    节点A    节点B
    /  \       \
  节点C 节点D   节点E
              /
            节点F

术语:
- 根节点:没有父节点的节点
- 叶子节点:没有子节点的节点
- 父节点、子节点、兄弟节点
- 深度:从根到节点的距离(根深度=0)
- 高度:从节点到最深叶子的距离

7.2 二叉树
二叉树是每个节点最多有两个子节点的树。

class TreeNode {
    constructor(val, left = null, right = null) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

// 构建一个二叉树
//       1
//      / \
//     2   3
//    / \   \
//   4   5   6
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.right = new TreeNode(6);

7.3 二叉树的遍历
遍历是对树中所有节点的访问。

// 1. 前序遍历(根→左→右)
function preorderTraversal(root) {
    const result = [];

    function traverse(node) {
        if (node === null) return;

        result.push(node.val);      // 访问根节点
        traverse(node.left);        // 遍历左子树
        traverse(node.right);       // 遍历右子树
    }

    traverse(root);
    return result;
}

// 前序遍历结果:[1, 2, 4, 5, 3, 6]

// 2. 中序遍历(左→根→右)
function inorderTraversal(root) {
    const result = [];

    function traverse(node) {
        if (node === null) return;

        traverse(node.left);        // 遍历左子树
        result.push(node.val);      // 访问根节点
        traverse(node.right);       // 遍历右子树
    }

    traverse(root);
    return result;
}

// 中序遍历结果:[4, 2, 5, 1, 3, 6]
// 对于二叉搜索树,中序遍历得到有序序列

// 3. 后序遍历(左→右→根)
function postorderTraversal(root) {
    const result = [];

    function traverse(node) {
        if (node === null) return;

        traverse(node.left);        // 遍历左子树
        traverse(node.right);       // 遍历右子树
        result.push(node.val);      // 访问根节点
    }

    traverse(root);
    return result;
}

// 后序遍历结果:[4, 5, 2, 6, 3, 1]

// 4. 层序遍历(BFS,广度优先)
function levelOrderTraversal(root) {
    if (root === null) return [];

    const result = [];
    const queue = [root];

    while (queue.length > 0) {
        const levelSize = queue.length;
        const currentLevel = [];

        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();
            currentLevel.push(node.val);

            if (node.left) queue.push(node.left);
            if (node.right) queue.push(node.right);
        }

        result.push(currentLevel);
    }

    return result;
}

// 层序遍历结果:[[1], [2, 3], [4, 5, 6]]

7.4 二叉搜索树(BST)
二叉搜索树的特性:

左子树所有节点的值 < 根节点的值

右子树所有节点的值 > 根节点的值

左右子树也都是二叉搜索树

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

    // 插入节点
    insert(val) {
        const newNode = new TreeNode(val);

        if (this.root === null) {
            this.root = newNode;
            return this;
        }

        let current = this.root;
        while (true) {
            if (val < current.val) {
                if (current.left === null) {
                    current.left = newNode;
                    return this;
                }
                current = current.left;
            } else if (val > current.val) {
                if (current.right === null) {
                    current.right = newNode;
                    return this;
                }
                current = current.right;
            } else {
                // 值相等,不插入(或根据需求处理)
                return this;
            }
        }
    }

    // 查找节点
    search(val) {
        let current = this.root;

        while (current !== null) {
            if (val === current.val) {
                return current;
            } else if (val < current.val) {
                current = current.left;
            } else {
                current = current.right;
            }
        }

        return null;  // 未找到
    }

    // 查找最小值
    findMin() {
        if (this.root === null) return null;

        let current = this.root;
        while (current.left !== null) {
            current = current.left;
        }
        return current.val;
    }

    // 查找最大值
    findMax() {
        if (this.root === null) return null;

        let current = this.root;
        while (current.right !== null) {
            current = current.right;
        }
        return current.val;
    }

    // 删除节点
    delete(val) {
        this.root = this._deleteNode(this.root, val);
    }

    _deleteNode(node, val) {
        if (node === null) return null;

        if (val < node.val) {
            node.left = this._deleteNode(node.left, val);
            return node;
        } else if (val > node.val) {
            node.right = this._deleteNode(node.right, val);
            return node;
        } else {
            // 找到要删除的节点

            // 情况1:叶子节点
            if (node.left === null && node.right === null) {
                return null;
            }

            // 情况2:只有一个子节点
            if (node.left === null) {
                return node.right;
            }
            if (node.right === null) {
                return node.left;
            }

            // 情况3:有两个子节点
            // 找到右子树的最小节点(后继节点)
            let minNode = this._findMin(node.right);
            node.val = minNode.val;
            node.right = this._deleteNode(node.right, minNode.val);
            return node;
        }
    }

    _findMin(node) {
        while (node.left !== null) {
            node = node.left;
        }
        return node;
    }

    // 中序遍历(得到有序序列)
    inorder() {
        const result = [];
        this._inorder(this.root, result);
        return result;
    }

    _inorder(node, result) {
        if (node !== null) {
            this._inorder(node.left, result);
            result.push(node.val);
            this._inorder(node.right, result);
        }
    }
}

// 使用示例
const bst = new BinarySearchTree();
bst.insert(50);
bst.insert(30);
bst.insert(70);
bst.insert(20);
bst.insert(40);
bst.insert(60);
bst.insert(80);

console.log(bst.inorder());  // [20, 30, 40, 50, 60, 70, 80]
console.log(bst.search(40)); // TreeNode { val: 40, left: null, right: null }
console.log(bst.findMin());  // 20
console.log(bst.findMax());  // 80

bst.delete(50);
console.log(bst.inorder());  // [20, 30, 40, 60, 70, 80](50被删除)

7.5 树的实战应用

// 1. 计算二叉树的最大深度
function maxDepth(root) {
    if (root === null) return 0;

    const leftDepth = maxDepth(root.left);
    const rightDepth = maxDepth(root.right);

    return Math.max(leftDepth, rightDepth) + 1;
}

// 2. 判断是否是平衡二叉树(左右子树高度差不超过1)
function isBalanced(root) {
    function height(node) {
        if (node === null) return 0;

        const leftHeight = height(node.left);
        if (leftHeight === -1) return -1;

        const rightHeight = height(node.right);
        if (rightHeight === -1) return -1;

        if (Math.abs(leftHeight - rightHeight) > 1) return -1;

        return Math.max(leftHeight, rightHeight) + 1;
    }

    return height(root) !== -1;
}

// 3. 最近公共祖先(LCA)
function lowestCommonAncestor(root, p, q) {
    if (root === null || root === p || root === q) {
        return root;
    }

    const left = lowestCommonAncestor(root.left, p, q);
    const right = lowestCommonAncestor(root.right, p, q);

    if (left !== null && right !== null) {
        return root;  // p 和 q 分别在左右子树
    }

    return left !== null ? left : right;
}

来源:
https://unbgv.cn/

相关文章
|
13天前
|
人工智能 JSON 供应链
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
LucianaiB分享零成本畅用JVS Claw教程(学生认证享7个月使用权),并开源GeoMind项目——将JVS改造为科研与产业地理情报可视化AI助手,支持飞书文档解析、地理编码与腾讯地图可视化,助力产业关系图谱构建。
23495 11
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
|
17天前
|
人工智能 缓存 BI
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro,跑完 Skills —— OA 审批、大屏、报表、部署 5 大实战场景后的真实体验 ![](https://oscimg.oschina.net/oscnet/up608d34aeb6bafc47f
5475 20
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
|
18天前
|
人工智能 JSON BI
DeepSeek V4 来了!超越 Claude Sonnet 4.5,赶紧对接 Claude Code 体验一把
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro 的真实体验与避坑记录 本文记录我将 Claude Code 对接 DeepSeek 最新模型(V4Pro)后的真实体验,测试了 Skills 自动化查询和积木报表 AI 建表两个场景——有惊喜,也踩
6539 16
|
7天前
|
人工智能 缓存 Shell
Claude Code 全攻略:命令大全 + 实战工作流(完整版)
Claude Code 是一款运行在终端环境下的 AI 编码助手,能够直接在项目目录中理解代码结构、编辑文件、执行命令、执行开发计划,并支持持久化记忆、上下文压缩、后台任务、多模型切换等专业能力。对于日常开发、项目维护、快速重构、代码审查等场景,它可以大幅减少手动操作、提升编码效率。本文从常用命令、界面模式、核心指令、记忆机制、图片处理、进阶工作流等维度完整说明,帮助开发者快速上手并稳定使用。
1664 3
|
6天前
|
前端开发 API 内存技术
对比claude code等编程cli工具与deepseek v4的适配情况
DeepSeek V4发布后,多家编程工具因未适配其强制要求的`reasoning_content`字段而报错。本文对比Claude Code、GitHub Copilot、Langcli、OpenCode及DeepSeek-TUI等主流工具的兼容性:Claude Code需按官方方式配置;Langcli表现最佳,开箱即用且无报错;Copilot与OpenCode暂未修复问题;DeepSeek-TUI尚处早期阶段。
1130 3
对比claude code等编程cli工具与deepseek v4的适配情况
|
2天前
|
人工智能 BI 持续交付
Claude Code 深度适配 DeepSeek V4-Pro 实测:全场景通关与真实体验报告
在 AI 编程工具日趋主流的今天,Claude Code 凭借强大的任务执行、工具调用与工程化能力,成为开发者与自动化运维的核心效率工具。但随着原生模型账号稳定性问题频发,寻找一套兼容、稳定、能力在线的替代方案变得尤为重要。DeepSeek V4-Pro 作为新一代高性能大模型,提供了完整兼容 Claude 协议的 API 接口,只需简单配置即可无缝驱动 Claude Code,且在任务执行、工具调用、复杂流程处理上表现极为稳定。
838 0
|
1月前
|
人工智能 自然语言处理 安全
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
本文介绍了Claude Code终端AI助手的使用指南,主要内容包括:1)常用命令如版本查看、项目启动和更新;2)三种工作模式切换及界面说明;3)核心功能指令速查表,包含初始化、压缩对话、清除历史等操作;4)详细解析了/init、/help、/clear、/compact、/memory等关键命令的使用场景和语法。文章通过丰富的界面截图和场景示例,帮助开发者快速掌握如何通过命令行和交互界面高效使用Claude Code进行项目开发,特别强调了CLAUDE.md文件作为项目知识库的核心作用。
27256 65
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)