递归编程之我见

Table of Contents

  • 1 前言

  • 2 递归简介

  • 3 利用数学知识深入对递归的认知
    • 3.1 “求解最大素数伴侣数量”的实现

    • 3.2 进一步改进
      • 3.2.1 减少测试的重复性

      • 3.2.2 当找到最优解时提前结束

  • 4 完整的程序代码

前言

今天做了一个题目《素数伴侣》,具体如下:

  1. 输入:偶数个正整数,如,1,2,3,4

  2. 处理过程:将偶数个正整数进行配对处理,如a: (1,2)(3,4); b: (1,3)(2,4); c:
    (1,4)(2,3),配对的整数进行求和,a: 3,7; b: 4,6; c: 5,5; 和为素数的配对数称为素数伴侣,如 a组合中的(1,2),
    (3,4); c组合中的(1,4), (2,3).

  3. 输出: 可能的组合中出现的最大的素数伴侣的个数,上例为:2

看到这个题目时候,最初想到的就是用递归的方法进行求解,但是本人对于递归的认知,基本是停留在,求阶乘,或是一些很简单就能够实现的问题。

于是经过反复的思考,终于将问题进行解决了,在此就对解决过程中的收获进行详细的记录,希望对看者能有所帮助。

递归简介

递归,从字面上理解就是层层递进,最终回归到远点的意思。用梦中梦来进行类比,层层递进,就是:“你梦到梦中的你做梦,梦中的你也梦中的自己做梦”;回归到原点就是:“梦中的你的梦中的你,醒来了;梦中的你醒来了;最后你醒来了”,
当然这只是一个类比,假如有谁能够清晰的感受到逐一醒来的过程,我将深表佩服。 用一个函数来进行表示,就类似于:

Function dream(you):
If (you don‘t dream you dreaming):
Return
dream(you in you dream)
break()

用另一种方式进行理解:

dream(你)
dream(梦中的你)
dream(梦中的你的梦中的你)
....; 这个过程直到下一个梦中的你没有梦到自己做梦为止
break
break
break

我最初接触递归的时候,看到的第一个例子是求阶乘(想必大家也看到的第一个例子也差不多一样):

Function factorial(n):
If (n == 1):
Return 1
Return n*factiorial(n-1)

当n为3时,

factorial(3)
factorial(2)
factorial(1)
return 1
return 2*1
return 3*2

利用数学知识深入对递归的认知

阶乘可以用一些的公式进行表示:

???????????????????????????????f(n)=n×f(n?1) if n>1f(n)=1 if n=1

从公式中很容易就能够看出递归实现的四条基本(引用自《数据结构与算法分析 - c语言描述)法则中的两条:

  • 基准情形。必须总有某些基准情形,它无需递归就能解出。如,n = 1时的情况

  • 不断推进。对于那些需要递归求解的情形,每一次递归的调用都必须要使求解状况朝接近基准情形的方向推进。也就是公式中,n > 1的情况

另外两条是:

  • 设计法则。假定所有的递归调用都能够运行。

  • 合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归中做重复性工作。

注意,本文的介绍主要是基于前两条以及最后一条法则的

那么如何利用数学公式的形式来帮助理解前言中提到的问题呢?

???????????????????????input={1,2,3,4}countMaxPrimerPairs(input)=testPairs(input.get(i,j))+countMaxPrimerPairs(input.delete(i,j)) where 0≤i≤input.size(),i+1≤j≤input.size()countMaxPrimerPairs(input)=0 if input.size()=0

注意,其实这个数学公式模型是存在问题的,在下文中将会给出问题的所在。

“求解最大素数伴侣数量”的实现

这里通过java实现。

从上面的公式中,我们可以发现,要实现整个求解过程,递归的书写已经不再是重点中的重点了,很容易可以通过直接翻译得到如下的代码:


static public int countMaxPrimerPairs(Input input) {
if (input.size() == 0) {
return 0;
}

int nPairs = 0;
int record = 0;
for (int i = 0; i < input.size(); i++) {
for (int j = i+1; j < input.size(); j++) {
record = testPairs(input.get(i,j)) +
countMaxPrimerPairs(input.delete(i,j));
if (nPairs < record) {
nPairs = record;
}
}
}

return nPairs;
}

因此这里的重点是,如何实现Input结构,它应该具有的成员变量和方法有:


