Map&Set

本文探讨了Java中的Map接口及其常见子类如TreeMap和HashMap,讲解了Set接口及其特性,特别是与List的区别。此外,还深入讨论了搜索树(如BST和RBTree)的概念和操作,以及哈希表的哈希冲突解决策略和扩容机制。最后提到了哈希函数在MD5中的应用以及负载因子在扩容中的作用,并提及了面试题——复制带随机指针的链表。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二分搜索树—(RBTree)—TreeMap / TreeSet
哈希表 —HashSet / HashMap

Map接口(映射关系)

存储键值对。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Map接口常见子类添加问题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Set

披着Set外衣的Map,也是collection接口的子接口。
collection接口: 线性表接口一次保存一个元素 ,其子类的都可以进行for-each循环
和List集合区别: Set 集合保存的元素不能重复
【使用Set集合去重处理】
在这里插入图片描述

搜索树

1、搜索树有哪些?–>RBTree、AVL(平衡BST)、2-3树、B树等等
2、概念:

BST:二分搜索树/二叉搜索树/二叉排序树
1、是二叉树
2、节点严格要求:每个树的左子树值<树根节点值<所有右子树值
JDK中BST不存在重复节点!!!
3、存储的节点必须具备可比较的能力(实现Compara接口或者传入比较器)

3、向BST添加一个节点:
【新添加的数一定是在叶子节点操作】
时间复杂度:平均logn
最坏 n–单支数

/**
     * 添加一个新节点
     *
     * @param val
     */
    public void add(int val) {
        root = add(root, val);
    }

    /**
     * 向当前以root为根的BST中插入一个新元素 ,返回插入后的根
     *
     * @param root
     * @param val
     * @return
     */
    private Node add(Node root, int val) {
        if (root == null) {
            Node node = new Node(val);
            size++;
            return node;
        }
        if (val < root.val) {
            root.left = add(root.left, val);
        }
        if (val > root.val) {
            root.rigth = add(root.rigth, val);
        }
        return root;
    }

4、判断value是否在BST中
平均:logn
最坏:n

 /**
     * 判断BST中是否存在值为value的节点
     *
     * @param val
     * @return
     */
    public boolean contains(int val) {
        return contains(root, val);
    }

    /**
     * 传入一个根节点和val判断该节点是否存在
     *
     * @param root
     * @param val
     * @return
     */
    private boolean contains(Node root, int val) {
        if (root == null) {
            return false;
        }
        if (root.val == val) {
            return true;
        } else if (val < root.val) {
            return contains(root.left, val);
        } else {
            return contains(root.rigth, val);
        }
    }

5、按照先序遍历打印BST

 @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        generateBSTString(root, 0, sb);
        return sb.toString();
    }

    /**
     * 按照先序遍历,将BST节点值存于sb中
     *
     * @param root
     * @param height
     * @param sb
     */
    private void generateBSTString(Node root, int height, StringBuilder sb) {
        if (root == null) {
            sb.append(generateHeightStr(height)).append("Null\n");
            return;
        }
        sb.append(generateHeightStr(height)).append(root.val).append("\n");
        generateBSTString(root.left, height + 1, sb);
        generateBSTString(root.rigth, height + 1, sb);
    }

    /**
     * 按照节点高度打印 --
     * 树根的第一层 => --
     * 第二层  ----
     *
     * @param height
     * @return
     */
    private String generateHeightStr(int height) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < height; i++) {
            sb.append("--");
        }
        return sb.toString();
    }

6、BST中最大最小值

先序遍历中。第一个左子树为空的节点–最小值
第一个右子树为空的节点–最大值

  public int findMin() {
        if (size == 0) {
            throw new NoSuchElementException("bst is empty! no element!");
        }
        return min(root).val;
    }

    public int findMax() {
        if (size == 0) {
            throw new NoSuchElementException("bst is empty! no element!");
        }
        return max(root).val;
    }

    /**
     * 以roor为根的BST找到最大值的节点
     *
     * @param root
     * @return
     */
    private Node max(Node root) {
        if (root.rigth == null) {
            return root;
        }
        return max(root.rigth);
    }

    /**
     * 以roor为根的BST找到最小值的节点
     *
     * @param root
     * @return
     */
    private Node min(Node root) {
        if (root.left == null) {
            return root;
        }
        return min(root.left);
    }

