• 递归编程之我见


    前言

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

    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
    

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

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

    egin{equation} egin{cases} & f(n) = n imes f(n-1) ext{ if } n > 1 \ & f(n) = 1 ext{ if } n = 1 end{cases} end{equation}

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

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

    另外两条是:

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

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

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

    egin{equation} egin{cases} & input = left{ 1, 2, 3, 4 ight} \ & countMaxPrimerPairs(input) = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i, j)) ext{ where } 0 le i le input.size(), i+1 le j le input.size() \ & countMaxPrimerPairs(input) = 0 ext{ if } input.size() = 0 end{cases} end{equation}

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

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

    这里通过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("	");
                            
                            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("	");
            }
            System.out.println("return " + nPairs);
            return nPairs;
    }   

    此时输出的结果为:

    (2,3)
            (4,5)
            return 0
    (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("	");
                                            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("	");
                    }
                    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 0
    (2,4)
            (3,5)
            return 0
    (2,5)
            (3,4)
            return 1
            return 1
            return 0
            return 1
    return 2
    2  
    

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

    egin{equation} egin{cases} & input = left{ 1, 2, 3, 4 ight} \ & countMaxPrimerPairs(input) = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i, j)) ext{ where } 0 le i le input.size(), i+1 le j le input.size() \ & countMaxPrimerPairs(input) = 0 ext{ if } input.size() = 0 end{cases} end{equation}

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

    egin{equation} egin{cases} & input = left{ 1, 2, 3, 4 ight} \ & countMaxPrimerPairs(input) = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i, j)) ext{ where } i=0, i+1 le j le input.size() \ & countMaxPrimerPairs(input) = 0 ext{ if } input.size() = 0 end{cases} end{equation}

    此时得到的结果如下:

    (2,3)
            (4,5)
            return 0
    (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)
        return 0
    (2,4)
        (3,5)
        return 0
    (2,5)
        (3,4)
    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("	");
                                    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("	");
                    }
                    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
  • 相关阅读:
    jquery遍历节点 children(),next(),prev(),siblings()closest() 等一些常用方法...
    jq 分页
    java8 array、list操作 汇【4】)- Java8 Lambda表达式 函数式编程【思想】
    java8 array、list操作 汇【2】)- (Function,Consumer,Predicate,Supplier)应用
    Java8 (Function,Consumer,Predicate,Supplier)详解
    Elasticsearch
    org.apache.commons.lang3.Validate
    freemarker逻辑判断写法#if
    11 Sping框架--AOP的相关概念及其应用
    10 Spring框架--基于注解的IOC配置
  • 原文地址:https://www.cnblogs.com/grass-and-moon/p/3764280.html
Copyright © 2020-2023  润新知