勇者抽刃向强者的博客

梦是现实的延续,现实是梦的终结

移除元素

1.题目内容

给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

1
2
3
4
5
6
7
8
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}

示例 1:

1
2
3
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

1
2
3
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100

2.解法

(1)双指针

思路及算法

由于题目要求删除数组中等于 val 的元素,因此输出数组的长度一定小于等于输入数组的长度,我们可以把输出的数组直接写在输入数组上。可以使用双指针:右指针 right 指向当前将要处理的元素,左指针 left 指向下一个将要赋值的位置。

如果右指针指向的元素不等于 val,它一定是输出数组的一个元素,我们就将右指针指向的元素复制到左指针位置,然后将左右指针同时右移;

如果右指针指向的元素等于 val,它不能在输出数组里,此时左指针不动,右指针右移一位。

整个过程保持不变的性质是:区间 [0,left) 中的元素都不等于 val。当左右指针遍历完输入数组以后,left的值就是输出数组的长度。

这样的算法在最坏情况下(输入数组中没有元素等于 val),左右指针各遍历了数组一次。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C++
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int n = nums.size();
int left = 0;
for (int right = 0; right < n; right++) {
if (nums[right] != val) {
nums[left] = nums[right];
left++;
}
}
return left;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Java
class Solution {
public int removeElement(int[] nums, int val) {
int n = nums.length;
int left = 0;
for (int right = 0; right < n; right++) {
if (nums[right] != val) {
nums[left] = nums[right];
left++;
}
}
return left;
}
}

复杂度分析

  • 时间复杂度:O(n),其中 n 为序列的长度。我们只需要遍历该序列至多两次。

  • 空间复杂度:O(1)。我们只需要常数的空间保存若干变量。

(2)双指针优化

思路及算法

如果要移除的元素恰好在数组的开头,例如序列 [1,2,3,4,5],当 val 为 1 时,我们需要把每一个元素都左移一位。注意到题目中说:「元素的顺序可以改变」。实际上我们可以直接将最后一个元素 5 移动到序列开头,取代元素 1,得到序列 [5,2,3,4],同样满足题目要求。这个优化在序列中 val 元素的数量较少时非常有效。

实现方面,我们依然使用双指针,两个指针初始时分别位于数组的首尾,向中间移动遍历该序列。

如果左指针 left 指向的元素等于 val,此时将右指针 right 指向的元素复制到左指针 left 的位置,然后右指针 right 左移一位。如果赋值过来的元素恰好也等于 val,可以继续把右指针 right 指向的元素的值赋值过来(左指针 left 指向的等于 val 的元素的位置继续被覆盖),直到左指针指向的元素的值不等于 val 为止。

当左指针 left 和右指针 right 重合的时候,左右指针遍历完数组中所有的元素。

这样的方法两个指针在最坏的情况下合起来只遍历了数组一次。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C++
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int left = 0, right = nums.size();
while (left < right) {
if (nums[left] == val) {
nums[left] = nums[right - 1];
right--;
} else {
left++;
}
}
return left;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Java
class Solution {
public int removeElement(int[] nums, int val) {
int left = 0;
int right = nums.length;
while (left < right) {
if (nums[left] == val) {
nums[left] = nums[right - 1];
right--;
} else {
left++;
}
}
return left;
}
}

复杂度分析

  • 时间复杂度:O(n),其中 n 为序列的长度。我们只需要遍历该序列至多一次。

  • 空间复杂度:O(1)。我们只需要常数的空间保存若干变量。

快乐数

1.题目内容

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

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

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

示例 1:

1
2
3
4
5
6
7
输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

示例 2:

1
2
输入:n = 2
输出:false

提示:

  • 1 <= n <= 231 - 1

2.解法

(1)哈希集合检测

思路及算法

我们可以先举几个例子。我们从 7 开始。则下一个数字是 49(因为 7^2^=49),然后下一个数字是 97(因为 4^2^+9^2^=97)。我们可以不断重复该的过程,直到我们得到 1。因为我们得到了 1,我们知道 7 是一个快乐数,函数应该返回 true。

fig1

再举一个例子,让我们从 116 开始。通过反复通过平方和计算下一个数字,我们最终得到 58,再继续计算之后,我们又回到 58。由于我们回到了一个已经计算过的数字,可以知道有一个循环,因此不可能达到 1。所以对于 116,函数应该返回 false。

fig2

根据我们的探索,我们猜测会有以下三种可能。

  1. 最终会得到 111。
  2. 最终会进入循环。
  3. 值会越来越大,最后接近无穷大。

第三个情况比较难以检测和处理。我们怎么知道它会继续变大,而不是最终得到 111 呢?我们可以仔细想一想,每一位数的最大数字的下一位数是多少。

Digits Largest Next
1 9 81
2 99 162
3 999 243
4 9999 324
13 9999999999999 1053

对于 333 位数的数字,它不可能大于 243。这意味着它要么被困在 243以下的循环内,要么跌到 1。4 位或 4 位以上的数字在每一步都会丢失一位,直到降到 3 位为止。所以我们知道,最坏的情况下,算法可能会在 243 以下的所有数字上循环,然后回到它已经到过的一个循环或者回到 1。但它不会无限期地进行下去,所以我们排除第三种选择。

