Python递归与递推的练习(初步了解复杂度,全排列的价值,奇妙的变换,数正方形,高塔登顶方案)

一.了解复杂度

1.1 为什么要考虑复杂度

在比赛中,编程题会有时间和空间的限制,所以我们需要根据时间复杂度和空间复杂度来判断用什么样的算法。

在本章中递归的时间复杂度比递推慢好多所有我们在写代码时对递归和递推的选择中应该尽量考虑递推。

复杂度的计算这里不做讲述,入门学习只需要了解为什么要有复杂度并且可以有能力做i出判断

1.2 常见的时间的复杂度(又快到慢)

1.2.1常数时间复杂度 O(1)

无论输入规模多大,算法执行时间都是固定的。

复制代码
int getFirst(int arr[], int n) {
    return arr[0];  // 只执行一次,与数组大小n无关
}

1.2.2 对数时间复杂度 O(log⁡n)

这个 log⁡log,在大多数情况下,都是以 22 为底,即 O(log⁡2n)O(log2​n),每一步都将问题规模缩小为原来的一部分(通常是一半)。

二分查找示例

复制代码
int binarySearch(int arr[], int n, int target) {
    int left = 0, right = n - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (arr[mid] == target) 
            return mid;
        else if (arr[mid] < target) 
            left = mid + 1;
        else 
            right = mid - 1;
    }
    
    return -1;  // 没找到
}

在这个例子中,每一步我们都将搜索范围缩小一半,所以复杂度是O(log⁡n)

1.2.3 线性时间复杂度 O(n)

算法执行时间与输入规模成正比。

顺序查找示例

复制代码
int linearSearch(int arr[], int n, int target) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == target)
            return i;
    }
    return -1;  // 没找到
}

这个算法最坏情况下需要遍历整个数组,所以复杂度是O(n).

1.2.4线性对数时间复杂度 O(nlog⁡n)

通常出现在分治算法中,如归并排序和快速排序。

归并排序示例

复制代码
void merge(int arr[], int left, int mid, int right) {
    // 合并两个已排序的子数组
    // 这一步的复杂度是O(n)
}

void mergeSort(int arr[], int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        
        mergeSort(arr, left, mid);        // 递归排序左半部分
        mergeSort(arr, mid + 1, right);   // 递归排序右半部分
        
        merge(arr, left, mid, right);     // 合并结果
    }
}

归并排序的时间复杂度是O(nlog⁡n),因为我们将数组分成两半(log⁡n层)并在每一层执行O(n)的操作。

1.2.5 平方时间复杂度 O(n2)

通常出现在嵌套循环中。

冒泡排序示例

复制代码
void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换arr[j]和arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

这个算法有两层嵌套循环,所以复杂度是O(n2)。

1.2.6 立方时间复杂度 O(n3)

通常出现在三层嵌套循环中。

Floyd算法(求所有点对之间的最短路径)示例

复制代码
void floyd(int graph[MAX][MAX], int n) {
    for (int k = 0; k < n; k++) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (graph[i][k] + graph[k][j] < graph[i][j])
                    graph[i][j] = graph[i][k] + graph[k][j];
            }
        }
    }
}

这个算法有三层嵌套循环,所以复杂度是O(n3)。

1.2.7 指数时间复杂度 O(2n)

通常出现在需要列举所有可能性的算法中。

递归求解斐波那契数列(未优化)示例

