算法基础 : Java map集合介绍及算法题中的妙用

Java Map集合介绍

Map是Java集合框架中用于存储键值对(Key-Value) 的接口,它不继承自Collection接口,具有以下核心特性:

  • 键唯一性:每个键(Key)只能对应一个值(Value),键重复时会覆盖旧值。
  • 键值映射:通过键可以快速查找对应的值,类似于现实中的“字典”。
  • 无序性:默认存储顺序不保证与插入顺序一致(除特定实现类)。
常用实现类及特点
  1. HashMap(最常用)

    • 底层基于哈希表实现,查询、添加、删除操作的平均时间复杂度为O(1)
    • 键和值都允许为null(但键只能有一个null)。
    • 无序存储(键的位置由哈希值决定)。
  2. TreeMap

    • 底层基于红黑树实现,键会按自然排序(或自定义排序)规则有序排列。
    • 查询、添加、删除操作的时间复杂度为O(log n)
    • 适合需要对键进行排序或范围查询的场景(如获取最大/最小键)。
  3. LinkedHashMap

    • 继承自HashMap,底层通过哈希表+双向链表实现,维护键值对的插入顺序(或访问顺序)。
    • 性能略低于HashMap,但兼具哈希表的查询效率和顺序性。
  4. Hashtable

    • 古老的实现类(JDK1.0),线程安全但性能较低,已被HashMap替代(多线程场景推荐ConcurrentHashMap)。
核心方法

Map接口的常用方法(与算法题密切相关):

  • put(K key, V value):添加键值对(键存在则覆盖)。
  • get(Object key):根据键获取值(不存在则返回null)。
  • containsKey(Object key):判断键是否存在(返回boolean)。
  • containsValue(Object value):判断值是否存在(返回boolean)。
  • remove(Object key):根据键删除键值对。
  • keySet():返回所有键的集合(Set)。
  • values():返回所有值的集合(Collection)。
  • entrySet():返回所有键值对的集合(Set<Map.Entry<K,V>>)。

Map在算法题中的妙用

Map的核心价值在于键值映射高效的键查询,尤其适合处理“计数”“映射关系”“缓存”等场景。以下是典型应用:

场景1:统计元素出现次数(频率统计)

问题:给定字符串,统计每个字符出现的次数(LeetCode 387变种)。
思路:用HashMap的键存储元素,值存储出现次数,遍历元素时更新计数。

public Map<Character, Integer> countFrequency(String s) {
    Map<Character, Integer> freqMap = new HashMap<>();
    for (char c : s.toCharArray()) {
        // 若键存在则+1,否则初始化为1
        freqMap.put(c, freqMap.getOrDefault(c, 0) + 1);
    }
    return freqMap;
}

优势:一次遍历完成统计,时间复杂度O(n),避免嵌套循环计数。

场景2:两数之和(返回索引)

问题:给定数组和目标值,返回两个元素的索引,使它们的和为目标值(LeetCode 1)。
思路:用HashMap存储“元素值→索引”的映射,遍历数组时,通过target - 当前元素查找互补元素是否存在。

public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        int complement = target - nums[i];
        if (map.containsKey(complement)) {
            // 找到互补元素,返回索引
            return new int[]{map.get(complement), i};
        }
        map.put(nums[i], i); // 存储当前元素及其索引
    }
    throw new IllegalArgumentException("No solution");
}

优势:时间复杂度O(n),优于暴力解法的O(n²)。

场景3:字符串同构

问题:判断两个字符串是否同构(如“egg”和“add”同构,字符映射关系唯一,LeetCode 205)。
思路:用两个HashMap分别存储s→tt→s的映射,确保映射关系唯一。

public boolean isIsomorphic(String s, String t) {
    if (s.length() != t.length()) return false;
    
    Map<Character, Character> sToT = new HashMap<>();
    Map<Character, Character> tToS = new HashMap<>();
    
    for (int i = 0; i < s.length(); i++) {
        char sc = s.charAt(i);
        char tc = t.charAt(i);
        
        // 若s→t映射已存在且不匹配当前tc,返回false
        if (sToT.containsKey(sc) && sToT.get(sc) != tc) {
            return false;
        }
        // 若t→s映射已存在且不匹配当前sc,返回false
        if (tToS.containsKey(tc) && tToS.get(tc) != sc) {
            return false;
        }
        
        sToT.put(sc, tc);
        tToS.put(tc, sc);
    }
    return true;
}

优势:通过双向映射确保字符关系唯一,时间复杂度O(n)。

场景4:LRU缓存(最近最少使用)

问题:设计一个LRU缓存结构,支持get和put操作,且淘汰最近最少使用的元素(LeetCode 146)。
思路:利用LinkedHashMap的“访问顺序”特性(构造时指定accessOrder=true),超过容量时自动移除最久未访问的元素。