class Input {
private ArrayList<Integer> arrayList;
public Input();
public void add(int a);
public Pair get(int i, int j);
public int size();
public Input delete(int i, int j);
}

这里选用了ArrayList对数据进行存储,具体的实现如下:


/**
* 记录输入的参数信息
* @author Zz
*
*/
class Input {
private ArrayList<Integer> arrayList;

public Input() {
arrayList = new ArrayList<>();
}

public Input(ArrayList<Integer> list) {
arrayList = new ArrayList<>(list);
}

/**
* 添加元素
* @param a
*/
public void add(int a) {
arrayList.add(new Integer(a));
}

/**
* 获得相应位置的元素
* @param i 第i个位置
* @param j 第j个位置
* @return Pair
*/
public Pair get(int i, int j) {
return new Pair(arrayList.get(i).intValue(),
arrayList.get(j).intValue());
}

public int size() {
return arrayList.size();
}

public Input delete(int i, int j) {
@SuppressWarnings("unchecked")
ArrayList<Integer> list = (ArrayList<Integer>) arrayList.clone();
list.remove(i);
list.remove(j-1); // 第一个移除,使得所有的元素都向前移动了一个位置
Input input = new Input(list);

return input;
}
}

完整的代码如下:

import java.util.ArrayList;

public class Main {
// 计算最大素数伴侣对数
static public int countMaxPrimerPairs(Input input) {
if (input.size() == 0) {
return 0;
}

int nPairs = 0;
int record = 0;
for (int i = 0; i < input.size(); i++) {
for (int j = i+1; j < input.size(); j++) {
record = testPairs(input.get(i,j)) +
countMaxPrimerPairs(input.delete(i,j));
if (nPairs < record) {
nPairs = record;
}
}
}

return nPairs;
}

// 测试一个数据对是否是素数伴侣
static public int testPairs(Pair p) {
int n = p.x + p.y;
System.out.println(p);
if (isPrimer(n)) {
return 1;
}

return 0;
}

// 判断一个数是否是质数
static private boolean isPrimer(int n) {
if (n==2 || n==3 || n==5) return true;

if (n%2 == 0) return false;

int sqrtV = (int) Math.sqrt(n);
for (int i=3; i<sqrtV; i=i+2) {
if (n%i == 0) {
return false;
}
}

return true;
}

public static void main(String[] args) {
// TODO Auto-generated method stub
Input input = new Input();
input.add(2);
input.add(3);
input.add(4);
input.add(5);

int n = countMaxPrimerPairs(input);
System.out.println(n);
}

}

/**
* 记录输入的参数信息
* @author Zz
*
*/
class Input {
private ArrayList<Integer> arrayList;

public Input() {
arrayList = new ArrayList<>();
}

public Input(ArrayList<Integer> list) {
arrayList = new ArrayList<>(list);
}

/**
* 添加元素
* @param a
*/
public void add(int a) {
arrayList.add(new Integer(a));
}

/**
* 获得相应位置的元素
* @param i 第i个位置
* @param j 第j个位置
* @return Pair
*/
public Pair get(int i, int j) {
return new Pair(arrayList.get(i).intValue(),
arrayList.get(j).intValue());
}

public int size() {
return arrayList.size();
}

public Input delete(int i, int j) {
// 使用浅复制,具体数值在内存中只存在一份
@SuppressWarnings("unchecked")
ArrayList<Integer> list = (ArrayList<Integer>) arrayList.clone();
list.remove(i);
list.remove(j-1); // 第一个移除,使得所有的元素都向前移动了一个位置
Input input = new Input(list);

return input;
}
}

/**
* 用来记录配对点的信息
* @author Zz
*
*/
class Pair {
public final int x;
public final int y;

public Pair(int a, int b) {
x = a;
y = b;
}

public String toString() {
return "(" + x + "," + y + ")";
}
}

此时输出为:

(2,3)
(4,5)
(2,4)
(3,5)
(2,5)
(3,4)
(3,4)
(2,5)
(3,5)
(2,4)
(4,5)
(2,3)
2

进一步改进

输出的内容中,显示了测试过的数据对,以及最大的素数伴侣对。为了层次性的显示测试过的数据对,将countMaxPrimerPairs方法添加了层次参数,如下:


/**
* 计算最大素数伴侣对数
* @param input
* @param level 标记递归中具体的层次
* @return
*/
static public int countMaxPrimerPairs(Input input, int level) {
if (input.size() == 0) {
return 0;
}

int nPairs = 0;
int record = 0;
for (int i = 0; i < input.size(); i++) {
for (int j = i+1; j < input.size(); j++) {

for (int k=0; k<level; ++k) System.out.print("\t");

record = testPairs(input.get(i,j)) +
countMaxPrimerPairs(input.delete(i,j), level+1);
if (nPairs < record) {
nPairs = record;
}
}
}

for (int k=0; k<level; ++k) {
System.out.print("\t");
}
System.out.println("return " + nPairs);
return nPairs;
}

此时输出的结果为:

(2,3)
(4,5)
return 1
(2,4)
(3,5)
return 0
(2,5)
(3,4)
return 1
(3,4)
(2,5)
return 1
(3,5)
(2,4)
return 0
(4,5)
(2,3)
return 1
return 2
2

我们从输出的结果中发现如下问题:

  1. 相同的数据对被重复测试了两次

  2. 当找到最大的测试对为input.size/2的时候仍然继续递归过程

减少测试的重复性

对第一个问题的解决可以采用Hashmap对进行过的测试进行记录,当调用到已经测试过的,直接利用记录中的结果,增加与更改后的代码如下:


public class Main {
private static Hashtable<String, Integer> table;

static public void initTable() {
table = new Hashtable<String, Integer>();
}

/**
* 计算最大素数伴侣对数
* @param input
* @param level 标记递归中具体的层次
* @return
*/
static public int countMaxPrimerPairs(Input input, int level) {
if (input.size() == 0) {
return 0;
}

int nPairs = 0;
int record = 0;
Pair pair;
int testResult;

for (int i = 0; i < input.size(); i++) {
for (int j = i+1; j < input.size(); j++) {
pair = input.get(i, j);

if (table.containsKey(pair.toString())) {
testResult = table.get(pair.toString());
} else {
for (int k=0; k<level; ++k) System.out.print("\t");
testResult = testPairs(pair);
table.put(pair.toString(), new Integer(testResult));
}

record = testResult +
countMaxPrimerPairs(input.delete(i,j), level+1);
if (nPairs < record) {
nPairs = record;
}
}
}

for (int k=0; k<level; ++k) {
System.out.print("\t");
}
System.out.println("return " + nPairs);
return nPairs;
}

// 测试一个数据对是否是素数伴侣
static public int testPairs(Pair p) {
...
}

// 判断一个数是否是质数
static private boolean isPrimer(int n) {
...
}

public static void main(String[] args) {
// TODO Auto-generated method stub
initTable();

...
}

}

此时输出的结果为:

(2,3)
(4,5)
return 1
(2,4)
(3,5)
return 0
(2,5)
(3,4)
return 1
return 1
return 0
return 1
return 2
2

发现我们避免了反复测试,但并没有避免反复递归,于是认为最初建立相应的数学模型的时候出现了问题,让我们再来看一下相应的数学模型。

???????????????????????input={1,2,3,4}countMaxPrimerPairs(input)=testPairs(input.get(i,j))+countMaxPrimerPairs(input.delete(i,j)) where 0≤i≤input.size(),i+1≤j≤input.size()countMaxPrimerPairs(input)=0 if input.size()=0

在第二个公式中,使用了i,j两个变量,这就是导致最终结果中被反复计算了两遍的罪魁祸首,为什么这样说呢?因为不管最终的配对情况如何,第一个数,必然会和其中的一个数形成配对,于是第一个变量i应该是一个定值,于是对公式模型进行了更改:

???????????????????????input={1,2,3,4}countMaxPrimerPairs(input)=testPairs(input.get(i,j))+countMaxPrimerPairs(input.delete(i,j)) where i=0,i+1≤j≤input.size()countMaxPrimerPairs(input)=0 if input.size()=0

此时得到的结果如下:

(2,3)
(4,5)
return 1
(2,4)
(3,5)
return 0
(2,5)
(3,4)
return 1
return 2
2

当输入参数为6个整数时,结果如下:

(2,3)
(4,5)
(6,7)
return 1
(4,6)
(5,7)
return 0
(4,7)
(5,6)
return 1
return 2
(2,4)
(3,5)
return 1
(3,6)
return 0
(3,7)
return 1
return 1
(2,5)
(3,4)
return 1
return 1
return 0
return 2
(2,6)
return 0
return 1
return 1
return 1
(2,7)
return 1
return 0
return 1
return 2
return 3
3

