递归引入
首先要了解递归函数中重要的两个概念:
1、不符合递归的条件。基线条件(base case)编写递归函数时,必须告诉它何时停止递归。基线条件则指的是函数不再调用自己,从而避免形成无限循环。
2、符合递归的条件。递归条件(recursive case)。递归条件指的是函数调用自己。
分而治之
D&C解决问题的过程包括两个步骤。
(1) 找出基线条件,这种条件必须尽可能简单。
(2) 不断将问题分解(或者说缩小规模),直到符合基线条件。
D&C的工作原理:
(1) 找出简单的基线条件;
(2) 确定如何缩小问题的规模,使其符合基线条件。
D&C并非可用于解决问题的算法,而是一种解决问题的思路。我们再来看一个例子。
给定一个数字数组。 计算它的元素的和。
使用最基本的方法:
public static int sum(int[] arry){
int total = 0;
for (int i = 0; i < arry.length; i++) {
total = total+arry[i];
}
return total;
}
使用D&C工作的原理的方案图
例如对数字数组 2,4,6 进行求和
第一步:找出基线条件。最简单的数组什么样呢?请想想这个问题,再接着往下读。如果数
组不包含任何元素或只包含一个元素。
第二步:每次递归调用都必须离空数组更近一步。如何缩小问题的规模呢?
将数组的元素个数逐渐减少。
1、将第一个元素拿出来后数组只剩2个元素的新数组A,
2、重复第一步 第二次再将第一个元素拿走,新数组A只剩1一个元素的新数组B,
3、重复第一步 第三次再将第一个元素拿走,
4、新数组B没有元素,数组为空,此时符合基线条件。不能重复跳出返回结果。
使用D&C工作的原理的方法实现:
public static int sum(int[] arry){
//基线条件
if (arry.length == 0){
return 0;
}else {
//递归条件
int arry_new[] = new int[arry.length-1]; //一个新数组,不包含第一个元素外的其他所有元素
int index = 0;
for (int i = 1; i < arry.length; i++) {
arry_new[index] = arry[i];
index++;
}
return arry[0] + sum(arry_new);
}
}
递归 和 分而治之的结合应用 快速排序
对分而治之的概念有所理解后,现在来看看D&C在快速排序中应用
对数组进行排序,想想什么样的数组排序速度最快?只有一个元素或者0个元素的数组排序是最快的!在这种情况下,只需原样返回数组——根本就不用排序。
伪代码
def quicksort(arry)
if arry.length < 2:
return arry
我们来看看更长的数组。对包含两个元素的数组进行排序也很容易。
比较2个元素的大小即可。如果第一个元素小于第二个元素交换他们位置即可。
包含三个元素的数组呢?
别忘了,你要使用D&C,因此需要将数组分解,直到满足基线条件。下面介绍快速排序的工
作原理。首先,从数组中选择一个元素,这个元素被称为基准值(pivot)。
暂时先将第一个元素作为基准值。
接下来,找出比基准值小的元素以及比基准值大的元素。
现有一个小于基准值的子数组,基准值,大于基准值的子数组。
如果子数组是有序的,就可以像下面这样合并得到一个有序的数组:
左边的数组 + 基准值 + 右边的数组。
在这里,就是[10, 15] + [33] + [],结果为有序数组[10, 15, 33]。
如何对子数组进行排序呢?对于包含两个元素的数组(左边的子数组)以及空数组(右边的
子数组),快速排序知道如何将它们排序,因此只要对这两个子数组进行快速排序,再合并结果,
就能得到一个有序数组!伪代码如下:
quicksort([15, 10]) + [33] + quicksort([])
不管将哪个元素用作基准值,这都管用。假设你将15用作基准值。
这个子数组都只有一个元素,而你知道如何对这些数组进行排序。现在你就知道如何对包含
三个元素的数组进行排序了,步骤如下。
1、找出基准值
2、将数组分成两个子数组,小于基准值的数组和大于基准值的数组
3、对两个子数组进行快速排序。
下面根据基本步骤来写相应伪代码
int provit = arry[0];//定义基准值
less[] //小于基准值的子数组
greater[]//大于基准值的子数组
for(int x = 0; x < arry.length; x++){
if arry[x] > provit{
greater.add(arry[x])
}else if arr[x] < provit{
less.add(arry[x])
}else if x > 0{
//如果等于基准值,且下标不是基准值
less.add(arry[x])
}
}
//左边的数组 + 基准值 + 右边的数组
quickSort(less) + provit + quickSort(greater);
基于以上理论的 Java代码实现
public static List quicksort(List<Integer> arry){
if (arry.size() < 2){
return arry;
}
int x = arry.size()/2;
Integer provit = arry.get(x);//找出基准值
ArrayList less = new ArrayList<Integer>();//构建小于基准值的子数组
ArrayList greater = new ArrayList<Integer>();//构建大于基准值的子数组
for (int i = 0; i < arry.size(); i++) {
if (arry.get(i) < provit){
less.add(arry.get(i));
}else if (arry.get(i) > provit){
greater.add(arry.get(i));
}else if (i != x){ //如果等于基准值,但不是基准值的下标也要添加到子数组,以免漏数据,如果没有这个判断的添加数据那么数组中相同的元素就会被漏掉
less.add(arry.get(i));
}
}
//左边数组加基准值和右边数组合并
List<Integer> list = new ArrayList<>();
list.addAll(quicksort(less));
list.add(provit);
list.addAll(quicksort(greater));
return list;
}
public static void main(String[] args) {
int[] array = {2,15,6,2,4,9,4};
ArrayList<Integer> luanList = new ArrayList<>();
for (int i = 0; i < array.length; i++) {
luanList.add(array[i]);
}
System.out.println("排序前:"+luanList);
System.out.println("排序后:"+quicksort(luanList));
}
来看看网上常见的快速排序代码,原理不变,也是将数组中找一个基准值,然后一分为2,左边和右边分别进行快排。其中涉及到一个核心问题如何将数组一分二。需要一个分区函数来解决这个问题。
分区函数 构建首先要明确输入和输出。
输入的肯定会有一个数组,如果我对这个数组的部分元素进行分组就需要输入数组的开始和结束。
输出是什么呢?输出的是数组中基准值的正确下标。此时该下标的左边都是小于基准值的,下标的右边都是大于基准值的。代码如下:
public static int partition(int arr[], int lowIndex, int highIndex) {
// 定义数组中的基准值 输入的结束下标为基准值
int pivot = arr[highIndex];
// 定义数组中的最后一个最小元素的下标
int i = (lowIndex - 1);
for (int j = lowIndex; j < highIndex; j++) {
// 如果当前元素小于或者等于基准值
if (arr[j] <= pivot) {
i++; // 增量 i
// 交换元素
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 将基准值移动到数组的中间位置左边的全是小于基准值的,右边的全部大于基准值
int temp = arr[i + 1];
arr[i + 1] = pivot;
arr[highIndex] = temp;
return i + 1;
}
下面是网上常见的快速排序Java代码
public static void sortq(int arr[], int lowIndex, int highIndex) {
if (lowIndex < highIndex) {
int pi = partition(arr, lowIndex, highIndex);
sortq(arr, lowIndex, pi - 1);
sortq(arr, pi + 1, highIndex);
}
}