Leetcoder热题100(一):哈希,双指针

#哈希,双指针#

1.哈希

1.1 两数之和

题目描述:给定一个整数数组 nums 和一个整数目标值 target,找出 和为目标值 target  的

 两个 整数,并返回它们的数组下标。

部分示例:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

额外要求

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

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

进阶要求:你可以想出一个时间复杂度小于 O(n2) 的算法吗?

代码部分:

class Solution {
    public int[] twoSum(int[] nums, int target) {
         HashMap<Integer,Integer> map = new HashMap<>();
         int[] result = new int[2];
         for(int i = 0; i < nums.length; i++){
             if(map.containsKey(target - nums[i])){
                 result[0] = i;
                 result[1] = map.get(target - nums[i]);
                 break;
             }
             else{
                 map.put(nums[i],i);
             }
         }
         return result;
    }
}

思路与复杂度:

  1. 创建一个哈希表 map,用于存储数组元素和它们的索引。

  2. 遍历数组 nums 中的每个元素 nums[i]

    • 检查当前元素的补数(target - nums[i])是否在哈希表 map 中。
    • 如果补数存在于哈希表中,说明找到了两个数的和等于目标值,返回它们的索引。
    • 如果补数不存在于哈希表中,将当前元素及其索引存储到哈希表中。
  3. 如果遍历完整个数组都没有找到满足条件的数对,则返回一个空数组。

代码的时间复杂度为 O(n),其中 n 是数组 nums 的长度。在最坏情况下,需要遍历整个数组一次,并在哈希表中进行常数时间的查找和插入操作。因此,整个算法的时间复杂度为 O(n)。

1.2 字母异位词分组

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

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

部分示例:

输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

代码部分:

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        HashMap<String, List<String>> map = new HashMap<>();
        for (int i = 0; i < strs.length; i++) {
            String str = strs[i];
            char[] chars = str.toCharArray();
            Arrays.sort(chars);
            String sortedStr = new String(chars);
            if (map.containsKey(sortedStr)) {
                List<String> group = map.get(sortedStr);
                group.add(str);
            } else {
                List<String> group = new ArrayList<>();
                group.add(str);
                map.put(sortedStr, group);
            }
        }
        return new ArrayList<>(map.values());
    }
}

思路与复杂度:

  1. 创建一个哈希表 map,用于存储排序后的字符串作为键,以及具有相同排序字符串的字符串列表作为值。

  2. 遍历输入的字符串数组 strs

    • 将当前字符串 str 转换为字符数组 chars
    • 对字符数组 chars 进行排序,得到排序后的字符串 sortedStr
    • 检查排序后的字符串 sortedStr 是否已经在哈希表 map 中。
    • 如果已经存在,将当前字符串 str 加入对应的字符串列表中。
    • 如果不存在,创建一个新的字符串列表,将当前字符串 str 添加到列表中,并将排序后的字符串 sortedStr 作为键存储到哈希表 map 中。
  3. 返回哈希表 map 中的所有值组成的列表。

代码的时间复杂度分析:

  • 遍历字符串数组 strs 的时间复杂度为 O(n),其中 n 是字符串数组的长度。
  • 对每个字符串进行排序的时间复杂度为 O(klogk),其中 k 是字符串的平均长度。
  • 在哈希表中查找和插入操作的平均时间复杂度为 O(1)。
  • 总体时间复杂度为 O(n * klogk)。

1.3 最长连续序列

题目描述:给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度

额外要求:请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

部分示例:

输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

代码部分:

class Solution {
    public int longestConsecutive(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        
        Set<Integer> numSet = new HashSet<>();
        for (int num : nums) {
            numSet.add(num);
        }
        
        int maxCount = 0;
        for (int num : nums) {
            if (!numSet.contains(num - 1)) {
                int count = 1;
                while (numSet.contains(num + 1)) {
                    num++;
                    count++;
                }
                maxCount = Math.max(maxCount, count);
            }
        }
        
        return maxCount;
    }
}

思路与复杂度:

  1. 首先检查数组 nums 的长度,如果为空数组,则直接返回 0。

  2. 创建一个哈希集合 numSet,用于存储数组中的数字。

  3. 遍历数组 nums,将数组中的每个数字添加到哈希集合 numSet 中。

  4. 初始化一个变量 maxCount 为 0,用于记录最长连续序列的长度。

  5. 对于数组 nums 中的每个数字 num,判断 num 的前一个数字 num-1 是否存在于哈希集合 numSet 中:

    • 如果不存在,说明 num 是一个连续序列的起点。
    • 在循环中,通过不断增加 num 直到 num+1 不在哈希集合 numSet 中,统计连续序列的长度。
    • 更新 maxCount 为当前连续序列的长度和 maxCount 中的较大值。
  6. 返回 maxCount,即最长连续序列的长度。

