一、移动零
题目描述:
给定一个数组 nums,需原地操作将所有 0 移动到数组末尾,同时保持非零元素的相对顺序。
示例:
- 输入
nums = [0,1,0,3,12],输出[1,3,12,0,0] - 输入
nums = [0],输出[0]
解题思路:
本题核心是原地整理非零元素,让0自然'沉淀'到末尾 ,采用双指针法实现:
- 定义指针
dest:标记已处理的非零元素的最后位置 (初始为-1,表示暂无非零元素)。 - 定义指针
cur:遍历整个数组,寻找非零元素。 - 遍历过程:
- 若
nums[cur]是 0,直接跳过(留到后面)。 - 若
nums[cur]非零,将dest向后移动一位(指向新的非零元素位置),然后交换nums[dest]和nums[cur]。
- 若
- 最终,
dest及之前的位置都是非零元素(保持原顺序),dest之后的位置自然都是 0。
完整代码:
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int dest = -1, cur = 0;
for(cur = 0; cur < nums.size(); cur++)
{
if(nums[cur] == 0)
{
continue;
}
else
{
swap(nums[++dest], nums[cur]);
}
}
}
};
复杂度分析:
- 时间复杂度 :O(n)O(n)O(n)。仅遍历数组一次,每个元素最多被交换一次。
- 空间复杂度 :O(1)O(1)O(1)。仅使用常数级额外变量,完全满足"原地操作"要求。
二、复写零
题目描述:
给你一个长度固定的整数数组 arr,将每个出现的零复写一遍,并右移其余元素。要求:
- 不能在数组长度外写入元素
- 必须原地修改数组,不能使用额外数组
示例 :
输入:arr = [1,0,2,3,0,4,5,0]
输出:[1,0,0,2,3,0,0,4](最后一个0因数组长度限制无法复写)
解题思路:
如果直接从前往后遍历复写零,会覆盖后续未处理的元素;如果用额外数组存储结果,又不符合"原地修改"的要求。
因此我们采用先找边界、再从后往前填充的双指针策略:
-
第一步:确定有效元素的边界
- 用指针
cur遍历原数组,dest模拟"复写零后"的数组长度(遇到非零元素,dest+1;遇到零,dest+2)。 - 当
dest超过数组长度时,停止遍历。此时cur左侧的元素是最终数组需要包含的元素。
- 用指针
-
第二步:处理边界溢出情况
- 若
dest == n(数组长度),说明最后一个元素是零且复写后超出长度,此时只需要写一个零到数组末尾。
- 若
-
第三步:从后往前填充数组
- 从
cur位置倒序遍历,将元素写入dest位置:非零元素直接写,零则写两次(注意dest指针同步左移)。
- 从
完整代码:
cpp
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int cur = 0; // 遍历原数组的指针
int dest = -1; // 模拟复写零后的数组长度指针
int n = arr.size();
// 第一步:找到有效元素的边界(cur左侧元素是最终数组需要包含的元素)
while (cur < n) {
if (arr[cur] != 0) {
dest++; // 非零元素,dest+1
} else {
dest += 2; // 零元素,复写后占两个位置,dest+2
}
if (dest >= n - 1) { // 当dest到达数组末尾(或超出),停止遍历
break;
}
cur++;
}
// 第二步:处理dest刚好等于n的情况(最后一个零复写后超出数组长度)
if (dest == n) {
dest--; // 回退到数组最后一个位置
arr[dest--] = 0; // 只写一个零
cur--; // cur指针回退
}
// 第三步:从后往前填充数组
while (cur >= 0) {
if (arr[cur] != 0) {
arr[dest--] = arr[cur]; // 非零元素直接填充
} else {
arr[dest--] = 0; // 零元素填充两次
arr[dest--] = 0;
}
cur--;
}
}
};
复杂度分析:
- 时间复杂度:O(n)。只需要遍历数组两次(一次找边界,一次填充),n是数组长度。
- 空间复杂度:O(1)。只使用了常量级别的额外变量,完全原地修改。
三、快乐数
题目描述:
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例:
- 输入
n = 19,输出true(过程:12+92=82→82+22=68→62+82=100→12+02+02=11^2+9^2=82 → 8^2+2^2=68 → 6^2+8^2=100 → 1^2+0^2+0^2=112+92=82→82+22=68→62+82=100→12+02+02=1) - 输入
n = 2,输出false
解题思路:
快乐数的核心矛盾是:过程要么终止于 1,要么陷入循环 (因为数字的平方和范围有限,比如 3 位数的最大平方和是 92+92+92=2439^2+9^2+9^2=24392+92+92=243,不可能无限增大)。
因此,我们可以用 快慢指针算法 来判断是否存在循环:
- 慢指针
slow:每次走 1 步(计算 1 次平方和) - 快指针
fast:每次走 2 步(计算 2 次平方和) - 若过程中出现
slow == fast,说明进入循环:- 若相遇时的值是 1 → 是快乐数
- 否则 → 不是快乐数
完整代码:
cpp
class Solution {
public:
int Sum(int n){
int sum = 0;
while(n){
int t = n % 10;
sum += t * t;
n = n / 10;
}
return sum;
}
bool isHappy(int n) {
int slow = n, fast = Sum(n);
while(slow != fast){
slow = Sum(slow);
fast = Sum(Sum(fast));
}
return slow == 1;
}
};
复杂度分析:
- 时间复杂度 :O(logn)O(\log n)O(logn)。每次计算平方和的时间是数字的位数(即 log10n\log_{10}nlog10n),而快慢指针相遇的次数是有限的(因为平方和范围固定)。
- 空间复杂度 :O(1)O(1)O(1)。仅使用了常数级别的额外变量,比哈希集合法(空间 O(logn)O(\log n)O(logn))更优。