即使在代码中你不需要处理第三种情况,你仍然需要理解为什么它永远不会发生,这样你就可以证明为什么你不处理它。

算法分为两部分,我们需要设计和编写代码。

  1. 给一个数字 n,它的下一个数字是什么?
  2. 按照一系列的数字来判断我们是否进入了一个循环。

第 1 部分我们按照题目的要求做数位分离,求平方和。

第 2 部分可以使用哈希集合完成。每次生成链中的下一个数字时,我们都会检查它是否已经在哈希集合中。

  • 如果它不在哈希集合中,我们应该添加它。

  • 如果它在哈希集合中,这意味着我们处于一个循环中,因此应该返回 false。

    我们使用哈希集合而不是向量、列表或数组的原因是因为我们反复检查其中是否存在某数字。检查数字是否在哈希集合中需要 O(1)O(1)O(1) 的时间,而对于其他数据结构,则需要 O(n)O(n)O(n) 的时间。选择正确的数据结构是解决这些问题的关键部分。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //Java
    class Solution {
    private int getNext(int n) {
    int totalSum = 0;
    while (n > 0) {
    int d = n % 10;
    n = n / 10;
    totalSum += d * d;
    }
    return totalSum;
    }

    public boolean isHappy(int n) {
    Set<Integer> seen = new HashSet<>();
    while (n != 1 && !seen.contains(n)) {
    seen.add(n);
    n = getNext(n);
    }
    return n == 1;
    }
    }

复杂度分析

确定这个问题的时间复杂度对于一个「简单」级别的问题来说是一个挑战。如果您对这些问题还不熟悉,可以尝试只计算 getNext(n) 函数的时间复杂度。

  • 时间复杂度:O(243⋅3+log⁡n+log⁡log⁡n+log⁡log⁡log⁡n)…= O(log⁡n)。

    • 查找给定数字的下一个值的成本为 O(log⁡n),因为我们正在处理数字中的每位数字,而数字中的位数由 log⁡n 给定。
      要计算出总的时间复杂度,我们需要仔细考虑循环中有多少个数字,它们有多大。
    • 我们在上面确定,一旦一个数字低于 243,它就不可能回到 243 以上。因此,我们就可以用 243 以下最长循环的长度来代替 243,不过,因为常数无论如何都无关紧要,所以我们不会担心它。
    • 对于高于 243 的 n,我们需要考虑循环中每个数高于 243 的成本。通过数学运算,我们可以证明在最坏的情况下,这些成本将是 O(log⁡n)+O(log⁡log⁡n)+O(log⁡log⁡log⁡n)…。幸运的是,O(log⁡n) 是占主导地位的部分,而其他部分相比之下都很小(总的来说,它们的总和小于log⁡n),所以我们可以忽略它们。
  • 空间复杂度:O(log⁡n)。与时间复杂度密切相关的是衡量我们放入哈希集合中的数字以及它们有多大的指标。对于足够大的 n,大部分空间将由 n 本身占用。我们可以很容易地优化到 O(243⋅3)=O(1),方法是只保存集合中小于 243 的数字,因为对于较高的数字,无论如何都不可能返回到它们。

(2)快慢指针法

思路及算法

通过反复调用 getNext(n) 得到的链是一个隐式的链表。隐式意味着我们没有实际的链表节点和指针,但数据仍然形成链表结构。起始数字是链表的头 “节点”,链中的所有其他数字都是节点。next 指针是通过调用 getNext(n) 函数获得。

意识到我们实际有个链表,那么这个问题就可以转换为检测一个链表是否有环。因此我们在这里可以使用弗洛伊德循环查找算法。这个算法是两个奔跑选手,一个跑的快,一个跑得慢。在龟兔赛跑的寓言中,跑的慢的称为 “乌龟”,跑得快的称为 “兔子”。

不管乌龟和兔子在循环中从哪里开始,它们最终都会相遇。这是因为兔子每走一步就向乌龟靠近一个节点(在它们的移动方向上)。

代码

我们不是只跟踪链表中的一个值,而是跟踪两个值,称为快跑者和慢跑者。

在算法的每一步中,慢速在链表中前进 1 个节点,快跑者前进 2 个节点(对 getNext(n) 函数的嵌套调用)。

如果 n 是一个快乐数,即没有循环,那么快跑者最终会比慢跑者先到达数字 1。

如果 n 不是一个快乐的数字,那么最终快跑者和慢跑者将在同一个数字上相遇。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Java
def isHappy(self, n: int) -> bool:
def get_next(number):
total_sum = 0
while number > 0:
number, digit = divmod(number, 10)
total_sum += digit ** 2
return total_sum

slow_runner = n
fast_runner = get_next(n)
while fast_runner != 1 and slow_runner != fast_runner:
slow_runner = get_next(slow_runner)
fast_runner = get_next(get_next(fast_runner))
return fast_runner == 1