7、删除最大值、最小值
【最小值】第一个左子树为空的节点
【最大值】第一个右子树为空的节点

 /**
     * 删除BST中最小节点
     *
     * @return
     */
    public int removeMin() {
        if (size == 0) {
            throw new NoSuchElementException("bst is empty! cannot remove!");
        }
        Node node = min(root);
        root = removeMin(root);
        return node.val;
    }

    /**
     * 传入以root为根的BST  删除最小值的节点,返回删除后的根节点
     *
     * @param root
     * @return
     */
    private Node removeMin(Node root) {
        if (root.left == null) {
            Node right = root.rigth;
            root.left = right.rigth = root = null;
            size--;
            return right;
        }
        root.left = removeMin(root.left);
        return root;
    }

    /**
     * 删除最大值
     *
     * @return
     */
    public int removeMax() {
        if (size == 0) {
            throw new NoSuchElementException("bst is empty! cannot remove!");
        }
        Node node = max(root);
        root = removeMax(root);
        return node.val;
    }

    /**
     * 传入根节点,返回删除最小值后的根节点
     *
     * @param root
     * @return
     */
    private Node removeMax(Node root) {
        if (root.rigth == null) {
            Node node = root.left;
            root.rigth = root.left = root = null;
            size--;
            return node;
        }
        root.rigth = removeMax(root.rigth);
        return root;
    }

8、删除任意一个值为value的节点
【Hibbard 算法找前驱或者后继】
【前驱:要删除节点的左子树最大值】
【后继:要删除节点右子树最小值】

 /**
     * 删除值为value的节点
     *
     * @param val
     */
    public void remove(int val) {
        if (size == 0) {
            throw new NoSuchElementException("bst is empty! cannot remove!");
        }
        root = remove(root, val);
    }

    /**
     * 在当前 root 为根的BST,删除值为val的节点,返回删除后的根节点
     *
     * @param root
     * @param val
     * @return
     */
    private Node remove(Node root, int val) {
        if (root == null) {
            //遍历树找不到值为val的节点
            throw new NoSuchElementException("bst has not this node!");
        } else if (val < root.val) {
            root.left = remove(root.left, val);
            return root;
        } else if (val > root.val) {
            root.rigth = remove(root.rigth, val);
            return root;
        } else {
            //树不为空
            //root就是待删除的节点
            if (root.left == null) {
                Node node = root.rigth;
                root.rigth = root = null;
                size--;
                return node;
            }
            if (root.rigth == null) {
                Node node = root.left;
                root.left = root = null;
                size--;
                return node;
            }
            //左右 子树都不为空
            //Hibbard Deletion
            //找到当前右子树最小值节点
            Node successor = min(root.rigth);
            //1、先在右子树中删除最小值
            successor.rigth = removeMin(root.rigth);
            //2、连接root.left
            successor.left = root.left;
            root.left = root.rigth = root = null;
            return successor;
        }
    }

哈希表

【一种方法将任意数据类型转为数组索引–哈希函数,用空间换时间】
【借鉴了数组的随机访问能力】
1、哈希冲突

不同key经过hash函数的计算得到了相同的值

2、hash函数的设计

哈希冲突在数学领域理论上一定存在

3、原则

尽可能在空间和时间上求平衡,利用最少的空间获取一个较为平均的数字分布

在这里插入图片描述
4、解决哈希冲突

1、闭散列(缺点:难查难删除、工程中很少使用)–线性探测

遇到位置不为空可以向后寻找第一个不为空的位置存入(或者二次哈希–再模一次)

【若整个哈希表冲突非常严重,此时查找一个元素从o(1)–>o(n)】

2、开散列

出现 冲突,就将此位置变为链表

冲突严重的情况解决办法:①针对整个数组扩容-----C++中的STL的Map
②将冲突严重的链表再次变为新的哈希表/二分搜索书-----JDK
在这里插入图片描述

5、MD5用于字符串计算hash值
特点:
①定长,无论输入数据多长,得到的MD5值长度固定
②分散 ,若输入稍有偏差则MD5值相差万里
③不可逆,通过MD5还原字符串很难
④稳定性,根据相同的数据计算的MD5稳定,不会变化—所有hash函数都要满足的

用途:
①作为hash计算
②用于加密
③对比文件内容

扩容

