题目一
767. 重构字符串
给定一个字符串S,检查是否能重新排布其中的字母,使得两相邻的字符不同。
若可行,输出任意可行的结果。若不可行,返回空字符串。
示例 1:
输入: S = “aab”
输出: “aba”
示例 2:
输入: S = “aaab”
输出: “”
注意:
S 只包含小写字母并且长度在[1, 500]区间内。
我没做出来
没悟到maxCharCounts > (length + 1) / 2
和使用优先级队列。
官方方法一:基于最大堆的贪心算法
class Solution {
public String reorganizeString(String S) {
if (S.length() < 2) {
return S;
}
// 26个字母计数数组
int[] counts = new int[26];
int maxCount = 0; // 字母最多数量计数
int length = S.length();
for (int i = 0; i < length; i++) {
char c = S.charAt(i);
counts[c - 'a']++;
maxCount = Math.max(maxCount, counts[c - 'a']);
}
// 如果最多字符的个数比 字符串+1的一半还要多就说明题目无解
// 比如 aaab 3 > (4+1)/2 -> 3 > 2 比如 aaa 3 > (3+1)/2 -> 3 > 2
// 只要不是这种情况,那么肯定能够构造成满足题意的字符串
if (maxCount > (length + 1) / 2) {
return "";
}
// 优先级队列 字符数量大的放在最前面
PriorityQueue<Character> queue = new PriorityQueue<Character>(new Comparator<Character>() {
public int compare(Character letter1, Character letter2) {
return counts[letter2 - 'a'] - counts[letter1 - 'a'];
}
});
for (char c = 'a'; c <= 'z'; c++) {
if (counts[c - 'a'] > 0) {
queue.offer(c); // 按顺序将不为0 的字母加进队列中
}
}
StringBuilder sb = new StringBuilder();
while (queue.size() > 1) { // 因为一次取两个字符所以控制条件是队列得大于1
char letter1 = queue.poll(); // 取出一个字母
char letter2 = queue.poll(); // 再取出一个字母
// 拼接字符串
sb.append(letter1);
sb.append(letter2);
// 得到该字母在计数数组中的索引
int index1 = letter1 - 'a', index2 = letter2 - 'a';
counts[index1]--;
counts[index2]--; // 拼接了一次就减去一下
// 如果该字母计数没减到0就放进队列继续排队,等下一轮的--
if (counts[index1] > 0) {
queue.offer(letter1);
}
if (counts[index2] > 0) {
queue.offer(letter2);
}
}
// 这时队列中就剩一个字母了
if (queue.size() > 0) {
sb.append(queue.poll()); // 拼接到字符串上即可
}
return sb.toString();
}
}
复杂度分析
-
时间复杂度:O(nlog∣Σ∣+∣Σ∣),其中 n 是字符串的长度,Σ 是字符集,在本题中字符集为所有小写字母,∣Σ∣=26。
遍历字符串并统计每个字母的出现次数,时间复杂度是O(n)。
将每个字母加入最大堆,字母个数最多为 ∣Σ∣,这里设真正出现的小写字母数量为∣Σ ′∣,那么时间复杂度是 O(∣Σ∣) 加上 O(∣Σ ′∣log∣Σ ′∣) 或 O(∣Σ ′∣)。前者是对数组进行遍历的时间复杂度 O(∣Σ∣),而后者取决于是将每个字母依次加入最大堆,时间复杂度为 O(∣Σ ′∣log∣Σ ′∣);还是直接使用一次堆的初始化操作,时间复杂度为 O(∣Σ ′∣)。
重构字符串需要对最大堆进行取出元素和添加元素的操作,取出元素和添加元素的次数都不会超过 n 次,每次操作的时间复杂度是 O(log∣Σ ′∣),因此总时间复杂度是 O(nlog∣Σ ′∣)。由于真正出现的小写字母数量为 ∣Σ ′∣ 一定小于等于字符串的长度 n,因此上面的时间复杂度中O(n),O(∣Σ ′∣log∣Σ ′∣) 和 O(∣Σ ′∣) 在渐进意义下均小于 O(nlog∣Σ ′∣),只需要保留 O(∣Σ∣)。由于 ∣Σ ′∣≤∣Σ∣,为了不引入额外符号,可以将时间复杂度 O(nlog∣Σ ′∣) 写成 O(nlog∣Σ∣)。
总时间复杂度是 O(nlog∣Σ∣+∣Σ∣)。 -
空间复杂度:O(∣Σ∣),其中 Σ 是字符集,在本题中字符集为所有小写字母,∣Σ∣=26。这里不计算存储最终答案字符串需要的空间(以及由于语言特性,在构造字符串时需要的额外缓存空间),空间复杂度主要取决于统计每个字母出现次数的数组和优先队列。
方法二:基于计数的贪心算法
class Solution {
public String reorganizeString(String S) {
if (S.length() < 2) {
return S;
}
int[] counts = new int[26];
int maxCount = 0;
int length = S.length();
for (int i = 0; i < length; i++) {
char c = S.charAt(i);
counts[c - 'a']++;
maxCount = Math.max(maxCount, counts[c - 'a']);
}
if (maxCount > (length + 1) / 2) {
return "";
}
char[] reorganizeArray = new char[length];
int evenIndex = 0, oddIndex = 1;
int halfLength = length / 2;
for (int i = 0; i < 26; i++) {
char c = (char) ('a' + i);
while (counts[i] > 0 && counts[i] <= halfLength && oddIndex < length) {
reorganizeArray[oddIndex] = c;
counts[i]--;
oddIndex += 2;
}
while (counts[i] > 0) {
reorganizeArray[evenIndex] = c;
counts[i]--;
evenIndex += 2;
}
}
return new String(reorganizeArray);
}
}
复杂度分析
-
时间复杂度:O(n+∣Σ∣),其中 n 是字符串的长度,Σ 是字符集,在本题中字符集为所有小写字母,∣Σ∣=26。
遍历字符串并统计每个字母的出现次数,时间复杂度是O(n)。
重构字符串需要进行 n 次放置字母的操作,并遍历每个字母得到出现次数,时间复杂度是 O(n+∣Σ∣)。
总时间复杂度是 O(n+∣Σ∣)。 -
空间复杂度:O(∣Σ∣),其中 n 是字符串的长度,Σ 是字符集,在本题中字符集为所有小写字母,∣Σ∣=26。这里不计算存储最终答案字符串需要的空间(以及由于语言特性,在构造字符串时需要的额外缓存空间),空间复杂度主要取决于统计每个字母出现次数的数组和优先队列。空间复杂度主要取决于统计每个字母出现次数的数组。
题目二
136. 只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出: 1
示例 2:
输入: [4,1,2,1,2]
输出: 4
暴力排序Hash表能解出答案,但是不能满足不使用额外空间
因此这题需要用到异或的思想!
class Solution {
public int singleNumber(int[] nums) {
/*
System.out.println(2 ^ 2); // 0
System.out.println(2 ^ 1); // 3
System.out.println(0 ^ 1); // 1
System.out.println(0 ^ 2); // 2
System.out.println(singleNumber(new int[]{2, 2, 1})); // 1
System.out.println(singleNumber(new int[]{4, 1, 2, 1, 2})); // 4
System.out.println(4 ^ 1); // 5
System.out.println(5 ^ 2); // 7
System.out.println(7 ^ 1); // 6
System.out.println(6 ^ 2); // 4
异或满足三条定律:
1. 交换律:a ^ b ^ c == a ^ c ^ b
2. 0与任何数异或为任何数:0 ^ a == a
3. 相同的数异或为0:a ^ a == 0
*/
for (int i = 1; i < nums.length; i++) {
nums[0] = nums[0] ^ nums[i];
}
return nums[0];
}
}
复杂度分析
- 时间复杂度:O(n),其中 n 是数组长度。只需要对数组遍历一次。
- 空间复杂度:O(1)。
题目三
350. 两个数组的交集 II
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]
说明:
输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
我们可以不考虑输出结果的顺序。
进阶:
如果给定的数组已经排好序呢?你将如何优化你的算法?
如果 nums1 的大小比 nums2 小很多,哪种方法更优?
如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
我的答案
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
HashMap<Integer, Integer> count = new HashMap<>();
for(int i : nums1){
count.put(i, count.getOrDefault(i, 0)+1);
}
List<Integer> ret = new ArrayList<>();
for(int i : nums2){
if(count.get(i) != null && count.get(i) != 0){
ret.add(i); // 确保该数在nums1中存在,且计数到减到0
count.put(i, count.get(i)-1);
}
}
// 想要转换成int[]类型,就得先转成IntStream。
// 这里就通过mapToInt()把Stream<Integer>调用Integer::valueOf来转成IntStream
// 而IntStream中默认toArray()转成int[]。
return ret.stream().mapToInt(Integer::valueOf).toArray();
}
}
复杂度分析
-
时间复杂度:O(m+n),其中 m 和 n 分别是两个数组的长度。需要遍历两个数组并对哈希表进行操作,哈希表操作的时间复杂度是 O(1),因此总时间复杂度与两个数组的长度和呈线性关系。
-
空间复杂度:O(m)或O(n),其中 m 和 n 分别是两个数组的长度。
官方答案方法二:排序
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
int length1 = nums1.length, length2 = nums2.length;
int[] intersection = new int[Math.min(length1, length2)];
int index1 = 0, index2 = 0, index = 0;
while (index1 < length1 && index2 < length2) {
if (nums1[index1] < nums2[index2]) {
index1++;
} else if (nums1[index1] > nums2[index2]) {
index2++;
} else {
intersection[index] = nums1[index1];
index1++;
index2++;
index++;
}
}
return Arrays.copyOfRange(intersection, 0, index);
}
}
复杂度分析
-
时间复杂度:O(mlogm+nlogn),其中 m 和 n 分别是两个数组的长度。对两个数组进行排序的时间复杂度是 O(mlogm+nlogn),遍历两个数组的时间复杂度是O(m+n),因此总时间复杂度是O(mlogm+nlogn)。
-
空间复杂度:O(min(m,n)),其中 m 和 n 分别是两个数组的长度。为返回值创建一个数组 intersection,其长度为较短的数组的长度。不过在 C++ 中,我们可以直接创建一个 vector,不需要把答案临时存放在一个额外的数组中,所以这种实现的空间复杂度为 O(1)。
另外:
如果 nums2 的元素存储在磁盘上,磁盘内存是有限的,并且你不能一次加载所有的元素到内存中。那么就无法高效地对nums2进行排序,因此推荐使用方法一而不是方法二。在方法一中,nums2只关系到查询操作,因此每次读取 nums2中的一部分数据,并进行处理即可。