2024年9月

在 C 语言中,动态数组指的是在程序运行时动态分配的数组,其大小不需要在编译时确定。我们通过动态内存分配函数(如 malloccallocrealloc)来实现动态数组。

动态数组的基本操作

  1. malloc 分配动态数组
    malloc 函数可以分配指定大小的内存。它返回一个 void* 类型的指针,需要强制转换为适当的指针类型。

    语法:

    void* malloc(size_t size);

    例如,创建一个动态分配的整型数组:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int n;  // 数组的大小
        printf("Enter the number of elements: ");
        scanf("%d", &n);
    
        // 动态分配一个包含 n 个整数的数组
        int* array = (int*) malloc(n * sizeof(int));
    
        // 检查内存分配是否成功
        if (array == NULL) {
            printf("Memory allocation failed!\n");
            return 1;
        }
    
        // 使用数组
        for (int i = 0; i < n; i++) {
            array[i] = i + 1;  // 示例填充数组
        }
    
        // 打印数组
        for (int i = 0; i < n; i++) {
            printf("%d ", array[i]);
        }
        printf("\n");
    
        // 释放内存
        free(array);
    
        return 0;
    }
    • malloc 分配 n * sizeof(int) 个字节,用于存储 n 个整数。
    • 如果内存分配失败,malloc 返回 NULL,程序可以通过这个指针检查分配是否成功。
    • 使用完动态数组后,必须调用 free() 来释放分配的内存。
  2. calloc 分配动态数组
    calloc 是另一种动态内存分配函数,它会将分配的内存初始化为零。

    语法:

    void* calloc(size_t num, size_t size);

    例如:

    int* array = (int*) calloc(n, sizeof(int));

    callocmalloc 的主要区别是,calloc 分配的内存会被初始化为零,而 malloc 分配的内存是未初始化的。

  3. realloc 调整动态数组大小
    如果你需要调整动态数组的大小,可以使用 realloc 函数。它会重新分配内存,保留原有的数据内容。

    语法:

    void* realloc(void* ptr, size_t new_size);

    例如:

    array = (int*) realloc(array, new_size * sizeof(int));
    • realloc 接收一个指向之前分配内存的指针 ptr 和新的大小 new_size
    • 如果重新分配失败,realloc 返回 NULL,原来的内存块依然有效。

示例:使用 realloc 调整数组大小

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n = 5;
    int* array = (int*) malloc(n * sizeof(int));

    // 填充数组
    for (int i = 0; i < n; i++) {
        array[i] = i + 1;
    }

    printf("Original array: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");

    // 调整数组大小
    int new_size = 10;
    array = (int*) realloc(array, new_size * sizeof(int));

    // 为新数组的元素赋值
    for (int i = n; i < new_size; i++) {
        array[i] = i + 1;
    }

    printf("Resized array: ");
    for (int i = 0; i < new_size; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");

    // 释放内存
    free(array);

    return 0;
}

总结:

  • 动态数组的大小在运行时分配,不需要在编译时确定。
  • 使用 malloccalloc 来分配动态数组,使用 realloc 来调整数组大小。
  • 使用完动态数组后,记得调用 free() 来释放内存。

在 C++17 及之后的版本中,标准库中引入了一个内置的 gcd 函数,用于计算两个整数的最大公约数(GCD)。它位于 <numeric> 头文件中,并使用欧几里得算法来高效地计算 GCD。

使用方法:

  • 该函数位于 std 命名空间下,函数签名如下:

    template <class T>
    T gcd(T a, T b);
  • 该模板函数可以接受任意整数类型的参数,包括 intlonglong long 等。

示例代码:

#include <iostream>
#include <numeric>  // 包含 gcd 函数
#include <vector>

int main() {
    int a = 56;
    int b = 98;

    // 使用 C++17 的内置 gcd 函数
    int result = std::gcd(a, b);
    std::cout << "GCD of " << a << " and " << b << " is " << result << std::endl;

    return 0;
}

输出:

GCD of 56 and 98 is 14

