算法刷题--移除元素

文章目录

    • [1、27 移除元素](#1、27 移除元素)
    • [2、26 删除有序数组中的重复项](#2、26 删除有序数组中的重复项)
    • [3、283 移动零](#3、283 移动零)
    • [4、844 比较含退格的字符串](#4、844 比较含退格的字符串)
      • 优化版
      • [1. `strcmp` 函数的返回值](#1. strcmp 函数的返回值)
      • [2. 逻辑表达式 `== 0` 的本质](#2. 逻辑表达式 == 0 的本质)
      • [3. 对比两种写法](#3. 对比两种写法)
      • [4. 常见的坑:千万别漏掉 `== 0`](#4. 常见的坑:千万别漏掉 == 0)
      • 总结
    • [5、997 有序数组的平方](#5、997 有序数组的平方)
      • [1. 观察切入点:寻找"单调性"](#1. 观察切入点:寻找“单调性”)
      • [2. 核心原理:对撞指针(Two Pointers)](#2. 核心原理:对撞指针(Two Pointers))
      • [3. 思路推演:从结果倒推](#3. 思路推演:从结果倒推)
      • [4. 为什么这样做最优?](#4. 为什么这样做最优?)
      • [5. 避坑指南(面试常问)](#5. 避坑指南(面试常问))
    • 杂记
      • [1. 核心快捷键](#1. 核心快捷键)
      • [2. 其他常用的"规范化"小技巧](#2. 其他常用的“规范化”小技巧)
      • [3. 如何在界面上找到它?](#3. 如何在界面上找到它?)
      • [4. 为什么代码规范很重要?](#4. 为什么代码规范很重要?)

本系列不出意外的话会按照代码随想录的题单进行刷题~
xixi 复试加油~一志愿上岸!!!

1、27 移除元素

题目

代码:

阴差阳错属于双指针法了(快慢指针)

  • i 充当了快指针:遍历原数组。
  • count 充当了慢指针:指向新数组(去除了 val 后)的下一个存放位置。
c 复制代码
int removeElement(int* nums, int numsSize, int val) {
    int i=0;
    int count=0;
    for(i=0;i<numsSize;i++){
        if(nums[i] != val){
            nums[count] = nums[i];
            count++;
        }
        else{
            continue;
        }
    }
    return count;
}

时间复杂度:O(n)

空间复杂度:O(1)

可以优化的地方

  1. 简洁性优化
    else { continue; } 是多余的。在 for 循环中,如果没有命中 if,它自然会进入下一次循环。去掉它可以让代码更清爽。
c 复制代码
int removeElement(int* nums, int numsSize, int val) {
    int count=0;
    for(int i = 0;i < numsSize;i++){
        if(nums[i] != val){
            nums[count++] = nums[i];
        }
    }
    return count;
}
  1. 双指针从两端移动

现在的做法(快慢指针)在某些情况下会有冗余操作。

**假设场景:**数组是 [1, 2, 3, 4, 5],你要删除 val = 1。

  • 你的代码会把 2, 3, 4, 5 这四个数全都向左移动一位。
  • 优化思路:如果 val 在数组开头,而数组很长,与其移动后面所有的元素,不如直接把最后一个元素覆盖到开头。

这种方法叫 "双指针优化(交换/覆盖法)",特别适用于 "要删除的元素很少" 的情况.

c 复制代码
int removeElement(int* nums, int numsSize, int val) {
    int left = 0,right = numsSize-1;
    while(left <= right){
        if(nums[left] == val){
            nums[left] = nums[right];
            right--;
        }
        else{
            left++;
        }
    }
    return left;
}

2、26 删除有序数组中的重复项

题目:

代码:

c 复制代码
int removeDuplicates(int* nums, int numsSize) {
    int left = 0;
    for(int right = 1;right<numsSize;right++){
        if(nums[left] != nums[right]){
            left++;
            nums[left] = nums[right];
            
        }
    }
    return left+1;
}

时间复杂度:O(n)

空间复杂度:O(1)

优化点

减少"自己给自己赋值"

虽然你的代码是对的,但考虑一个极端情况:如果数组里完全没有重复项(例如 [1, 2, 3, 4, 5])。

  • 当前逻辑:当 right = 1 时,nums[0] != nums[1],执行 nums[1] = nums[1]。
  • 改进点:在赋值前增加一个判断 if (right - left > 1)。只有当两个指针之间有间距(即跳过了一些重复项)时,才执行赋值。
    代码:
c 复制代码
int removeDuplicates(int* nums, int numsSize) {
    if(numsSize == 0) return 0;//增加空数组检查,增加鲁棒性
    int left = 0;
    for(int right = 1;right < numsSize;right++){
        if(nums[left] != nums[right]){
            left++;
            if(right > left){//只有指针不相邻时才移动数据
                nums[left] = nums[right];
            }
        }
    }
    return left + 1;
}
  1. 深度思考:通用模板掌握了这一题,其实你已经可以解决一类问题了。比如这道题的进阶版:LeetCode 第 80 题(删除有序数组中的重复项 II)。要求:每个元素最多出现 两次。解题套路(针对"最多保留 k 个重复项"):只要把判断条件改一下即可:if (nums[right] != nums[left - (k-1)])对于本题( k = 1 k=1 k=1),其实逻辑就是 nums[right] != nums[left]。

何为鲁棒性

gimini老师的解答:简单来说,"鲁棒性" 是英文 Robustness 的音译。在计算机和工程领域,它的核心含义是系统的健壮性强健性

如果一个系统具有很强的鲁棒性,就说明它**"抗造""皮实"**。即使面对意外情况、错误的输入或者恶劣的环境,它也能保持运行,而不会轻易崩溃。


如何形象地理解鲁棒性?

我们可以用几个生活中的例子来对比:

  • 玻璃杯 vs. 不锈钢杯

  • 玻璃杯掉在地上会碎(鲁棒性低)。

  • 不锈钢杯掉在地上可能只是一点凹痕,依然能装水(鲁棒性高)。

  • 普通人 vs. 运动员

  • 普通人可能吹点凉风就感冒了。

  • 运动员在严寒酷暑中依然能保持高水平竞技状态,这说明运动员的身体系统更"鲁棒"。


在编程中,鲁棒性意味着什么?

回到你写的代码,一个"鲁棒性高"的程序通常能处理以下三种情况:

1. 处理异常输入

比如一个计算平方根的函数,如果用户输入了负数、字母或者空值,程序是直接蓝屏崩溃,还是能优雅地报错提示"输入无效"?

  • 不鲁棒:直接死机。
  • 鲁棒 :通过 if (x < 0) 判断并给回反馈。
2. 应对极端边界

正如你刚才写的 removeDuplicates 函数:

c 复制代码
if (numsSize == 0) return 0; // 这就是为了增加鲁棒性

如果我不传数组(长度为 0),没有这行代码,你的 for 循环或访问 nums[0] 可能就会导致内存非法访问。加上这一行,你的代码就变得更"鲁棒"了。

3. 容错能力

在分布式系统或网络程序中,如果网络突然断了一秒钟,鲁棒的系统会自动重连,而不是直接退出程序。


为什么鲁棒性很重要?

在实际开发中,"墨菲定律" 总是起作用的:如果一个错误可能发生,它就一定会发生。

  • 面试中 :面试官非常看重代码的鲁棒性。他们不仅看你能不能写出核心算法,还看你有没有考虑到 null 指针、数组越界、极大值溢出等边界情况。
  • 工程中:鲁棒性差的软件意味着频繁的宕机和高昂的维护成本。

总结

鲁棒性 = 稳定性 + 容错能力。

以后在刷题时,你可以多问自己一句话:"如果输入的数据非常奇怪(很大、很小、空的、重复的),我的代码还会正常运行吗?"

3、283 移动零

题目

代码

覆盖法

先统计非零元素全部挪到前面,最后把后面的位置全部手动补 0。

c 复制代码
void moveZeroes(int* nums, int numsSize) {
   int n = numsSize;
   int i = 0;
   int left = 0;
   int count = 0;
   for(i = 0;i < n;i++){
    if(nums[i] != 0){
        nums[left++] = nums[i];
        count++;
    }
   }
   for(i=count;i<n;i++){
        nums[i] = 0;
   }
}

时间复杂度:O(n)

空间复杂度:O(1)
简洁版

c 复制代码
void moveZeroes(int* nums, int numsSize) {
   int n = numsSize;
   int i = 0;
   int left = 0;
   for(i = 0;i < n;i++){
    if(nums[i] != 0){
        nums[left++] = nums[i];
    }
   }
   while(left < n){
        nums[left++] = 0;
   }
}

双指针交换法

指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。

右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。

注意到以下性质:

左指针左边均为非零数;

右指针左边直到左指针处均为零。

因此每次交换,都是将左指针的零与右指针的非零数交换,且非零数的相对顺序并未改变。

作者:力扣官方题解

链接:https://leetcode.cn/problems/move-zeroes/solutions/489622/yi-dong-ling-by-leetcode-solution/

来源:力扣(LeetCode)

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

c 复制代码
void moveZeroes(int* nums, int numsSize) {
    int left = 0;
    for (int right = 0; right < numsSize; right++) {
        if (nums[right] != 0) {
            // 只有当两者不相等时才交换,避免自交换
            if (right > left) {
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
            }
            left++;
        }
    }
}

紧凑版

c 复制代码
void moveZeroes(int* nums, int numsSize) {
    int left = 0;
    for(int right = 0;right < numsSize;right++){
        if(nums[right] != 0){
            if(right > left){
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
            }
            left++;
        }
    }
}

4、844 比较含退格的字符串

题目:

代码:

c 复制代码
bool backspaceCompare(char* s, char* t) {
    int len_s = strlen(s);
    int len_t = strlen(t);
    int a = 0;
    for(int i = 0;i < len_s;i++){
        if(s[i] != '#'){
            s[a++] = s[i];
        }
        else{
            if(a>0){
                a--;
            }
        }
    }
    s[a] = '\0';
    a = 0;
    for(int i = 0;i < len_t;i++){
        if(t[i] != '#'){
            t[a++] = t[i];
        }
        else{
            if(a>0){
                a--;
            }
        }
    }
    t[a] = '\0';
    if(strcmp(s,t) == 0){
        return true;
    }
    else{
        return false;
    }
}

简直就是垃圾~唉目前还不太会对字符串进行操作,加油加油

时间复杂度:O(n)

空间复杂度:O(1)

优化版

1.不用strlen,直接判断是否到'\0'即结束

c 复制代码
bool backspaceCompare(char* s, char* t) {
    int a = 0;
    for(int i = 0;s[i] != '\0';i++){
        if(s[i] != '#'){
            s[a++] = s[i];
        }
        else{
            if(a>0){
                a--;
            }
        }
    }
    s[a] = '\0';
    a = 0;
    for(int i = 0;t[i] != '\0';i++){
        if(t[i] != '#'){
            t[a++] = t[i];
        }
        else{
            if(a>0){
                a--;
            }
        }
    }
    t[a] = '\0';
    if(strcmp(s,t) == 0){
        return true;
    }
    else{
        return false;
    }
}

2.封装成函数

c 复制代码
void processstring(char* s){
    int slow = 0;
    for(int fast = 0;s[fast] != '\0';fast++){
        if(s[fast] != '#'){
            s[slow++] = s[fast];
        }
        else{
            if(slow>0){
                slow--;
            }
        }
    }
    s[slow] = '\0';
}
bool backspaceCompare(char* s, char* t) {
    processstring(s);
    processstring(t);
    return strcmp(s,t) == 0;
}

return strcmp(s, t) == 0;为什么可以直接return这个?

gimini老师的解答:

这是一个非常好的问题。这涉及到 C 语言中 布尔值(Boolean)函数返回值 的底层逻辑。

我们可以拆成两部分来理解:

1. strcmp 函数的返回值

首先,我们要明确 strcmp(s, t) 返回的是什么。它不是返回 truefalse,而是返回一个整数

  • 0:表示两个字符串完全相等。
  • 正数 :表示 s 的 ASCII 码顺序在 t 之后。
  • 负数 :表示 s 的 ASCII 码顺序在 t 之前。

2. 逻辑表达式 == 0 的本质

当你写 strcmp(s, t) == 0 时,这实际上是一个判断题。计算机在执行这行代码时,会进行以下逻辑转换:

  1. 执行函数 :先算出 strcmp(s, t) 的结果。假设结果是 0
  2. 进行比较 :判断 0 == 0 是否成立。
  3. 得出结果
  • 如果成立(即字符串相等),整个表达式的结果就是 真 (true)
  • 如果不成立(结果是非 0),整个表达式的结果就是 假 (false)

3. 对比两种写法

你可以把你的代码想象成这两个版本的演变:

版本 A(新手常用):

c 复制代码
int result = strcmp(s, t);
if (result == 0) {
    return true;
} else {
    return false;
}

版本 B(进阶写法):

c 复制代码
// 直接把判断的结果返回给调用者
return strcmp(s, t) == 0;

为什么版本 B 更好?

  • 简洁 :省去了冗余的 if-else 分支。
  • 效率:编译器生成的汇编指令更少,执行速度理论上更快。
  • 清晰:对于有经验的开发者来说,一眼就能看出这个函数的意图是"返回这两个字符串是否相等的判断结果"。

4. 常见的坑:千万别漏掉 == 0

在 C 语言中,如果你直接写 return strcmp(s, t);,那就大错特错了。

  • 因为在 C 语言里,0 代表"假",非 0 代表"真"
  • 如果字符串相等strcmp 返回 0,函数会返回 false
  • 如果字符串不相等strcmp 返回非 0,函数反而会返回 true
    这和我们的逻辑正好相反!所以必须写上 == 0

总结

return strcmp(s, t) == 0; 的意思就是:"请告诉我『strcmp 的结果等于 0』这件事是否为真。"


优化版:模拟栈(空间 O ( N ) O(N) O(N))

这是最直观的方法:利用双指针模拟栈的操作。遇到非 # 入栈,遇到 # 且栈不为空则出栈。

跟封装成函数是一样的,直接贴代码~

c 复制代码
void build(char *s){
    int slow = 0;
    for(int fast = 0;s[fast] != '\0';fast++){
        if(s[fast] != '#'){
            s[slow++] = s[fast];
        }
        else if(slow > 0){
            slow--;//退格:慢指针往回走一步
        }
    }
    s[slow] = '\0';//必须加上字符串结束符
}
bool backspaceCompare(char* s, char* t) {
    build(s);
    build(t);
    return strcmp(s,t) == 0;
}

优化版:从后往前双指针(空间 O ( 1 ) O(1) O(1))

面试官最喜欢的其实是从后往前遍历。 为什么? 因为退格符只会影响它前面的字符。如果我们从后往前看,就能立刻知道当前的字符是否应该被跳过。

算法逻辑:

准备两个指针 i, j 分别指向 s 和 t 的末尾。

用变量 skipS 和 skipT 记录当前需要跳过的字符数。

从后往前找第一个"有效"字符:

如果是 #,skipS++。

如果是普通字符且 skipS > 0,说明它被退格了,skipS--。

否则,这个字符就是最终留下的。

比较这两个有效字符是否相等。

c 复制代码
bool backspaceCompare(char* s, char* t) {
    int i = strlen(s) - 1, j = strlen(t) - 1;
    int skipS = 0, skipT = 0;
    while (i >= 0 || j >= 0) {
        // 找到s中逆序第一个有效的字符
        while (i >= 0) {
            if (s[i] == '#') {
                skipS++;
                i--;
            } else if (skipS > 0) {
                skipS--;
                i--;
            } else {
                break;
            }
        }

        // 找到t中逆序第一个有效的字符
        while (j >= 0) {
            if (t[j] == '#') {
                skipT++;
                j--;
            } else if (skipT > 0) {
                skipT--;
                j--;
            } else {
                break;
            }
        }
        // 比较
        if (i >= 0 && j >= 0) {
            if (s[i] != t[j]) {
                return false;
            }
        } 
        else {
            if (i >= 0 || j >= 0) {//一个等于0,另一个大于0
                return false;
            }
        }
        i--;
        j--;
    }
    return true;
}

5、997 有序数组的平方

题目

思路 from gimini

  1. 观察切入点(为什么这道题要用这个方法?)
  2. 核心原理(这个算法解决的是什么根本矛盾?)
  3. 推演过程(逻辑是怎么一步步从模糊变得清晰的?)
  4. 避坑指南(这里的陷阱在哪里?)

拆解:

1. 观察切入点:寻找"单调性"

  • 初级观察 :题目给的是有序数组。一看到"有序",第一反应通常是二分查找或双指针。
  • 深度观察 :平方运算有个特性------越小的负数,平方后越大;越大的正数,平方后也越大。
  • 发现矛盾 :原数组是递增的(例如 [-4, -1, 0, 3, 10]),但平方之后,最大值分布在数组的两端 (16 和 100),最小值则在数组的中间靠拢。

2. 核心原理:对撞指针(Two Pointers)

这道题的本质是:两个单调序列的合并

如果你把负数部分看作一个序列,正数部分看作一个序列,平方后,这两个序列其实是从两头往中间"变小"的。

  • 既然最大值一定在两头,我们就从两头往中间找。

3. 思路推演:从结果倒推

既然我们能确定"当前最大值"一定在 leftright 指向的位置,那我们就:

  1. 准备一个新数组,长度和原数组一样。
  2. 倒着填 :从新数组的最后一个位置(索引为 numsSize - 1)开始填入。
  3. 对撞比较
  • 比较 nums[left] * nums[left]nums[right] * nums[right]
  • 谁大,谁就填入新数组的当前位置。
  • 被选中的指针向中间移动一步,而落选的指针保持不动,等待下一轮比较。

4. 为什么这样做最优?

  • 时间复杂度:。我们只遍历了一遍数组,没有用到任何排序算法(排序通常需要 )。
  • 逻辑美感:它利用了题目给出的"有序"条件,将原本混乱的平方结果,通过"两头取大"的方式,直接变成了有序输出。

5. 避坑指南(面试常问)

  • 空间换时间:注意这道题必须开辟一个新数组,因为如果你在原数组上直接修改,会覆盖掉还没参与比较的数字。
  • 循环终止条件 :这里必须是 while(left <= right),因为最后一个元素(即平方后最小的那个)也需要被放入新数组。

你需要特别注意:

  • 如何分配返回数组的空间?(使用 malloc
  • 如何定义三个指针(left, right, 以及新数组的填充指针 k)?

代码:

c 复制代码
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
    int left = 0,right = numsSize-1;
    int ans = numsSize - 1;
    int* s = (int*)malloc(sizeof(int)*numsSize);
    *returnSize = numsSize;
    while(left <= right){
        int m = pow(nums[left],2);
        int n = pow(nums[right],2);
        if(m > n){
            s[ans--] = m;
            left++;
        }
        else{
            s[ans--] = n;
            right--;
        }
    }
    return s;
}

时间复杂度:O(n)

空间复杂度:O(1)

优化方向:

A. 避开 pow() 函数

原因:pow(x, 2) 是一个通用的数学函数,它内部是通过对数运算来实现的,返回值是 double 类型。对于简单的整数平方计算,pow 显得太重了(计算量大且存在浮点数转换开销)。

优化:直接用 nums[left] * nums[left]。这不仅快,而且直接得到整数,没有精度损失。

c 复制代码
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
    int left = 0,right = numsSize-1;
    int ans = numsSize - 1;
    int* s = (int*)malloc(sizeof(int)*numsSize);
    *returnSize = numsSize;
    while(left <= right){
        int m = nums[left] * nums[left];
        int n = nums[right] * nums[right];
        if(m > n){
            s[ans--] = m;
            left++;
        }
        else{
            s[ans--] = n;
            right--;
        }
    }
    return s;
}

暴力解:先全部平方再排序

代码:

c 复制代码
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int cmp(const void * a, const void * b){
    return *(int*)a - *(int*)b;
}
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
    *returnSize = numsSize;
    int* s = (int*)malloc(sizeof(int)*numsSize);
    for(int i = 0;i < numsSize;i++){
        s[i] = nums[i] * nums[i];
    }
    qsort(s,numsSize,sizeof(int),cmp);
    return s;
}

杂记

快捷键 from gimini

在力扣(LeetCode)的编辑器中,确实有快捷键可以一键让混乱的代码变得整齐规范。

这个功能通常被称为 "格式化代码" (Format Code)

1. 核心快捷键

根据你使用的操作系统,快捷键如下:

  • Windows / Linux : Shift + Alt + F
  • macOS : Shift + Option + F

按下之后,编辑器会自动帮你调整缩进、空格、括号位置等,让代码符合该语言的标准规范(例如 C 语言通常会遵循类似 Google 或 LLVM 的风格)。


2. 其他常用的"规范化"小技巧

除了格式化,这几个快捷键也能帮你快速理清代码逻辑:

  • 全选代码 : Ctrl + A (Mac: Cmd + A)
  • 缩进/反缩进 : 选中多行后按 Tab 增加缩进,按 Shift + Tab 减少缩进。
  • 注释/取消注释 : Ctrl + / (Mac: Cmd + /)。这在调试时非常有用,不用手动去打 //
  • 智能提示 : 如果你写一半卡住了,可以按 Ctrl + Space 强制触发自动补全。

3. 如何在界面上找到它?

如果你不习惯用快捷键,也可以手动操作:

  1. 点击代码编辑区域右上角的 "设置"图标(齿轮状)
  2. 在弹出的菜单中,你可以看到 "自动格式化" 的选项。
  3. 有些版本的力扣,在编辑器的右下角或者右键菜单里也有 Format Code 选项。

4. 为什么代码规范很重要?

作为初学者,养成格式化代码的习惯有几个好处:

  • 容易排错 :缩进对齐了,你一眼就能看出 iffor 的括号是不是漏写了。
  • 鲁棒性暗示:整洁的代码会给面试官一种"这人逻辑严密、基本功扎实"的职业印象。
  • 阅读顺畅:当你回看自己一周前写的代码时,规范的格式能帮你快速找回思路。

相关推荐
ballball~~1 天前
正态(高斯)分布(Gaussian distribution)
算法·概率论
Blossom.1181 天前
强化学习推荐系统实战:从DQN到PPO的演进与落地
人工智能·python·深度学习·算法·机器学习·chatgpt·自动化
AI科技星1 天前
引力场与磁场的几何统一:磁矢势方程的第一性原理推导、验证与诠释
数据结构·人工智能·经验分享·线性代数·算法·计算机视觉·概率论
Frank_refuel1 天前
C++日期类实现
开发语言·c++·算法
Jeremy爱编码1 天前
电话号码的字母组合
java·算法·leetcode
YuTaoShao1 天前
【LeetCode 每日一题】1339. 分裂二叉树的最大乘积
算法·leetcode·职场和发展
Neil今天也要学习1 天前
永磁同步电机控制算法--基于增量式模型的鲁棒无差拍电流预测控制
单片机·嵌入式硬件·算法
leoufung1 天前
LeetCode 172. Factorial Trailing Zeroes 题解
算法·leetcode·职场和发展
姓蔡小朋友1 天前
算法-子串
java·数据结构·算法