复杂度分析

  • 时间复杂度:O(log⁡n)。该分析建立在对前一种方法的分析的基础上,但是这次我们需要跟踪两个指针而不是一个指针来分析,以及在它们相遇前需要绕着这个循环走多少次。
    • 如果没有循环,那么快跑者将先到达 1,慢跑者将到达链表中的一半。我们知道最坏的情况下,成本是 O(2⋅log⁡n)=O(log⁡n)。
    • 一旦两个指针都在循环中,在每个循环中,快跑者将离慢跑者更近一步。一旦快跑者落后慢跑者一步,他们就会在下一步相遇。假设循环中有 k 个数字。如果他们的起点是相隔 k−1 的位置(这是他们可以开始的最远的距离),那么快跑者需要 k−1 步才能到达慢跑者,这对于我们的目的来说也是不变的。因此,主操作仍然在计算起始 n 的下一个值,即 O(log⁡n)。
  • 空间复杂度:O(1),对于这种方法,我们不需要哈希集来检测循环。指针需要常数的额外空间。

(3)数学

思路及算法

根据我们之前的分析,我们知道它必须低于 243。因此,我们知道任何循环都必须包含小于 243 的数字,用这么小的数字,编写一个能找到所有周期的强力程序并不困难。

如果这样做,您会发现只有一个循环:4→16→37→58→89→145→42→20→4。所有其他数字都在进入这个循环的链上,或者在进入 111 的链上。

因此,我们可以硬编码一个包含这些数字的散列集,如果我们达到其中一个数字,那么我们就知道在循环中。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Java
class Solution {

private static Set<Integer> cycleMembers =
new HashSet<>(Arrays.asList(4, 16, 37, 58, 89, 145, 42, 20));

public int getNext(int n) {
int totalSum = 0;
while (n > 0) {
int d = n % 10;
n = n / 10;
totalSum += d * d;
}
return totalSum;
}


public boolean isHappy(int n) {
while (n != 1 && !cycleMembers.contains(n)) {
n = getNext(n);
}
return n == 1;
}
}

复杂度分析

  • 时间复杂度:O(log⁡n)。
  • 空间复杂度:O(1),我们没有保留我们所遇到的数字的历史记录。硬编码哈希集的大小是固定的。

单词规律

1.题目内容

给定一种规律 pattern 和一个字符串 s ,判断 s 是否遵循相同的规律。

这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 s 中的每个非空单词之间存在着双向连接的对应规律。

示例1:

1
2
输入: pattern = "abba", s = "dog cat cat dog"
输出: true

示例 2:

1
2
输入:pattern = "abba", s = "dog cat cat fish"
输出: false

示例 3:

1
2
输入: pattern = "aaaa", s = "dog cat cat dog"
输出: false

提示:

  • 1 <= pattern.length <= 300
  • pattern 只包含小写英文字母
  • 1 <= s.length <= 3000
  • s 只包含小写英文字母和 ' '
  • s 不包含 任何前导或尾随对空格
  • s 中每个单词都被 单个空格 分隔

2.解法

哈希表

思路及解法

在本题中,我们需要判断字符与字符串之间是否恰好一一对应。即任意一个字符都对应着唯一的字符串,任意一个字符串也只被唯一的一个字符对应。在集合论中,这种关系被称为「双射」。

想要解决本题,我们可以利用哈希表记录每一个字符对应的字符串,以及每一个字符串对应的字符。然后我们枚举每一对字符与字符串的配对过程,不断更新哈希表,如果发生了冲突,则说明给定的输入不满足双射关系。

