首页 > 其他分享 >递归 & 分治

递归 & 分治

时间:2024-09-16 21:54:08浏览次数:11  
标签:sort 问题 递归 start 分治 seg merge

递归

定义

递归(英语:Recursion),在数学和计算机科学中是指在函数的定义中使用函数自身的方法,在计算机科学中还额外指一种通过重复将问题分解为同类的子问题而解决问题的方法。

引入

递归的基本思想是某个函数直接或者间接地调用自身,这样原问题的求解就转换为了许多性质相同但是规模更小的子问题。求解时只需要关注如何把原问题划分成符合条件的子问题,而不需要过分关注这个子问题是如何被解决的

以下是一些有助于理解递归的例子:

1:如何给一堆数字排序?答:分成两半,先排左半边再排右半边,最后合并就行了,至于怎么排左边和右边,请重新阅读这句话。
2:你今年几岁?答:去年的岁数加一岁,1000 年我出生。

递归在数学中非常常见。例如,集合论对自然数的正式定义是:1 是一个自然数,每个自然数都有一个后继,这一个后继也是自然数。递归代码最重要的两个特征:结束条件和自我调用。自我调用是在解决子问题,而结束条件定义了最简子问题的答案。

int func(传入数值) {
  if (终止条件) return 最小子问题解;
  return func(缩小规模);
}

为什么要写递归
1.结构清晰,可读性强。例如,分别用不同的方法实现 归并排序:

​//c++
// 不使用递归的归并排序算法
template <typename T>
void merge_sort(vector<T> a) {
  int n = a.size();
  for (int seg = 1; seg < n; seg = seg + seg)
    for (int start = 0; start < n - seg; start += seg + seg)
      merge(a, start, start + seg - 1, std::min(start + seg + seg - 1, n - 1));
}

// 使用递归的归并排序算法
template <typename T>
void merge_sort(vector<T> a, int front, int end) {
  if (front >= end) return;
  int mid = front + (end - front) / 2;
  merge_sort(a, front, mid);
  merge_sort(a, mid + 1, end);
  merge(a, front, mid, end);
}

​
#Python​
# 不使用递归的归并排序算法
def merge_sort(a):
    n = len(a)
    seg, start = 1, 0
    while seg < n:
        while start < n - seg:
            merge(a, start, start + seg - 1, min(start + seg + seg - 1, n - 1))
            start = start + seg + seg
        seg = seg + seg


# 使用递归的归并排序算法
def merge_sort(a, front, end):
    if front >= end:
        return
    mid = front + (end - front) / 2
    merge_sort(a, front, mid)
    merge_sort(a, mid + 1, end)
    merge(a, front, mid, end)

​

2.显然,递归版本比非递归版本更易理解。递归版本的做法一目了然:把左半边排序,把右半边排序,最后合并两边。而非递归版本看起来不知所云,充斥着各种难以理解的边界计算细节,特别容易出 bug,且难以调试。练习分析问题的结构。当发现问题可以被分解成相同结构的小问题时,递归写多了就能敏锐发现这个特点,进而高效解决问题。

递归的缺点
在程序执行中,递归是利用堆栈来实现的。每当进入一个函数调用,栈就会增加一层栈帧,每次函数返回,栈就会减少一层栈帧。而栈不是无限大的,当递归层数过多时,就会造成 栈溢出 的后果。

显然有时候递归处理是高效的,比如归并排序;有时候是低效的,比如数孙悟空身上的毛,因为堆栈会消耗额外空间,而简单的递推不会消耗空间。比如这个例子,给一个链表头,计算它的长度:

// 典型的递推遍历框架
int size(Node *head) {
  int size = 0;
  for (Node *p = head; p != nullptr; p = p->next) size++;
  return size;
}

// 我就是要写递归,递归天下第一
int size_recursion(Node *head) {
  if (head == nullptr) return 0;
  return size_recursion(head->next) + 1;
}

分治
定义

分治(英语:Divide and Conquer),字面上的解释是「分而治之」,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

过程
分治算法的核心思想就是「分而治之」。大概的流程可以分为三步:分解 -> 解决 -> 合并。

分解原问题为结构相同的子问题。
分解到某个容易求解的边界之后,进行递归求解。
将子问题的解合并成原问题的解。
分治法能解决的问题一般有如下特征:

该问题的规模缩小到一定的程度就可以容易地解决。
该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质,利用该问题分解出的子问题的解可以合并为该问题的解。
该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。

注意

如果各子问题是不独立的,则分治法要重复地解公共的子问题,也就做了许多不必要的工作。此时虽然也可用分治法,但一般用 动态规划 较好。

以归并排序为例。假设实现归并排序的函数名为 merge_sort。明确该函数的职责,即 对传入的一个数组排序。这个问题显然可以分解。给一个数组排序等于给该数组的左右两半分别排序,然后合并成一个数组。

void merge_sort(一个数组) {
  if (可以很容易处理) return;
  merge_sort(左半个数组);
  merge_sort(右半个数组);
  merge(左半个数组, 右半个数组);
}

