二叉树与哈希表从0到1实现指南:代码、逻辑与避坑技巧

二叉树:从结构定义到核心操作实现

要实现二叉树,第一步得明确“节点”这个基本单元——它是二叉树的“细胞”。无论是Java还是Python,节点的定义逻辑都一致:一个存储值的变量,加上左右两个子节点指针(或引用)。

二叉树与哈希表从0到1实现指南:代码、逻辑与避坑技巧

1. 节点定义:Java vs Python

Java实现

public class TreeNode {
    int val; // 节点值
    TreeNode left; // 左子节点
    TreeNode right; // 右子节点

    // 构造方法
    public TreeNode(int val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

Python实现

class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

2. 核心操作:插入、遍历与删除

二叉树的操作中,插入是基础,遍历是高频需求,删除是难点——我们逐个拆解。

插入操作(以二叉搜索树为例)

二叉搜索树(BST)的插入规则是:左子树所有节点值小于根节点,右子树所有节点值大于根节点。插入逻辑可以用递归或迭代实现,递归更易理解:

Java递归插入

public TreeNode insert(TreeNode root, int val) {
    // 如果根节点为空,直接创建新节点
    if (root == null) {
        return new TreeNode(val);
    }
    // 小于根节点,插入左子树
    if (val < root.val) {
        root.left = insert(root.left, val);
    }
    // 大于等于根节点,插入右子树
    else {
        root.right = insert(root.right, val);
    }
    return root;
}
遍历操作:递归与非递归实现

遍历是访问二叉树所有节点的方式,常见的有前序(根→左→右)、中序(左→根→右)、后序(左→右→根)。递归写法简单,但非递归写法更考察对栈的理解(面试常考)。

前序遍历:非递归实现(Java)
用栈模拟递归过程,先压根节点,再压右子节点(因为栈是后进先出,要保证左子节点先处理):

public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    Stack<TreeNode> stack = new Stack<>();
    if (root != null) stack.push(root);

    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        res.add(node.val); // 处理根节点
        // 先压右子节点,再压左子节点
        if (node.right != null) stack.push(node.right);
        if (node.left != null) stack.push(node.left);
    }
    return res;
}
删除操作:最容易踩坑的环节

删除节点分三种情况,处理逻辑需严谨:
1. 叶子节点:直接删除(无后顾之忧);
2. 只有一个子节点:用子节点替换当前节点;
3. 有两个子节点:找右子树的最小节点(或左子树的最大节点)替换当前节点,再删除那个最小节点(保持BST有序性)。

Java删除实现

public TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) return null; // 空树,直接返回

    // 寻找要删除的节点
    if (key < root.val) {
        root.left = deleteNode(root.left, key);
    } else if (key > root.val) {
        root.right = deleteNode(root.right, key);
    } else {
        // 找到目标节点,开始处理
        if (root.left == null) {
            return root.right; // 左子树为空,返回右子树
        } else if (root.right == null) {
            return root.left; // 右子树为空,返回左子树
        }
        // 有两个子节点:找右子树最小节点
        TreeNode minNode = findMin(root.right);
        root.val = minNode.val; // 替换当前节点值
        root.right = deleteNode(root.right, minNode.val); // 删除最小节点
    }
    return root;
}

// 辅助方法:找树的最小节点(左子树最深处)
private TreeNode findMin(TreeNode node) {
    while (node.left != null) {
        node = node.left;
    }
    return node;
}

哈希表:哈希函数、冲突解决与完整逻辑

哈希表的核心是“键→索引”的映射(哈希函数),以及处理“多个键映射到同一索引”的冲突问题。我们以链地址法(最常用的冲突解决方式)为例实现哈希表。

1. 哈希表的结构设计

哈希表由“数组+链表”组成:数组存储链表头节点,每个链表存储冲突的键值对。

Java哈希表节点定义

class HashNode<K, V> {
    K key;
    V value;
    HashNode<K, V> next; // 下一个节点(解决冲突)

    public HashNode(K key, V value) {
        this.key = key;
        this.value = value;
        this.next = null;
    }
}

2. 哈希函数:如何把键变成索引?

哈希函数的目标是“均匀分布”,减少冲突。常见的方法是:
– 取键的哈希码(hashCode());
– 对数组长度取模(保证索引在数组范围内)。

Java哈希函数实现

private int hash(K key) {
    if (key == null) return 0;
    int hash = key.hashCode();
    // 用“与运算”代替取模(要求数组长度是2的幂,性能更高)
    return hash & (table.length - 1);
}

3. 核心操作:put、get、remove

