Map And Set

目录

11.1 有效的数独 (中等):数组记录

11.2 矩阵置零(中等):使用标记数组

11.3 从中序遍历和后序遍历序列构造二叉树(中等):哈希表 + 递归

11.4 相交链表(简单):哈希集合

11.5 最长连续序列(中等):哈希表

11.6 复制带随机指针的链表(中等):哈希表 + 回溯

11.7 多数元素(简单):哈希表

11.8 克隆图(中等):深度优先搜索 + 哈希表

11.9 单词拆分(中等):动态规划 + 哈希集合

11.10 分数到小数(中等):哈希表 + 长除法

11.11 重复的DNA序列(中等):哈希表

11.12 快乐数(简单):哈希集合

11.13 哈希表总结!!!


11.1 有效的数独 (中等):数组记录

题目:请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。

  1. 数字 1-9 在每一行只能出现一次。

  2. 数字 1-9 在每一列只能出现一次。

  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

思想:由于数独只能每行、每列、每个 3x3 宫内只能出现一次,因此我们设置三个数组,分别保存出现的数字信息:

  • 使用line[i][x]存储第i行是否出现过x + 1这个值

  • 使用column[j][x]存储第j列是否出现过x + 1这个值

  • 使用seg[i / 3][j / 3][x]存储第[i/3 ,j/3]个位置是否出现过x + 1这个值

注意:由于数组索引从0开始,而数字是 1-9 ,因此需要对每次的x - 1


总结:寻求所有可能结果,首选回溯法


代码

class Solution {
    public boolean isValidSudoku(char[][] board) {
        int[][] rows = new int[9][9];
        int[][] columns = new int[9][9];
        int[][][] subboxes = new int[3][3][9];
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char c = board[i][j];
                if (c != '.') {
                    int index = c - '0' - 1;
                    rows[i][index]++;
                    columns[j][index]++;
                    subboxes[i / 3][j / 3][index]++;
                    if (rows[i][index] > 1 || columns[j][index] > 1 || subboxes[i / 3][j / 3][index] > 1) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
}

11.2 矩阵置零(中等):使用标记数组

题目:给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法

思想:使用两个标记数组分别记录每一行每一列是否有零出现

  • 遍历该数组,若某个元素为0,那么该元素所在的行和列所对应标记数组的位置为true

  • 遍历标记数组,用标记数组更新原数组即可


总结:寻求所有可能结果,首选回溯法


代码

class Solution {
    public void setZeroes(int[][] matrix) {
        //拿到数组的长与短
        int m = matrix.length;
        int n = matrix[0].length;
​
        //定义两个标记数组
        boolean[] row = new boolean[m];
        boolean[] col = new boolean[n];
​
        //若有值为 0,则将数组标记为true
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(matrix[i][j] == 0){
                    row[i] = true;
                    col[j] = true;
                }
            }
        }
​
        //若数组为true,则将这一行的值和这一列的值均置为true
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(row[i] || col[j]){
                    matrix[i][j] = 0;
                }
            }
        }
    }
}

11.3 从中序遍历和后序遍历序列构造二叉树(中等):哈希表 + 递归

题目:给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树

思想:后序遍历的最后一个值就是根节点,后序遍历的思想是:左子树、右子树、根节点,因此当我们从后向前取后序遍历数组时,应该先构造的是右子树,然后构造的是左子树

  • 使用哈希表存储中序序列,从而高效的查询元素

  • 定义递归函数help(in_left, in_right)表示当前递归到中序遍历子树的左右边界,开始时为help(0, n - 1)

  • in_left > in_right,子树为空

  • 查询后序遍历中最后一个节点为根节点,索引为index,从in_leftindex - 1属于左子树,从index + 1in_right属于右子树

  • 注意:后续遍历列表中的每次取最后一个数为根节点,因此每次先创建右子树节点,然后创建左子树(每次先被构造出来的为右子树)


总结:巧妙运用后序遍历的规律,找到节点之间的对应关系


代码

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    //创建几个变量,用于不同方法使用:当前索引位置、中序遍历数组、后序遍历数组
    int post_index;
    int[] inorder;
    int[] postorder;
​
    //创建一个Map数组,用来存放中序遍历的元素与对应索引
    Map map = new HashMap<>();
    
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        //将中序遍历的数组存到map中
        int i = 0;
        for(int v : inorder){
            map.put(v, i++);
        }