复制代码
int fibonacci(int n) {
    if (n <= 1)
        return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

这个未优化的算法时间复杂度是O(2n),因为对于每个n,我们需要计算两个子问题。

1.2.8 阶乘时间复杂度 O(n!)

通常出现在需要列举全排列的算法中。

暴力求解旅行商问题示例

复制代码
int tsp(int graph[MAX][MAX], int n) {
    // 存储所有城市
    vector<int> cities;
    for (int i = 1; i < n; i++)
        cities.push_back(i);

    int min_path = INT_MAX;
    
    // 尝试所有可能的城市访问顺序
    do {
        int current_path = 0;
        
        // 计算当前路径长度
        int j = 0;
        for (int i = 0; i < cities.size(); i++) {
            current_path += graph[j][cities[i]];
            j = cities[i];
        }
        current_path += graph[j][0];  // 返回起点
        
        min_path = min(min_path, current_path);
    } while (next_permutation(cities.begin(), cities.end())); // 求全排列
    
    return min_path;
}

这个算法需要枚举所有可能的城市访问顺序,时间复杂度为O(n!)。

1.3 常见的空间复杂度

1.3.1 常数空间复杂度 O(1)

算法使用的额外空间与输入规模无关。

复制代码
int findMax(int arr[], int n) {
    int max_val = arr[0];  // 只使用一个变量
    
    for (int i = 1; i < n; i++) {
        if (arr[i] > max_val)
            max_val = arr[i];
    }
    
    return max_val;
}

这个算法只使用了一个额外变量,空间复杂度是O(1)。

1.3.2 线性空间复杂度 O(n)

算法使用的额外空间与输入规模成正比。

复制代码
int[] duplicateArray(int arr[], int n) {
    int[] result = new int[n];  // 创建一个大小为n的新数组
    
    for (int i = 0; i < n; i++) {
        result[i] = arr[i];
    }
    
    return result;
}

这个算法创建了一个与输入大小相同的新数组,空间复杂度是O(n)。

1.3.3 递归调用栈的空间复杂度(不需要太了解)

递归算法会使用调用栈空间,需要考虑递归深度。

复制代码
int factorial(int n) {
    if (n <= 1)
        return 1;
    return n * factorial(n - 1);
}

这个递归算法的空间复杂度是O(n),因为递归深度为n。

1.3.4 平方空间复杂度 O(n2)

复制代码
int[][] createMatrix(int n) {
    int[][] matrix = new int[n][n];  // 创建一个n×n的矩阵
    
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            matrix[i][j] = i * j;
        }
    }
    
    return matrix;
}

这个算法创建了一个n×n的矩阵,空间复杂度是O(n2)。

二.递归与递推的比较

2.1递归与递推定义比较

**递归:**是指在函数的定义中使用函数自身的方式

示例:

python 复制代码
def factorial_recursive(n):
    # 基本情况
    if n == 0 or n == 1:
        return 1
    # 递归情况
    else:
        return n * factorial_recursive(n - 1)

# 测试
print(factorial_recursive(5))  

递推: 是指通过已知的初始条件,利用特定的递推关系,逐步推导出后续的结果。递推通常使用循环结构来实现,避免了函数的嵌套调用。

示例:

python 复制代码
def factorial_iterative(n):
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

# 测试
print(factorial_iterative(5))  

2.2 性能比较

时间复杂度

  • 递归:在大多数情况下,递归的时间复杂度与递推相同,但由于递归存在函数调用的开销,可能会导致实际运行时间更长。例如,计算斐波那契数列时,简单的递归实现会有大量的重复计算,时间复杂度为 O(2n)。

    def fibonacci_recursive(n):
    if n == 0 or n == 1:
    return n
    else:
    return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)

    测试

    print(fibonacci_recursive(5))

  • 递推:递推的时间复杂度通常较低,因为它避免了重复计算。同样是计算斐波那契数列,递推实现的时间复杂度为 O(n)。

    def fibonacci_iterative(n):
    if n == 0 or n == 1:
    return n
    a, b = 0, 1
    for i in range(2, n + 1):
    a, b = b, a + b
    return b

    测试

    print(fibonacci_iterative(5))

空间复杂度

  • 递归:递归会使用系统栈来保存每一层的函数调用信息,因此空间复杂度与递归深度成正比。在最坏情况下,递归的空间复杂度可能达到 O(n)。
  • 递推:递推通常只需要常数级的额外空间,因此空间复杂度为 O(1)。

2.3适用场景

递归
  • 问题可以自然地分解为规模更小的子问题,且子问题的结构与原问题相同。
  • 问题的求解过程具有明显的递归性质,如树的遍历、图的深度优先搜索等。
递推
  • 问题的初始条件和递推关系明确,且不需要使用递归的思想来解决。
  • 对时间和空间复杂度有较高要求,需要避免递归带来的额外开销。

三.全排列的价值

问题描述

对于一个排列 A=(a1,a2,⋯,an)A=(a1​,a2​,⋯,an​), 定义价值 cici​ 为 a1a1​ 至 ai−1ai−1​ 中小于 aiai​ 的数 的个数, 即 ci=∣{aj∣j<i,aj<ai}∣。 ci​=∣{aj​∣j<i,aj​<ai​}∣。 ​