代码的时间复杂度分析:

  • 将数组中的数字添加到哈希集合的时间复杂度为 O(n),其中 n 是数组的长度。
  • 遍历数组的时间复杂度为 O(n)。
  • 在循环中,对于每个起点数字,通过向后递增找到连续序列的长度,时间复杂度为 O(1)。
  • 总体时间复杂度为 O(n)。

 写在后边:

提交后发现执行时间并不低,而且显著高于使用排序的O(nlogn)

分析原因如下:

  1. 创建哈希集合 numSet 并将数组中的数字添加到集合中的操作,时间复杂度为 O(n),其中 n 是数组的长度。

  2. 对数组进行第一次遍历,将所有的数字添加到哈希集合 numSet 中,时间复杂度为 O(n)。

  3. 对数组进行第二次遍历,对于每个数字 num,通过判断 num-1 是否存在于哈希集合 numSet 中来确定是否是连续序列的起点。这个判断操作需要在哈希集合中进行查找,平均时间复杂度为 O(1)。因此,这个循环的时间复杂度为 O(n)。

  4. 在内层循环中,通过不断递增 num 直到 num+1 不在哈希集合 numSet 中,统计连续序列的长度。这个递增的过程需要在哈希集合中进行查找,平均时间复杂度为 O(1)。因此,内层循环的时间复杂度为 O(1)。

2. 双指针

2.1 移动零

题目描述:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

额外要求:必须在不复制数组的情况下原地对数组进行操作。

代码部分:

class Solution {
    public void moveZeroes(int[] nums) {
        int nonZeroIndex = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] != 0) {
                nums[nonZeroIndex] = nums[i];
                nonZeroIndex++;
            }
        }
        for (; nonZeroIndex < nums.length; nonZeroIndex++) {
            nums[nonZeroIndex] = 0;
        }
    }
}

思路与复杂度:

  1. 初始化一个变量 nonZeroIndex,表示非零元素的索引位置,初始值为 0。

  2. 遍历数组 nums

    • 如果当前元素 nums[i] 不等于 0,将其赋值给 nums[nonZeroIndex],然后将 nonZeroIndex 增加 1,即将非零元素移动到前面。
    • 如果当前元素 nums[i] 等于 0,则不执行任何操作,继续遍历下一个元素。
  3. 在第一次遍历结束后,nonZeroIndex 表示了非零元素移动到前面后的索引位置。

  4. 在剩余的位置上,将数组 nums 中的元素都设置为 0,即将零元素移动到末尾。

  5. 完成操作后,数组 nums 中的零元素已经被移动到了末尾。

代码的时间复杂度分析:

  • 遍历数组 nums 的时间复杂度为 O(n),其中 n 是数组的长度。
  • 在遍历过程中,只对非零元素进行操作,不包括零元素。因此,不论数组中有多少个零元素,都只进行了一次遍历操作。
  • 因此,该代码的时间复杂度为 O(n)。

2.2 盛最多水的容器

题目描述:给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

部分示例:

额外要求:你不能倾斜容器。

代码部分:

class Solution {
    public int maxArea(int[] height) {
        int max = 0;
        int left = 0;
        int right = height.length - 1;
        
        while (left < right) {
            int area = Math.min(height[left], height[right]) * (right - left);
            max = Math.max(max, area);
            
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        
        return max;
    }
}

思路与复杂度:

  1. 初始化变量 max 为 0,用于记录最大的面积。

  2. 初始化两个指针 left 和 right 分别指向数组的首尾两个元素。

  3. 使用双指针法来计算面积:

    • 计算当前指针位置所围成的面积,面积的计算公式为:Math.min(height[left], height[right]) * (right - left),其中 Math.min(height[left], height[right]) 表示两个指针所指的柱子的较小高度,(right - left) 表示两个指针之间的距离。
    • 更新 max 为当前面积和 max 中的较大值。
  4. 根据两个指针所指柱子的高度比较,移动指针:

    • 如果 height[left] 小于 height[right],则将 left 指针右移一位。
    • 否则,将 right 指针左移一位。
  5. 重复步骤 3 和步骤 4,直到两个指针相遇。

  6. 返回最大面积 max

代码的时间复杂度分析:

  • 使用双指针法,每次移动指针都能排除掉一个柱子,因此最多需要遍历一次数组。
  • 因此,该代码的时间复杂度为 O(n),其中 n 是数组的长度。

2.3 三数之和

题目描述:给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

额外要求:答案中不可以包含重复的三元组。

代码部分:

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            map.put(0 - nums[i], nums[i]);
        }
        int num1;
        int num2;
        ArrayList<ArrayList<Integer>> numbers = new ArrayList<ArrayList<Integer>>();
        Arrays.sort(nums);
        for (num1 = 0; num1 < nums.length; num1++) {
            for (num2 = num1 + 1; num2 < nums.length; num2++) {
                int complement = nums[num2] + nums[num1];
                if (map.containsKey(complement)) {
                    ArrayList<Integer> ns = new ArrayList<Integer>();
                    ns.add(nums[num1]);
                    ns.add(nums[num2]);
                    ns.add(map.get(complement));
                    numbers.add(ns);
                }
            }
        }
        return numbers;
    }
}

思路与复杂度:

  1. 创建一个哈希映射 map,用于存储每个数的相反数作为键,以及原数作为值。这样可以快速查找是否存在满足条件的组合。

  2. 使用双重循环遍历数组 nums 中的数:

    • 第一个循环变量 num1 从数组的开头开始。
    • 第二个循环变量 num2 从 num1 + 1 的位置开始,遍历数组剩余部分的数。
  3. 在内层循环中,计算两个数的和 complement,其中 complement = nums[num2] + nums[num1]

  4. 检查 complement 是否存在于哈希映射 map 中:

    • 如果存在,说明找到了满足条件的组合,将三个数的值添加到结果列表中。
    • 注意,由于 map 存储的是每个数的相反数,所以这里找到的是和为零的组合。
  5. 返回结果列表 numbers,其中包含了所有满足条件的组合。

代码的时间复杂度分析:

  • 创建哈希映射 map 的时间复杂度为 O(n),其中 n 是数组的长度。
  • 使用双重循环遍历数组的时间复杂度为 O(n^2)。
  • 在内层循环中,通过哈希映射的查找操作的时间复杂度为 O(1)。
  • 因此,整个代码的时间复杂度为 O(n^2)。

写在后边:另一种思路

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> numbers = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length - 2; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue; // 跳过重复的元素
            }
            int target = -nums[i];
            int left = i + 1;
            int right = nums.length - 1;
            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum == target) {
                    numbers.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++; // 跳过重复的元素
                    }
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--; // 跳过重复的元素
                    }
                    left++;
                    right--;
                } else if (sum < target) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return numbers;
    }
}
  1. 创建一个结果列表 numbers,用于存储满足条件的组合结果。

  2. 对数组 nums 进行排序,以便在后续的操作中处理重复元素。

  3. 使用一个循环遍历数组 nums,循环变量 i 从 0 开始到倒数第三个元素。

  4. 在循环中,判断当前位置的元素是否与前一个元素相同(去重操作):

    • 如果当前元素与前一个元素相同且不是第一个元素(i > 0 && nums[i] == nums[i - 1]),则跳过当前元素,继续下一次循环。
    • 这样可以避免在结果列表中出现重复的组合。
  5. 计算目标值 target,即当前元素的相反数(target = -nums[i])。

  6. 使用双指针法来寻找满足条件的组合:

    • 初始化左指针 left 为 i + 1,指向当前元素的下一个元素。
    • 初始化右指针 right 为数组的最后一个元素。
  7. 在循环中,判断左指针和右指针之间的元素之和 sum 与目标值 target 的关系:

    • 如果 sum 等于 target,说明找到了满足条件的组合:
      • 将当前组合 [nums[i], nums[left], nums[right]] 添加到结果列表 numbers 中。
      • 通过移动指针跳过重复的元素:
        • 左指针 left 向右移动,直到找到下一个不重复的元素。
        • 右指针 right 向左移动,直到找到下一个不重复的元素。
      • 继续寻找下一个组合,将左指针 left 右移一位,右指针 right 左移一位。
    • 如果 sum 小于 target,说明当前和偏小,左指针 left 向右移动一位,寻找更大的元素。
    • 如果 sum 大于 target,说明当前和偏大,右指针 right 向左移动一位,寻找更小的元素。
  8. 返回结果列表 numbers,其中包含了所有满足条件的组合。

该算法的时间复杂度为 O(n^2),其中 n 是数组的长度。排序数组的时间复杂度为 O(nlogn),外层循环的时间复杂度为 O(n),内层循环的时间复杂度为 O(n)。因此,整体的时间复杂度为 O(n^2)。