此时可以发现,我们不仅将整个遍历过程实现了一遍,并且消除了重复检测的步骤。

当找到最优解时提前结束

从结果中我们可以发现,在对数据进行递归的时候,有可能很早就得到的最优解,既“素数伴侣数”为“输入数据长度的一半”,但是上述的程序在找到最优解之后依然在进行运算,这大大增加了程序运行的时间,因此,在函数中我们增加了一个判断,用于提前退出递归程序。

if (nPairs < record) {
nPairs = record;

// 增加了提前退出循环的判断
if (nPairs == input.size()/2) return nPairs;
}


此时的结果为:

(2,3)
(4,5)
2

可见,程序很好的按照最初的预期进行运行了。

完整的程序代码

import java.util.ArrayList;
import java.util.Hashtable;

public class Main {
private static Hashtable<String, Integer> table;

static public void initTable() {
table = new Hashtable<String, Integer>();
}

/**
* 计算最大素数伴侣对数
* @param input
* @param level 标记递归中具体的层次
* @return
*/
static public int countMaxPrimerPairs(Input input, int level) {
if (input.size() == 0) {
return 0;
}

int nPairs = 0;
int record = 0;
Pair pair;
int testResult;

for (int j = 1; j < input.size(); j++) {
pair = input.get(0, j);

if (table.containsKey(pair.toString())) {
testResult = table.get(pair.toString());
} else {
for (int k=0; k<level; ++k) System.out.print("\t");
testResult = testPairs(pair);
table.put(pair.toString(), new Integer(testResult));
}

record = testResult +
countMaxPrimerPairs(input.delete(0,j), level+1);

if (nPairs < record) {
nPairs = record;
if (nPairs == input.size()/2) return nPairs;
}
}

for (int k=0; k<level; ++k) {
System.out.print("\t");
}
System.out.println("return " + nPairs);
return nPairs;
}

// 测试一个数据对是否是素数伴侣
static public int testPairs(Pair p) {
int n = p.x + p.y;
System.out.println(p);
if (isPrimer(n)) {
return 1;
}

return 0;
}

// 判断一个数是否是质数
static private boolean isPrimer(int n) {
if (n==2 || n==3 || n==5) return true;

if (n%2 == 0) return false;

int sqrtV = (int) Math.sqrt(n);
for (int i=3; i<sqrtV; i=i+2) {
if (n%i == 0) {
return false;
}
}

return true;
}

public static void main(String[] args) {
// TODO Auto-generated method stub
initTable();

Input input = new Input();
input.add(2);
input.add(3);
input.add(4);
input.add(5);

int n = countMaxPrimerPairs(input, 0);
System.out.println(n);
}

}

/**
* 记录输入的参数信息
* @author Zz
*
*/
class Input {
private ArrayList<Integer> arrayList;

public Input() {
arrayList = new ArrayList<>();
}

public Input(ArrayList<Integer> list) {
arrayList = new ArrayList<>(list);
}

/**
* 添加元素
* @param a
*/
public void add(int a) {
arrayList.add(new Integer(a));
}

/**
* 获得相应位置的元素
* @param i 第i个位置
* @param j 第j个位置
* @return Pair
*/
public Pair get(int i, int j) {
return new Pair(arrayList.get(i).intValue(),
arrayList.get(j).intValue());
}

public int size() {
return arrayList.size();
}

public Input delete(int i, int j) {
// 使用浅复制,具体数值在内存中只存在一份
@SuppressWarnings("unchecked")
ArrayList<Integer> list = (ArrayList<Integer>) arrayList.clone();
list.remove(i);
list.remove(j-1); // 第一个移除,使得所有的元素都向前移动了一个位置
Input input = new Input(list);

return input;
}
}

/**
* 用来记录配对点的信息
* @author Zz
*
*/
class Pair {
public final int x;
public final int y;

public Pair(int a, int b) {
x = a;
y = b;
}

public String toString() {
return "(" + x + "," + y + ")";
}
}

递归编程之我见,布布扣,bubuko.com

时间: 2024-10-13 09:59:34

递归编程之我见的相关文章

分治法与递归编程步骤

