文章目录
什么是递归?
递归是通过将一个问题分解成更小的相似子问题来解决问题的方法。每个子问题都是相同的问题的简化版本,直到达到基本情况或边界情况,然后逐级返回结果。
递归的基本原理
递归算法的基本原理可以概括为以下几个步骤:
-
定义基本情况:递归算法必须有一个或多个基本情况,即停止递归的条件。这些条件通常是最简单或最小的问题的情况,可以直接解决而不需要进一步的递归。
-
将问题分解:在递归步骤中,问题被分解为一个或多个规模较小且相似的子问题。这通常涉及将原始问题拆分成更小的子问题,并将它们传递给递归函数本身。
-
递归调用:递归函数会调用自身来解决子问题。这是递归的关键部分。
-
合并结果:当子问题解决后,它们的结果将被合并以解决原始问题。
示例
1. 阶乘函数
def factorial(n):
# 基本情况
if n == 0:
return 1
# 递归调用
else:
return n * factorial(n - 1)
2. Ackermann函数
Ackermann函数是一个著名的递归函数,以其快速增长而闻名,它的定义如下:
A(m, n) =
- n + 1, if m = 0
- A(m - 1, 1), if m > 0 and n = 0
- A(m - 1, A(m, n - 1)), if m > 0 and n > 0
def ackermann(m, n):
if m == 0:
return n + 1
elif m > 0 and n == 0:
return ackermann(m - 1, 1)
elif m > 0 and n > 0:
return ackermann(m - 1, ackermann(m, n - 1))
3. 全排列
#include <iostream>
#include <vector>
using namespace std;
void permute(vector<int>& nums, int start, vector<vector<int>>& result) {
if (start == nums.size()) {
result.push_back(nums);
return;
}
for (int i = start; i < nums.size(); ++i) {
swap(nums[start], nums[i]);
permute(nums, start + 1, result);
swap(nums[start], nums[i]); // 恢复原始顺序
}
}
int main() {
// 输入全排列的元素
vector<int> nums;
int n;
cin >> n;
for (int i = 0; i < n; ++i) {
nums.push_back(i+1);
}
vector<vector<int>> result;
permute(nums, 0, result);
// 输出所有全排列
for (const auto& perm : result) {
for (int num : perm) {
cout << num << " ";
}
cout << endl;
}
return 0;
}
4. 划分
#include <iostream>
#include <vector>
using namespace std;
// 定义一个结构体来表示划分
struct Partition {
vector<int> parts;
};
void printPartition(const Partition& partition) {
for (int i = 0; i < partition.parts.size(); ++i)
{
cout << partition.parts[i];
if (i < partition.parts.size() - 1) {
cout << "+";
}
}
cout << endl;
}
void generatePartitions(int n, int m, Partition& current) {
if (n == 0)
{
printPartition(current);
return;
}
if (n < 0 || m == 0)
{
return;
}
// 包含m的情况
current.parts.push_back(m);
generatePartitions(n - m, m, current);
current.parts.pop_back(); // 回溯
// 不包含m的情况
generatePartitions(n, m - 1, current);
}
int main()
{
int n;
cin >> n;
Partition current;
generatePartitions(n, n, current);
return 0;
}
将一个集合划分为k个子集合,使得每个子集合的元素之和相等。
5. 汉诺塔
汉诺塔(Hanoi)问题是一个经典的递归问题,涉及到将一堆盘子从一个柱子移动到另一个柱子,同时遵守以下规则:
- 只能一次移动一个盘子。
- 永远不会把一个较大的盘子放在一个较小的盘子之上。
#include <iostream>
using namespace std;
void hanoi(int n, char source, char auxiliary, char destination) {
if (n == 1) {
cout << "移动盘子 " << n << " 从 " << source << " 到 " << destination << endl;
return;
}
hanoi(n - 1, source, destination, auxiliary);
cout << "移动盘子 " << n << " 从 " << source << " 到 " << destination << endl;
hanoi(n - 1, auxiliary, source, destination);
}
int main() {
int num_disks;
cin >> num_disks;
hanoi(num_disks, 'A', 'B', 'C'); // 'A', 'B', 'C' 分别代表三个柱子
return 0;
}
hanoi
函数接受三个参数:盘子的数量 n
,源柱子 source
,辅助柱子 auxiliary
,和目标柱子 destination
。递归的思想是将 n-1
个盘子从源柱子经由辅助柱子移动到目标柱子,然后将第 n
个盘子从源柱子移动到目标柱子,最后将 n-1
个盘子从辅助柱子移动到目标柱子。
6.简单选择排序
简单选择排序是其中一种最简单但也最不高效的排序算法之一。
6.1.简单选择排序的原理
简单选择排序是一种基于比较的排序算法,其基本思想是不断地从未排序的元素中选择最小(或最大)的元素,然后将其放置在已排序部分的末尾。这个过程不断重复,直到所有元素都被排序完毕。
6.2.简单选择排序的步骤
下面是简单选择排序的具体步骤:
-
初始状态:将整个待排序的数组分为已排序区间和未排序区间。初始时,已排序区间为空,未排序区间包含所有元素。
-
找到最小元素:在未排序区间中找到最小的元素,通常需要遍历整个未排序区间来查找。
-
交换元素:将找到的最小元素与未排序区间的第一个元素交换位置,将其放入已排序区间的末尾。
-
重复步骤2和步骤3:不断重复步骤2和步骤3,直到未排序区间变为空。此时,已排序区间包含了所有元素,它们按照升序排列。
#include <iostream>
using namespace std;
// 函数:找到数组中的最小元素的索引
int findMinIndex(int arr[], int start, int end) {
int minIndex = start;
for (int i = start + 1; i < end; ++i) {
if (arr[i] < arr[minIndex]) {
minIndex = i;
}
}
return minIndex;
}
// 递归函数:通过选择排序对数组排序
void recursiveSelectionSort(int arr[], int n, int currentIndex = 0) {
if (currentIndex == n - 1) {
// 当前索引达到数组末尾,排序完成
return;
}
// 找到未排序部分的最小元素索引
int minIndex = findMinIndex(arr, currentIndex, n);
// 交换最小元素与当前位置元素
swap(arr[currentIndex], arr[minIndex]);
// 递归调用,继续排序下一个元素
recursiveSelectionSort(arr, n, currentIndex + 1);
}
int main() {
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
cout << "原始数组:";
for (int i = 0; i < n; ++i) {
cout << arr[i] << " ";
}
cout << endl;
recursiveSelectionSort(arr, n);
cout << "排序后数组:";
for (int i = 0; i < n; ++i) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
7.冒泡排序
冒泡排序(Bubble Sort)是计算机科学中最简单的排序算法之一,它的原理和工作方式非常直观。
7.1.冒泡排序的原理
冒泡排序是一种比较排序算法,它的基本思想是反复比较相邻的两个元素,如果它们的顺序不正确,就交换它们,直到整个数组都排序完成。这个过程像气泡一样,较大的元素会逐渐“冒泡”到数组的末尾,因此得名冒泡排序。
7.2.冒泡排序的步骤
-
比较相邻元素:从数组的第一个元素开始,依次比较相邻的两个元素。
-
交换元素位置:如果相邻元素的顺序不正确(例如,前一个元素大于后一个元素),则交换它们的位置。
-
一轮结束:完成一轮比较和可能的交换后,最大的元素已经“冒泡”到数组的末尾。
-
继续下一轮:重复以上步骤,但不包括已经排序好的末尾元素。每轮排序将下一个最大的元素“冒泡”到合适的位置。
-
重复直到排序完成:不断重复步骤1至步骤4,直到整个数组都按照升序排列。
#include <iostream>
using namespace std;
// 辅助函数:将最大元素移动到未排序部分的末尾
void bubbleMaxToEnd(int arr[], int n) {
if (n == 1) {
return; // 基本情况:只有一个元素,无需操作
}
// 内循环:比较相邻元素,将较大元素向后移动
for (int i = 0; i < n - 1; ++i) {
if (arr[i] > arr[i + 1]) {
swap(arr[i], arr[i + 1]);
}
}
}
// 冒泡排序递归函数
void recursiveBubbleSort(int arr[], int n) {
if (n <= 1) {
return; // 基本情况:只有一个元素或没有元素,无需排序
}
// 将最大元素移动到未排序部分的末尾
bubbleMaxToEnd(arr, n);
// 递归调用:对未排序部分继续进行冒泡排序
recursiveBubbleSort(arr, n - 1);
}
int main() {
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
cout << "原始数组:";
for (int i = 0; i < n; ++i) {
cout << arr[i] << " ";
}
cout << endl;
recursiveBubbleSort(arr, n);
cout << "排序后数组:";
for (int i = 0; i < n; ++i) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
八皇后
在8×8的棋盘上放置八个皇后,以确保它们互不攻击,即没有两个皇后在同一行、同一列或同一斜线上。
8.1.背景
八皇后问题起源于国际象棋,其中皇后是一种强大的棋子,它可以在水平、垂直和斜线方向上移动任意多个格子。这个问题首次由高斯(Carl Friedrich Gauss)在18世纪提出,但直到19世纪才引起广泛关注。八皇后问题代表了一类称为“N皇后问题”的问题,其中N表示要放置的皇后数量,而棋盘的大小可以是N×N。
8.2.解决方法
解决八皇后问题的主要方法之一是使用回溯算法。回溯算法是一种通过尝试不同的可能性来解决问题的方法,如果当前尝试的方法不行,就返回到上一步并尝试其他方法。
-
从棋盘的第一行开始,逐行放置皇后,确保每一行只有一个皇后。
-
对于每一行,从左到右尝试放置皇后,检查是否与之前的皇后冲突。冲突意味着在同一行、同一列或同一斜线上有另一个皇后。
-
如果找到一个可以放置皇后的位置,将皇后放在那里,并继续到下一行。
-
如果找不到可行的位置,则回溯到上一行,将之前的皇后重新放置,并继续尝试其他位置。
-
重复步骤3和步骤4,直到成功放置八个皇后或确定无解。
-
所有皇后成功放置后,记录解决方案,并继续寻找其他解决方案。
#include <iostream>
using namespace std;
const int boardSize = 8; // 棋盘大小
int num = 0; // 解的个数
// 打印棋盘
void printBoard(int board[boardSize][boardSize]) {
for (int i = 0; i < boardSize; ++i) {
for (int j = 0; j < boardSize; ++j) {
cout << (board[i][j] ? "Q" : ".") << " ";
}
cout << endl;
}
}
// 检查在(row, col)放置皇后是否安全
bool isSafe(int board[boardSize][boardSize], int row, int col) {
// 检查列是否有其他皇后
for (int i = 0; i < row; ++i) {
if (board[i][col]) {
return false;
}
}
// 检查左上到右下的对角线是否有其他皇后
for (int i = row, j = col; i >= 0 && j >= 0; --i, --j) {
if (board[i][j]) {
return false;
}
}
// 检查左下到右上的对角线是否有其他皇后
for (int i = row, j = col; i >= 0 && j < boardSize; --i, ++j) {
if (board[i][j]) {
return false;
}
}
return true;
}
// 递归函数:解决八皇后问题
bool solveNQueens(int board[boardSize][boardSize], int row) {
if (row == boardSize) {
// 所有皇后都已放置,找到了一个解决方案
printBoard(board);
cout << endl;
num++;
return true;
}
bool foundSolution = false;
for (int col = 0; col < boardSize; ++col) {
if (isSafe(board, row, col)) {
// 放置皇后
board[row][col] = 1;
// 递归尝试下一行
foundSolution = solveNQueens(board, row + 1) || foundSolution;
// 回溯
board[row][col] = 0;
}
}
return foundSolution;
}
int main() {
int board[boardSize][boardSize] = { 0 }; // 初始化棋盘
if (!solveNQueens(board, 0)) {
cout << "无解" << endl;
}
cout << "共有" << num << "种解法" << endl;
return 0;
}
第一归纳法
第一归纳法是一种证明数学命题的方法,通常用于证明对于所有正整数n都成立的陈述。它的基本思想可以概括为以下三个步骤:
-
基本情况(Base Case):首先,证明当n等于某个特定正整数时,陈述成立。这个特定的正整数通常是最小的正整数,例如1。
-
归纳假设(Inductive Hypothesis):假设当n等于某个正整数k时,陈述也成立,即我们假设第k个情况是正确的。
-
归纳步骤(Inductive Step):接下来,我们使用归纳假设来证明当n等于k+1时,陈述仍然成立。这通常涉及将问题从n=k的情况推广到n=k+1的情况。
通过这三个步骤,可以证明陈述对于所有正整数都成立。第一归纳法的关键在于将问题分解成小的部分,并通过递推的方式证明它们的正确性。
第二归纳法
第二归纳法与第一归纳法类似,但更适用于证明一般性的数学命题。它的基本思想可以概括为以下三个步骤:
-
基本情况(Base Case):与第一归纳法相同,首先证明陈述对于某个特定情况成立。
-
归纳假设(Inductive Hypothesis):假设对于所有n小于等于某个正整数k,陈述都成立。
-
归纳步骤(Inductive Step):接下来,使用归纳假设来证明对于n等于k+1的情况,陈述也成立。
第二归纳法的关键在于它不仅仅适用于正整数,还可以用于更一般的情况,例如自然数或其他数学结构。
应用举例
1 + 2 + 3 + . . . + n = n ( n + 1 ) 2 1 + 2 + 3 + ... + n = \frac{n(n+1)}{2} 1+2+3+...+n=2n(n+1)
第一归纳法:
- 基本情况:当n等于1时,左边是1,右边是 1 ( 1 + 1 ) 2 \frac{1(1+1)}{2} 21(1+1),两边相等。
- 归纳假设:假设对于某个正整数k,公式成立,即 1 + 2 + 3 + . . . + k = k ( k + 1 ) 2 1 + 2 + 3 + ... + k = \frac{k(k+1)}{2} 1+2+3+...+k=2k(k+1)。
- 归纳步骤:我们将左边的和扩展到n=k+1,得到 1 + 2 + 3 + . . . + k + ( k + 1 ) 1 + 2 + 3 + ... + k + (k+1) 1+2+3+...+k+(k+1),然后使用归纳假设,得到 k ( k + 1 ) 2 + ( k + 1 ) \frac{k(k+1)}{2} + (k+1) 2k(k+1)+(k+1),简化后等于 ( k + 1 ) ( k + 2 ) 2 \frac{(k+1)(k+2)}{2} 2(k+1)(k+2),与右边相等。
第二归纳法:
- 基本情况:同样是n等于1,左边等于1,右边等于 1 ( 1 + 1 ) 2 \frac{1(1+1)}{2} 21(1+1)。
- 归纳假设:假设对于所有n小于等于k,公式成立。
- 归纳步骤:考虑n=k+1的情况,使用归纳假设,我们知道 1 + 2 + 3 + . . . + k = k ( k + 1 ) 2 1 + 2 + 3 + ... + k = \frac{k(k+1)}{2} 1+2+3+...+k=2k(k+1)。现在将它与k+1相加,得到左边的和,然后使用右边的公式 k ( k + 1 ) 2 + ( k + 1 ) \frac{k(k+1)}{2} + (k+1) 2k(k+1)+(k+1),简化后等于 ( k + 1 ) ( k + 2 ) 2 \frac{(k+1)(k+2)}{2} 2(k+1)(k+2),与右边相等。
这两种归纳法都可以成功证明这个数学命题。
如何使用递归
递归算法是许多经典计算机科学问题的解决方法之一,包括树的遍历、图的深度优先搜索等。在使用递归时,确保定义了基本情况以避免无限递归。此外,递归可能不是最高效的解决方案,因此在某些情况下,迭代可能更好。