负载因子 loadFactor = 哈希表中有效元素/哈希表长度,值越大冲突越严重,值越小数组利用率低
1、扩容与否与负载因子相关:数组长度*负载因子 <= 有效元素个数
JDK默认负载因子=0.75
阿里巴巴实验数据 负载因子=10

public class MyHashMap {
    private int size;
    private Node[] hashTable;
    private int m;
    private static final double LOAD_FACTOR = 0.75;

    public MyHashMap() {
        this(16);
    }

    public MyHashMap(int x) {
        this.hashTable = new Node[x];
        this.m = x;
    }

    /**
     * 对key求哈希
     *
     * @param key
     * @return
     */
    public int hash(int key) {
        return Math.abs(key) % m;
    }

    /**
     * 将键值对保存入哈希表中
     * 既是新增又是修改
     *
     * @param key
     * @param value
     */
    public int put(int key, int value) {
        int index = hash(key);
        //遍历链表,查看key值是否存在
        for (Node i = hashTable[index]; i != null; i = i.next) {
            if (i.key == key) {
                int oldVal = i.value;
                i.value = value;
                return oldVal;
            }
        }
        Node node = new Node(key, value);
        node.next = hashTable[index];
        hashTable[index] = node;//头插
        size++;
        //添加之后判断是否扩容
        if (size >= hashTable.length * LOAD_FACTOR) {
            resize();
        }
        return value;
    }

    /**
     * 哈希表扩容
     * 新数组的长度变为原来一倍
     */
    private void resize() {
        Node[] newTable = new Node[hashTable.length << 1];//产生新数组
        //元素搬移
        this.m = newTable.length;
        for (int i = 0; i < hashTable.length; i++) {
            for (Node x = hashTable[i]; x != null; ) {
                Node next = x.next;
                int index = hash(x.key);
                x.next = newTable[index];
                newTable[index] = x;
            }
        }
        hashTable = newTable;
    }

    /**
     * 判断当前key是否在表中存在
     *
     * @param key
     * @return
     */
    public boolean containsKey(int key) {
        int index = hash(key);
        for (Node x = hashTable[index]; x != null; x = x.next) {
            if (x.key == key) {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断value是否存在
     *
     * @param val
     * @return
     */
    public boolean containsVal(int val) {
        for (Node node : hashTable) {
            for (Node j = node; j != null; j = j.next) {
                if (j.value == val) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 判断键值对是否存在
     *
     * @param key
     * @param val
     * @return
     */
    public boolean todd(int key, int val) {
        int index = hash(key);
        Node node = hashTable[index];
        while (node != null) {
            if (node.value == val) {
                return true;
            }
            node = node.next;
        }
        return false;
    }

    /**
     * 删除
     * @param key
     * @param val
     * @return
     */
    public boolean remove(int key, int val) {
        int index = hash(key);
        Node node = hashTable[index];
        //头节点是待删除结点
        if (node.key == key && node.value == val) {
            hashTable[index] = node.next;
            node = node.next = null;
            size--;
            return true;
        }
        Node prev = node;
        while (prev.next != null) {
            if (prev.next.key == key && prev.next.value == val) {
                Node cur = prev.next;
                prev.next = cur.next;
                cur = cur.next = null;
                size--;
                return true;
            } else {
                prev = prev.next;
            }
        }
        //遍历完链表都找不到,报错
        throw new NoSuchElementException("can not find! remove error!");
    }
}

class Node {
    int key;//对Key值进行hash运算
    int value;
    Node next;

    public Node(int key, int value) {
        this.key = key;
        this.value = value;
    }
}

jdk8之后的HashMap为:数组+链表+红黑树
jdk8之前为:数组+链表


面试题

  1. 复制带随机指针的链表
    LeetCode原题
    在这里插入图片描述
public Node copyRandomList(Node head) {
        Map<Node, Node> map = new HashMap<>();
        //1、遍历原链表构造Map集合,维护原链表和新链表的映射关系
        //原1 = 新1;
        for (Node i = head; i != null; i = i.next) {
            Node node = new Node(i.val);
            map.put(i, node);
        }
        //通过原1找到新1,通过原random找到新random
        for (Node i = head; i != null; i = i.next) {
            //新链表的next=新链表的next
            map.get(i).next = map.get(i.next);
            //新链表的random=新链表的random
            map.get(i).random = map.get(i.random);
        }
        return map.get(head);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值