传给它半个数组,那么处理完后这半个数组就已经被排好了。注意到,merge_sort 与二叉树的后序遍历模板极其相似。因为分治算法的套路是 分解 -> 解决(触底)-> 合并(回溯),先左右分解,再处理合并,回溯就是在退栈,即相当于后序遍历。

merge 函数的实现方式与两个有序链表的合并一致。

要点
写递归的要点
明白一个函数的作用并相信它能完成这个任务,千万不要跳进这个函数里面企图探究更多细节, 否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。

以遍历二叉树为例。

void traverse(TreeNode* root) {
  if (root == nullptr) return;
  traverse(root->left);
  traverse(root->right);
}

这几行代码就足以遍历任何一棵二叉树了。对于递归函数 traverse(root),只要相信给它一个根节点 root,它就能遍历这棵树。所以只需要把这个节点的左右节点再传给这个函数就行了。

同样扩展到遍历一棵 N 叉树。与二叉树的写法一模一样。不过,对于 N 叉树,显然没有中序遍历。

void traverse(TreeNode* root) {
  if (root == nullptr) return;
  for (auto child : root->children) traverse(child);
}

区别
递归与枚举的区别
递归和枚举的区别在于:枚举是横向地把问题划分,然后依次求解子问题;而递归是把问题逐级分解,是纵向的拆分。

递归与分治的区别
递归是一种编程技巧,一种解决问题的思维方式;分治算法很大程度上是基于递归的,解决更具体问题的算法思想。

标签:sort,问题,递归,start,分治,seg,merge
From: https://blog.csdn.net/liucinxi/article/details/142307214

相关文章

  • 深层剖析函数递归的作用
    目录什么是递归递归的限制条件递归的举例递归与迭代扩展学习1.什么是递归1.1递归的含义在C语言中,递归就是函数自己调用自己。什么意思呢?接下来我将写一段代码展示例一:以上便是函数调用函数的例子,那么打印出来的值是多少呢?那么这个值为什么是27呢,就需要用到递......
  • C++递归——蛇形方阵
    #题目描述#———————————————————思考线—————————————————————#思路分析#其实这道题并没有想象中那么麻烦,我们只需要关注第一轮也就是最外圈是如何赋值的即可。重难点:1.每一个断点到底是应该哪一个循环赋值?2.每一次换行第一个......
  • 洛谷题单指南-分治与倍增-P2415 集合求和
    原题链接:https://www.luogu.com.cn/problem/P2415题意解读:计算集合所有子集中元素之和。解题思路:集合的特性:互异性,元素各不相同来看样例:23,可以组成的子集有空23232和3都出现2次再比如:123,可以组成的子集有空12312 13231231,2,3各出现4次由于在集合中......
  • 【二叉树的最大深度】带你理解递归奥妙!
    ......
  • [独家原创]基于(牛顿拉夫逊)NRBO-Transformer单变量时序预测-递归预测未来数据 【24年
    [独家原创]基于(牛顿拉夫逊)NRBO-Transformer单变量时序预测-递归预测未来数据【24年新算法】(单输入单输出)你先用你就是创新!!!可以自行控制预测未来的数据个数!!!NRBO优化的超参数为:自注意力机制头数、正则化系数、初始化学习率1.程序已经调试好,无需更改代码替换数据集即可运行......
  • 【代码随想录Day17】二叉树Part05|练习递归
    654.最大二叉树题目链接/文章讲解:代码随想录视频讲解:又是构造二叉树,又有很多坑!|LeetCode:654.最大二叉树_哔哩哔哩_bilibili思路和昨天的从中序与后序遍历序列构造二叉树很像,那一题是根节点对数组分割,这一题是最大元素对数组分割。代码解释:基本检查:如果输入数组nums......
  • 洛谷题单指南-分治与倍增-P7167 [eJOI2020 Day1] Fountain
    原题链接:https://www.luogu.com.cn/problem/P7167题意解读:从喷泉任意一个圆盘倒水,水流经的圆盘直径必须递增,水最后流到哪个圆盘。解题思路:1、枚举法有30%的数据范围在N<=1000,Q<=1000,因此枚举也可以得到30分。可以通过单调栈预计算每个圆盘后面第一个直径更大的圆盘位置Next[......
  • 时间复杂度计算 递归
    我们先拿出2021csp-s程序题中一道看着就头大的程序题,要求分析solve1的复杂度。设T(n)⁡\operatorname{T(n)}T(n)表示数组长度为......
  • LeetCode算法—分治法
    思路:分治法的核心思想是“分而治之”,即将一个复杂的问题分成多个较小的子问题,分别求解这些子问题,然后将子问题的解合并,得到原问题的解。具体到求众数的问题上,分治法通过递归地将数组分成两部分,分别找出每一部分的众数,最后通过合并步骤来确定整个数组的众数。LeetCode169多数元素......
  • 洛谷题单指南-分治与倍增-P1226 【模板】快速幂
    原题链接:https://www.luogu.com.cn/problem/P1226题意解读:快速幂模版题。解题思路:1、分治法要计算a^b,可以对b分情况讨论:如果b是偶数,即b=2t,a^b=a^t*a^t如果b是奇数,即b=2t+1,a^b=a*a^t*a^t很容易用递归实现100分代码:#include<bits/stdc++.h>usingnamespa......