在实际代码中,我们枚举 pattern\textit{pattern}pattern 中的每一个字符,利用双指针来均摊线性地找到该字符在 str\textit{str}str 中对应的字符串。每次确定一个字符与字符串的组合,我们就检查是否出现冲突,最后我们再检查两字符串是否比较完毕即可。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//C++
class Solution {
public:
bool wordPattern(string pattern, string str) {
unordered_map<string, char> str2ch;
unordered_map<char, string> ch2str;
int m = str.length();
int i = 0;
for (auto ch : pattern) {
if (i >= m) {
return false;
}
int j = i;
while (j < m && str[j] != ' ') j++;
const string &tmp = str.substr(i, j - i);
if (str2ch.count(tmp) && str2ch[tmp] != ch) {
return false;
}
if (ch2str.count(ch) && ch2str[ch] != tmp) {
return false;
}
str2ch[tmp] = ch;
ch2str[ch] = tmp;
i = j + 1;
}
return i >= m;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//Java
class Solution {
public boolean wordPattern(String pattern, String str) {
Map<String, Character> str2ch = new HashMap<String, Character>();
Map<Character, String> ch2str = new HashMap<Character, String>();
int m = str.length();
int i = 0;
for (int p = 0; p < pattern.length(); ++p) {
char ch = pattern.charAt(p);
if (i >= m) {
return false;
}
int j = i;
while (j < m && str.charAt(j) != ' ') {
j++;
}
String tmp = str.substring(i, j);
if (str2ch.containsKey(tmp) && str2ch.get(tmp) != ch) {
return false;
}
if (ch2str.containsKey(ch) && !tmp.equals(ch2str.get(ch))) {
return false;
}
str2ch.put(tmp, ch);
ch2str.put(ch, tmp);
i = j + 1;
}
return i >= m;
}
}

复杂度分析

  • 时间复杂度:O(n+m),其中 n 为 pattern 的长度,m 为 str 的长度。插入和查询哈希表的均摊时间复杂度均为 O(n+m)。每一个字符至多只被遍历一次。

  • 空间复杂度:O(n+m),其中 n 为 pattern 的长度,m 为 str 的长度。最坏情况下,我们需要存储 pattern 中的每一个字符和 str 中的每一个字符串。

存在重复元素 II

1.题目内容

给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 ij ,满足 nums[i] == nums[j]abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false

示例 1:

1
2
输入:nums = [1,2,3,1], k = 3
输出:true

示例 2:

1
2
输入:nums = [1,0,1,1], k = 1
输出:true

示例 3:

1
2
输入:nums = [1,2,3,1,2,3], k = 2
输出:false

提示:

  • 1 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • 0 <= k <= 105

2.解法

(1)哈希表

思路及算法

从左到右遍历数组 nums,当遍历到下标 i 时,如果存在下标 j<i 使得 nums[i]=nums[j],则当 i−j≤k 时即找到了两个符合要求的下标 j 和 i。

如果在下标 i 之前存在多个元素都和 nums[i] 相等,为了判断是否存在满足 nums[i]=nums[j] 且 i−j≤k 的下标 j,应该在这些元素中寻找下标最大的元素,将最大下标记为 j,判断 i−j≤k 是否成立。

如果 i−j≤k,则找到了两个符合要求的下标 j 和 i;如果 i−j>k,则在下标 i 之前不存在任何元素满足与 nums[i] 相等且下标差的绝对值不超过 k,理由如下。

假设存在下标 j′ 满足 j′<j<i 且 nums[j′]=nums[j]=nums[i],则 i−j′>i−j 可以使用哈希表记录每个元素的最大下标。从左到右遍历数组 nums,当遍历到下标 i 时,进行如下操作:

  1. 如果哈希表中已经存在和 nums[i] 相等的元素且该元素在哈希表中记录的下标 j 满足 i−j≤k,返回 true;

  2. 将 nums[i] 和下标 iii 存入哈希表,此时 iii 是 nums[i] 的最大下标。

上述两步操作的顺序不能改变,因为当遍历到下标 i 时,只能在下标 i 之前的元素中寻找与当前元素相等的元素及该元素的最大下标。

当遍历结束时,如果没有遇到两个相等元素的下标差的绝对值不超过 k,返回 false。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//C++
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_map<int, int> dictionary;
int length = nums.size();
for (int i = 0; i < length; i++) {
int num = nums[i];
if (dictionary.count(num) && i - dictionary[num] <= k) {
return true;
}
dictionary[num] = i;
}
return false;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Java
class Solution {
public boolean containsNearbyDuplicate(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
int length = nums.length;
for (int i = 0; i < length; i++) {
int num = nums[i];
if (map.containsKey(num) && i - map.get(num) <= k) {
return true;
}
map.put(num, i);
}
return false;
}
}

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。需要遍历数组一次,对于每个元素,哈希表的操作时间都是 O(1)。

  • 空间复杂度:O(n),其中 n 是数组 nums 的长度。需要使用哈希表记录每个元素的最大下标,哈希表中的元素个数不会超过 n。

(2)滑动窗口

思路及算法

考虑数组 nums 中的每个长度不超过 k+1 的滑动窗口,同一个滑动窗口中的任意两个下标差的绝对值不超过 k。如果存在一个滑动窗口,其中有重复元素,则存在两个不同的下标 i 和 j 满足 nums[i]=nums[j] 且 ∣i−j∣≤k。如果所有滑动窗口中都没有重复元素,则不存在符合要求的下标。因此,只要遍历每个滑动窗口,判断滑动窗口中是否有重复元素即可。

如果一个滑动窗口的结束下标是 i,则该滑动窗口的开始下标是 max⁡(0,i−k)。可以使用哈希集合存储滑动窗口中的元素。从左到右遍历数组 nums,当遍历到下标 i 时,具体操作如下:

  1. 如果 i>k,则下标 i−k−1 处的元素被移出滑动窗口,因此将 nums[i−k−1] 从哈希集合中删除;

  2. 判断 nums[i] 是否在哈希集合中,如果在哈希集合中则在同一个滑动窗口中有重复元素,返回 true,如果不在哈希集合中则将其加入哈希集合。

当遍历结束时,如果所有滑动窗口中都没有重复元素,返回 false。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//C++
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_set<int> s;
int length = nums.size();
for (int i = 0; i < length; i++) {
if (i > k) {
s.erase(nums[i - k - 1]);
}
if (s.count(nums[i])) {
return true;
}
s.emplace(nums[i]);
}
return false;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Java
class Solution {
public boolean containsNearbyDuplicate(int[] nums, int k) {
Set<Integer> set = new HashSet<Integer>();
int length = nums.length;
for (int i = 0; i < length; i++) {
if (i > k) {
set.remove(nums[i - k - 1]);
}
if (!set.add(nums[i])) {
return true;
}
}
return false;
}
}

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums的长度。需要遍历数组一次,对于每个元素,哈希集合的操作时间都是 O(1)。

  • 空间复杂度:O(k),其中 k 是判断重复元素时允许的下标差的绝对值的最大值。需要使用哈希集合存储滑动窗口中的元素,任意时刻滑动窗口中的元素个数最多为 k+1 个。

同构字符串

1.题目内容

给定两个字符串 st ,判断它们是否是同构的。

如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

示例 1:

1
2
输入:s = "egg", t = "add"
输出:true

示例 2:

1
2
输入:s = "foo", t = "bar"
输出:false

示例 3:

1
2
输入:s = "paper", t = "title"
输出:true

提示:

  • 1 <= s.length <= 5 * 104
  • t.length == s.length
  • st 由任意有效的 ASCII 字符组成

2.解法

哈希表

思路及算法

此题是需要我们判断 s 和 t 每个位置上的字符是否都一一对应,即 s 的任意一个字符被 t 中唯一的字符对应,同时 t 的任意一个字符被 s 中唯一的字符对应。这也被称为「双射」的关系。

以示例 2 为例,t 中的字符 a 和 r 虽然有唯一的映射 o,但对于 s 中的字符 o 来说其存在两个映射 {a,r},故不满足条件。

因此,我们维护两张哈希表,第一张哈希表 s2t 以 s 中字符为键,映射至 t 的字符为值,第二张哈希表 t2s 以 t 中字符为键,映射至 s 的字符为值。从左至右遍历两个字符串的字符,不断更新两张哈希表,如果出现冲突(即当前下标 index 对应的字符 s[index] 已经存在映射且不为 t[index] 或当前下标 index 对应的字符 t[index] 已经存在映射且不为 s[index])时说明两个字符串无法构成同构,返回 false。

如果遍历结束没有出现冲突,则表明两个字符串是同构的,返回 true 即可。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//C++
class Solution {
public:
bool isIsomorphic(string s, string t) {
unordered_map<char, char> s2t;
unordered_map<char, char> t2s;
int len = s.length();
for (int i = 0; i < len; ++i) {
char x = s[i], y = t[i];
if ((s2t.count(x) && s2t[x] != y) || (t2s.count(y) && t2s[y] != x)) {
return false;
}
s2t[x] = y;
t2s[y] = x;
}
return true;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Java
class Solution {
public boolean isIsomorphic(String s, String t) {
Map<Character, Character> s2t = new HashMap<Character, Character>();
Map<Character, Character> t2s = new HashMap<Character, Character>();
int len = s.length();
for (int i = 0; i < len; ++i) {
char x = s.charAt(i), y = t.charAt(i);
if ((s2t.containsKey(x) && s2t.get(x) != y) || (t2s.containsKey(y) && t2s.get(y) != x)) {
return false;
}
s2t.put(x, y);
t2s.put(y, x);
}
return true;
}
}

复杂度分析

  • 时间复杂度:O(n),其中 n 为字符串的长度。我们只需同时遍历一遍字符串 s 和 t 即可。
  • 空间复杂度:O(∣Σ∣),其中 Σ 是字符串的字符集。哈希表存储字符的空间取决于字符串的字符集大小,最坏情况下每个字符均不相同,需要 O(∣Σ∣) 的空间。

有效的字母异位词

1.题目内容

给定两个字符串 st ,编写一个函数来判断 t 是否是 s 的字母异位词。

注意:st 中每个字符出现的次数都相同,则称 st 互为字母异位词。

示例 1:

1
2
输入: s = "anagram", t = "nagaram"
输出: true

示例 2:

1
2
输入: s = "rat", t = "car"
输出: false

提示:

  • 1 <= s.length, t.length <= 5 * 104
  • st 仅包含小写字母

2.解法

(1)排序

思路及算法

t 是 s 的异位词等价于「两个字符串排序后相等」。因此我们可以对字符串 s 和 t 分别排序,看排序后的字符串是否相等即可判断。此外,如果 s 和 t 的长度不同,t 必然不是 s 的异位词。

代码

1
2
3
4
5
6
7
8
9
10
11
12
//C++
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.length() != t.length()) {
return false;
}
sort(s.begin(), s.end());
sort(t.begin(), t.end());
return s == t;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
//Java
class Solution {
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
char[] str1 = s.toCharArray();
char[] str2 = t.toCharArray();
Arrays.sort(str1);
Arrays.sort(str2);
return Arrays.equals(str1, str2);
}
}

复杂度分析

  • 时间复杂度:O(nlog⁡n),其中 n 为 s 的长度。排序的时间复杂度为 O(nlog⁡n),比较两个字符串是否相等时间复杂度为 O(n),因此总体时间复杂度为 O(nlog⁡n+n)=O(nlog⁡n)。

  • 空间复杂度:O(log⁡n)。排序需要 O(log⁡n) 的空间复杂度。注意,在某些语言(比如 Java & JavaScript)中字符串是不可变的,因此我们需要额外的 O(n) 的空间来拷贝字符串。但是我们忽略这一复杂度分析,因为:

    • 这依赖于语言的细节;
    • 这取决于函数的设计方式,例如,可以将函数参数类型更改为 char[]。

(2)哈希表

思路及算法

从另一个角度考虑,t 是 s 的异位词等价于「两个字符串中字符出现的种类和次数均相等」。由于字符串只包含 26 个小写字母,因此我们可以维护一个长度为 26的频次数组 table,先遍历记录字符串 s 中字符出现的频次,然后遍历字符串 t,减去 table 中对应的频次,如果出现 table[i]<0,则说明 t 包含一个不在 s 中的额外字符,返回 false 即可。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//C++
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.length() != t.length()) {
return false;
}
vector<int> table(26, 0);
for (auto& ch: s) {
table[ch - 'a']++;
}
for (auto& ch: t) {
table[ch - 'a']--;
if (table[ch - 'a'] < 0) {
return false;
}
}
return true;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Java
class Solution {
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;
}
}

