首页 > 代码库 > 递归编程之我见

递归编程之我见

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
<script id="MathJax-Element-1" type="math/tex; mode=display">\begin{equation} \begin{cases} & f(n) = n \times f(n-1) \text{ if } n > 1 \\ & f(n) = 1\text{ if } n = 1 \end{cases} \end{equation}</script>

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

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

另外两条是:

  • 设计法则。假定所有的递归调用都能够运行。
  • 合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归中做重复性工作。

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

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

???????????????????????input={1,2,3,4}countMaxPrimerPairs(input)=testPairs(input.get(i,j))+countMaxPrimerPairs(input.delete(i,j)) where 0iinput.size(),i+1jinput.size()countMaxPrimerPairs(input)=0 if input.size()=0
<script id="MathJax-Element-2" type="math/tex; mode=display">\begin{equation} \begin{cases} & input = \left\{ 1, 2, 3, 4 \right\} \\ & countMaxPrimerPairs(input) = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i, j)) \text{ where } 0 \le i \le input.size(), i+1 \le j \le input.size() \\ & countMaxPrimerPairs(input) = 0 \text{ if } input.size() = 0 \end{cases} \end{equation}</script>

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

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

这里通过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 + ")";
        }
}
View Code

此时输出为:

(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 0iinput.size(),i+1jinput.size()countMaxPrimerPairs(input)=0 if input.size()=0
<script id="MathJax-Element-3" type="math/tex; mode=display">\begin{equation} \begin{cases} & input = \left\{ 1, 2, 3, 4 \right\} \\ & countMaxPrimerPairs(input) = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i, j)) \text{ where } 0 \le i \le input.size(), i+1 \le j \le input.size() \\ & countMaxPrimerPairs(input) = 0 \text{ if } input.size() = 0 \end{cases} \end{equation}</script>

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

???????????????????????input={1,2,3,4}countMaxPrimerPairs(input)=testPairs(input.get(i,j))+countMaxPrimerPairs(input.delete(i,j)) where i=0,i+1jinput.size()countMaxPrimerPairs(input)=0 if input.size()=0
<script id="MathJax-Element-4" type="math/tex; mode=display">\begin{equation} \begin{cases} & input = \left\{ 1, 2, 3, 4 \right\} \\ & countMaxPrimerPairs(input) = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i, j)) \text{ where } i=0, i+1 \le j \le input.size() \\ & countMaxPrimerPairs(input) = 0 \text{ if } input.size() = 0 \end{cases} \end{equation}</script>

此时得到的结果如下:

(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 + ")";
        }
}    
View Code