文章目录
前言
本文记录力扣Hot100里面关于"技巧"的五道题,包括常见解法和一些关键步骤理解,也有例子便于大家理解
一、只出现一次的数字
1.题目
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
2.代码
java
class Solution {
public int singleNumber(int[] nums) {
// 初始化结果变量为0,因为0和任何数异或都等于这个数本身
int single = 0;
// 增强for循环,遍历数组中的每一个数字
for (int num : nums) {
// 核心操作:将当前结果与数组元素做异或运算(^= 是异或赋值运算符)
single ^= num;
}
// 遍历结束后,single就是唯一出现一次的数字
return single;
}
}
3.理解
异或运算是二进制位运算,规则是:对应位相同则为0,不同则为1。比如 5 ^ 3:
- 5 的二进制:101
- 3 的二进制:011
- 异或结果:110(即十进制6)
而代码能生效,全靠这3个关键特性:
- 0 异或任何数 = 任何数本身:a ^ 0 = a(比如 3 ^ 0 = 3)
- 任何数异或自身 = 0:a ^ a = 0(比如 5 ^ 5 = 0)
- 异或满足交换律和结合律:a ^ b ^ c = a ^ c ^ b = (a ^ a)
4.例子
例子:nums = [4,1,2,1,2]
代码执行流程:
- 初始化
single = 0; - 第一次遍历到数字4:
single = 0 ^ 4 = 4(特性1),single变为4;
- 第二次遍历到数字1:
single = 4 ^ 1 = 5(暂时保留,因为1还没遇到第二个),single变为5;
- 第三次遍历到数字2:
single = 5 ^ 2 = 7(继续保留,2也没遇到第二个),single变为7;
- 第四次遍历到数字1:
single = 7 ^ 1 = 6(此时1遇到第二个,抵消了之前的1,相当于4 ^ 2),single变为6;
- 第五次遍历到数字2:
single = 6 ^ 2 = 4(此时2遇到第二个,抵消了之前的2,只剩4),single变为4;
- 遍历结束,返回
single的值4------这就是数组中只出现一次的数字。
简化理解(用交换律/结合律):
其实不用逐步算,直接把重复的数配对抵消:
4 ^ 1 ^ 2 ^ 1 ^ 2 = 4 ^ (1 ^ 1) ^ (2 ^ 2) = 4 ^ 0 ^ 0 = 4,结果完全一致。
二、多数元素
1.题目
给定一个大小为 n 的数组 nums ,返回其中的多数元素 。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的 ,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
2.代码
java
class Solution {
public int majorityElement(int[] nums) {
// 初始化计数器,用来记录当前候选元素的"票数"
int count = 0;
// 初始化结果变量,用来保存当前的候选元素
int res = 0;
// 遍历数组中的每一个数字
for(int num : nums){
// 条件1:如果当前票数为0,说明之前的候选元素被抵消完了,更换候选为当前数字
if(count == 0){
res = num;
}
// 条件2:如果当前数字等于候选元素,票数+1;否则票数-1
if(num == res){
count++;
}else{
count--;
}
}
// 遍历结束后,res就是出现次数超过一半的多数元素
return res;
}
}
3.例子
例子:nums = [3,3,4,2,4,4,2,4,4]
初始化:count = 0,res = 0
第1轮:遍历数字3
- 条件1:
count == 0→ 把候选res更新为3; - 条件2:当前数字3 == 候选3 →
count从0 → 1; - 此时状态:
res=3,count=1。
第2轮:遍历数字3
- 条件1:
count≠0→ 不更换候选; - 条件2:当前数字3 == 候选3 →
count从1 → 2; - 此时状态:
res=3,count=2。
第3轮:遍历数字4
- 条件1:
count≠0→ 不更换候选; - 条件2:当前数字4 ≠ 候选3 →
count从2 → 1; - 此时状态:
res=3,count=1。
第4轮:遍历数字2
- 条件1:
count≠0→ 不更换候选; - 条件2:当前数字2 ≠ 候选3 →
count从1 → 0; - 此时状态:
res=3,count=0。
第5轮:遍历数字4
- 条件1:
count == 0→ 把候选res更新为4; - 条件2:当前数字4 == 候选4 →
count从0 → 1; - 此时状态:
res=4,count=1。
第6轮:遍历数字4
- 条件1:
count≠0→ 不更换候选; - 条件2:当前数字4 == 候选4 →
count从1 → 2; - 此时状态:
res=4,count=2。
第7轮:遍历数字2
- 条件1:
count≠0→ 不更换候选; - 条件2:当前数字2 ≠ 候选4 →
count从2 → 1; - 此时状态:
res=4,count=1。
第8轮:遍历数字4
- 条件1:
count≠0→ 不更换候选; - 条件2:当前数字4 == 候选4 →
count从1 → 2; - 此时状态:
res=4,count=2。
第9轮:遍历数字4
- 条件1:
count≠0→ 不更换候选; - 条件2:当前数字4 == 候选4 →
count从2 → 3; - 此时状态:
res=4,count=3。
遍历结束
返回res=4,正是数组中出现次数超过一半的多数元素。
三、颜色分类
1.题目
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
2.代码
java
class Solution {
public void sortColors(int[] nums) {
// 获取数组长度
int n = nums.length;
// p0:0的右边界(下一个0要放的位置),初始在0;p2:2的左边界(下一个2要放的位置),初始在最后一位
int p0 = 0, p2 = n - 1;
// i是遍历指针,注意终止条件是i <= p2(p2左边的元素还没处理完,p2右边全是2)
for (int i = 0; i <= p2; ++i) {
// 如果当前元素是2,就交换到p2位置,同时p2左移
// 用while而非if:交换过来的元素可能还是2,需要继续交换直到当前i位置不是2
while (i <= p2 && nums[i] == 2) {
// 交换nums[i]和nums[p2]
int temp = nums[i];
nums[i] = nums[p2];
nums[p2] = temp;
// p2左移,因为该位置已经放好2了
--p2;
}
// 处理完2之后,检查当前元素是否是0:如果是,交换到p0位置,p0右移
if (nums[i] == 0) {
int temp = nums[i];
nums[i] = nums[p0];
nums[p0] = temp;
// p0右移,因为该位置已经放好0了
++p0;
}
// 注意:如果当前元素是1,无需处理,直接i++即可(循环会自动推进)
}
}
}
3.例子
例子:nums = [2,0,2,1,1,0]
初始状态:
- 数组:
[2,0,2,1,1,0] n = 6,p0 = 0,p2 = 5,i = 0
第1轮:i=0
- 检查
nums[i] = 2,进入while循环(i<=p2成立):- 交换
nums[0]和nums[5]→ 数组变为[0,0,2,1,1,2]; p2左移1 →p2 = 4;- 再次检查
nums[0] = 0(不是2),退出while循环;
- 交换
- 检查
nums[i] = 0:- 交换
nums[0]和nums[p0=0](自己和自己交换,数组不变); p0右移1 →p0 = 1;
- 交换
i++→i = 1;
- 当前状态:数组
[0,0,2,1,1,2],p0=1,p2=4,i=1。
第2轮:i=1
- 检查
nums[i] = 0,不进入while(不是2); - 检查
nums[i] = 0:- 交换
nums[1]和nums[p0=1](自己和自己交换,数组不变); p0右移1 →p0 = 2;
- 交换
i++→i = 2;
- 当前状态:数组
[0,0,2,1,1,2],p0=2,p2=4,i=2。
第3轮:i=2
- 检查
nums[i] = 2,进入while循环(i<=p2成立):- 交换
nums[2]和nums[4]→ 数组变为[0,0,1,1,2,2]; p2左移1 →p2 = 3;- 再次检查
nums[2] = 1(不是2),退出while循环;
- 交换
- 检查
nums[i] = 1,无需处理; i++→i = 3;
- 当前状态:数组
[0,0,1,1,2,2],p0=2,p2=3,i=3。
第4轮:i=3
- 检查
nums[i] = 1,不进入while; - 检查
nums[i] = 1,无需处理; i++→i = 4;
- 当前状态:数组
[0,0,1,1,2,2],p0=2,p2=3,i=4。
循环终止
此时i=4,p2=3,i <= p2不成立,循环结束。
最终数组:[0,0,1,1,2,2](排序完成)。
四、下一个排列
1.题目
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
- 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列 。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列 。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。 - 例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
- 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
- 而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
2.代码
java
class Solution {
public void nextPermutation(int[] nums) {
// 步骤1:从后往前找第一个 nums[i] < nums[i+1] 的位置 i
int i = nums.length - 2; // 从倒数第二个元素开始(避免i+1越界)
while (i >= 0 && nums[i] >= nums[i + 1]) { // 只要nums[i] >= nums[i+1],就继续往前找
i--;
}
// 步骤2:如果找到i(说明不是最大排列),找j并交换
if (i >= 0) { // i=-1时,说明数组是降序(最大排列),无需交换
int j = nums.length - 1; // 从最后一个元素开始找
while (j >= 0 && nums[i] >= nums[j]) { // 找第一个比nums[i]大的nums[j]
j--;
}
swap(nums, i, j); // 交换i和j,保证i位置是"最小的更大值"
}
// 步骤3:反转i+1到末尾的元素(把降序改成升序,得到最小后续)
reverse(nums, i + 1);
}
// 辅助方法:交换数组中两个位置的元素
public void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
// 辅助方法:反转数组中从start到末尾的元素
public void reverse(int[] nums, int start) {
int left = start, right = nums.length - 1;
while (left < right) { // 双指针交换,直到相遇
swap(nums, left, right);
left++;
right--;
}
}
}
3.例子
例子1:普通排列(nums = [1,3,2])
目标:找到[1,3,2]的下一个排列(结果是[2,1,3])。
初始状态:数组[1,3,2],长度n=3。
步骤1:找升序拐点i
- 初始化
i = n-2 = 1(倒数第二个元素,索引1); - 检查
nums[i] = 3,nums[i+1] = 2→3 >= 2,满足循环条件,i--→i=0; - 检查
nums[i] = 1,nums[i+1] = 3→1 < 3,不满足循环条件,退出循环; - 最终
i=0(找到拐点,说明不是最大排列)。
步骤2:找j并交换
- 因为
i>=0,初始化j = n-1 = 2(最后一个元素,索引2); - 检查
nums[i] = 1,nums[j] = 2→1 < 2,不满足循环条件(nums[i] >= nums[j]),退出循环; - 交换
nums[i=0]和nums[j=2]:
原数组[1,3,2]→ 交换后[2,3,1];
步骤3:反转i+1到末尾
i+1 = 1,反转从索引1到末尾的元素([3,1]);- 反转过程:
- left=1,right=2 → 交换
nums[1]和nums[2]→ 数组变为[2,1,3]; - left=2,right=1 → 不满足
left < right,反转结束;
- left=1,right=2 → 交换
- 最终数组:
[2,1,3](正确的下一个排列)。
例子2:最大排列(nums = [3,2,1])
目标:当前是最大排列,返回最小排列([1,2,3])。
初始状态:数组[3,2,1],长度n=3。
步骤1:找升序拐点i
- 初始化
i = n-2 = 1; - 检查
nums[1]=2 >= nums[2]=1→i--→i=0; - 检查
nums[0]=3 >= nums[1]=2→i--→i=-1; - 退出循环,
i=-1(说明是最大排列)。
步骤2:找j并交换
i=-1,跳过交换步骤。
步骤3:反转i+1到末尾
i+1 = 0,反转从索引0到末尾的整个数组([3,2,1]);- 反转过程:
- left=0,right=2 → 交换
nums[0]和nums[2]→ 数组变为[1,2,3]; - left=1,right=1 → 不满足
left < right,反转结束;
- left=0,right=2 → 交换
- 最终数组:
[1,2,3](最小排列,符合要求)。
例子3:更复杂的普通排列(nums = [1,5,4,3,2])
目标:找到下一个排列(结果是[2,1,3,4,5])。
步骤1:找i
i = 5-2 = 3→nums[3]=3 >= nums[4]=2→i=2;nums[2]=4 >= nums[3]=3→i=1;nums[1]=5 >= nums[2]=4→i=0;nums[0]=1 < nums[1]=5→ 退出,i=0。
步骤2:找j并交换
j=4→nums[0]=1 < nums[4]=2→ 退出循环;- 交换
i=0和j=4→ 数组变为[2,5,4,3,1]。
步骤3:反转i+1到末尾
- 反转索引1到4的元素(
[5,4,3,1])→ 反转后[1,3,4,5]; - 最终数组:
[2,1,3,4,5](正确的下一个排列)。
五、寻找重复数
1.题目
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
示例 1:
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
输入:nums = [3,1,3,4,2]
输出:3
示例 3 :
输入:nums = [3,3,3,3,3]
输出:3
2.代码
java
class Solution {
public int findDuplicate(int[] nums) {
// 第一步:快慢指针找环内相遇点
int slow = nums[0]; // 慢指针:初始在索引0,走1步(nums[0])
int fast = nums[nums[0]]; // 快指针:初始在索引0,走2步(nums[nums[0]])
// 循环直到快慢指针相遇(证明环存在)
while (slow != fast) {
slow = nums[slow]; // 慢指针每次走1步:当前值 → 下一个索引
fast = nums[nums[fast]]; // 快指针每次走2步:当前值→下一个索引→再下一个索引
}
// 第二步:找环的入口(重复数)
fast = 0; // 快指针重置到起点(索引0)
// 两个指针以相同速度走,相遇点就是环的入口
while (slow != fast) {
slow = nums[slow]; // 慢指针从相遇点走
fast = nums[fast]; // 快指针从起点走
}
return slow; // 相遇点就是重复数
}
}
3.例子
例子:nums = [1,3,4,2,2]
第一步:快慢指针找环内相遇点
初始化:
- 慢指针(slow):从0号格子出发跳1步,到
nums[0] = 1(停在1号格子); - 快指针(fast):从0号格子出发跳2步,先到1号再到
nums[1] = 3(停在3号格子)。
开始循环跳,直到相遇:
- 第一次跳:
- 慢指针:从1号格子跳1步,到
nums[1] = 3(停在3号); - 快指针:从3号格子跳2步,先到2号再到
nums[2] = 4(停在4号); - 此时slow=3,fast=4,没相遇。
- 慢指针:从1号格子跳1步,到
- 第二次跳:
- 慢指针:从3号格子跳1步,到
nums[3] = 2(停在2号); - 快指针:从4号格子跳2步,先到2号再到
nums[2] = 4(停在4号); - 此时slow=2,fast=4,没相遇。
- 慢指针:从3号格子跳1步,到
- 第三次跳:
- 慢指针:从2号格子跳1步,到
nums[2] = 4(停在4号); - 快指针:从4号格子跳2步,先到2号再到
nums[2] = 4(停在4号); - 此时slow=4,fast=4,相遇了,退出第一步循环。
- 慢指针:从2号格子跳1步,到
第二步:找环的入口(重复数)
重置快指针到起点(0号格子),现在快慢指针每次都只跳1步,直到相遇:
- 第一次跳:
- 慢指针:从4号格子跳1步,到
nums[4] = 2(停在2号); - 快指针:从0号格子跳1步,到
nums[0] = 1(停在1号); - 此时slow=2,fast=1,没相遇。
- 慢指针:从4号格子跳1步,到
- 第二次跳:
- 慢指针:从2号格子跳1步,到
nums[2] = 4(停在4号); - 快指针:从1号格子跳1步,到
nums[1] = 3(停在3号); - 此时slow=4,fast=3,没相遇。
- 慢指针:从2号格子跳1步,到
- 第三次跳:
- 慢指针:从4号格子跳1步,到
nums[4] = 2(停在2号); - 快指针:从3号格子跳1步,到
nums[3] = 2(停在2号); - 此时slow=2,fast=2,相遇了!
- 慢指针:从4号格子跳1步,到
最终返回slow=2,这就是数组中重复的数字。
如果本篇文章对您有帮助,可以点赞,收藏或评论哦!!!关注主包不迷路,让我们一起向前进步吧!!