第二章递归

第二章

目标:

  • 对递归建立感觉
  • 学会评估算法性能
  • 能大致评估程序的执行时间

递归设计经验

  • 找重复(子问题)
  • 找重复中的变化量➡️参数
  • 找参数变化趋势➡️设计出口​

练习策略:

  • 循环改递归
  • 经典递归
  • 大量练习,总结规律,掌握套路
  • 找到感觉,挑战高难度

递归、查找和排序补充

什么是递归

image-20210315203041274

Exception in thread "main" java.lang.StackOverflowError(无限调用)

image-20210315203205124

设计出口!层层调用层层返回。

定义:

自身调用自身。

递归基础练习

求阶乘

代码:

//求n的阶乘
	
	/*找重复:用n*n-1的阶乘,求n-1的阶乘是原问题的重复,不同点在于规模更小(子问题)
	 * 
	 * 找变换:n越变越小,变化的量作为参数
	 * 找边界:出口
	 * */
	static int f1(int n) {
		
		if(n==1) {//出口
			return 1;
		}
		return n*f1(n-1);
	}

递归更通俗的理解:假设f1是完成某个功能,假设它已完成,你心安理得的调用它。

f(n)是求n的阶乘,f(n-1)是求n-1的阶乘

打印i到j
//	打印i到j
	/*找重复:
	 * 找变化:i在变
	 * 找边界:
	 * */
	static void f2(int i,int j) {
		if(i>j) {
			return;
		}
		System.out.print(i+" ");
		f2(i+1,j);
		
	}

自己处理一部分,委托别人处理一部分。最终综合起来就是结果。

对数组求和
//	数列求和
	/*找重复:划一块,自己弄。另一段给别人。
	 * 找变化:数组区间长度变化int begin
	 * 找边界:
	 * */
	static int f3(int arr[],int begin) {
		if(begin == arr.length-1) {
			return arr[arr.length-1];
		}
		return arr[begin] + f3(arr,begin+1);	
	}

注:变化之中去找参数和加参数,是递归的一个难点

image-20210315211739176
反转字符
//反转字符,好像是有一刀,给它拼成四个子问题
	static String reverse(String src,int end) {
		if(end == 0) {
			return ""+src.charAt(0);			
		}
		return src.charAt(end)+reverse(src,end-1);
	}
image-20210315212836984
斐波那契数列
image-20210315213750028

代码:

//斐波那契数列
	/*找重复:
	 * 找变化:n
	 * 找边界:
	 * */
	static int fib(int n) {
		if(n == 1||n == 2) {//只有一项
			return 1;
		}
		return fib(n-1) + fib(n-2);//n-1的求解是包括n-2的,划分并不均匀
	}

思路:

区别于之前的递归,之前的递归都是我自己解决其中的一点点,然后将剩下的递归委托给我的小弟。他给我一个结果我给他一合并就能得出最终结果。

这个题需要委托两次,但是也符合递归将一个大的任务拆解为更小的子任务,一个小弟给我求F(N-1)另一个小弟给我求F(N-2),我负责将他们合起来。

等价于两个子问题:

  • 求前一项
  • 求前两项
  • 两项求和

1. 分解为:直接量+小规模子问题

image-20210315214103573

2. 分解为:多个小规模子问题

image-20210315214115806 image-20210315214930964 image-20210315221032695
汉诺塔
image-20210316134232986

思路:

1~N从A移动B,C作为辅助

​ 等价于:

  1. 1~N-1从A移动到C,B为辅助

    image-20210319185344057

  2. 把N从A 移动到B

    image-20210319185412675
  3. 1~N-1从C移动到B,A为辅助

<img src="https://gitee.com/forgetc77/blog-img/raw/master/img/20210319185429.png" alt="image-20210319185429564" style="zoom:50%;" />

> 注:B为目标柱子

代码:


public class TowerOfHanoi {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		printHanoiTower(2,"A","B","C");
	}
/*
 *parameter n :圆盘数
 *parameter origin:原来的盘子
 *parameter end:目标盘
 *parameter help:辅助盘
 * */
	private static void printHanoiTower(int n, String origin, String end, String help) {
		if(n==1) {
			System.out.println("移动"+n+"从"+origin+"到"+end+"盘");
			return;
		}else {
			printHanoiTower(n-1,origin,help,end);//把前n-1个盘子移动到辅助空间上
			System.out.println("移动"+n+"从"+origin+"到"+help+"盘");//N可以顺利到达target,此时最后一个经过上面的if分支也完成了移动
			printHanoiTower(n-1,help,end,origin);//让n-1个盘子从辅助空间到原空间上去			
		}
		
	}

}

