跳至主要內容

哈希

holic-x...大约 8 分钟算法算法

难度说明:🟢简单🟡中等🔴困难

哈希

🟢01-两数之和(1)

1.题目内容

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。

2.题解思路

👻方法1:暴力枚举

暴力枚举:迭代数组中的每一个数 x ,看是否存在target - x (嵌套循环数组,外层循环遍历x,内层循环依次判断是否存在 x + y = target,存在则封装返回)。此处需注意的是每个位于x前面的数是在前面的循环中已经和x比较过了,因此对于y的起始是从x+1开始,且需注意数组遍历的临界值处理

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int len = nums.length;
        for(int i=0;i<len;i++){
            for(int j=i+1;j<len;j++){
                if(nums[i]+nums[j]==target){
                    return new int[]{i,j};
                }
            }
        }
        // 没有匹配到满足的条件
        return new int[0];
    }
}

复杂度分析

  • 时间复杂度:O(n2),n 是数组中的元素数量,最坏情况下数组中的任意两个数都要被匹配1次
  • 空间复杂度:O(1)

👻方法2:哈希表

​ 方法1中时间复杂度较高的原因是查找target-x的时间复杂度较高,需要一种优化方法能够快速寻找数组中是否存在元素目标,存在的找出其索引。因此引入哈希表,将寻找target-x的时间复杂度从O(N)降低到O(1)。

​ 外层循环遍历x,然后将x的值作为key,内层判断是否满足hashtable.containsKey(target - x),存在则直接返回,否则将对应索引 i 作为value存入哈希表

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int len = nums.length;
        Map<Integer,Integer> map = new HashMap<Integer,Integer>();
        for(int i=0;i<len;i++){
            if(map.containsKey(target - nums[i])){
                return new int[]{map.get(target - nums[i]),i};
            }
            map.put(nums[i],i);
        }
        // 没有匹配到满足的条件
        return new int[0];
    }
}

​ 对比方法1,相当于此处在循环过程中将x摘出来,放到一个HashMap中,然后每次循环都先判断map中是否匹配x + y = target这个条件,匹配则返回,不匹配则继续将x摘出来(保证不会让x和自己匹配),直到所有操作完成

  • 时间复杂度:O(n),n 是数组中的元素数量,对于么一个元素x可以O(1)快速寻找target - x
  • 空间复杂度:O(n),n 是数组中的元素数量,主要为哈希表的开销

3.答题反思(不足之处)

20240814:暴力拆解

(1)基本思路正确,但是需简化代码逻辑结构

(2)每个位于 i 前面的数据是比较过的数据了,所以 j 不需要从头开始,只需要接着 i 后面走即可

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];
        for(int i=0 ;i< nums.length ; i++){
            for(int j=i+1;j< nums.length ; j++){
                if((nums[i] + nums[j]) == target){
                    res[0] = i;
                    res[1] = j;
                    return res;
                }
            }
        }
        return null;
    }
}

🟡02-字母异位词分组(49)

1.题目内容

​ 给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表

字母异位词 是由重新排列源单词的所有字母得到的一个新单词

​ 例如输入["eat","tea","tan","ate","nat","bat"],输出[["bat"],["tan","nat"],["eat","tea","ate"]]

2.题解思路

​ 关键:抓住字母异位词的概念核心互为字母异位词的两个字符串包含的字母相同,因此此处可以有两种思路:排序、计数

排序:对两个字符串分别进行排序之后得到的字符串一定是相同的

计数:字符串中的相同字母出现的次数一定是相同的

方法1:排序

核心:定义Map<String,List<String>>,将字符串内部排序作为key,将对应字符串装入这个key对应的value(List 集合中)

  • 可以将字符串转化为字符数组,然后使用Arrays.sort工具类对字符数组进行排序
  • 对相同字母序列的字符串进行分组使用Map进行存储(key:某个内部排序后的字符串,value 对应的字母异位字符串列表)
  • 循环遍历字符串列表,先排序,根据当前key获取到对应的list列表,然后将当前字符串加入到获得的列表中,并更新map内容
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
    	// 定义结果集(存储排序后的字母异位词,和对应的分组)
        Map<String,List<String>> map = new HashMap<>();
        
        for(String str : strs){
            // 将字符串转化为字符数据后排序
            char[] strArr = str.toCharArray();
            // 使用工具类进行数组内部元素排序
            Arrays.sort(strArr);
       		String sortedStr = new String(strArr);
            // 获取当前key对应的list列表
            List<String> list = map.getOrDefault(sortedStr,new ArrayList<String>());// 此处直接获取指定key对应的list,不存在就创建一个新的
            // 将当前字符串加入到对应列表中
            list.add(str);
            // 更新map
            map.put(sortedStr,list);
        }
        // 将map的value值列表转化为ArrayList集合
        return new ArrayList<List<String>>(map.values());
    }
}