class LRUCache {
    private final int capacity;
    private final Map<Integer, Integer> cache;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        // 初始化LinkedHashMap,指定容量、负载因子和访问顺序
        this.cache = new LinkedHashMap<>(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
                // 当 size 超过容量时,自动移除最久未访问的元素
                return size() > capacity;
            }
        };
    }

    public int get(int key) {
        return cache.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        cache.put(key, value);
    }
}

优势:LinkedHashMap天然支持访问顺序维护,避免手动实现双向链表,代码简洁高效。

场景5:TreeMap实现区间映射

问题:给定多个区间,判断一个新的区间是否与已有区间重叠(简化版)。
思路:用TreeMap存储“区间起始点→区间结束点”,通过floorKeyceilingKey快速查找相邻区间。

public boolean isOverlapping(TreeMap<Integer, Integer> intervals, int newStart, int newEnd) {
    // 找到小于等于newStart的最大起始点
    Integer floorKey = intervals.floorKey(newStart);
    if (floorKey != null && intervals.get(floorKey) >= newStart) {
        return true; // 与前一个区间重叠
    }
    
    // 找到大于等于newStart的最小起始点
    Integer ceilingKey = intervals.ceilingKey(newStart);
    if (ceilingKey != null && ceilingKey <= newEnd) {
        return true; // 与后一个区间重叠
    }
    
    return false;
}

优势:TreeMap的有序性使其能快速定位相邻区间,时间复杂度O(log n)。

场景6:异位词分组

问题:将字符串数组中所有异位词(字母相同但顺序不同)分组(LeetCode 49)。
思路:用HashMap的键存储“排序后的字符串”(异位词排序后相同),值存储对应的字符串列表。

public List<List<String>> groupAnagrams(String[] strs) {
    Map<String, List<String>> map = new HashMap<>();
    
    for (String s : strs) {
        // 将字符串排序作为键(异位词排序后相同)
        char[] chars = s.toCharArray();
        Arrays.sort(chars);
        String key = new String(chars);
        
        // 若键不存在则初始化列表,再添加当前字符串
        map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
    }
    
    return new ArrayList<>(map.values());
}

优势:通过排序后的字符串作为唯一键,高效分组异位词,时间复杂度O(n * k log k)(k为字符串平均长度)。

总结

Map集合在算法题中的核心应用场景:

  1. 频率统计:用键存元素,值存计数,适合“出现次数”类问题。
  2. 映射关系:如两数之和的“值→索引”、同构字符串的双向映射。
  3. 缓存与顺序:LinkedHashMap维护插入/访问顺序,适合LRU等缓存场景。
  4. 区间与排序:TreeMap的有序性适合区间查询、范围判断等场景。

选择合适的实现类(HashMap优先用于快速查询,TreeMap用于有序场景),能显著提升算法效率,是解决复杂映射问题的关键工具。

Map集合在算法题中的应用非常灵活,除了前面提到的场景,还有许多针对特定问题的巧妙用法。以下是更多典型场景:

场景1:子数组和为K的数量

问题:给定整数数组和目标值K,计算和为K的连续子数组的数量(LeetCode 560)。
思路:用HashMap存储“前缀和→出现次数”的映射,遍历数组时通过当前前缀和 - K判断是否存在符合条件的前缀和,累加次数。

public int subarraySum(int[] nums, int k) {
    Map<Integer, Integer> prefixSumCount = new HashMap<>();
    prefixSumCount.put(0, 1); // 初始化:前缀和为0出现1次
    
    int currentSum = 0;
    int result = 0;
    
    for (int num : nums) {
        currentSum += num;
        // 若存在前缀和为currentSum - k,说明中间子数组和为k
        result += prefixSumCount.getOrDefault(currentSum - k, 0);
        // 更新当前前缀和的出现次数
        prefixSumCount.put(currentSum, prefixSumCount.getOrDefault(currentSum, 0) + 1);
    }
    return result;
}

场景2:四数相加II

问题:给定4个整数数组,计算有多少个元组(i,j,k,l)使得nums1[i] + nums2[j] + nums3[k] + nums4[l] = 0(LeetCode 454)。
思路:用HashMap存储前两个数组的和及其出现次数,再遍历后两个数组,通过-sum查找互补的和,累加次数。

public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
    Map<Integer, Integer> sumCount = new HashMap<>();
    
    // 存储nums1和nums2的所有可能和及其次数
    for (int a : nums1) {
        for (int b : nums2) {
            int sum = a + b;
            sumCount.put(sum, sumCount.getOrDefault(sum, 0) + 1);
        }
    }
    
    int result = 0;
    // 查找nums3和nums4的和的互补值
    for (int c : nums3) {
        for (int d : nums4) {
            int target = - (c + d);
            result += sumCount.getOrDefault(target, 0);
        }
    }
    return result;
}