put操作:插入或更新键值对

逻辑:
1. 计算键的索引;
2. 遍历链表,若键已存在则更新值;
3. 若键不存在,插入链表头部;
4. 检查负载因子(size/数组长度),超过阈值(通常0.75)则扩容(数组翻倍)。

Java put实现

class HashMap<K, V> {
    private static final int DEFAULT_CAPACITY = 16; // 默认容量(2的幂)
    private static final double LOAD_FACTOR = 0.75; // 负载因子
    private HashNode<K, V>[] table;
    private int size;

    public HashMap() {
        table = new HashNode[DEFAULT_CAPACITY];
        size = 0;
    }

    public void put(K key, V value) {
        int index = hash(key);
        HashNode<K, V> node = table[index];

        // 遍历链表,更新已有键
        while (node != null) {
            if (node.key.equals(key)) {
                node.value = value;
                return;
            }
            node = node.next;
        }

        // 插入新节点到链表头部
        HashNode<K, V> newNode = new HashNode<>(key, value);
        newNode.next = table[index];
        table[index] = newNode;
        size++;

        // 检查扩容
        if (size > LOAD_FACTOR * table.length) {
            resize();
        }
    }

    // 扩容方法:数组翻倍,重新哈希所有节点
    private void resize() {
        HashNode<K, V>[] oldTable = table;
        int newCapacity = oldTable.length * 2;
        table = new HashNode[newCapacity];
        size = 0; // 重置size,重新插入

        for (HashNode<K, V> oldNode : oldTable) {
            while (oldNode != null) {
                put(oldNode.key, oldNode.value); // 重新哈希
                oldNode = oldNode.next;
            }
        }
    }
}
get操作:根据键查找值

逻辑:计算索引→遍历链表→返回对应值(无则返回null)。

Java get实现

public V get(K key) {
    int index = hash(key);
    HashNode<K, V> node = table[index];

    while (node != null) {
        if (node.key.equals(key)) {
            return node.value;
        }
        node = node.next;
    }
    return null; // 键不存在
}

避坑指南:那些容易踩的实现陷阱

1. 二叉树:删除时忘记处理子节点

:删除有两个子节点的节点时,只替换了值,但没删除右子树的最小节点→导致树中存在重复值。
解决:必须递归删除右子树的最小节点(如deleteNode(root.right, minNode.val))。

2. 哈希表:哈希函数导致的聚类问题

:用“取模”哈希时,数组长度不是质数→键的哈希码会集中在某些索引(比如长度为10,哈希码为1、11、21的键都会映射到索引1)。
解决:数组长度用质数,或用“与运算”(要求长度是2的幂,性能更好)。

3. 哈希表:扩容时没有重新哈希

:扩容后数组长度变化,旧的索引失效→导致get不到值。
解决:扩容时必须遍历所有旧节点,重新计算索引并插入新数组(如resize()方法中的put(oldNode.key, oldNode.value))。

4. 二叉树:非递归遍历的栈操作顺序

:前序遍历压栈时先压左子节点→导致右子节点先处理(顺序错误)。
解决:先压右子节点,再压左子节点(栈是后进先出,左子节点后压先出)。

5. 哈希表:负载因子设置不合理

:负载因子过高(如1.0)→冲突增多,查询时间变长;过低(如0.1)→浪费内存。
解决:保持负载因子在0.7~0.8之间(行业默认值)。

最后:用代码验证你的实现

学完实现逻辑,最好的方法是写一个小例子测试:
– 二叉树:插入10、5、15、3、7,删除10→看是否得到正确的结构;
– 哈希表:put(“name”, “Alice”)、put(“age”, 25)、get(“name”)→验证是否返回”Alice”。

比如Python二叉树测试:

root = TreeNode(10)
root = insert(root, 5)
root = insert(root, 15)
root = insert(root, 3)
root = insert(root, 7)
root = deleteNode(root, 10)
# 遍历结果应为[7,5,3,15](前序)
print(preorderTraversal(root))

哈希表测试:

HashMap<String, String> map = new HashMap<>();
map.put("name", "Alice");
map.put("age", "25");
System.out.println(map.get("name")); // 输出Alice
System.out.println(map.remove("age")); // 输出25
System.out.println(map.get("age")); // 输出null

原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/265

(0)

相关推荐

  • GitHub协作与Pull Request全流程实战指南

    准备工作:明确权限与选择分支策略 团队协作前,先确认两件事:一是你有目标仓库的访问权限——如果是组织仓库,需要管理员分配「Write」及以上权限;如果是公开仓库,通过Fork获取个…

    6天前