二分查找递归与非递归的时间比较_数据结构和算法递归

fa74b13f76ebdc0ac40bf994c7e73635.gif

1. 递归的定义

递归:就是在运行的过程中调用自己。递归必须要有三个要素:

「①、边界条件」

「②、递归前进段」

「③、递归返回段」

当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

2. 求一个数的阶乘:n!

n! = n*(n-1)*(n-2)*......1

「规定:」

「①、0!=1」

「②、1!=1」

「③、负数没有阶乘」

上面的表达式我们先用for循环改写:

public static int getFactorialFor(int n) {
    int temp = 1;
    if (n >= 0) {
        for (int i = 1; i <= n; i++) {
            temp = temp * i;
        }
    } else {
        return -1;
    }
    return temp;
}

改用递归来写:

public static int getFactorial(int n) {
    if (n >= 0) {
        if (n == 0) {
            System.out.println(n + "!=1");
            return 1;
        } else {
            System.out.println(n);
            int temp = n * getFactorial(n - 1);
            System.out.println(n + "!=" + temp);
            return temp;
        }
    }
    return -1;
}

这段递归程序的边界条件就是n==0时,返回1,具体调用过程如下:

e3ea3d75b17a0c3b762475a8954c6ff7.png

3. 递归的二分查找

注意:二分查找的数组一定是有序的!!!

在有序数组array[]中,不断将数组的中间值(mid)和被查找的值比较,如果被查找的值等于array[mid],就返回下标mid; 否则,就将查找范围缩小一半。如果被查找的值小于array[mid], 就继续在左半边查找;如果被查找的值大于array[mid],  就继续在右半边查找。直到查找到该值或者查找范围为空时, 查找结束。不用递归的二分查找如下:

public static int findTwoPoint(int[] array, int key) {
    int start = 0;
    int last = array.length - 1;
    while (start <= last) {
        int mid = (last - start) / 2 + start;//防止直接相加造成int范围溢出
        if (key == array[mid]) {//查找值等于当前值,返回数组下标
            return mid;
        }
        if (key > array[mid]) {//查找值比当前值大
            start = mid + 1;
        }
        if (key //查找值比当前值小
            last = mid - 1;
        }
    }
    return -1;
}

二分查找用递归来改写,相信也很简单。边界条件是找到当前值,或者查找范围为空。否则每一次查找都将范围缩小一半。

public static int search(int[] array, int key, int low, int high) {
    int mid = (high - low) / 2 + low;
    if (key == array[mid]) {//查找值等于当前值,返回数组下标
        return mid;
    } else if (low > high) {//找不到查找值,返回-1
        return -1;
    } else {
        if (key //查找值比当前值小
            return search(array, key, low, mid - 1);
        }
        if (key > array[mid]) {//查找值比当前值大
            return search(array, key, mid + 1, high);
        }
    }
    return -1;
}

递归的二分查找和非递归的二分查找效率都为O(logN),递归的二分查找更加简洁,便于理解,但是速度会比非递归的慢。

4. 分治算法

当我们求解某些问题时,由于这些问题要处理的数据相当多,或求解过程相当复杂,使得直接求解法在时间上相当长,或者根本无法直接求出。对于这类问题,我们往往先把它分解成几个子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个问题的解法。如果这些子问题还较大,难以解决,可以再把它们分成几个更小的子问题,以此类推,直至可以直接求出解为止。这就是分治策略的基本思想。

上面讲的递归的二分查找法就是一个分治算法的典型例子,分治算法常常是一个方法,在这个方法中含有两个对自身的递归调用,分别对应于问题的两个部分。

二分查找中,将查找范围分成比查找值大的一部分和比查找值小的一部分,每次递归调用只会有一个部分执行。

5. 汉诺塔问题

汉诺塔问题是由很多放置在三个塔座上的盘子组成的一个古老的难题。如下图所示,所有盘子的直径是不同的,并且盘子中央都有一个洞使得它们刚好可以放在塔座上。所有的盘子刚开始都放置在A 塔座上。这个难题的目标是将所有的盘子都从塔座A移动到塔座C上,每次只可以移动一个盘子,并且任何一个盘子都不可以放置在比自己小的盘子之上。

19c9a193ab3e96265181a629fe6f200d.png

试想一下,如果只有两个盘子,盘子从小到大我们以数字命名(也可以想象为直径),两个盘子上面就是盘子1,下面是盘子2,那么我们只需要将盘子1先移动到B塔座上,然后将盘子2移动到C塔座,最后将盘子1移动到C塔座上。即完成2个盘子从A到C的移动。