​
        //为创建的变量赋值:从后续遍历的倒数第一个节点开始
        post_index = postorder.length - 1;
        this.inorder = inorder;
        this.postorder = postorder;
​
        return help(0, postorder.length - 1);
    }
​
    public TreeNode help(int in_left, int in_right){
        //当左边界大于右边界,说明子树为空,直接返回null
        if(in_left > in_right){
            return null;
        }
​
        //得到根节点的值
        int root_val = postorder[post_index];
        //从后往前取后序遍历的值,将post_index--
        post_index--;
        //构造为树的根节点
        TreeNode root = new TreeNode(root_val);
​
        //取得根节点的索引,索引的左边都是左子树,右边都是右子树
        int root_index = map.get(root_val);
​
        //先构造右子树
        root.right = help(root_index + 1, in_right);
​
        //后构造左子树
        root.left = help(in_left, root_index - 1);
​
        return root;
    }
}

11.4 相交链表(简单):哈希集合

题目:给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

思想:将其中一个链表存入哈希集合,遍历另一个链表,当遇到相同元素时返回即可


总结:运用哈希集合的不重复性


代码

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Set set = new HashSet<>();
​
        while(headA != null){
            set.add(headA);
            headA = headA.next;
        }
​
        while(headB != null){
            if(set.contains(headB)){
                return headB;
            }
            headB = headB.next;
        }
        return null;
    }
}

11.5 最长连续序列(中等):哈希表

题目:给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

思想:为了满足时间复杂度的要求,使用一个哈希表来存储数组中的元素,此时已经进行了去重

  • 一个剪枝操作:由于可能存在一个序列,已经是一个x,x+1,x+2,⋯ ,x+y的连续序列,如果此时依旧从x + 1处匹配,就会浪费过多的时间,因此我们进行剪枝:

    • 要枚举的数一定不存在前驱数x - 1,否则就应该从x - 1匹配,而不是从x匹配,因此将其跳过


总结:利用剪枝操作,满足题目的复杂度约束


代码

class Solution {
    public int longestConsecutive(int[] nums) {
        if(nums == null || nums.length == 0){
            return 0;
        }
        Set set = new HashSet<>();
        for(int v : nums){
            set.add(v);
        }
        int res = 0;
​
        for(int num : set){
            //进行剪枝操作
            if(!set.contains(num - 1)){
                int curr = num;
                int currRes = 1;
​
                //如果set中存在x + 1,curr++,并记录当前的结果
                while(set.contains(curr + 1)){
                    curr++;
                    currRes++;
                }
​
                res = Math.max(res, currRes);
            }
        }
        return res;
    }
}

11.6 复制带随机指针的链表(中等):哈希表 + 回溯

题目:给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。构造这个链表的 深拷贝

思想:由于该链表存在一个存在一个随机指针,如果直接复制链表及其两个指针,随机指针指向的节点可能还未创建;因此我们利用回溯的方法,让每个节点的拷贝都独立完成

  • 对当前节点进行拷贝,然后拷贝当前节点的next和当前节点的随即指针指向的节点,最后将创建的节点返回


总结:利用map快速的查看节点的拷贝情况


代码

/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;
​
    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/
​
class Solution {
    //创建一个哈希表,存储当前指针和新指针的拷贝情况
    Map map = new HashMap<>();
​
    public Node copyRandomList(Node head) {
        //若head为空则返回null
        if(head == null){
            return null;
        }
​
        //当前map中还未创建新指针时
        if(!map.containsKey(head)){
            Node newHead = new Node(head.val);
            map.put(head, newHead);
            //新节点的next 和 random就是head的next 和 random
            newHead.next = copyRandomList(head.next);
            newHead.random = copyRandomList(head.random);
        }
​
        return map.get(head);
    }
}

11.7 多数元素(简单):哈希表

题目:给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

思想:使用哈希表存储每个元素出现的次数


总结:利用map快速的查看节点的拷贝情况


代码

class Solution {
    public int majorityElement(int[] nums) {
        //拿到每个元素出现的次数
        Map map = count(nums);
​
        //遍历数组,找到出现次数最多的元素
        Map.Entry maxCountEntry = null;
        for(Map.Entry  entry : map.entrySet()){
            //
            if(maxCountEntry == null || entry.getValue() > maxCountEntry.getValue()){
                maxCountEntry = entry;
            }
        }
        return maxCountEntry.getKey();
​
    }
​
    //获取每个元素出现的次数
    public Map count(int[] nums){
        Map map = new HashMap<>();
        //记录nums中每一个数出现的次数
        for(int v : nums){
            if(!map.containsKey(v)){
                map.put(v, 1);
            }else{
                map.put(v, map.get(v) + 1);
            }
        }
        return map;
    }
}