复杂度分析

  • 时间复杂度:O(n),其中 n 为 s 的长度。

  • 空间复杂度:O(S),其中 S 为字符集大小,此处 S=26。

赎金信

1.题目内容

给你两个字符串:ransomNotemagazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。

示例 1:

1
2
输入:ransomNote = "a", magazine = "b"
输出:false

示例 2:

1
2
输入:ransomNote = "aa", magazine = "ab"
输出:false

示例 3:

1
2
输入:ransomNote = "aa", magazine = "aab"
输出:true

提示:

  • 1 <= ransomNote.length, magazine.length <= 105
  • ransomNotemagazine 由小写英文字母组成

2.解法

字符统计法

思路及算法

题目要求使用字符串magazine中的字符来构建新的字符串ransomNote,且ransomNote中的每个字符只能使用一次,只需要满足字符串magazine中的每个英文字母(’a’-’z’)的统计次数都大于等于ransomNote中相同字母的统计次数即可。

  • 如果字符串magazine的长度小于字符串ransomNote的长度,则我们可以肯定magazine无法构成ransomNote,此时直接返回 false
  • 首先统计magazine中每个英文字母a的次数cnt[a],再遍历统计ransomNote中每个英文字母的次数,如果发现ransomNote中存在某个英文字母c的统计次数大于magazine中该字母统计次数cnt[c],则此时我们直接返回false

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//C++
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
if (ransomNote.size() > magazine.size()) {
return false;
}
vector<int> cnt(26);
for (auto & c : magazine) {
cnt[c - 'a']++;
}
for (auto & c : ransomNote) {
cnt[c - 'a']--;
if (cnt[c - 'a'] < 0) {
return false;
}
}
return true;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Java
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
if (ransomNote.length() > magazine.length()) {
return false;
}
int[] cnt = new int[26];
for (char c : magazine.toCharArray()) {
cnt[c - 'a']++;
}
for (char c : ransomNote.toCharArray()) {
cnt[c - 'a']--;
if(cnt[c - 'a'] < 0) {
return false;
}
}
return true;
}
}

