1)递归函数的特点和使用场景是什么?
递归函数的定义:
递归函数是在函数的定义中使用函数自身的函数。简单来说,就是一个函数在它的函数体内部调用自身,以解决具有递归性质的问题。这种函数调用自身的行为会反复进行,直到满足特定的终止条件。
示例说明:
以计算阶乘为例,一个正整数n的阶乘(用n!表示)的定义是:n!=n*(n-1)(n-2)...*1,这个计算过程可以用递归函数来实现。
int factorial(int n)
{
if (n == 0 || n == 1) {
return 1; //终止条件
} else {
return n * factorial(n - 1); //函数调用自身
}
}
执行过程如下:
假设我们调用factorial(3),首先,n = 3,不满足终止条件,所以执行return 3 * factorial(2)
然后,计算factorial(2),此时n = 2,同样不满足终止条件,执行return 2 * factorial(1)
接着,计算factorial(1),此时n = 1,满足终止条件,返回 1
回到factorial(2)的计算中,factorial(2)得到的值为2 * 1 = 2
再回到factorial(3)的计算中,factorial(3)得到的值为3 * 2 = 6
递归函数的关键要素
终止条件:这是递归函数中至关重要的部分。如果没有终止条件,函数将会无限地调用自身,导致栈溢出(程序运行时内存耗尽)。终止条件用于定义递归的边界,在这个边界上,函数可以直接返回一个结果,而不再调用自身。
问题分解:递归函数能够将一个复杂的问题逐步分解为更简单、规模更小的子问题,这些子问题的结构与原问题相同。在阶乘的例子中,计算 n的阶乘被分解为计算n-1 的阶乘这个子问题,然后通过不断地分解,直到达到终止条件。
递归函数的特点:
自我调用:递归函数最显著的特点是在函数体内部会调用自身。例如,计算阶乘的递归函数factorial(n),当n > 1时,factorial(n) = n * factorial(n - 1),函数在执行过程中会不断地调用自身来逐步求解问题。
终止条件:必须有一个或多个终止条件,以防止函数无限递归。对于阶乘函数,当n = 0或n = 1时,factorial(n)=1,这就是终止条件。如果没有这个终止条件,函数会一直调用自身,导致栈溢出错误。
问题分解:递归函数能够将一个复杂的问题分解为规模更小、与原问题结构相同的子问题。以斐波那契数列为例,F(n)=F(n - 1)+F(n - 2)(n > 1),它将计算第n个斐波那契数的问题分解为计算第n - 1个和第n - 2个斐波那契数的子问题,直到分解到n = 0或n = 1(此时F(0)=0,F(1)=1)。
栈的使用:每次函数调用自身时,当前函数的状态(包括局部变量、参数等)会被保存到系统栈中。当最内层的递归调用返回后,系统会从栈中恢复上一层函数的状态,继续执行。这就使得递归函数在执行过程中,栈的使用情况会随着递归深度的增加而增长。
使用场景:
1.数学计算
阶乘计算:如前面提到的n!的计算。n的阶乘定义为n * (n - 1) * (n - 2) ... * 1,可以用递归函数很自然地表示这个计算过程(如上)。
斐波那契数列:斐波那契数列的定义是F(n)=F(n - 1)+F(n - 2)(n > 1),F(0)=0,F(1)=1。通过递归函数可以直接按照这个定义来计算数列中的值。
int fibonacci(int n) {
if (n <= 1) { //终止条件
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
2.数据结构遍历
树的遍历:对于二叉树,有先序遍历、中序遍历和后序遍历。以先序遍历为例,它的规则是根节点 - 左子树 - 右子树。可以使用递归函数来实现,先访问根节点,然后递归地遍历左子树和右子树。例如,对于一个二叉树节点结构体struct TreeNode { int val; TreeNode left; TreeNode* right; };,先序遍历的递归函数可以写成:
void preorderTraversal(TreeNode* root) {
if (root == nullptr) {
return;
}
cout << root->val << " ";
preorderTraversal(root->left);
preorderTraversal(root->right);
}
图的深度优先搜索(DFS):在图的深度优先搜索中,从一个起始顶点开始,沿着一条路径尽可能深地访问顶点,直到不能再继续或者达到目标顶点,然后回溯。递归函数可以很好地实现这个过程。例如,在一个简单的无向图数据结构中,用邻接表表示图,visited数组标记顶点是否被访问过,递归的深度优先搜索函数可以写成(C++):
v:表示当前正在访问的顶点的编号
visited:一个布尔类型的向量引用,用于记录每个顶点是否已经被访问过
adjList:一个二维整数向量的引用,用于表示图的邻接表表示。其中adjList[i]存储了与顶点i相邻的所有顶点的编号
它从给定的顶点v开始,递归地遍历图中的所有连通部分,将访问到的顶点编号输出,确保每个顶点只被访问一次。
void DFS(int v, vector<bool>& visited, vector<vector<int>>& adjList) {
visited[v] = true;//首先将当前顶点v标记为已访问,然后输出该顶点的编号
cout << v << " ";
for (int u : adjList[v]) {//遍历与顶点v相邻的所有顶点
if (!visited[u]) { //如果发现某个相邻顶点u尚未被访问过,那么就递归地调用DFS函数,以继续对该顶点进行深度优先搜索
DFS(u, visited, adjList);
}
}
}
3.分治法问题求解
归并排序:归并排序是一种高效的排序算法,它采用分治法的思想。将一个数组分成两半,分别对这两半进行排序(这是递归调用排序的过程),然后将排好序的两半合并起来。递归函数用于处理子数组的排序过程,代码示例如下:
// 归并排序的递归函数
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);
}
}
2)什么是回调函数?有什么特点?
定义:
回调函数是一种作为参数传递给另一个函数的函数。这个被传入的函数(通常称为高阶函数)会在某个特定的时间点或者满足特定条件时调用这个作为参数的函数。简单来说,就是把一个函数 A 传递给函数 B,然后让函数 B 在适当的时候去执行函数 A。
示例说明:
下面的cmp函数就是回调函数,sort函数(被传入的)是一个高阶函数,它接收一个比较函数(这里是cmp)作为参数。sort函数在内部对数组nums进行排序的过程中,会在需要比较两个元素大小时调用cmp函数。
bool cmp(int a,int b)
{
return a > b;//从大到小排序
}
int findKthLargest(vector<int>& nums, int k) {
sort(nums.begin(),nums.end(),cmp);
for(auto it:nums)
{
cout<<it<<" ";
}
}
};
特点:
1.灵活性高
可以方便地定制功能:将数组进行降序排序时,只需要修改回调函数,直接将cmp函数内部改为return a<b,十分灵活简单。
bool cmp(int a,int b)
{
return a < b;//从小到大排序
}
int findKthLargest(vector<int>& nums, int k) {
sort(nums.begin(),nums.end(),cmp);
for(auto it:nums)
{
cout<<it<<" ";
}
}
};
2.解耦性好
有助于分离不同的功能模块。以事件驱动编程为例,在一个图形用户界面(GUI)应用中,当用户点击一个按钮时,系统会触发一个 “点击事件”。通过将处理点击事件的函数作为回调函数传递给按钮的事件处理机制,按钮本身不需要知道具体如何处理点击操作,而只需要在被点击时调用这个回调函数即可。这样,按钮的显示和交互逻辑与具体的操作处理逻辑被分离,使得代码结构更加清晰。
3.异步支持
在异步编程中扮演重要角色。比如在 JavaScript 的setTimeout函数,它接受一个回调函数和一个延迟时间(以毫秒为单位)作为参数。当延迟时间结束后,浏览器会将这个回调函数放入事件循环队列中等待执行。这样,程序可以在等待异步操作(如文件读取、网络请求等)完成的过程中,继续执行其他任务,提高了程序的运行效率。例如:
console.log("开始");
setTimeout(() => {
console.log("延迟后的操作");
}, 1000);
console.log("结束");
4.可扩展性强
可以方便地添加新的功能。例如,在一个数据处理管道中,有一个函数用于读取数据,一个函数用于清洗数据,一个函数用于分析数据。如果想要在分析数据之后添加一个新的功能,比如数据可视化,可以通过将数据可视化的函数作为回调函数传递给分析数据的函数或者整个数据处理管道,在适当的位置调用这个新的回调函数,从而实现功能的扩展,而无需对原有的数据处理函数进行大规模的修改。