复杂度分析

  • 时间复杂度:O(nk logk)(n为strs中字符串数量,k为strs中字符串最大长度)
    • 需要遍历n个字符串,对于每个字符串需要O(k logk)时间进行排序以及O(1)的时间更新哈希表,因此总时间复杂度为O(nk logk)
  • 空间复杂度:O(nk)
    • 需要用哈希表存储全部字符串

利用Java8的Stream API简化

  • 根据排序后字母序列相同的字符串进行分组
return new ArrayList<>(Arrays.stream(strs).collect(Collectors.groupingBy(s -> Arrays.toString(s.codePoints().sorted().toArray()))).values());

方法2:计数

​ 思路:统计每个字符串中每个字符出现的次数,然后依次比较是否一致,如果都一致认为是字母异位词

🟡03-最长连续序列(128)

1.题目内容

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

​ 例如:

  • 输入:nums = [100,4,200,1,3,2];输出:4 (1,2,3,4)
  • 输入:nums = [0,3,7,2,5,8,4,6,0,1];输出:9(1,2,3,4,5,6,7,8,)

2.题解思路

核心:排序、去重后得到一个有序的集合,然后校验当前元素和其下一个元素是否连续,连续则进入下一个判断,如果出现不连续的情况则断掉

注意:需注意去重、排序的处理,避免先排序后又引入无序集合做去重导致"排序失效"

  • 先将数组进行去重、排序,获得一个新的集合
    • 如果用的是无序集合,则对Set处理(涉及到一个边界概念)
    • 如果用的是有序集合,则循环遍历校验当前元素和其下一个元素不连续(相差不为1)则说明连续序列断了,当前元素所在的位置就是最长的连续序列**(推荐)**
// 排序、去重(大数组会占用一定的性能消耗),然后遍历统计最长连续序列
public class Solution128 {
    public int longestConsecutive(int[] nums) {
        // 判断nums是否为空
        if(nums==null || nums.length==0){
            return 0;
        }
        // 定义一个新集合,对nums进行去重、排序
        Arrays.sort(nums);
        List<Integer> list = new ArrayList<>();
        for (int num : nums) {
            if(!list.contains(num)){
                list.add(num);
            }
        }
        // 连续序列可能从中间才开始,需要一个指针current来进行记录,定义maxLength记录最大值
        int current = 1;
        int maxLength = 1;
        
        // 遍历新集合,依次校验当前元素和其下一个元素是否连续,如果连续则计数+1并更新maxLength,如果断掉则当前current即为当前的连续序列大小,连续序列断掉需要重新计数
        for (int i=0;i<list.size()-1;i++) {
            if( list.get(i+1) - list.get(i) == 1){
               // 满足连续,计数+1
                current ++;
                // 更新当前的maxLength,取大值
                maxLength = Math.max(current,maxLength);
            }else{
                // 连续序列断掉,current又重新计数
                current = 1;
            }
        }
        return maxLength;
    }
}
// 推荐解法:进一步优化:只对数组进行排序而不用额外去重,对于重复元素则直接跳过校验
class Solution {
    public int longestConsecutive(int[] nums) {
        if (nums == null || nums.length ==0) {
            return 0;
        } else {
            // 排序
            Arrays.sort(nums);
            int maxLength = 1, current = 1;
            // 遍历时跳过重复的元素
            for(int i=1; i<nums.length; i++) {
                if(nums[i] - nums[i-1] ==1) {
                    current++;
                    maxLength = Math.max(maxLength, current);
                } else if(nums[i] == nums[i-1]) {
                    continue;
                } else {
                    current = 1;
                }
            }
            return maxLength;
        }   
    }
}

3.答题反思

  • 误区1:误认为"起点一定要从最小的数开始",但实际上连续序列可能是在中间(例如0,3,4,5,连续序列最长为3)
    • 错误点在于认为启动从最小的数开始,断掉了就跳出循环
public class Solution128 {
    public int longestConsecutive(int[] nums) {
        // 判断nums是否为空
        if(nums==null || nums.length==0){
            return 0;
        }
        // 定义一个新集合,对nums进行去重、排序
        Arrays.sort(nums);
        List<Integer> list = new ArrayList<>();
        for (int num : nums) {
            if(!list.contains(num)){
                list.add(num);
            }
        }
        // 遍历新集合,依次校验当前元素和其下一个元素是否连续,如果连续则跳出当次循环继续下一个校验,如果断掉则当前元素位置即为最大连续序列
        // 误区:忽略了连续序列可能从中间才开始,需要一个指针来进行记录
        int maxLength = 1;
        for (int i=0;i<list.size()-1;i++) {
            if( list.get(i+1) - list.get(i) == 1){
               // 满足连续,计数+1
                maxLength ++;
                // 跳出当次循环
                continue;
            }else{
                return maxLength;
            }
        }
        return maxLength;
    }
}
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3