复杂度分析

  • 时间复杂度:O(m+n),其中m是字符串 ransomNote 的长度,n 是字符串 magazine 的长度,我们只需要遍历两个字符一次即可。

  • 空间复杂度:O(∣S∣),S 是字符集,这道题中S为全部小写英语字母,因此 ∣S∣=26

最长连续序列

1.题目内容

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

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

示例 1:

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

示例 2:

1
2
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109

2.解法

哈希表解法

思路及算法

我们考虑枚举数组中的每个数 x,考虑以其为起点,不断尝试匹配 x+1,x+2,⋯ 是否存在,假设最长匹配到了 x+y,那么以 x为起点的最长连续序列即为x,x+1,x+2,⋯ ,x+y,其长度为 y+1,我们不断枚举并更新答案即可。

对于匹配的过程,暴力的方法是 O(n)遍历数组去看是否存在这个数,但其实更高效的方法是用一个哈希表存储数组中的数,这样查看一个数是否存在即能优化至 O(1)的时间复杂度。

仅仅是这样我们的算法时间复杂度最坏情况下还是会达到 O(n^2^)(即外层需要枚举 O(n)个数,内层需要暴力匹配 O(n)次),无法满足题目的要求。但仔细分析这个过程,我们会发现其中执行了很多不必要的枚举,如果已知有一个 x,x+1,x+2,⋯ ,x+y的连续序列,而我们却重新从 x+1,x+2或者是 x+y处开始尝试匹配,那么得到的结果肯定不会优于枚举 x 为起点的答案,因此我们在外层循环的时候碰到这种情况跳过即可。

那么怎么判断是否跳过呢?由于我们要枚举的数 x一定是在数组中不存在前驱数 x−1 的,不然按照上面的分析我们会从 x−1 开始尝试匹配,因此我们每次在哈希表中检查是否存在 x−1即能判断是否需要跳过了。

增加了判断跳过的逻辑之后,时间复杂度是多少呢?外层循环需要 O(n) 的时间复杂度,只有当一个数是连续序列的第一个数的情况下才会进入内层循环,然后在内层循环中匹配连续序列中的数,因此数组中的每个数只会进入内层循环一次。根据上述分析可知,总时间复杂度为 O(n),符合题目要求。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//C++
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> num_set;
for (const int& num : nums) {
num_set.insert(num);
}