二分查找
image-20210319205244731 image-20210319205936829

代码:

private static int binarySearch(int[] x, int low, int high, int target) {
		 //当left>right说明没有找到数据,这是跳出递归的条件
		if(low>high) {
			return -1;
		}
		 //在这个过程中,mid = (left+right)/2、以及下面递归中的mid+1或mid-1可以看成是递归条件
        // 的一个变换,通过这些递归条件的不断变换,当达到left>right时结束递归逐层返回-1,表明
        // 目标元素不存在
//		int mid = (low+high)/2;
		int mid = low + ((high - low)>>1);//(low + high)>>>;防止溢出,移位也更高效
		int midVal = x[mid];
		if(midVal>target) {
			//目标元素小于中间的元素,说明目标元素在中间元素的左边,对左半部分的元素进行递归二分查找
			return binarySearch(x, low, mid-1, target);
		}else if(midVal<target) {
//			/目标元素大于中间元素,说明目标元素在中间元素的右边,对右半部分的元素进行递归二分查找
			return binarySearch(x, mid+1, high, target);
		}else {
			return mid;
		}
		
	}

注意:它有一个前提,就是必须在有序数据中进行查找。

等价于三个子问题

  • ​ 左边找(递归)

  • ​ 中间比

  • ​ 右边找(递归)

    左查找和右查找只选其一!

最大公约数
	//最大公约数
	static  int gcd(int m,int n) {
		if(n==0)return m;//即m%n == 0 
		return gcd(n,m%n);
	}
插入排序
image-20210316134153089
希尔排序

思路:

希尔排序本身也是一个插入排序,也称为缩小增量排序,是直接插入排序算法的一种更高效的改进版本,希尔排序是非稳定的排序算法。

增量是用于分组的,增量为n就会被分为n组。

一趟一个增量,用增量来分组,组内执行插入排序。

冒泡是交换,选择是求最大最小,插入挪动数组

  1. 增量为5即+4
image-20210319210958673
  1. 缩小增量为3,即+2

image-20210319211037232

image-20210319211109236

  1. 缩小增量为1即相邻为一组

    image-20210319211241315

代码:

import com.sun.org.apache.xalan.internal.xsltc.compiler.util.Util;

public class ShellSort {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int [] arr = {9,8,4,6,7,5,10,3,2,1};
		shellSort(arr);
		for (int i = 0; i < arr.length; i++) {
			System.out.print(arr[i]+" ");
		}
	}
	public static void shellSort(int []arr) {
		//双层循环
		//外层循环确定增量,不断的缩小增量
		for (int interval = arr.length/2; interval >0; interval = interval/2) {
			//增量为1的插入排序
//			for (int i = 1; i < arr.length; i++) {
//				int target = arr[i];
//				int j = i-1;
//				while(target<arr[j]) {
//					arr[j+1] = arr[j];
//					j--;
//				}
//				arr[j+1] = target;
//			}
			for (int i = interval; i < arr.length; i++) {
				int target = arr[i];
				int j = i-interval;
				while(j>-1&&target<arr[j]) {
					arr[j+interval] = arr[j];
					j-=interval;
				}
				arr[j+interval] = target;
			}
		}
		
	}

}

image-20210319211759209

image-20210319213519775

小结
image-20210316133918615

找重复:

​ 1.找到一种划分方法

​ 2.找到递推公式或者等价转换

​ 都是父问题转化为求解子问题

找变化的量

​ 变化的量通常作为参数

找出口

根据参数变化的趋势,对边界进行控制,适时的终止递归

如何评估算法性能

O表示法

image-20210319214344953

大O举例

image-20210319214540951

image-20210319214657383 image-20210319215027693 image-20210319215116015
算法复杂度/拥有的时间 1s可处理的规模
n 100000000(1Ghz)
n^2 10000
n^3 ≈500
2^n 27
lgn 2^100000000
算法复杂度/n的规模
lgn 27/10^8
n 1
n^2 100000000
image-20210320081113632 image-20210320081206472

经典算法分析

顺序查找:O(n)

二分查找:O(lgn)

\[2^x = 10^8 \]

\[x = log_2{10^8}≈27 \]

测试算法效率

	public static void duration(long x) {
		System.out.println(System.currentTimeMillis()-x+"ms");//打印系统当前的时间
	}

完成测试代码:


public class BinarySearch {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int [] x =new int[10000*10000];
		for (int i = 0; i < x.length; i++) {
			x[i] = i + 1;//给x[0]~x[9999999]赋值1~100000000
		}
		int target = 10000*10000;
		long now = System.currentTimeMillis();//获取当前系统时间
		int index = binarySearch(x,0,x.length-1,target);
		duration(now);
		System.out.println(target+"所在的位置是"+index);
		
		now = System.currentTimeMillis();
		index = search(x,target);
		System.out.println(target+"所在的位置是"+index);
		duration(now);
	}
	private static int search(int[] x, int target) {
		for (int i = 0; i < x.length; i++) {
			if(x[i]==target) {
				return i;
			}
		}
		return -1;
	}
	private static int binarySearch(int[] x, int low, int high, int target) {
		 //当left>right说明没有找到数据,这是跳出递归的条件
		if(low>high) {
			return -1;
		}
		 //在这个过程中,mid = (left+right)/2、以及下面递归中的mid+1或mid-1可以看成是递归条件
        // 的一个变换,通过这些递归条件的不断变换,当达到left>right时结束递归逐层返回-1,表明
        // 目标元素不存在
//		int mid = (low+high)/2;
		int mid = low + ((high - low)>>1);//(low + high)>>>;防止溢出,移位也更高效
		int midVal = x[mid];
		if(midVal>target) {
			//目标元素小于中间的元素,说明目标元素在中间元素的左边,对左半部分的元素进行递归二分查找
			return binarySearch(x, low, mid-1, target);
		}else if(midVal<target) {
//			/目标元素大于中间元素,说明目标元素在中间元素的右边,对右半部分的元素进行递归二分查找
			return binarySearch(x, mid+1, high, target);
		}else {
			return mid;
		}
		
	}
	public static void duration(long x) {
		System.out.println(System.currentTimeMillis()-x+"ms");//打印系统当前的时间
	}
}

冒泡、插入、选择排序:

  • 冒泡排序(交换)849589-20171015223238449-2146169197
image-20210320205151794
  • 插入排序(挪动)

    • 从第一个元素开始,该元素可以认为已经被排序;
    • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
    • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
    • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
    • 将新元素插入到该位置后;
    • 重复步骤2~5。

    3

image-20210320205657910
  • 选择排序:(求最大最小)

思路

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

重复第二步,直到所有元素均排序完毕。

selectionSort

image-20210320210341444

感受下性能的差别:Arrays.sort()的执行时间对比

Arrays.sort()用的是快速排序算法。时间复杂度为O(nlgn)

image-20210320210901965

image-20210320210925247

\[2^{27}≈10^8 \]

希尔排序的性能分析

  • 如果原始数据的大部分元素都已经排好序,那么插入排序的速度很快(因为需要移动的元素很少)

  • 快?

    • 无序的时候,元素少
    • 元素多的时候,已经基本有序,后面的比较次数和移动元素就越来越少
  • 最好与最坏

    image-20210328143520849

    image-20210328145628737

排序算法稳定性

image-20210328145806291

image-20210328145848867

image-20210328150823862

image-20210328150837487

image-20210328150848334

image-20210328151034587

相关题解

题1:小白上楼梯(递归设计)

image-20210328151136596

思路:

走到第n阶的上一次所在位置可能是在n-1/ n-2 /n-3阶,因此分别计算到达n-1/ n-2 /n-3这三个位置的方法数,将其加起来就是能够到达n阶楼梯的方法

image-20210328151928761

代码:
private static int f(int n) {
		if(n == 0) {
			return 0;
		}
		if (n == 1) {
			return 1;
		}
		if (n == 2) {
			return 2;
		}
		if (n == 3) {
			return 4;
		}
		// TODO Auto-generated method stub
		return f(n - 1) + f(n - 2) + f(n - 3);
	}
static int f(int n) {
		if(n==0) {
			return 1;	//理论上n=0的时候,f(n)=0,但是,为了验算正确,对代码做调整,令f(0)=1;
		}
		if(n==1) {
			return 1;
		}
		if(n==2) {
			return 2;
		}
		return f(n-1)+f(n-2)+f(n-3);
	}

题2:旋转数组的最小数字(改造二分法)

image-20210328155132231

思路:
image-20210328155717889

最小值一定是最大值的右侧

位于无序的位置

旋转之后的数组实际上可以划分为两个排序的子数组,而且前面的子数组的元素都是大于或者等于后面子数组的元素。我们还注意到最小的元素刚好是这两个子数组的分界线。在排序的数组中我们可以利用二分查找来实现O(logn)的查找。本题给出的数组在一定程度上是排序的,因此我们可以试着用二分查找的思路来寻找这个最小的元素。

代码:
package _02_01recursion;