特点和优点:

  1. 高效实现std::gcd 函数使用的是经过优化的欧几里得算法,能够高效地计算两个整数的最大公约数。
  2. 支持的类型:它支持任意整数类型,包括有符号和无符号的整数类型。
  3. C++ 标准库保证:因为是标准库提供的函数,能确保跨平台的兼容性和性能优化。

使用范围:

  • 你可以使用 std::gcd 来计算各种整数类型的 GCD,包括 intlonglong long,甚至自定义类型,只要它们遵循整数类型的基本规则。

示例:结合多个数的 GCD

你可以利用 std::gcd 来计算多个数的 GCD。假设我们想要计算一个数组中所有元素的 GCD,可以像这样结合 std::gcdstd::accumulate

#include <iostream>
#include <numeric>  // 包含 gcd 和 accumulate 函数
#include <vector>

int main() {
    std::vector<int> nums = {56, 98, 42};

    // 使用 accumulate 来计算多个数的 GCD
    int result = std::accumulate(nums.begin(), nums.end(), nums[0], std::gcd<int, int>);

    std::cout << "GCD of the array is " << result << std::endl;

    return 0;
}

输出:

GCD of the array is 14

在c语言中也可以这样实现

在标准的 C 语言库中(如 stdio.hstdlib.h),没有内置的 gcd 函数。因此,如果需要在 C 语言中计算两个整数的最大公约数(GCD),你需要自己编写该函数。

代码:

long long gcd(long long a,long long b)
{
    while(a != b)
    {
        if(a > b)
        {
            a = a - b;
        }
        else
        {
            b = b - a;
        }
    }
    return a;  // 返回a或b,因为最后它们相等并且是GCD
}

在每次循环中通过减法逐步接近两个数的最大公约数,源于“更相减损术”的原理。这种方法可以追溯到古代的数学家欧几里得,并且可以用来证明两个数的最大公约数在减法中逐渐逼近。

这段代码的目的是计算两个数 ab 的最大公约数(GCD,Greatest Common Divisor)以下是对代码的解释:

  1. while 循环执行的是一种较原始的 GCD 计算方法,称为“减法法”。它通过逐步减少两个数的值直到它们相等。

    • 在每次循环中,它比较 aabb 的大小。如果 aa 大于 bb,就用 aa - bb 更新 aa,否则用 bb - aa 更新 bb。最终,aabb 会变成相等的数,而这个数就是它们的最大公约数。

原理:

假设我们有两个数 ab,且 a > b。在减法法中,我们每次用较大的数减去较小的数,直到两个数相等。以下是这种方法的核心思想:

  1. 保持最大公约数不变

    • dab 的最大公约数,即 d = gcd(a, b),这意味着 ab 都可以被 d 整除(即 a = d * mb = d * n,其中 mn 是整数)。
    • 当我们用较大的数 a 减去较小的数 b 后,得到的新数 a' = a - b
    • 注意到 a' = a - b 仍然可以被 d 整除,因为:
      [
      a' = a - b = d m - d n = d * (m - n)
      ]
      所以 a' 也是 d 的倍数,仍然与 b 有相同的最大公约数。
  2. 收敛到最大公约数

    • 在每次减法后,新的数对 (a', b)(a, b') 的差值越来越小。最终,两个数会变得相等,而这个相等的数就是 ab 的最大公约数。
    • 由于在每次减法中,两个数的最大公约数保持不变,最终我们得到的相等数(即 aa = bb)就是它们的最大公约数。

举例说明:

假设 a = 24b = 18,我们通过每次减法逐步计算最大公约数:

  1. 第一次循环:a = 24, b = 18,因为 a > b,所以 a = a - b = 24 - 18 = 6
  2. 第二次循环:a = 6, b = 18,因为 b > a,所以 b = b - a = 18 - 6 = 12
  3. 第三次循环:a = 6, b = 12,因为 b > a,所以 b = b - a = 12 - 6 = 6
  4. 最后,a = b = 6,所以最大公约数是 6。

每一步中,最大公约数一直保持不变,直到最后两个数相等为止,而这个相等的数就是最大公约数。

优化:

实际计算 GCD 可以使用 欧几里得算法,它比这种减法法更高效。改进后的代码如下:

递归实现的代码示例:

long long gcd(long long a,long long b)
{
    if(b == 0)
        return a;
    return gcd(b, a % b);  // 欧几里得算法
}

迭代实现的代码示例:

除了递归,你也可以用迭代的方式实现:

#include <iostream>

long long gcd(long long a, long long b) {
    while (b != 0) {
        long long temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

解释:

  1. 欧几里得算法的原理是通过递归地使用模运算来逐步缩小问题规模,直到其中一个数为 0。此时,另一个数就是它们的最大公约数。
  2. 在循环中,a % b 计算出 a 除以 b 的余数,接着交换 ab 的值,继续计算,直到 b 为 0。

这个实现方法比使用减法的方式更高效,尤其对于较大的数。

欧几里得算法(Euclidean algorithm)是一个用于计算两个整数的最大公约数(GCD,Greatest Common Divisor)的经典算法。它的基本原理是基于一个非常重要的数学定理:

定理:

dab 的最大公约数,则 d 也是 ba % b 的最大公约数。

证明:

ab 是两个整数,且 a > b,则可以写成如下的关系:
a = b * q + r
其中:

  • q 是商(整数),
  • r 是余数,即 r = a % b

根据这个关系式,可以重写为:
r = a - b * q

现在,让我们从 d = gcd(a, b) 出发进行推导,假设 dab 的最大公约数。根据最大公约数的定义:

  • d 可以整除 a,即 a = d * m,其中 m 是某个整数。
  • d 也可以整除 b,即 b = d * n,其中 n 是某个整数。

接下来,我们要证明 d 也可以整除 r(即 a % b)。根据前面的等式:
r = a - b * q
代入 a = d * mb = d * n
r = d m - (d n) * q
r = d (m - n q)

因此,rd 的倍数,这说明 d 也整除 r

结论:

  • 因为 d 整除 b,并且 d 也整除 r,根据最大公约数的定义,dbr 的公约数。
  • 因此,d = gcd(b, r),而 r = a % b,这就证明了:
    gcd(a, b) = gcd(b, a % b)

这个过程可以重复,直到余数为 0。此时,剩下的数就是 ab 的最大公约数。

算法原理:

  1. 给定两个整数 ab,其中 a > b
  2. 根据定理,gcd(a, b) 等于 gcd(b, a % b)。换句话说,较大的数 a 可以被较小的数 b 除尽,并且我们继续用 a 除以 b 的余数来替换 a
  3. 不断重复这个过程,将 ba % b 作为新的一对数,直到其中一个数变为 0。
  4. 当其中一个数为 0 时,另一个数就是它们的最大公约数。

欧几里得算法的核心思想是基于一个重要的数学定理,这个定理解释了为什么最大公约数(GCD)可以通过 ab 转换为 ba % b 来计算。下面是这个定理的详细解释。

总结:

  • 在欧几里得算法中,我们通过每次将 a 替换为 a % b,并不断计算余数,最终将问题缩小。
  • 由于最大公约数 d 可以同时整除 ba % b,因此 gcd(a, b) 可以递归地转换为 gcd(b, a % b)
  • 最终,当余数为 0 时,b 就是 ab 的最大公约数。

这个定理和算法的关键在于利用了“除法余数不影响公约数”的特性,使得我们可以通过递归或迭代高效地计算出最大公约数。

欧几里得算法的优点:

  • 高效:每一步都通过取模运算来减少较大数的大小,快速缩小问题的规模。相比于简单的逐个枚举约数,欧几里得算法能在对数时间内找到最大公约数。
  • 简单实现:该算法只需要简单的取模运算,容易实现且计算量小。

另:求最小公倍数的方法:
另两数相乘然后除以最大公约数。
tip:为了防止爆int可以先除,再乘。

C++ 标准库中的 sort 函数是一个非常常用的函数,用来对序列进行排序。它位于头文件 <algorithm> 中。sort 函数使用了一种混合了快速排序、插入排序和堆排序的算法(通常是 introsort),在最坏情况下的时间复杂度为 O(n log n),而最好的情况下是 O(n log n) 或 O(n)。

函数签名:

template< class RandomIt >
void sort( RandomIt first, RandomIt last );

template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp );

参数解释:

  1. firstlast:这两个参数是迭代器,表示要排序的范围。first 是序列的开始迭代器,last 是序列的结束迭代器,排序的范围为 [first, last),注意 last 是不包含在排序范围内的。
    例如:sort(dp,dp+n) 、第一个参数就是从dp[0]开始,第二个参数表示到dp[n-1]结束,又例如 sort(dp+1,dp+7)、就是表示对从dp[1]开始 dp[6]结束的元素进行排序。
  2. Compare(可选):这是一个二元比较函数,定义排序规则。如果不提供该参数,默认是按升序排序(使用 < 运算符)。如果你想自定义排序规则,例如降序或基于某个属性排序,可以传入这个函数对象。

示例代码:

1. 基本使用(升序排序)

#include <iostream>
#include <algorithm>  // 包含 sort 函数
#include <vector>

int main() {
    std::vector<int> nums = {4, 2, 8, 5, 7, 1};
    
    // 使用默认的升序排序
    std::sort(nums.begin(), nums.end());
    
    std::cout << "Sorted in ascending order: ";
    for (int n : nums) {
        std::cout << n << " ";
    }
    std::cout << std::endl;

    return 0;
}

输出:

Sorted in ascending order: 1 2 4 5 7 8

2. 自定义比较函数(降序排序)

#include <iostream>
#include <algorithm>
#include <vector>

int main() {
    std::vector<int> nums = {4, 2, 8, 5, 7, 1};
    
    // 使用自定义的降序比较函数
    std::sort(nums.begin(), nums.end(), std::greater<int>());
    
    std::cout << "Sorted in descending order: ";
    for (int n : nums) {
        std::cout << n << " ";
    }
    std::cout << std::endl;

    return 0;
}

输出:

Sorted in descending order: 8 7 5 4 2 1

自定义比较函数

你可以定义一个自己的比较函数来排序。例如,假设我们有一个自定义的结构体,并且想要基于某个字段进行排序:

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>

struct Person {
    std::string name;
    int age;
};

// 自定义比较函数,按 age 排序
bool compareByAge(const Person& a, const Person& b) {
    return a.age < b.age;  // 按年龄升序
}

int main() {
    std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
    
    // 使用自定义比较函数排序
    std::sort(people.begin(), people.end(), compareByAge);
    
    std::cout << "Sorted by age: \n";
    for (const Person& person : people) {
        std::cout << person.name << ": " << person.age << "\n";
    }

    return 0;
}

输出:

Sorted by age: 
Bob: 25
Alice: 30
Charlie: 35

总结:

  • C++ 的 sort 函数提供了灵活的排序功能,既可以使用默认的升序排序,也可以通过自定义比较函数实现各种自定义排序逻辑。
  • sort 适用于随机访问迭代器,因此可以对 std::vectorstd::array 等容器进行排序。
  • 如果你需要保持元素之间的顺序不变(例如对于相同的元素),可以使用 stable_sort,它是稳定排序,能保持相同元素的原有顺序。

调和级数(Harmonic Series)是一种数学级数,它的形式为:

1
即:调和级数的第 n 项为前 n 个倒数的和。

特点:

  1. 发散性:虽然调和级数中的每一项(从第二项开始)都越来越小,但总和却没有上界,随着 n 越来越大,调和级数的和也会无限增大。因此调和级数是发散的,即没有一个有限的极限值。
  2. 应用:调和级数在数学分析、数论、概率论等领域都有应用,比如用于估算某些算法的时间复杂度等。

举例:

  • 当 n=1 时,调和级数的和为
  • 当 n=2 时,调和级数的和为
  • 当 n=3 时,调和级数的和为

调和级数增长速度:

虽然调和级数的每一项越来越小,但由于它是一个发散级数,尽管增长速度很慢,和仍然是无限大的。当 n 足够大时, 的值会逐渐逼近 (其中 γ\gammaγ 是欧拉常数,大约为 0.577)。

简单来说,调和级数是描述一连串倒数相加的数列,随着项数增加,虽然每项越来越小,但总和会逐渐变大。