int longestStreak = 0;

for (const int& num : num_set) {
if (!num_set.count(num - 1)) {
int currentNum = num;
int currentStreak = 1;

while (num_set.count(currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}

longestStreak = max(longestStreak, currentStreak);
}
}

return longestStreak;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Java
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> num_set = new HashSet<Integer>();
for (int num : nums) {
num_set.add(num);
}

int longestStreak = 0;

for (int num : num_set) {
if (!num_set.contains(num - 1)) {
int currentNum = num;
int currentStreak = 1;

while (num_set.contains(currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}

longestStreak = Math.max(longestStreak, currentStreak);
}
}

return longestStreak;
}
}

复杂度分析

  • 时间复杂度:O(n),其中 n为数组的长度。具体分析已在上面正文中给出。

  • 空间复杂度:O(n)。哈希表存储数组中所有的数需要 O(n) 的空间。

字母异位词分组

1.题目内容

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

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

示例 1:

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

示例 2:

1
2
输入: strs = [""]
输出: [[""]]

示例 3:

1
2
输入: strs = ["a"]
输出: [["a"]]

提示:

  • 1 <= strs.length <= 104
  • 0 <= strs[i].length <= 100
  • strs[i] 仅包含小写字母

2.解法

(1)排序

思路及算法

由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//C++
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp;
for (string& str: strs) {
string key = str;
sort(key.begin(), key.end());
mp[key].emplace_back(str);
}
vector<vector<string>> ans;
for (auto it = mp.begin(); it != mp.end(); ++it) {
ans.emplace_back(it->second);
}
return ans;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Java
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
char[] array = str.toCharArray();
Arrays.sort(array);
String key = new String(array);
List<String> list = map.getOrDefault(key, new ArrayList<String>());
list.add(str);
map.put(key, list);
}
return new ArrayList<List<String>>(map.values());
}
}

复杂度分析

  • 时间复杂度:O(nklog⁡k),其中 n 是 strs中的字符串的数量,k是 strs 中的字符串的的最大长度。需要遍历 n 个字符串,对于每个字符串,需要 O(klog⁡k)的时间进行排序以及 O(1)的时间更新哈希表,因此总时间复杂度是 O(nklog⁡k)。

  • 空间复杂度:O(nk),其中 n 是 strs中的字符串的数量,k是 strs 中的字符串的的最大长度。需要用哈希表存储全部字符串。

(2)计数

思路及算法

由于互为字母异位词的两个字符串包含的字母相同,因此两个字符串中的相同字母出现的次数一定是相同的,故可以将每个字母出现的次数使用字符串表示,作为哈希表的键。

由于字符串只包含小写字母,因此对于每个字符串,可以使用长度为 26 的数组记录每个字母出现的次数。需要注意的是,在使用数组作为哈希表的键时,不同语言的支持程度不同,因此不同语言的实现方式也不同。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//C++
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
// 自定义对 array<int, 26> 类型的哈希函数
auto arrayHash = [fn = hash<int>{}] (const array<int, 26>& arr) -> size_t {
return accumulate(arr.begin(), arr.end(), 0u, [&](size_t acc, int num) {
return (acc << 1) ^ fn(num);
});
};

unordered_map<array<int, 26>, vector<string>, decltype(arrayHash)> mp(0, arrayHash);
for (string& str: strs) {
array<int, 26> counts{};
int length = str.length();
for (int i = 0; i < length; ++i) {
counts[str[i] - 'a'] ++;
}
mp[counts].emplace_back(str);
}
vector<vector<string>> ans;
for (auto it = mp.begin(); it != mp.end(); ++it) {
ans.emplace_back(it->second);
}
return ans;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//Java
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
int[] counts = new int[26];
int length = str.length();
for (int i = 0; i < length; i++) {
counts[str.charAt(i) - 'a']++;
}
// 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串,作为哈希表的键
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 26; i++) {
if (counts[i] != 0) {
sb.append((char) ('a' + i));
sb.append(counts[i]);
}
}
String key = sb.toString();
List<String> list = map.getOrDefault(key, new ArrayList<String>());
list.add(str);
map.put(key, list);
}
return new ArrayList<List<String>>(map.values());
}
}

复杂度分析

  • 时间复杂度:O(n(k+∣Σ∣)),其中 n 是 strs 中的字符串的数量,k是 strs中的字符串的的最大长度,Σ是字符集,在本题中字符集为所有小写字,∣Σ∣=26。需要遍历 n个字符串,对于每个字符串,需要 O(k) 的时间计算每个字母出现的次数,O(∣Σ∣)的时间生成哈希表的键,以及 O(1)的时间更新哈希表,因此总时间复杂度是 O(n(k+∣Σ∣))。

  • 空间复杂度:O(n(k+∣Σ∣)),其中 n 是 strs 中的字符串的数量,k是 strs中的字符串的最大长度,Σ 是字符集,在本题中字符集为所有小写字母,∣Σ∣=26。需要用哈希表存储全部字符串,而记录每个字符串中每个字母出现次数的数组需要的空间为 O(∣Σ∣),在渐进意义下小于 O(n(k+∣Σ∣)),可以忽略不计。