public class Case01_旋转数组的最小数字 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] arr = { 5, 1, 2, 3, 4 };

		int res = min(arr);
		System.out.println(res);

		int[] arr2 = { 1, 0, 1, 1, 1 };
		int res2 = min(arr2);
		System.out.println(res2);
	}

	private static int min(int[] arr) {
		if(arr==null) {
			return -1;
		}
		int length = arr.length;
		int start = 0;
		int end = length - 1;
		// 考虑没有旋转的特殊的旋转
		if (arr[start] < arr[end])
			return arr[start];
//		start 和begin 指向相邻元素退出
		while (start + 1 < end) {
			int mid = start + (end - start) >> 1;
			//如果首位中间都相同,为了避免错误采用顺序查找
			if (arr[start] == arr[end] && arr[mid] == arr[end]) {
				return MinInOrder(arr, start, end);
			}
			if (arr[mid] >= arr[start]) {// 中间的大于了开头那么左侧有序,从右侧开始找
				start = mid;
			} else {
				end = mid;
			}
		}
		return arr[end];
	}

	private static int MinInOrder(int[] arr, int start, int end) {
		int result = arr[start];
		for (int i = start + 1; i < end; i++) {
			if (result > arr[i]) {
				result = arr[i];
			}
		}
		return result;
	}

}

当着两个数相同,并且它们中间的数相同的也相同时,我们把mid赋给了last,也就是认为此时最小的数字位于中间数字的后面。但是是错误的。

需要在二分查找之前添加一个筛选条件如果满足arr[mid] = arr[start]并且arr[mid] =arr[end]那么就采用顺序查找:

			if (arr[start] == arr[end] && arr[mid] == arr[end]) {
				return MinInOrder(arr, start, end);
			}

image-20210328161619943

题3:在有空字符串的有序字符串数组中查找

image-20210328163623256

思路:

image-20210328191056152

代码:
package _02_01recursion;

public class Case02_特殊有序数组中查找 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String[] arr = { "a", "", "ac", "", "ad", "b", "", "ba" };
		int res = indexOf(arr, "abc");
		System.out.println(res);
	}

	private static int indexOf(String[] arr, String string) {
		int begin = 0;
		int end = arr.length - 1;

		while (begin <= end) {
			int indexOfMid = begin + ((end - begin) >> 1);
			//防止indexOfHid下标取得的是空字符串,如果是则++,直到不是空字符串
			while (arr[indexOfMid].equals("")) {
				indexOfMid++;
			}
			//防止越界// 此处特别注意,如果是"abc",要考虑这个逻辑
			if (indexOfMid > end) {
				return -1;
			}
			if (arr[indexOfMid].compareTo(string) > 0) {
				end = indexOfMid - 1;
			} else if (arr[indexOfMid].compareTo(string) < 0) {
				begin = indexOfMid + 1;
			} else {
				return indexOfMid;
			}

		}
		return -1;
	}

}

注意!!

防止越界 此处特别注意,如果是"abc",要考虑这个逻辑

			if (indexOfMid > end) {
				return -1;
			}

题4:最长连续递增子序列(部分有序)【不会,参考别人的】

image-20210328191938527

public class Case04_最大连续递增子序列 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int nums[] = {1,3,5,4,7};
		int res = findMaxCIS(nums);
	}
	
	public static int  findMaxCIS(int nums[]) {
		if(nums.length == 0)
            return 0;
		int max = 0;
        int count = 1;
        for(int i=0;i<nums.length-1;i++){
            if(nums[i]<nums[i+1]){
            	count++;
            }else{
                if(max<count)
                    max = count;
                count = 1;
            }
        }

        return Math.max(count,max);
		
	}
}

题5:设计一个高效求a的n次幂的算法

package _02_01recursion;

public class Case05_a的n次幂 {

	public static void main(String[] args) {
		int n = 15;
		int a = 2;
		int res = pow(a,n);
		System.out.println(res);
	}
//O(n)
	private static int pow0(int a, int n) {
		int res = 1;
		for (int i = 0; i < n; i++) {
			res*=a;
		}
		return res;
	}
//
	private static int pow(int a, int n) {
		//出口
		if(n==0)
			return 1;
		int res = a;
		int ex = 1;
		//能翻
		while(ex<<1<=n) {
//			翻
			res = res*res;
//			指数
			ex<<=1;
		}
//		不能翻
//		差n-ex次方没有乘到里面
		return res*pow(a,n-ex);
	}
}

posted @ 2021-03-28 20:11  记录学习Blog  阅读(79)  评论(0编辑  收藏  举报