目录
例题7: 按照字符串出现的顺序重组字符串 leetcode 451
例题7: 一个使用查找表的经典问题 leetcode 1 两数之和
1.两类查找
例题1 两个数组的交集:leetcode 349
解题思路:这是典型的第一类查找,查找有无,所以我们会想到使用Set,将第一个数组的元素放到数组中,再判断第二个数组的元素是否存在于第一个元素的数组中,如果存在则加入到一个新的Set,最后返回这个新的Set
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> temp = new HashSet<>();
Set<Integer> re = new HashSet<>();
for (int i = 0; i < nums1.length; i++) {
temp.add(nums1[i]);
}
for (int i = 0; i < nums2.length; i++) {
if (temp.contains(nums2[i])) {
re.add(nums2[i]);
}
}
int[] reArr = new int[re.size()];
Object[] objects = re.toArray();
for (int i = 0; i < objects.length; i++) {
reArr[i] = (int) objects[i];
}
return reArr;
}
例题2 两个数组的交集 II:leetcode 350
解题思路:这次跟上一题相比我们需要考虑元素重复的情况,所以将Set换成List,判断是否存在的条件依然不变,只是需要判断完成后要删除元素。
但是这属于第二种查找,键值对的类型,需要用键值对来记录元素出现的频次
public int[] intersect(int[] nums1, int[] nums2) {
List<Integer> temp = new ArrayList<>();
List<Integer> re = new ArrayList<>();
for (int i = 0; i < nums1.length; i++) {
temp.add(nums1[i]);
}
for (int i = 0; i < nums2.length; i++) {
if (temp.contains(nums2[i])) {
re.add(nums2[i]);
temp.remove(Integer.valueOf(nums2[i]));
}
}
int[] reArr = new int[re.size()];
Object[] objects = re.toArray();
for (int i = 0; i < objects.length; i++) {
reArr[i] = (int) objects[i];
}
return reArr;
}
思考,在数组有序的情况下,我们就可以不借用辅助的数据结构,而是可以很容易的实现一个O(logn)级别的算法来实现查找,比如二分查找。
还有一类的查找是非常快的,就是哈希表,但是他也失去了数据的顺序性
例题3 有效的字母异位词:leetcode 242
s
和t
仅包含小写字母
解题思路:
解法1:t 是 ss 的异位词等价于「两个字符串排序后相等」,因此我们可以对字符串 ss 和 tt 分别排序,看排序后的字符串是否相等即可判断。此外,如果 ss 和 tt 的长度不同,tt 必然不是 ss 的异位词。
时间复杂度是O(nlogn),空间复杂度O(logn)
解法2:使用哈希表来记录字符出现的频次
tt 是 ss 的异位词等价于「两个字符串中字符出现的种类和次数均相等」。由于字符串只包含 2626 个小写字母,因此我们可以维护一个长度为 2626 的频次数组 \textit{table}table,先遍历记录字符串 ss 中字符出现的频次,然后遍历字符串 tt,减去 \textit{table}table 中对应的频次,如果出现 \textit{table}[i]<0table[i]<0,则说明 tt 包含一个不在 ss 中的额外字符,返回 \text{false}false 即可。
时间复杂度O(n) 空间复杂度O(s)
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
int[] table = new int[26];
for (int i = 0; i < s.length(); i++) {
table[s.charAt(i) - 'a']++;
}
for (int i = 0; i < t.length(); i++) {
table[t.charAt(i) - 'a']--;
if (table[t.charAt(i) - 'a'] < 0) {
return false;
}
}
return true;
}
如果t跟s不仅仅包含的是小写字母吗,而是所有的字符,比如包含了大写的字母,这个时候可以扩大数组的长度为256,但是如果我还包含了中文,这个时候长度会很大,所以这个时候需要真正的hash表来存贮字符出现的频次。
public static boolean isBigAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
Map<Character, Integer> table = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
table.put(s.charAt(i), table.getOrDefault(s.charAt(i), 0) + 1);
}
for (int i = 0; i < t.length(); i++) {
table.put(t.charAt(i), table.getOrDefault(t.charAt(i), 0) - 1);
if (table.get(t.charAt(i)) < 0) {
return false;
}
}
return true;
}
例题4 : 快乐数 leetcode202
解题思路:
解法一 快慢指针法
解法二 用哈希集合检测循环,他有两种的可能一种是循环到结果为1
一种是进入一个死循环
有没有第三种可能 值会愈来愈大 不会进入死循环呢?答案是没有的,对于一个一位数来说,他分解后最大的值不会超过81,二位数分解后最大的值不会超过162,三位数分解后最大的值不会超过243,以此类推,最后都会在一个可控的范围内循环,所以用一个哈希表存储每次循环出现过的数字,当出现相同的数字的 时候,就说明已经可以返回false了。
public static boolean isHappy(int n) {
Set<Integer> stringList = new HashSet<>();
while (n != 1) {
n = getNext(n);
if (stringList.contains(n)) {
return false;
}else {
stringList.add(n);
}
}
return true;
}
private static int getNext(int n) {
int re = 0;
while (n > 0) {
int i = n % 10;
re = re + i * i;
n = n / 10;
}
return re;
}
时间复杂度:O(logn)
空间复杂度:O(logn)
例题5: 单词找规律 leetcode 290
解题思路:
我们需要判断字符与字符串之间是否恰好一一对应。即任意一个字符都对应着唯一的字符串,任意一个字符串也只被唯一的一个字符对应。在集合论中,这种关系被称为「双射」。
想要解决本题,我们可以利用哈希表记录每一个字符对应的字符串,以及每一个字符串对应的字符。然后我们枚举每一对字符与字符串的配对过程,不断更新哈希表,如果发生了冲突,则说明给定的输入不满足双射关系。
如果key不存在,插入成功,返回null;如果key存在,返回之前对应的value。 以pattern = "abba", str = "dog cat cat dog"为例, 第1次:map.put('a',0)返回null,map.put("dog",0)返回null,两者相等; 第2次:map.put('b',1)返回null,map.put("cat",1)返回null,两者相等; 第3次:map.put('b',2)返回1,map.put("cat",2)返回1,两者相等; 第4次:map.put('a',3)返回0,map.put("dog",3)返回0,两者相等, 结果为 true。
需要注意的是插入的是同一个对象返回来的才是同一个对象
代码:
public static boolean wordPattern(String pattern, String s) {
Map<Object, Object> strMap = new HashMap<>();
Map<Object, Object> cha = new HashMap<>();
String[] s1 = s.split(" ");
if (s1.length != pattern.length()) {
return false;
}
//果key重复了,返回的是map.get(key),
// 注意这里是Integer 把同一个Integer作为value存进去 当hash冲突的时候返回来的才是同一个对象,如果存的是int 则返回来的是两个不同的Integer
for (Integer i = 0; i < s1.length; i++) {
if (strMap.put(s1[i], String.valueOf(i)) != cha.put(pattern.charAt(i), String.valueOf(i))) {
return false;
}
}
return true;
}
例题6: 同构字符串 leetcode 205
解题思路:这个题目和上个题目的解题思路是一样的,不再多说
代码:
public boolean isIsomorphic(String s, String t) {
if (s.length() != t.length()) {
return false;
}
Map<Character, Integer> sMap = new HashMap<>();
Map<Character, Integer> tMap = new HashMap<>();
for (Integer i = 0; i < s.length(); i++) {
if (sMap.put(s.charAt(i), i) != tMap.put(t.charAt(i), i)) {
return false;
}
}
return true;
}
例题7: 按照字符串出现的顺序重组字符串 leetcode 451
解题思路:
当出现频次的时候,肯定会想到使键值的方式来存储字符出现的频率,但是还有一个频率次数的排序。这是一个对map的value进行排序,所以需要将map转化为一个List,然后对value进行排序就可以达到目的。有序的Map我们会想到的是TreeMap,但是TreeMap是对Key进行自然排序
代码:
public String frequencySort(String s) {
Map<Character, Integer> sMap = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
sMap.put(s.charAt(i), sMap.getOrDefault(s.charAt(i), 0) + 1);
}
List<Map.Entry<Character, Integer>> list = new ArrayList<>(sMap.entrySet());
Collections.sort(list, new Comparator<Map.Entry<Character, Integer>>() {
@Override
public int compare(Map.Entry<Character, Integer> o1, Map.Entry<Character, Integer> o2) {
return -(o1.getValue().compareTo(o2.getValue()));
}
});
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<Character, Integer> characterIntegerEntry : list) {
for (int i = 0; i < characterIntegerEntry.getValue(); i++) {
stringBuilder.append(characterIntegerEntry.getKey());
}
}
return stringBuilder.toString();
}
复杂度分析
时间复杂度:O(n + k \log k)O(n+klogk),其中 nn 是字符串 ss 的长度,kk 是字符串 ss 包含的不同字符的个数,这道题中 ss 只包含大写字母、小写字母和数字,因此 k=26 + 26 + 10 = 62k=26+26+10=62。
遍历字符串统计每个字符出现的频率需要 O(n)O(n) 的时间。
将字符按照出现频率排序需要 O(k \log k)O(klogk) 的时间。
生成排序后的字符串,需要遍历 kk 个不同字符,需要 O(k)O(k) 的时间,拼接字符串需要 O(n)O(n) 的时间。
因此总时间复杂度是 O(n + k \log k + k + n)=O(n + k \log k)O(n+klogk+k+n)=O(n+klogk)。
空间复杂度:O(n + k)O(n+k),其中 nn 是字符串 ss 的长度,kk 是字符串 ss 包含的不同字符的个数。空间复杂度主要取决于哈希表、列表和生成的排序后的字符串。
例题7: 一个使用查找表的经典问题 leetcode 1 两数之和 towSum
解题思路:
思路1: 暴力解法,遍历两次 找到和等于目标值target的坐标返回,这个算法的时间复杂度为O(n2)
思路2:采用双索引指正碰撞,但是数组是无序的,所以要先拍个序,时间复杂度O(nlogn),然后双指针碰撞,O(n),所以总的时间复杂度是O(nlogn)+O(n),但是要返回的是排序前的索引
思路3:将数组的元素放到一个哈希表里面,遍历哈希表,找到target-v存在的即可。
代码:
public static int[] twoSumHash(int[] nums, int target) {
Map<Integer, Integer> integerIntegerMap = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int key = target - nums[i];
if (integerIntegerMap.keySet().contains(key)) {
return new int[]{integerIntegerMap.get(key), i};
} else {
integerIntegerMap.put(nums[i], i);
}
}
return new int[]{-1, -1};
}
时间复杂度为O(n)
空间复杂度O(n)
思路二的解法:
public static int[] twoSum(int[] nums, int target) {
int[] originArr = Arrays.copyOf(nums, nums.length);
//1.先将数组排序
Arrays.sort(nums);
//2.采用双指针碰撞的方法找到这组解
int left = 0;
int right = nums.length - 1;
while (left < right) {
if (nums[left] + nums[right] == target) {
left = nums[left];
right = nums[right];
break;
} else if (nums[right] + nums[left] > target) {
right--;
} else {
left++;
}
}
//找到在原数组中的索引
int index1 = -1;
int index2 = -1;
for (int i = 0; i < originArr.length; i++) {
if (originArr[i] == left && index1 == -1) {
index1 = i;
}else if (originArr[i] == right && index2 == -1) {
index2 = i;
}
}
return new int[]{index1, index2};
}
时间复杂度为O(nlogn)+O(n)
空间复杂度O(n)
例题8: 三数之和 leetcode 15
解题思路:
1.暴力解法
2.使用双指针法将三数之和转换为两数之和 需要注意的是threeSum如何去重?我们在twoSum中的去重是while循环跳过与当前值相同的元素,三数之和时候我们循环数组,然后在剩余的数组中找两数之和 我们可以确保twoSum返回的是去了重的,所以关键点在于,不能让第一个数重复,至于后面的两个数,我们复用的 twoSum
函数会保证它们不重复。这是第一个要去重的地方,
第二个要去重的地方就是twoSum的时候我要从原数组的那个位置开始扫描?不能每次从0下标开始,扫描也不能包含当前这个下标 ,必须从i+1的位置开始往后扫描,比如确定第一个数为0,扫描整个数组得到剩下两个元素为0,-1,当第一个数为-1时 扫描整个数组会得到0,1,此时这两个就是重复的解,所以必须从i+1的位置开始扫描,不能包含i,包含i会出现错解,而不是重复的解
代码:
// 三数之和 返回的是不重复的三个数的组合
public static List<List<Integer>> threeSum(int[] nums) {
//a+b+c=0; 转化为两数之和 卡在了如何转化这里? //难点是是什么? 难点是确定第一个数 数组中每个元素都有可能是第一个数
Arrays.sort(nums);
List<List<Integer>> lists = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
int left = i; //这个扫描不能重0开始 不然和twnsum组合就会出现重复
int right = nums.length - 1;
// x+num[i]=0
List<List<Integer>> listList = twoSumVariant2(nums, i, left, right);
for (List<Integer> list : listList) {
list.add(nums[i]);
lists.add(list);
}
while (i < nums.length - 1 && nums[i] == nums[i + 1]) i++;
}
return lists;
}
//从指定范围内的数组返回不重复的解
private static List<List<Integer>> twoSumVariant2(int[] nums, int i, int left, int right) {
List<List<Integer>> list = new ArrayList<>();
int target = -nums[i];
while (left < right) {
//要跳过元素本身 不然重复
if (left == i) {
left++;
} else if (right == i) {
right--;
} else if (nums[left] + nums[right] == target) {
ArrayList<Integer> integers = new ArrayList<>();
integers.add(nums[left]);
integers.add(nums[right]);
list.add(integers);
int leftValue = nums[left];
int rightValue = nums[right];
//跳过重复的元素,这样返回的解才是唯一的
while (left < right && nums[left] == leftValue) left++;
while (left < right && nums[right] == rightValue) right--;
} else if (nums[left] + nums[right] > target) {
right--;
} else if (nums[left] + nums[right] < target) {
left++;
}
}
return list;
}
至此,3Sum
问题就解决了,时间复杂度不难算,排序的复杂度为 O(NlogN)
,twoSumTarget
函数中的双指针操作为 O(N)
,threeSumTarget
函数在 for 循环中调用 twoSumTarget
所以总的时间复杂度就是 O(NlogN + N^2) = O(N^2)
。
例题9: 四数之和 leetcode 18
解题思路,当然还是利用双指针法 四数之和可以转化为三数之和。我们上一道题找的是a+b+c=0,如果将他泛化a+b+c=target 这不就是这道题目四数之和吗?
看似有思路细节非常的多
a+b+c+d=target
第一步是要遍历数组,target是变化的=target - nums[i]; 遍历target要注意的点是每次循环完成需要判断当前的值和下一个的值是不是相等如果相等需要跳过,不然会有重复的解,然后就是从i+1的位置找到threeSum
第二步进入threeSum以后,需要和上层循环做一样的事情,就是从先从j的位置开始遍历,然后计算出新的target,然后将j+1的位置和新的target交给twoSum。最后就是判断当前的值和下一个的值是不是相等如果相等需要跳过
第三步就是进入twoSum,通过双指针法找到两个解返回即可。
public static List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
//先确定其中一个数,找出剩下不重复的三元组合 a+b+c+d=target a+b+c=target-d
List<List<Integer>> lists = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
// 从i+1开始找到三个和为target的元素
int newTarget = target - nums[i];
List<List<Integer>> re = threeSum(nums, i + 1, newTarget);
for (List<Integer> integers : re) {
integers.add(nums[i]);
lists.add(integers);
}
while (i < nums.length - 1 && nums[i] == nums[i + 1]) i++;
}
return lists;
}
private static List<List<Integer>> threeSum(int[] nums, int i, int target) {
//a+b+c=target => a+b=target-c
List<List<Integer>> re = new ArrayList<>();
if (i > nums.length - 1) {
return re;
}
//从[left,right]中找到a+b=targetValue
for (int j = i; j < nums.length; j++) {
int targetValue = target - nums[j];
List<List<Integer>> lists = twoSumVariant2(nums, j + 1, targetValue);
for (List<Integer> integers : lists) {
integers.add(nums[j]);
re.add(integers);
}
// 如果j与j+1则需要去重
while (j < nums.length - 1 && nums[j] == nums[j + 1]) j++;
}
return re;
}
//在一个数组中从j位置开始,找个两个元素和等于targetValue
private static List<List<Integer>> twoSumVariant2(int[] nums, int j, int targetValue) {
List<List<Integer>> lists = new ArrayList<>();
if (j > nums.length - 1) {
return lists;
}
int left = j;
int right = nums.length - 1;
while (left < right) {
if (nums[left] + nums[right] == targetValue) {
ArrayList<Integer> integers = new ArrayList<>();
integers.add(nums[left]);
integers.add(nums[right]);
lists.add(integers);
int leftValue = nums[left];
int rightValue = nums[right];
while (left < right && nums[left] == leftValue) left++;
while (left < right && nums[right] == rightValue) right--;
} else if (nums[left] + nums[right] < targetValue) {
left++;
} else if (nums[left] + nums[right] > targetValue) {
right--;
}
}
return lists;
}
时间复杂度:O(n^3)
空间复杂度:O(log n)
例题10: 最接近的三数之和 leetcode 16
例题13: 四数之和2 leetcode 454
例题14: 字母异位词分组 leetcode 49
例题15: 回旋镖的数量 leetcode 447
例题16: 直线上最多的点数 leetcode 149
滑动窗口+查找表的问题
例题17: 存在重复元素2 leetcode 219
例题18: 存在重复元素 leetcode 217
例题19: 存在重复元素3 leetcode 220