【C语言16天强化训练】从基础入门到进阶:Day 10


🔥个人主页艾莉丝努力练剑

❄专栏传送门:《C语言》《数据结构与算法》C语言刷题12天IO强训LeetCode代码强化刷题洛谷刷题C/C++基础知识知识强化补充C/C++干货分享&学习过程记录

🍉学习方向:C/C++方向学习者

⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太平

前言:距离我们学完C语言已经过去一段时间了,在学习了初阶的数据结构之后,博主还要更新的内容就是【C语言16天强化训练】,之前博主更新过一个【C语言刷题12天IO强训】的专栏,那个只是从入门到进阶的IO模式真题的训练。【C语言16天强化训练】既有IO型,也有接口型。和前面一样,今天依然是训练五道选择题和两道编程算法题,希望大家能够有所收获!



目录

一、五道选择题

[1.1 题目1](#1.1 题目1)

[1.2 题目2](#1.2 题目2)

[1.3 题目3](#1.3 题目3)

[1.4 题目4](#1.4 题目4)

[1.5 题目5](#1.5 题目5)

二、两道算法题

[2.1 不用加减乘除做加法](#2.1 不用加减乘除做加法)

[2.1.1 题目理解](#2.1.1 题目理解)

[2.1.2 思路](#2.1.2 思路)

[2.2 找到所有数组中消失的数字](#2.2 找到所有数组中消失的数字)

[2.2.1 题目理解](#2.2.1 题目理解)

[2.2.2 思路](#2.2.2 思路)

[2.2.3 为什么选择负号标记法?](#2.2.3 为什么选择负号标记法?)

[2.2.4 负号标记法的巧妙之处](#2.2.4 负号标记法的巧妙之处)

[2.2.5 与其他方法的对比](#2.2.5 与其他方法的对比)

结尾


一、五道选择题

1.1 题目1

**题干:**求函数返回值,传入 -1 ,则在64位机器上函数返回( )

cpp 复制代码
int func(int x)
{
    int count = 0;
    while (x)
    {
        count++;
        x = x&(x - 1);//与运算
    } 
    return count;
}

A. 死循环 B. 64 C. 32 D. 16

解析:正确答案就是B选项。

函数 func 的作用是计算整数 x 的二进制表示中1的个数(即汉明重量)。通过循环 x = x & (x - 1) 每次会消除二进制表示中最右边的1,直到 x 变为0。

传入 -1:在64位机器上,-1 的二进制表示是所有位都是1(即64个1)。因此,循环会执行64次(每次消除一个1),最后返回 count = 64。

1.2 题目2

**题干:**读代码选结果( )

cpp 复制代码
int count = 0;
int x = -1;
while(x)
{
    count++;
    x = x >> 1;
}
printf("%d",count)

A. 1 B. 2 C. 32 D. 死循环,没结果

解析:正确答案就是D选项。

这里 x 是带符号整数(默认是 signed int),初始为 -1(二进制所有位为1)。执行右移操作 x = x >> 1 是算术右移(对于负数,高位补1)。因此,无论右移多少次,x 始终不会变为0(因为高位一直补1,保持所有位为1,即始终为-1)。所以循环条件 x 永远为非0,导致死循环。

1.3 题目3

**题干:**下述赋值语句错误的是( )

A. a = (b = (c = 2 , d = 3)) B. i++ C. a / b = 2 D. a = a < a + 1

解析:正确答案就是C选项。

A. 正确,逗号表达式返回最后一个值(d=3),然后赋值给b,再赋值给a;
B. 正确,自增操作;
C. 错误,a/b是一个表达式(值),不能作为左值被赋值;
D. 正确,先计算a < a+1(恒为真,即1),然后赋值给a。

1.4 题目4

**题干:**若有 int w=1, x=2, y=3, z=4; 则条件表达 w < x ? w : y < z ? y : z 的值是( )

A. 1 B. 2 C. 3 D. 4

解析:正确答案就是A选项。

题干所给表达式:w < x ? w : y < z ? y : z
结合性:从右到左(条件运算符是右结合),所以等价于:w < x ? w : (y < z ? y : z)

**计算:**w=1, x=2 -> w<x 为真(1),所以返回 w(即1)
因此w < x ? w : y < z ? y : z的值为1。

1.5 题目5

**题干:**以下程序运行后的输出结果是( )

cpp 复制代码
int main()
{
    int a=1,b=2,m=0,n=0,k;
    k=(n=b<a)&&(m=a);
    printf("%d,%d\n",k,m);
    return 0;
}

A. 0,0 B. 0,1 C. 1,0 D. 1,1

解析:正确答案就是A选项。

(1)先计算 n = b<a(即n=2<1,为假,所以n=0);

(2)由于 && 短路:第一个操作数 (n=b<a) 结果为0(假),所以第二个表达式 (m=a) 不会执行(m保持原值0);

(3)k 为整个逻辑与的结果(0)。

因此输出:k=0, m=0。

选择题答案如下:

1.1 B

1.2 D

1.3 C

1.4 A

1.5 A

校对一下,大家都做对了吗?


二、两道算法题

2.1 不用加减乘除做加法

牛客链接:JZ65 不用加减乘除做加法

题目描述:

2.1.1 题目理解

为了解决这个问题,我们需要在不使用加减乘除运算符的情况下实现两个整数的加法。我们可以利用位运算来模拟加法过程。

这道题是接口型的,下面是C语言的模版(如果是IO型就可以不用管它们了)------

2.1.2 思路

C语言思路:

1、算法原理:使用位运算模拟二进制加法。

(1)异或运算(~):得到不考虑进位的和;

(2)与运算 + 左移(& + << 1):得到进位值。

2、迭代过程

(1)每次循环计算当前位的和(不考虑进位)和进位值;

(2)将和赋值给 num1,进位赋值给 num2;

(3)重复直到进位为0。

3、处理负数:由于C语言中使用补码表示负数,该算法同样适用于负数加法。

4、时间复杂度:O(1),因为整数位数固定(通常是32位),最多循环32次。

代码演示:

cpp 复制代码
//C语言实现
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param num1 int整型
 * @param num2 int整型
 * @return int整型
 */
int Add(int num1, int num2) 
{
    while (num2 != 0)
    {
        int sum = num1 ^ num2;
        int carry = (num1 & num2) << 1;
        num1 = sum;
        num2 = carry;
    }
    return num1;
}

时间复杂度O(1)

空间复杂度O(1)

博主这里再展示一下完整的C语言代码,包含了测试用例------

代码演示:

cpp 复制代码
//完整C语言代码
#include <stdio.h>

int Add(int num1, int num2) 
{
    while (num2 != 0) 
    {
        int sum = num1 ^ num2;
        int carry = (num1 & num2) << 1;
        num1 = sum;
        num2 = carry;
    }
    return num1;
}

int main() 
{
    // 测试用例
    printf("1 + 2 = %d\n", Add(1, 2));      // 输出: 3
    printf("0 + 0 = %d\n", Add(0, 0));      // 输出: 0
    printf("5 + 7 = %d\n", Add(5, 7));      // 输出: 12
    printf("-1 + 1 = %d\n", Add(-1, 1));    // 输出: 0
    printf("10 + -5 = %d\n", Add(10, -5));  // 输出: 5

    return 0;
}

时间复杂度O(1)

空间复杂度O(1)

我们学习了C++之后也可以尝试用C++来实现一下,看看自己前段时间C++学得怎么样------

C++思路:

1、问题分析:题目要求不使用四则运算符号实现加法。我们可以使用位运算来模拟加法的过程。

2、关键观察:二进制加法的每一位可以分解为:

(1)非进位和 :使用异或运算(^)得到,即 num1 ^ num2;

(2)进位 :使用与运算(&)并左移一位得到,即 (num1 & num2) << 1。

3、迭代过程:将非进位和与进位相加,直到进位为0。每次迭代都更新非进位和和进位,直到没有进位为止。

代码演示:

cpp 复制代码
//C++实现
class Solution {
public:
    int Add(int num1, int num2)
    {
        while (num2 != 0)
        {
            int sum = num1 ^ num2;
            int carry = (num1 & num2) << 1;
            num1 = sum;
            num2 = carry;
        }
        return num1;
    }
};

时间复杂度:O(1),空间复杂度: O(1)****。
1、循环条件 :当进位(num2)不为0时,继续循环。

2、计算非进位和 :使用异或运算 num1 ^ num2 得到当前位的和,不考虑进位。

3、计算进位 :使用与运算 num1 & num2 并左移一位得到进位。

4、更新变量 :将非进位和赋值给 num1,进位赋值给 num2,进行下一次迭代。

5、返回结果 :当进位为0时,num1 即为最终的和,返回 num1。

使用位运算模拟二进制加法通过位运算高效地模拟了加法过程,避免了使用四则运算符号。

我们目前要写出来C++的写法是非常考验前面C++的学习情况好不好的,大家可以尝试去写一写,优先掌握C语言的写法,博主还没有介绍C++的算法题,之后会涉及的,敬请期待!

2.2 找到所有数组中消失的数字

力扣链接:448. 找到所有数组中消失的数字

力扣题解链接:负号标记法解决【找到所有数组中消失的数字】问题

题目描述:

2.2.1 题目理解

题目的本质是:在一个长度为 n 的数组中,所有数字理论上都应该在 [1, n] 范围内,但由于某些数字可能重复出现,导致其他数字缺失。我们需要找出所有缺失的数字。

本题中关键的约束条件------

1、数组长度 = n;

2、数字范围 = [1, n];

3、可能有重复数字;

4、需要找出所有缺失的数字。

2.2.2 思路

我们使用负号标记法,时间复杂度为 O(n),空间复杂度为 O(1)(不考虑输出数组):

1、第一次遍历 :对于每个数字 nums[i],我们将 nums[abs(nums[i])-1] 标记为负数;

2、第二次遍历 :所有仍然为正数的位置 i 表示数字 i+1 在原始数组中不存在。

这道题是接口型的,下面是C语言的模版(如果是IO型就可以不用管它们了)------

代码演示:

cpp 复制代码
//C语言实现------负号标记法
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* findDisappearedNumbers(int* nums, int numsSize, int* returnSize) {
    // 第一次遍历:使用负号标记法
    for (int i = 0; i < numsSize; i++) 
    {
        int index = abs(nums[i]) - 1; // 获取对应的索引位置
        if (nums[index] > 0) 
        {
            nums[index] = -nums[index]; // 标记为负数表示该数字存在
        }
    }

    // 统计缺失的数字数量
    int count = 0;
    for (int i = 0; i < numsSize; i++) 
    {
        if (nums[i] > 0) 
        {
            count++;
        }
    }

    // 分配结果数组
    int* result = (int*)malloc(count * sizeof(int));
    *returnSize = count;

    // 第二次遍历:收集缺失的数字
    int j = 0;
    for (int i = 0; i < numsSize; i++) 
    {
        if (nums[i] > 0) 
        {
            result[j++] = i + 1; // 索引+1就是缺失的数字
        }
    }

    return result;
}

时间复杂度O(n)

空间复杂度O(1)

我们学习了C++之后也可以尝试用C++来实现一下,看看自己前段时间C++学得怎么样------

代码演示:

cpp 复制代码
//C++实现------负号标记法
class Solution {
public:
    vector<int> findDisappearedNumbers(vector<int>& nums) 
    {
        int n = nums.size();

        // 使用负号标记法
        for (int i = 0; i < n; i++) 
        {
            int index = abs(nums[i]) - 1; // 获取对应的索引位置
            if (nums[index] > 0) 
            {
                nums[index] = -nums[index]; // 标记为负数表示该数字存在
            }
        }

        vector<int> result;

        // 收集缺失的数字
        for (int i = 0; i < n; i++) 
        {
            if (nums[i] > 0) 
            {
                result.push_back(i + 1); // 索引+1就是缺失的数字
            }
        }

        return result;
    }
};

时间复杂度: O(n)****,空间复杂度: O(1)****。

我们目前要写出来C++的写法是非常考验前面C++的学习情况好不好的,大家可以尝试去写一写,优先掌握C语言的写法,博主还没有介绍C++的算法题,之后会涉及的,敬请期待!

我们可以用几个示例来验证一下------

对于输入 [4, 3, 2, 7, 8, 2, 3, 1]

(1)第一次遍历后数组变为:[-4, -3, -2, -7, 8, 2, -3, -1];

(2)位置4和5(索引从0开始)的值仍为正数,所以缺失的数字是5和6。

负号标记法这种方法高效且不需要额外的空间(除了输出数组),非常适合处理大规模数据。

2.2.3 为什么选择负号标记法?

1、空间效率: 题目要求 O(1) 额外空间(不包括输出数组);

2、时间效率: 需要 O(n) 时间复杂度;

3、利用现有数组: 既然数字范围是 [1, n],我们可以用数组索引本身来记录信息。

2.2.4 负号标记法的巧妙之处

核心思想:用数组的索引位置来记录数字是否存在。

具体操作:

(1)对于数字 x,我们去查看位置 x-1;

(2)如果该位置的值为正,我们就将其标记为负;

(3)这样,最后仍然为正数的位置 i 就表示数字 i+1 不存在。

为什么这样可行?

(1)因为数组索引从 0 到 n-1,正好对应数字 1 到 n;

(2)负号标记不会改变绝对值,所以不影响后续的判断。

2.2.5 与其他方法的对比

方法1:使用哈希表(不适用)

cpp 复制代码
// 需要 O(n) 额外空间,不符合要求
unordered_set<int> seen;
for (int num : nums) seen.insert(num);
for (int i = 1; i <= n; i++) 
{
    if (!seen.count(i)) result.push_back(i);
}

方法2:排序后遍历(不最优)

cpp 复制代码
// 时间复杂度 O(n log n),空间复杂度 O(1) 但修改了原数组
sort(nums.begin(), nums.end());
// 然后遍历检查缺失数字

方法3:负号标记法(最优)

1、时间复杂度 : O(n) - 两次遍历;

2、空间复杂度 : O(1) - 原地修改,无需额外空间;

3、保持信息 : 通过负号标记,既记录了存在性,又保留了原始值的绝对值。


结尾

本文内容到这里就全部结束了,希望大家练习一下这几道题目,这些基础题最好完全掌握!

往期回顾:

【C语言16天强化训练】从基础入门到进阶:Day 9

【C语言16天强化训练】从基础入门到进阶:Day 8

【C语言16天强化训练】从基础入门到进阶:Day 7

【C语言16天强化训练】从基础入门到进阶:Day 6

【C语言16天强化训练】从基础入门到进阶:Day 5

【C语言16天强化训练】从基础入门到进阶:Day 4

【C语言16天强化训练】从基础入门到进阶:Day 3

【C语言16天强化训练】从基础入门到进阶:Day 2

【C语言16天强化训练】从基础入门到进阶:Day 1

结语: 感谢大家的阅读,记得给博主"一键四连",感谢友友们的支持和鼓励!