11.8 克隆图(中等):深度优先搜索 + 哈希表

题目:给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。图中的每个节点都包含它的值 valint) 和其邻居的列表(list[Node])。

思想:从给定节点出发,进行图的遍历,并在遍历的过程中完成深拷贝

  • 注意:由于给定的是无向边,因此两个节点可以相互指向,从而陷入死循环的问题,我们需要使用哈希表记录以及被克隆过的节点

  • 使用哈希表存储已被访问和克隆的节点,哈希表的key是原始图中的节点,value是克隆图中的对应节点

  • 从给定节点开始遍历图,若已经被访问过,则返回其克隆图中的对应节点


总结:利用map快速的查看节点的拷贝情况


代码

class Solution {
    //创建一个哈希表,存储节点和克隆节点
    Map map = new HashMap<>();
​
    public Node cloneGraph(Node node) {
        //若node未空,直接返回
        if(node == null){
            return null;
        }
​
        //每次拷贝之前,先判断是否已经拷贝过(拷贝过在进行拷贝就容易死循环);直接返回拷贝的值
        if(map.containsKey(node)){
            return map.get(node);
        }
​
        //若没有拷贝过:先拷贝节点值
        Node cloneNode = new Node(node.val);
​
        //将拷贝过的节点存入哈希表,避免重复拷贝
        map.put(node, cloneNode);
​
        //拷贝节点的邻居列表
        for(Node neighbor : node.neighbors){
            cloneNode.neighbors.add(cloneGraph(neighbor));
        }
​
        return cloneNode;
    }
}

11.9 单词拆分(中等):动态规划 + 哈希集合

题目:给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

思想:定义dp[i]表示字符串s的前i个字符组成的字符串s[0,...,i - 1]是否能被字典中若干个单词组成

  • 则要判断dp[i],必须枚举s[0,...,i - 1],将s[0,...,i - 1]按照分割点j分为两部分,我们有:

    • dp[i] = dp[j] && 子串 s[j..i−1] 是否出现在字典中

  • 用哈希集合存入字典,从而避免冗余操作


总结:利用动态规划求解前后逻辑相关的问题


代码