合并两个有序数组

1.题目内容

请你 合并 nums2nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n

示例 1:

1
2
3
4
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。

示例 2:

1
2
3
4
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。

示例 3:

1
2
3
4
5
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

提示:

  • nums1.length == m + n
  • nums2.length == n
  • 0 <= m, n <= 200
  • 1 <= m + n <= 200
  • -109 <= nums1[i], nums2[j] <= 109

2.解法

(1)直接合并后排序

算法

最直观的方法是先将数组 nums2 放进数组 nums1 的尾部,然后直接对整个数组进行排序。

代码

1
2
3
4
5
6
7
8
9
10
//C++
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
for (int i = 0; i != n; ++i) {
nums1[m + i] = nums2[i];
}
sort(nums1.begin(), nums1.end());
}
};
1
2
3
4
5
6
7
8
9
//Java
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
for (int i = 0; i != n; ++i) {
nums1[m + i] = nums2[i];
}
Arrays.sort(nums1);
}
}

复杂度排序

  • 时间复杂度:O((m+n)log⁡(m+n))。
    排序序列长度为 m+n,套用快速排序的时间复杂度即可,平均情况为 O((m+n)log⁡(m+n))。

  • 空间复杂度:O(log⁡(m+n))。
    排序序列长度为 m+n,套用快速排序的空间复杂度即可,平均情况为 O(log⁡(m+n))。

(2)双指针

算法

这一方法将两个数组看作队列,每次从两个数组头部取出比较小的数字放到结果中。

代码

我们为两个数组分别设置一个指针 p1 与 p2 来作为队列的头部指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//C++
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int p1 = 0, p2 = 0;
int sorted[m + n];
int cur;
while (p1 < m || p2 < n) {
if (p1 == m) {
cur = nums2[p2++];
} else if (p2 == n) {
cur = nums1[p1++];
} else if (nums1[p1] < nums2[p2]) {
cur = nums1[p1++];
} else {
cur = nums2[p2++];
}
sorted[p1 + p2 - 1] = cur;
}
for (int i = 0; i != m + n; ++i) {
nums1[i] = sorted[i];
}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Java
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = 0, p2 = 0;
int[] sorted = new int[m + n];
int cur;
while (p1 < m || p2 < n) {
if (p1 == m) {
cur = nums2[p2++];
} else if (p2 == n) {
cur = nums1[p1++];
} else if (nums1[p1] < nums2[p2]) {
cur = nums1[p1++];
} else {
cur = nums2[p2++];
}
sorted[p1 + p2 - 1] = cur;
}
for (int i = 0; i != m + n; ++i) {
nums1[i] = sorted[i];
}
}
}

复杂度分析

  • 时间复杂度:O(m+n)。
    指针移动单调递增,最多移动 m+n次,因此时间复杂度为 O(m+n)。

  • 空间复杂度:O(m+n)。
    需要建立长度为 m+n 的中间数组sorted。

(3)逆向双指针

算法

上面的双指针算法中,之所以要使用临时变量,是因为如果直接合并到数组 nums1 中,nums1中的元素可能会在取出之前被覆盖。那么如何直接避免覆盖 nums1中的元素呢?观察可知,nums1 的后半部分是空的,可以直接覆盖而不会影响结果。因此可以指针设置为从后向前遍历,每次取两者之中的较大者放进 nums1的最后面。

严格来说,在此遍历过程中的任意一个时刻,nums1数组中有 m−p1−1 个元素被放入 nums1的后半部,nums2 数组中有 n−p2−1个元素被放入 nums1
的后半部,而在指针 p1的后面,nums1 数组有 m+n−p1−1个位置。由于m+n−p1−1≥m−p1−1+n−p2−1等价于p2≥−1永远成立,因此 p1后面的位置永远足够容纳被插入的元素,不会产生 p1 的元素被覆盖的情况。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//C++
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int p1 = m - 1, p2 = n - 1;
int tail = m + n - 1;
int cur;
while (p1 >= 0 || p2 >= 0) {
if (p1 == -1) {
cur = nums2[p2--];
} else if (p2 == -1) {
cur = nums1[p1--];
} else if (nums1[p1] > nums2[p2]) {
cur = nums1[p1--];
} else {
cur = nums2[p2--];
}
nums1[tail--] = cur;
}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Java
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = m - 1, p2 = n - 1;
int tail = m + n - 1;
int cur;
while (p1 >= 0 || p2 >= 0) {
if (p1 == -1) {
cur = nums2[p2--];
} else if (p2 == -1) {
cur = nums1[p1--];
} else if (nums1[p1] > nums2[p2]) {
cur = nums1[p1--];
} else {
cur = nums2[p2--];
}
nums1[tail--] = cur;
}
}
}

复杂度分析

  • 时间复杂度:O(m+n)。
    指针移动单调递减,最多移动 m+n次,因此时间复杂度为 O(m+n)。

  • 空间复杂度:O(1)。

    直接对数组 nums1原地修改,不需要额外空间。

0%