场景3:最长和谐子序列

问题:找出数组中最长的和谐子序列(元素最大值与最小值之差为1,LeetCode 594)。
思路:用HashMap统计每个元素的出现次数,再遍历键集合,若key+1存在,则两者次数之和为和谐子序列长度。

public int findLHS(int[] nums) {
    Map<Integer, Integer> freqMap = new HashMap<>();
    for (int num : nums) {
        freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
    }
    
    int maxLen = 0;
    for (int key : freqMap.keySet()) {
        if (freqMap.containsKey(key + 1)) {
            // 和谐子序列长度 = 当前key次数 + key+1次数
            maxLen = Math.max(maxLen, freqMap.get(key) + freqMap.get(key + 1));
        }
    }
    return maxLen;
}

场景4:字符串中的第一个唯一字符

问题:找到字符串中第一个只出现一次的字符(LeetCode 387)。
思路:用HashMap统计每个字符的出现次数,再遍历字符串找到第一个次数为1的字符。

public int firstUniqChar(String s) {
    Map<Character, Integer> freqMap = new HashMap<>();
    for (char c : s.toCharArray()) {
        freqMap.put(c, freqMap.getOrDefault(c, 0) + 1);
    }
    
    for (int i = 0; i < s.length(); i++) {
        if (freqMap.get(s.charAt(i)) == 1) {
            return i;
        }
    }
    return -1;
}

场景5:TreeMap实现合并区间

问题:合并所有重叠的区间(LeetCode 56)。
思路:用TreeMap按区间起始点排序,遍历区间时合并重叠或相邻的区间。

public int[][] merge(int[][] intervals) {
    if (intervals.length == 0) return new int[0][];
    
    // 按区间起始点排序
    TreeMap<Integer, Integer> intervalMap = new TreeMap<>();
    for (int[] interval : intervals) {
        int start = interval[0];
        int end = interval[1];
        // 若当前区间与已有区间重叠,合并为更大的区间
        Integer floorKey = intervalMap.floorKey(end);
        while (floorKey != null && intervalMap.get(floorKey) >= start) {
            start = Math.min(start, floorKey);
            end = Math.max(end, intervalMap.get(floorKey));
            intervalMap.remove(floorKey);
            floorKey = intervalMap.floorKey(end);
        }
        intervalMap.put(start, end);
    }
    
    // 转换为结果数组
    int[][] result = new int[intervalMap.size()][2];
    int i = 0;
    for (Map.Entry<Integer, Integer> entry : intervalMap.entrySet()) {
        result[i][0] = entry.getKey();
        result[i][1] = entry.getValue();
        i++;
    }
    return result;
}

场景6:前缀树(字典树)的Map实现

问题:实现一个前缀树,支持插入和前缀查询(LeetCode 208)。
思路:用HashMap存储每个节点的子节点(字符→节点映射),简化树结构的实现。

class TrieNode {
    boolean isEnd; // 标记是否为单词结尾
    Map<Character, TrieNode> children;

    public TrieNode() {
        children = new HashMap<>();
        isEnd = false;
    }
}

class Trie {
    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    // 插入单词
    public void insert(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            node.children.putIfAbsent(c, new TrieNode());
            node = node.children.get(c);
        }
        node.isEnd = true;
    }

    // 检查前缀是否存在
    public boolean startsWith(String prefix) {
        TrieNode node = root;
        for (char c : prefix.toCharArray()) {
            if (!node.children.containsKey(c)) {
                return false;
            }
            node = node.children.get(c);
        }
        return true;
    }
}

总结

Map集合在算法中的扩展应用体现了其强大的灵活性:

  • 前缀和问题:通过映射前缀和与出现次数,快速计算子数组和相关问题。
  • 多数组关联:如四数相加,用Map存储中间结果减少嵌套循环。
  • 区间处理:TreeMap的有序性简化区间合并、重叠判断等操作。
  • 数据结构模拟:如用Map实现前缀树,简化子节点的存储与访问。

在解题时,合理利用Map的键值映射特性,往往能将复杂问题转化为线性或线性对数时间复杂度的解法,是优化算法的重要工具。

牛客网的MAP算法题

在牛客网的算法题中,Map集合同样有广泛应用,尤其在处理计数统计映射关系缓存中间结果等场景时非常高效。以下结合牛客网常见题型,介绍Map的具体应用:

场景1:统计字符出现次数(牛客网高频题)