class Solution {
    public boolean wordBreak(String s, List wordDict) {
        //将字典存入set中,去重
        Set set = new HashSet<>(wordDict);
        //设置dp,没有元素则返回true
        boolean dp[] = new boolean[s.length() + 1];
        dp[0] = true;
​
        //dp[i]成立的条件是dp[j] && 字典中是否存在s[j,...i - 1]
        for(int i = 1; i <= s.length(); i++){
            //输入一个分割点,位于[0, i]之间
            for(int j = 0; j < i; j++){
                if(dp[j] && set.contains(s.substring(j, i))){
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

11.10 分数到小数(中等):哈希表 + 长除法

题目:给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以 字符串形式返回小数 。如果小数部分为循环小数,则将循环的部分括在括号内。如果存在多个答案,只需返回 任意一个 。对于所有给定的输入,保证 答案字符串的长度小于 10的四次方

思想:为了避免结果溢出,将输入的int类型转换为long类型

  • 首先,判断结果的正负,若为负数,要加上-

  • 其次,判断计算后的结果,有三种可能:

    • 整数,根据正负直接返回

    • 有限小数,计算小数值拼接上小数点.返回

    • 无限循环小数,找出循环部分加入括号

  • 计算时,先计算整数部分,后根据余数计算小数部分

  • 计算小数部分:每次将余数×10,然后计算小数的下一位,得到新的余数,直到余数为0或者找到循环部分为止

  • 在寻找循环部分时,使用哈希表存储每个余数在小数部分第一次出现的下标(字符串的此时的长度就是每次余数的下标)

    • 注意:相同的余数计算得到的小数的下一位一定是相同的,如果计算中发现某一位的余数在之前出现过,就说明是循环部分

    • 使用哈希表存储每个余数在小数部分第一次出现的下标

    • 比如:假设存在下标 jk,满足j ≤ kremainderj=remainderk+1 ,则小数部分的第k + 1位和小数部分的第j 位相同,因此小数部分的第 j位到第 k位是一个循环节。在小数部分的第j位之前加上左括号,在小数部分的末尾(即第 k位之后)加上右括号


总结:理清除法中的所有逻辑,注意计算时的先后顺序


代码

class Solution {
    public String fractionToDecimal(int numerator, int denominator) {
        //创建结果字符串,一般用StringBuffer
        StringBuffer res = new StringBuffer();
​
        //创建一个Map,用来存储键为每次计算结果的余数,值为当前余数插入的下标(也就是结果字符串的长度)
        Map map = new HashMap<>();
        //避免溢出的转换
        long a = numerator, b = denominator;
​
        //先判断结果的正负;判断完后,取二者的绝对值相除
        if(a * b < 0) res.append("-");
        a = Math.abs(a);
        b = Math.abs(b);
        res.append(a / b);
        //如果结果为整数(没有余数),直接返回
        if(a % b == 0) return res.toString();
​
        //程序到这里就说明不为整数,此时需要加上小数点
        res.append(".");
​
        //计算结果为小数的情况: 注意,每次的余数乘以10再除分母; 
        //余数不为 0 且 未在map中出现过,说明还需要继续求解
        while((a = (a % b) * 10) > 0 && !map.containsKey(a)){
            //将余数加入Map
            map.put(a, res.length());
            res.append(a / b);
        }
        //程序走到这里,说明余数 a 为 0;或者出现了相同的余数
        //如果余数为0,说明是有限小数,将其返回
        if(a == 0) return res.toString();
        //如果出现了相同的余数,则在a第一次出现的位置加入左括号,在最后出现的位置加入右括号
        return res.insert(map.get(a).intValue(), "(").append(")").toString();
    }
}

11.11 重复的DNA序列(中等):哈希表

题目DNA序列 由一系列核苷酸组成,缩写为 'A', 'C', 'G''T'.。

  • 例如,"ACGAATTCCG" 是一个 DNA序列

在研究 DNA 时,识别 DNA 中的重复序列非常有用。给定一个表示 DNA序列 的字符串 s ,返回所有在 DNA 分子中出现不止一次的 长度为 10 的序列(子字符串)。你可以按 任意顺序 返回答案。

思想:用一个哈希表统计s中长度为10的子串出现的次数,返回所有出现次数超过10的子串


总结:利用哈希表的map.getOrDefault()方法,记录子串出现过的此时


代码

class Solution {
    public List findRepeatedDnaSequences(String s) {
        List res = new ArrayList<>();
        Map map = new HashMap<>();
        //将s中长度为10的子串存入即可
        if(s.length() < 10){
            return res;
        }
        //找到s中所有长度为10的子串,存入map中
        for(int i = 0; i <= s.length() - 10; i++){
            String sub = s.substring(i, i + 10);
            //将s中每一个长度为10的子串存入map中,注意使用map.getOrDefault()方法,创建初始值
            map.put(sub, map.getOrDefault(sub, 0) + 1);
            if(map.get(sub) == 2){
                res.add(sub);
            }
        }
        return res;
    }
}

11.12 快乐数(简单):哈希集合

题目:编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。

  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。

  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n快乐数 就返回 true ;不是,则返回 false

思想:将该数进行数位分离,求平方和;

  • 用一个哈希集合存储每次生成的平方和是否在哈希集合中,如果不在则添加,如果在就说明该数处于一个循环中,不可能返回1,也就不是快乐数


总结:要知道数的分离平方只有三种情况:最终得到1;进入一个循环;越来越大接近无穷大(不可能发生)。根据这几种情况求解,选择哈希集合判断是否出现过只需要O(1)的复杂度


代码

class Solution {
    public boolean isHappy(int n) {
        //存储生成的平方数
        Set set = new HashSet<>();
        while(n != 1 && !set.contains(n)){
            set.add(n);
            n = getNext(n);
        }
        return n == 1;
    }
​
    //获取每个位置的平方数
    public int getNext(int n){
        int res = 0;
        //每次取余数,然后将其除10,得到每一个位置的数
        while(n > 0){
            int temp = n % 10;
            n = n / 10;
            res += temp * temp;
        }
        return res;
    }
}

11.13 哈希表总结!!!

哈希表最大的好处在于寻找元素的时间复杂度仅为O(1),通常用来存入需要验证的数据,进而判断是否存在于哈希表内

哈希表的常用方法:

  • map.getOrDefault(key, new E())

  • map.containsKey(key), map.containsValue(value)

  • map.put(key, value)

  • map.values():获取所有的键值信息;Collection values = map.values();

你可能感兴趣的:(算法,leetcode,数据结构)