如果有三个盘子,那么我们将盘子1放到C塔座,盘子2放到B塔座,在将C塔座的盘子1放到B塔座上,然后将A塔座的盘子3放到C塔座上,然后将B塔座的盘子1放到A塔座,将B塔座的盘子2放到C塔座,最后将A塔座的盘子1放到C塔座上。

如果有四个,五个,N个盘子,那么我们应该怎么去做?这时候递归的思想就很好解决这样的问题了,当只有两个盘子的时候,我们只需要将B塔座作为中介,将盘子1先放到中介塔座B上,然后将盘子2放到目标塔座C上,最后将中介塔座B上的盘子放到目标塔座C上即可。

「所以无论有多少个盘子,我们都将其看做只有两个盘子。假设有 N 个盘子在塔座A上,我们将其看为两个盘子,其中(N-1)~1个盘子看成是一个盘子,最下面第N个盘子看成是一个盘子,那么解决办法为:」

「①、先将A塔座的第(N-1)~1个盘子看成是一个盘子,放到中介塔座B上,然后将第N个盘子放到目标塔座C上。」

「②、然后A塔座为空,看成是中介塔座,B塔座这时候有N-1个盘子,将第(N-2)~1个盘子看成是一个盘子,放到中介塔座A上,然后将B塔座的第(N-1)号盘子放到目标塔座C上。」

「③、这时候A塔座上有(N-2)个盘子,B塔座为空,又将B塔座视为中介塔座,重复①,②步骤,直到所有盘子都放到目标塔座C上结束。」

简单来说,递归算法为:

①、从初始塔座A上移动包含n-1个盘子到中介塔座B上。

②、将初始塔座A上剩余的一个盘子(最大的一个盘子)放到目标塔座C上。

③、将中介塔座B上n-1个盘子移动到目标塔座C上。

public class HannotaTest {

    /**
     * @param dish 盘子个数(也表示名称)
     * @param from 初始塔座
     * @param temp 中介塔座
     * @param to   目标塔座
     */
    public static void move(int dish, String from, String temp, String to) {
        if (dish == 1) {
            System.out.println("将盘子" + dish + "从塔座" + from + "移动到目标塔座" + to);
        } else {
            move(dish - 1, from, to, temp);//A为初始塔座,B为目标塔座,C为中介塔座
            System.out.println("将盘子" + dish + "从塔座" + from + "移动到目标塔座" + to);
            move(dish - 1, temp, from, to);//B为初始塔座,C为目标塔座,A为中介塔座
        }
    }

    public static void main(String[] args) {
        move(3, "A", "B", "C");
    }
}

「结果:」

将盘子1从塔座A移动到目标塔座C
将盘子2从塔座A移动到目标塔座B
将盘子1从塔座C移动到目标塔座B
将盘子3从塔座A移动到目标塔座C
将盘子1从塔座B移动到目标塔座A
将盘子2从塔座B移动到目标塔座C
将盘子1从塔座A移动到目标塔座C

6. 消除递归

一个算法作为一个递归的方法通常通概念上很容易理解,但是递归的使用在方法的调用和返回都会有额外的开销,通常情况下,用递归能实现的,用循环都可以实现,而且循环的效率会更高,所以在实际应用中,把递归的算法转换为非递归的算法是非常有用的。这种转换通常会使用到栈。

递归和栈有这紧密的联系,而且大多数编译器都是用栈来实现递归的,当调用一个方法时,编译器会把这个方法的所有参数和返回地址都压入栈中,然后把控制转移给这个方法。当这个方法返回时,这些值退栈。参数消失了,并且控制权重新回到返回地址处。

调用一个方法时所发生的事:

(1)当一个方法被调用时,它的参数和返回地址被压入一个栈中;

(2)这个方法可以通过获取栈顶元素的值来访问它的参数;

(3)当这个方法要返回时,它查看栈以获得返回地址,然后这个地址以及方法的所有参数退栈,并且销毁。

7. 递归的应用

7.1 背包问题

背包问题也是计算机中的经典问题。在最简单的形式中,包括试图将不同重量的数据项放到背包中,以使得背包最后达到指定的总重量。

比如:假设想要让背包精确地承重20磅,并且有 5 个可以放入的数据项,它们的重量分别是 11 磅,8 磅,7 磅,6 磅,5 磅。这个问题可能对于人类来说很简单,我们大概就可以计算出 8 磅+ 7 磅 + 5 磅 = 20 磅。但是如果让计算机来解决这个问题,就需要给计算机设定详细的指令了。

「算法如下:」

(1)如果在这个过程的任何时刻,选择的数据项的总和符合目标重量,那么工作便完成了。

(2)从选择的第一个数据项开始,剩余的数据项的加和必须符合背包的目标重量减去第一个数据项的重量,这是一个新的目标重量。