问题:输入一个字符串,统计每个字符出现的次数,并按字符ASCII码升序输出结果。
思路:用HashMap存储字符与出现次数的映射,再通过键集合排序输出。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String s = sc.nextLine();
        Map<Character, Integer> freqMap = new HashMap<>();
        
        // 统计次数
        for (char c : s.toCharArray()) {
            freqMap.put(c, freqMap.getOrDefault(c, 0) + 1);
        }
        
        // 按ASCII码排序(将键转为列表排序)
        List<Character> chars = new ArrayList<>(freqMap.keySet());
        Collections.sort(chars);
        
        // 输出结果
        for (char c : chars) {
            System.out.println(c + ":" + freqMap.get(c));
        }
    }
}

说明:牛客网常考此类基础统计题,Map的getOrDefault方法能简洁处理计数逻辑,配合排序满足输出要求。

场景2:两数之和(牛客网类似LeetCode 1的题型)

问题:给定一个整数数组和目标值,找出数组中两个数的索引,使它们的和为目标值(假设只有一个解)。
思路:用HashMap存储“元素值→索引”,遍历数组时查找互补元素。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) {
            nums[i] = sc.nextInt();
        }
        int target = sc.nextInt();
        
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            int complement = target - nums[i];
            if (map.containsKey(complement)) {
                System.out.println(map.get(complement) + " " + i);
                return;
            }
            map.put(nums[i], i);
        }
    }
}

说明:牛客网常以数组题形式出现,Map可将暴力解法的O(n²)优化为O(n),适合处理大规模输入。

场景3:数组中出现次数超过一半的数字(牛客网剑指Offer原题)

问题:找出数组中出现次数超过一半的数字(假设一定存在)。
思路:用HashMap统计每个数字的出现次数,遍历Map找到次数超过n/2的数字。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) {
            nums[i] = sc.nextInt();
        }
        
        Map<Integer, Integer> freqMap = new HashMap<>();
        for (int num : nums) {
            freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
            // 提前判断,找到结果即返回
            if (freqMap.get(num) > n / 2) {
                System.out.println(num);
                return;
            }
        }
    }
}

说明:这是牛客网经典的频率统计题,Map的实时计数特性可实现“找到即返回”,优化效率。

场景4:复杂链表的复制(牛客网剑指Offer原题)

问题:复制一个复杂链表,每个节点除了next指针,还有一个random指针指向链表中的任意节点或null。
思路:用HashMap存储“原节点→新节点”的映射,分两步复制:先复制节点值和next,再通过映射设置random

import java.util.*;

class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;
    RandomListNode(int label) {
        this.label = label;
    }
}

public class Solution {
    public RandomListNode Clone(RandomListNode pHead) {
        if (pHead == null) return null;
        
        Map<RandomListNode, RandomListNode> nodeMap = new HashMap<>();
        RandomListNode cur = pHead;
        
        // 第一步:复制所有节点(仅值),建立映射
        while (cur != null) {
            nodeMap.put(cur, new RandomListNode(cur.label));
            cur = cur.next;
        }
        
        // 第二步:设置next和random指针
        cur = pHead;
        while (cur != null) {
            nodeMap.get(cur).next = nodeMap.get(cur.next); // 新节点的next指向原节点next的新节点
            nodeMap.get(cur).random = nodeMap.get(cur.random); // 同理设置random
            cur = cur.next;
        }
        
        return nodeMap.get(pHead);
    }
}

说明:这道题体现了Map的“映射”核心价值,通过原节点与新节点的对应关系,避免了复杂的指针操作。

场景5:找出数组中只出现一次的数字(牛客网常见题)

问题:一个整型数组中,除了两个数字外,其他数字都出现了两次。请找出这两个只出现一次的数字。
思路:用HashMap统计每个数字的出现次数,最后筛选出次数为1的数字。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) {
            nums[i] = sc.nextInt();
        }
        
        Map<Integer, Integer> freqMap = new HashMap<>();
        for (int num : nums) {
            freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
        }
        
        List<Integer> result = new ArrayList<>();
        for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
            if (entry.getValue() == 1) {
                result.add(entry.getKey());
            }
        }
        
        // 按要求排序输出(牛客网可能要求升序)
        Collections.sort(result);
        System.out.println(result.get(0) + " " + result.get(1));
    }
}

说明:这道题用Map的计数功能可轻松解决,无需复杂的位运算(位运算虽更优,但Map实现更直观,适合快速解题)。

总结

在牛客网的算法题中,Map的应用主要集中在:

  1. 基础计数:如字符/数字出现次数统计,配合排序满足输出格式。
  2. 映射关系:如两数之和的索引映射、复杂链表的节点映射。
  3. 筛选特殊元素:如找出只出现一次或超过半数的元素。

Map的优势在于将“查找”操作的时间复杂度从O(n)降至O(1),尤其适合牛客网中对输入规模较大的题目(如n>10⁴),能有效避免超时。解题时需根据是否需要排序选择HashMap(无序,效率高)或TreeMap(有序,适合需要按键排序的场景)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值