题目要求:
一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
数据范围:数组长度 2≤n≤10002≤n≤1000,数组中每个数的大小 0<val≤10000000<val≤1000000
要求:空间复杂度 O(1)O(1),时间复杂度 O(n)O(n)
提示:输出时按非降序排列。
思路分析
- 异或的性质:
任何数与自身的异或结果为 0,即 a ^ a = 0。
任何数与 0 的异或结果为其自身,即 a ^ 0 = a。
异或操作满足交换律和结合律。
- 步骤:
步骤一:对数组中的所有数字进行异或操作。由于数组中除了两个数字只出现一次,其他数字都出现了两次,所有成对的数字异或后会变成 0,最终的结果就是这两个只出现一次的数字的异或结果 xor = a ^ b。
步骤二:找到 xor 中任意一个为 1 的位。这个位表示 a 和 b 在这一位上不同。我们可以利用这个位将数组分成两组,一组在该位上为 0,另一组在该位上为 1。这样,a 和 b 会被分到不同的组中。
步骤三:分别对这两组进行异或操作,最终得到两个只出现一次的数字。
- 时间复杂度:
整个过程只需要遍历数组几次,因此时间复杂度为 O(n)。
- 空间复杂度:
只使用了常数级别的额外空间,因此空间复杂度为 O(1)。
Java 实现
java
import java.util.Arrays;
public class FindTwoUniqueNumbers {
public static int[] findTwoUniqueNumbers(int[] nums) {
// 第一步:找到两个唯一数字的异或结果
int xor = 0;
for (int num : nums) {
xor ^= num;
}
// 第二步:找到xor中任意一个为1的位
// 这里我们找到最右边的1
int diff = xor & (-xor);
// 第三步:根据diff将数组分成两组,分别异或得到两个唯一数字
int num1 = 0, num2 = 0;
for (int num : nums) {
if ((num & diff) == 0) {
num1 ^= num;
} else {
num2 ^= num;
}
}
// 返回结果时按非降序排列
if (num1 < num2) {
return new int[]{num1, num2};
} else {
return new int[]{num2, num1};
}
}
public static void main(String[] args) {
int[] nums = {4, 1, 2, 1, 2, 5};
int[] result = findTwoUniqueNumbers(nums);
System.out.println(Arrays.toString(result)); // 输出: [4, 5]
}
}
代码讲解
- 第一步:
我们初始化一个变量 xor 为 0,然后遍历数组,将所有数字进行异或操作。由于成对的数字会相互抵消,最终 xor 的值就是两个唯一数字的异或结果。
- 第二步:
我们需要找到一个位,这个位在 xor 中为 1,表示 a 和 b 在这一位上不同。使用 xor & (xor) 可以找到 xor 中最右边的 1 位。
- 第三步:
根据找到的 diff 位,将数组分成两组:
一组是该位为 0 的数字。
另一组是该位为 1 的数字。
分别对这两组进行异或操作,最终得到两个唯一数字 num1 和 num2。
- 排序:
最后,根据题目要求,将结果按非降序排列。如果 num1 小于 num2,直接返回 [num1, num2],否则返回 [num2, num1]。
示例
对于输入数组 {4, 1, 2, 1, 2, 5}:
第一步:xor = 4 ^ 1 ^ 2 ^ 1 ^ 2 ^ 5 = 4 ^ 5 = 1。
第二步:diff = 1 & (1) = 1。
第三步:
第一组(该位为 0):4,4 ^ 4 = 0。
第二组(该位为 1):1, 2, 1, 2, 5,1 ^ 2 ^ 1 ^ 2 ^ 5 = 5。
最终结果:[4, 5]。
题目要求:
给定一个长度为n的数组arr,返回arr的最长无重复元素子数组的长度,无重复指的是所有数字都不相同。
子数组是连续的,比如[1,3,5,7,9]的子数组有[1,3],[3,5,7]等等,但是[1,3,7]不是子数组
思路分析:
- 滑动窗口的概念:
我们可以使用两个指针 left 和 right 来表示当前窗口的左右边界。
right 指针向右移动,尝试扩展窗口。
如果 arr[right] 在当前窗口中已经存在,我们需要移动 left 指针,直到窗口中不再包含重复元素。
- 使用数据结构记录元素出现的位置:
我们可以使用一个 HashMap 来记录每个元素最后出现的位置。
这样,当遇到一个重复元素时,我们可以快速地将 left 指针移动到该重复元素上一次出现位置的下一个位置。
- 维护最大长度:
在遍历过程中,我们始终维护一个变量 maxLength 来记录当前找到的最长无重复子数组的长度。
具体步骤
- 初始化:
创建一个空的 HashMap 来记录元素及其最后出现的位置。
初始化两个指针 left 和 right 为 0,以及 maxLength 为 0。
- 遍历数组:
对于每个元素 arr[right],检查它是否已经存在于 HashMap 中。
如果存在,说明遇到了重复元素,将 left 指针移动到 HashMap 中记录的位置的下一个位置。
更新 HashMap 中 arr[right] 的位置为 right。
计算当前窗口的长度 right left + 1,并更新 maxLength。
- 返回结果:
遍历完成后,maxLength 就是最长的无重复子数组的长度。
Java 实现
java
import java.util.HashMap;
public class LongestSubarrayWithoutRepeats {
public static int lengthOfLongestSubarray(int[] arr) {
// HashMap 用于存储元素及其最后出现的位置
HashMap<Integer, Integer> map = new HashMap<>();
int maxLength = 0;
int left = 0; // 左指针
for (int right = 0; right < arr.length; right++) {
// 如果当前元素已经在 HashMap 中,并且其位置在当前窗口内
if (map.containsKey(arr[right]) && map.get(arr[right]) >= left) {
// 将左指针移动到重复元素的下一个位置
left = map.get(arr[right]) + 1;
}
// 更新元素的位置
map.put(arr[right], right);
// 更新最大长度
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
public static void main(String[] args) {
int[] arr = {1, 3, 5, 7, 9, 3, 5, 7, 11};
int result = lengthOfLongestSubarray(arr);
System.out.println("最长的无重复子数组的长度是: " + result); // 输出: 5
}
}
代码讲解
- HashMap 的使用:
我们使用 HashMap<Integer, Integer> 来记录每个元素最后出现的位置。
例如,map.put(arr[right], right); 会在每次遇到一个元素时更新其在数组中的最新位置。
- 左指针的移动:
当我们遇到一个重复元素时,我们需要将 left 指针移动到该重复元素上一次出现位置的下一个位置。
这可以通过 left = map.get(arr[right]) + 1; 实现。
- 最大长度的更新:
在每次循环中,我们计算当前窗口的长度 right left + 1,并将其与 maxLength 进行比较,更新 maxLength 以记录最长的无重复子数组的长度。
- 示例:
对于数组 {1, 3, 5, 7, 9, 3, 5, 7, 11},最长的无重复子数组是 {1, 3, 5, 7, 9},长度为 5。
时间和空间复杂度
时间复杂度:O(n),其中 n 是数组的长度。每个元素最多被访问两次(一次被 right 指针访问,一次被 left 指针访问)。
空间复杂度:O(min(m, n)),其中 m 是元素的值域大小,n 是数组的长度。在最坏的情况下,HashMap 需要存储所有唯一的元素。
题目要求
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
示例1:
输入:n=3
输出:4
说明:有四种走法
示例2:
输入:n=5
输出:13
思路分析
- 状态定义:
dp[i] 表示到达第 i 级台阶的方法数。
- 状态转移方程:
小孩一次可以上1阶、2阶或3阶,因此到达第 i 级台阶的方法数等于到达第 i1 级台阶的方法数加上到达第 i2 级台阶的方法数再加上到达第 i3 级台阶的方法数。
即:dp[i] = dp[i1] + dp[i2] + dp[i3]
- 初始化:
当 i = 0 时,只有一种方法,即不移动,所以 dp[0] = 1。
当 i = 1 时,只有一种方法,即上1阶,所以 dp[1] = 1。
当 i = 2 时,有两种方法,即上1阶+1阶或直接上2阶,所以 dp[2] = 2。
- 结果取模:
由于结果可能很大,我们需要对结果取模 1000000007。
Java 实现
java
public class ClimbingStairs {
public static int countWays(int n) {
// 如果 n 小于等于 0,返回 0
if (n <= 0) return 0;
// 如果 n 等于 1,返回 1
if (n == 1) return 1;
// 如果 n 等于 2,返回 2
if (n == 2) return 2;
// 初始化 dp 数组
int[] dp = new int[n + 1];
dp[0] = 1; // 不移动
dp[1] = 1; // 上1阶
dp[2] = 2; // 上1阶+1阶 或 直接上2阶
// 计算 dp[i]
for (int i = 3; i <= n; i++) {
dp[i] = (dp[i - 1] + dp[i - 2] + dp[i - 3]) % 1000000007;
}
return dp[n];
}
public static void main(String[] args) {
int n = 5;
int result = countWays(n);
System.out.println("小孩有 " + result + " 种上楼的方法");
}
}
代码讲解
- 初始化:
dp[0] = 1:表示不移动的方法数为1。
dp[1] = 1:表示上1阶的方法数为1。
dp[2] = 2:表示上1阶+1阶 或 直接上2阶的方法数为2。
- 动态规划计算:
从 i = 3 开始,利用状态转移方程 dp[i] = dp[i1] + dp[i2] + dp[i3] 计算 dp[i]。
对结果取模 1000000007,以防止结果过大。
- 输出结果:
返回 dp[n],即到达第 n 级台阶的方法数。
示例
当 n = 3 时,dp[3] = dp[2] + dp[1] + dp[0] = 2 + 1 + 1 = 4,输出4。
当 n = 5 时,dp[5] = dp[4] + dp[3] + dp[2] = 7 + 4 + 2 = 13,输出13。