定义 AA的价值为 ∑i=1nci∑i=1n​ci​ 。

给定 n 求 1 至 n的全排列中所有排列的价值之和。

输入格式

输入一行包含一个整数 n。

输出格式

输出一行包含一个整数表示答案, 由于所有排列的价值之和可能很大, 请 输出这个数除以 998244353 的余数。

样例输入 1

复制代码
3

样例输出 1

复制代码
9

样例输入 2

复制代码
2022

样例输出 2

复制代码
593300958

样例说明

1 至 3 构成的所有排列的价值如下:

(1,2,3):0+1+2=3

(1,3,2):0+1+1=2

(2,1,3):0+0+2=2

(2,3,1):0+1+0=1

(3,1,2):0+0+1=1

(3,2,1):0+0+0=0

故总和为 3+2+2+1+1=9

分析:

假定4个数排序,

  1. 正排序:1,2,3,4价值和为6,反排序:4,3,2,1的价值和为0

  2. 正排序:1,3,2,4价值和为5,反排序:4,2,3,1的价值和为1

    ......

    依次推下去就会发现这种正排序和对应的反排序的价值和相加为一个定值6,所以4个数排序就是24种排序方式,12对排列式,价值和也就有12个6,总价值和就是72

所以当输入n个数时就有n!/2 对排列式,而我们可以靠第一个正排序就能推出这个定值价值和,说白了就是0+1+2+3,就是一个简单的等差数列求和,一对排列式的价值和就是n(n-1)/2

代码:

python 复制代码
a=998244353
n=int(input())
ans=n*(n-1)//2%a
for i in range(3,n+1):
  ans=ans*i%a
print(ans)

四.奇妙的变换

分析:

这道题可以直接按照题意来写,用递归写比较简单但是考虑到时间复杂度,所以可以调用sys库里面的 sys.setrecursionlimit()来提升时间复杂度

代码:

python 复制代码
import sys
sys.setrecursionlimit(100000)

n=int(input())
ans=0
MOD=998244353

def F(x):
    if x>10:
        ans=2*x*F(x-6)

    else:
        ans=x*(x-1)
    return ans

print(F(n)%MOD)

五.数正方形

分析:

这道题本身是不难的,最重要的是需要观察到底怎样才能组成一个正方形 我们发现由n*n个顶点能够组成1-n-1变长的正方形 于是我们先分别统计1-n-1的正方形个数 然后我们发现,当边长大于2时,其内部也能组成斜的正方形 进一步观察发现,边长为2的内部能组成一个,为3的内部能组成2个,以此类推,我们就能计算出来总共的正方形个数了

代码:

python 复制代码
n=int(input())
ans=0

for i in range(1,n):
  if i==1:
    ans+=(n-i)**2
  else:
    ans+=(n-i)**2*i

print(ans%(10**9+7))

总结

其实递归和递推中有很多题可以通过找规律得出,所以大家在写代码时可以多多观察题目带入几个数值找到规律,会对代码有很大帮助

相关推荐
C++ 老炮儿的技术栈4 小时前
设计模式,如单例模式、观察者模式在什么场景下使用
c++·笔记·学习·算法
CYRUS_STUDIO6 小时前
Android 自定义变形 MD5 算法
android·算法·安全
Vitalia8 小时前
⭐算法OJ⭐二叉树的后序遍历【树的遍历】(C++实现)Binary Tree Postorder Traversal
开发语言·c++·算法·二叉树
做一个码农都是奢望8 小时前
MATLAB 调用arduino uno
开发语言·算法·matlab
沈阳信息学奥赛培训9 小时前
C++语法之命名空间二
开发语言·c++·算法
猪猪成9 小时前
【FLOYD+并查集】蓝桥杯算法提高 Degrees of Separation
算法·图论
Dust-Chasing10 小时前
数据结构之顺序表和栈
c语言·数据结构·算法
烟锁池塘柳010 小时前
【数学建模】灰色关联分析模型详解与应用
算法·数学建模
睡觉待开机10 小时前
[算法] 贪心--矩阵消除游戏
算法·游戏·矩阵