分治法是一种很强大的算法设计方法.基本思想是:将原问题分解为几个规模小但类似于原问题的子问题,递归的求解这些子问题,然后再合并这些子问题的解来建立原问题的解. 在分治策略中,递归地求解一个问题,在每层递归中应用如下三个步骤: (1)分解(Divide):将原问题分解为一些子问题,子问题的形式与原问题一样,只是规模更小. (2)解决(Conquer):递归地解出子问题.如果子问题规模足够小,则停止递归,直接求解. (3)合并(Combine):将子问题的解组合成原问题的解. 分治思想体现在编码上,

编程:递归编程解决汉诺塔问题(用java实现)

//Li Cuiyun,October 14,2016.//用递归方法编程解决汉诺塔问题package tutorial_3_5;import java.util.*; public class HanoiTower { public static void main(String[] args) { // TODO Auto-generated method stub @SuppressWarnings("resource") Scanner sc=new Scanner(Syste

Ka的递归编程练习 Part4|Hanoi汉诺塔,双色汉诺塔的也有

1 #include <stdio.h> 2 void hanoi(int s,char a,char b,char c) //a是出发盘,b是中途盘,c是结束盘 3 { 4 if(s==0) return; 5 hanoi(s-1,a,c,b); //把最底下的从a借助c移动到b 6 printf("s from %c move to %c\n",a,c); 7 hanoi(s-1,b,a,c); //把刚才弄到b的借助a叠到c上 8 } 9 int main() 10

编程之我见

大多数编程都是激情不够,活也不行. 或者当下的技能完全可以胜任工作,拿到客观的薪水,就满足于温水之中,做一只安逸的青蛙. 想要学好编程必须富有激情,充满了性趣,这样你的活才行 理论:没有理论支撑,实践只是机械性的死记硬背代码. 实践:将理论穿插进实践,将理论都玩明白,姿势要舒服. 经验:遇到的问题多了,解决的多了,经验就多了. 复盘:从软件工程的角度去复盘整个项目,而不仅仅是一个单独的功能.例如什么样的数据应该放入缓存,如何防止缓存穿透. 编程的开始一般都是富有激情,但是活不行. 慢慢的因为没有

Ka的递归编程练习 Part8|回溯之二 排列组合

1 #include <stdio.h> 2 int resl[1000]={0},used[1000]={0}; 3 int n,r; 4 int tot=0; 5 void output() 6 { 7 tot++; 8 printf("<%d>:",tot); 9 int i; 10 for(i=1;i<=r;i++) 11 printf("%d ",resl[i]); 12 printf("\n"); 13

Ka的递归编程练习 Part7|素数环!

1 #include <stdio.h> 2 #include <math.h> 3 #define MAX 12 4 int used[21]={0},resl[21]={0},zs[50]={0};//used=已使用 resl=结果 zs=质数 5 int total=0; 6 void output() 7 { 8 total++; 9 printf("<%d>:",total); 10 int i; 11 for(i=1;i<=MAX

Ka的递归编程练习 Part6|简单背包问题,拒绝动规从我做起

1 #include <stdio.h> 2 #define M 10 3 int w[M]={1,3,5,7,9,11,13,15,17,19}; 4 int backpack(int n,int s) //n代表容量,s代表物品个数 5 { 6 if(n==0) return 1; //如果刚好装完,此支线满足条件,返回1 7 else if(n<0||s<=0) return 0; //此条支线出现了装一个重量超过限额或全部用完也未装满,返回0 8 else if(backp

Ka的递归编程练习 Part3|集合划分啦

1 #include<stdio.h> 2 int ss(int n,int k) 3 { 4 if(n==k||k==1) return 1; 5 return ss(n-1,k-1)+k*ss(n-1,k); 6 } 7 int main() 8 { 9 int s,n,k; 10 scanf("%d%d",&n,&k); 11 n>=k?s=ss(n,k):s=-1; 12 printf("%d",s); 13 return

编程思想之多线程与多进程(2)——线程优先级与线程安全

原文:http://blog.csdn.net/luoweifu/article/details/46701167 作者:luoweifu 转载请标名出处 <编程思想之多线程与多进程(1)--以操作系统的角度述说线程与进程>一文详细讲述了线程.进程的关系及在操作系统中的表现,这是多线程学习必须了解的基础.本文将接着讲一下线程优先级和线程安全. 线程优先级 现在主流操作系统(如Windows.Linux.Mac OS X)的任务调度除了具有前面提到的时间片轮转的特点外,还有优先级调度(Prior