(3)逐个的试每种剩余数据项组合的可能性,但是注意不要去试所有的组合,因为只要数据项的和大于目标重量的时候,就停止添加数据。

(4)如果没有合适的组合,放弃第一个数据项,并且从第二个数据项开始再重复一遍整个过程。

(5)继续从第三个数据项开始,如此下去直到你已经试验了所有的组合,这时才知道有没有解决方案。

具体实现过程:

public class Knapsack {
    private int[] weights; //可供选择的重量
    private boolean[] selects; //记录是否被选择

    public Knapsack(int[] weights) {
        this.weights = weights;
        selects = new boolean[weights.length];
    }

    /**
     * 找出符合承重重量的组合
     *
     * @param total 总重量
     * @param index 可供选择的重量下标
     */
    public void knapsack(int total, int index) {
        if (total 0 || total > 0 && index >= weights.length) {
            return;//没找到解决办法,直接返回
        }
        if (total == 0) {//总重量为0,则找到解决办法了
            for (int i = 0; i                 if (selects[i] == true) {
                    System.out.println(weights[i] + " ");
                }
            }
            System.out.println();
            return;
        }
        selects[index] = true;
        knapsack(total - weights[index], index + 1);
        selects[index] = false;
        knapsack(total, index + 1);
    }

    public static void main(String[] args) {
        int array[] = {11, 8, 7, 6, 5};
        int total = 20;
        Knapsack k = new Knapsack(array);
        k.knapsack(total, 0);
    }
}

7.2 组合:选择一支队伍

在数学中,组合是对事物的一种选择,而不考虑他们的顺序。

比如有5个登山队员,名称为 A,B,C,D和E。想要从这五个队员中选择三个队员去登峰,这时候如何列出所有的队员组合。(不考虑顺序)

还是以递归的思想来解决:首先这五个人的组合选择三个人分成两个部分,第一部分包含A队员,第二部分不包含A队员。假设把从 5 个人中选出 3 个人的组合简写为(5,3),规定 n 是这群人的大小,并且 k 是组队的大小。那么根据法则可以有:(n,k) = (n-1,k-1) + (n-1,k)

对于从 5 个人中选择 3 个人,有:(5,3) = (4,2)+(4,3)

(4,2)表示已经有A队员了,然后从剩下的4个队员中选择2个队员,(4,3)表示从5个人中剔除A队员,从剩下的4个队员中选择3个队员,这两种情况相加就是从5个队员中选择3个队员。

现在已经把一个大问题转换为两个小问题了。从4个人的人群中做两次选择(一次选择2个,一次选择3个),而不是从5个人的人群中选择3个。

从4个人的人群中选择2个人,又可以表示为:(4,2) = (3,1) + (3,2),以此类推,很容易想到递归的思想。

具体实现代码:

public class Combination {
    private char[] persons;//组中所有可供选择的人员
    private boolean[] selects;//标记成员是否被选中,选中为true

    public Combination(char[] persons) {
        this.persons = persons;
        selects = new boolean[persons.length];
    }

    public void showTeams(int teamNumber) {
        combination(teamNumber, 0);
    }

    /**
     * @param teamNumber 需要选择的队员数
     * @param index      从第几个队员开始选择
     */
    public void combination(int teamNumber, int index) {
        if (teamNumber == 0) {//当teamNumber=0时,找到一组
            for (int i = 0; i                 if (selects[i] == true) {
                    System.out.print(persons[i] + " ");
                }
            }
            System.out.println();
            return;
        }
        //index超过组中人员总数,表示未找到
        if (index >= persons.length) {
            return;
        }
        selects[index] = true;
        combination(teamNumber - 1, index + 1);
        selects[index] = false;
        combination(teamNumber, index + 1);
    }

    public static void main(String[] args) {
        char[] persons = {'A', 'B', 'C', 'D', 'E'};
        Combination cb = new Combination(persons);
        cb.showTeams(3);
    }
}

8. 总结

一个递归方法每次都是用不同的参数值反复调用自己,当某种参数值使得递归的方法返回,而不再调用自身,这种情况称为边界值,也叫基值。当递归方法返回时,递归过程通过逐渐完成各层方法实例的未执行部分,而从最内层返回到最外层的原始调用处。

阶乘、汉诺塔、归并排序等都可以用递归来实现,但是要注意任何可以用递归完成的算法用栈都能实现。当我们发现递归的方法效率比较低时,可以考虑用循环或者栈来代替它。

END

如有收获,请划至底部,点击"在看"。万分感谢!

bb2949f1343aac329c5b9af4951b4faf.png

欢迎长按下图关注公众号

5ef2a55ea3bc5dcc85d10c371b644987.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值