两种实现的优劣比较:

  • 第一个代码实现使用了哈希映射,可以在 O(1) 的时间内进行查找操作,因此在构建哈希映射时的时间复杂度为 O(n),而两重循环的时间复杂度为 O(n^2)。总体时间复杂度为 O(n^2)。缺点是它并没有处理重复元素的情况,结果中可能包含重复的组合,需要额外的处理。
  • 第二个代码实现使用了排序和双指针法,排序的时间复杂度为 O(nlogn),外层循环的时间复杂度为 O(n),内层循环的时间复杂度为 O(n)。总体时间复杂度为 O(n^2)。优点是它通过指针的移动和跳过重复元素的处理,避免了重复的组合,并且能够高效地找到满足条件的组合。

2.4 接雨水

题目描述:

部分示例:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

部分示例:

代码部分:

class Solution {
    public int trap(int[] height) {
        int left = 0;
        int right = height.length - 1;
        int leftMax = 0;
        int rightMax = 0;
        int rains = 0;
        
        while (left <= right) {
            if (height[left] <= height[right]) {
                if (height[left] > leftMax) {
                    leftMax = height[left];
                } else {
                    rains += leftMax - height[left];
                }
                left++;
            } else {
                if (height[right] > rightMax) {
                    rightMax = height[right];
                } else {
                    rains += rightMax - height[right];
                }
                right--;
            }
        }
        
        return rains;
    }
}
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        if (n == 0) {
            return 0;
        }
        
        int[] leftMax = new int[n]; // 存储每个位置的左边最大高度
        int[] rightMax = new int[n]; // 存储每个位置的右边最大高度
        
        leftMax[0] = height[0];
        for (int i = 1; i < n; i++) {
            leftMax[i] = Math.max(leftMax[i-1], height[i]);
        }
        
        rightMax[n-1] = height[n-1];
        for (int i = n-2; i >= 0; i--) {
            rightMax[i] = Math.max(rightMax[i+1], height[i]);
        }
        
        int rains = 0;
        for (int i = 0; i < n; i++) {
            int rain = Math.min(leftMax[i], rightMax[i]) - height[i];
            if (rain > 0) {
                rains += rain;
            }
        }
        
        return rains;
    }
}

思路及复杂度:

第一个代码实现使用了双指针的方式来计算接雨水的量。具体思路是:

  • 使用两个指针 left 和 right,分别指向数组的起始和末尾位置。
  • 初始化两个变量 leftMax 和 rightMax,分别表示左边和右边的最大高度,初始值为 0。
  • 使用一个循环,循环条件是 left <= right
    • 在循环中,判断当前位置的高度:
      • 如果 height[left] <= height[right],说明左边的高度较小或相等,可以计算左边的接雨水量:
        • 如果当前高度大于 leftMax,更新 leftMax 的值。
        • 否则,计算接雨水量并累加到 rains 中。
        • 将左指针 left 右移一位。
      • 如果 height[left] > height[right],说明右边的高度较小,可以计算右边的接雨水量:
        • 如果当前高度大于 rightMax,更新 rightMax 的值。
        • 否则,计算接雨水量并累加到 rains 中。
        • 将右指针 right 左移一位。

第二个代码实现使用了动态规划的方式来计算接雨水的量。具体思路是:

  • 首先创建两个数组 leftMax 和 rightMax,分别用于存储每个位置的左边最大高度和右边最大高度。
  • 遍历数组 height,计算每个位置的左边最大高度,并将结果存储在 leftMax 数组中。
  • 遍历数组 height,计算每个位置的右边最大高度,并将结果存储在 rightMax 数组中。
  • 遍历数组 height,计算每个位置的接雨水量,累加到 rains 中。

两种实现的思路略有不同,但都能够正确计算接雨水的量。第一个实现使用了双指针,根据当前位置的左右最大高度来计算接雨水量,而第二个实现使用了动态规划,先计算每个位置的左右最大高度,然后计算接雨水量。两种实现的时间复杂度和空间复杂度分析如下:

  • 第一个实现的时间复杂度为 O(n),其中 n 是数组的长度。由于只是对数组进行一次遍历,因此时间复杂度为 O(n)。

  • 第一个实现的空间复杂度为 O(1),只使用了常数级别的额外空间。

  • 第二个实现的时间复杂度为 O(n),其中 n 是数组的长度。需要对数组进行三次遍历,因此时间复杂度为 O(n)。

  • 第二个实现的空间复杂度为 O(n),需要额外使用两个大小为 n 的数组来存储左右最大高度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值