递归的概念:
直接或间接地调用自己的算法称为递归算法。
分治的基本思想
分治的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题互相独立且与原问题相同。递归地解这些子问题,然后将各个子问题的解合并得到原问题的解
递归分治的四个特性:
1、问题缩小到一定程度可以直接求解
2、(最优子结构性)原问题可分解为多个规模较小的相同子问题
3、子问题的解合并成为原问题的解
4、子问题相互独立
分治法的复杂性分析
一个分治法将规模为n的问题分成k个规模为n/k的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。
用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:
举例说明:
例1:
解:
f(n)=n2
nlogba=n3
所以n2=o(n3),使用第一条。
T(n)=O((n3)
例2:
解:
f(n)=n1/2
nlogba=n1/2
所以n1/2=θ(n1/2),使用第二条。
T(n)=O((n1/2logn)
典型问题
1、二分查找
class BinarySearch{
public static void main(String[] args){
int[] arr = {1,2,3,4,5,6,7,8};
int a = 8;
System.out.println(binarySearch(a,arr));
}
static int binarySearch(int a,int[] arr){
// 最大索引
int maxIndex = arr.length -1;
// 最小索引
int minIndex = 0;
// 中间索引
int halfIndex = minIndex+(maxIndex-minIndex)/2;
while (minIndex<=maxIndex){
// 找到时
if (arr[halfIndex]==a){
return halfIndex;
}else if (arr[halfIndex]<a){// 比a小时
minIndex = halfIndex + 1;
}else {// 比a大时
maxIndex = halfIndex - 1;
}
halfIndex = minIndex+(maxIndex-minIndex)/2;
}
return -1;
}
}
2、合并排序
基本思想:将待排序元素分成大小大致相同的2个子集合,分别对2个子集合进行排序,最终将排好序的子集合合并成为所要求的排好序的集合。
public class MergeSort {
public static void sort(int[] nums, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 左边
sort(nums, low, mid);
// 右边
sort(nums, mid + 1, high);
// 左右归并
merge(nums, low, mid, high);
}
}
public static void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
public static void main(String[]args)
{
int arr[]={1,2,7,4,6,5,3};
MergeSort mergeSort=new MergeSort();
mergeSort.sort(arr, 0, 6);
for(int i=0;i<=6;i++)
{
System.out.println(arr[i]);
}
}
}
3、快速排序
快速排序算法的思想是:找到一个数字作为标准,把比该数小的放左边,比该数大的放右边,然后把左边的数组进行快排,右边的数组进行快排,最后把结果合并。
对数组{a,b,c,…,n}。
1、首先设置一个左指针i指向数组最左边的元素(a),设置一个右指针j只想数组最右边的元素(n)。
2、设置一个基准元素,一般选取数组最左边的元素(a)。
3、将右指针指向的元素和基准元素进行比较。如果比基准元素大,右指针就向左移动一个,即J–,这样一直比较下去。那么什么时候停止这个比较呢?结束条件有两个:1、发现一个元素比基准元素小,右指针指向的元素就和左指针指向的元素互换。2、右指针j与左指针i相逢,直接退出。
4、将左指针指向的元素和基准元素进行比较,如果比基准元素小,左指针就向右移动一个,即i++,这样一直比较下去。那么什么时候停止这个比较呢?结束条件有两个:1、发现一个元素比基准元素大,右指针指向的元素就和左指针指向的元素互换。2、右指针j与左指针i相逢直接退出。
public class sort {
public void quickSort(int[] arr, int low, int high) {
if (low < high) {
int index = getIndex(arr, low, high);
//以排好序的元素为界,将原数组分为两部分,递归
quickSort(arr, 0, index - 1);
quickSort(arr, index + 1, high);
}
}
public int getIndex(int[] arr, int low, int high) {
int tmp=arr[low];
int i=low;
int j=high;
int t;
while (true) {
// 当队尾的元素大于等于基准数据时,向前挪动high指针
while(arr[j] >= tmp && i < j)
j--;
while(arr[i] <= tmp && i < j)//再找右边的
i++;
if(i < j)//交换两个数在数组中的位置
{
t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
else
break;
}
arr[low] = arr[i];
arr[i] = tmp;
return i;
}
public static void main (String[]args)
{
int a[]={4,5,7,1,3,2,6};
sort test=new sort();
test.quickSort(a,0,6);
for(int i=0;i<=6;i++)
{
System.out.println(a[i]);
}
}
}
4、线性时间选择问题(随机快排)
问题描述:对于给定的n个元素的数组a[0:n—1],要求从中找出第k小的元素
问题分析:我们知道,快速排序算法的一次排序的思想是:找到一个数字作为标准,把比该数小的放左边,比该数大的放右边。
要找到第k小的元素,最粗暴的就是全部排序,但这样做了很多多余的工作,借鉴快速排序算法的一次排序思想,我们可以随机选择一个元素作为标准,把小于它的放左边,大于它的放右边:
当这个标准左边的元素和它加起来为k的话,就找到第k小的数了
当这个标准左边的元素和它加起来小于k的话,就向右边继续找第(k-1-该数下标)小的数
当这个标准左边的元素和它加起来大于k的话,就向左边继续找第k小的数
这样做的问题是,由于这个标准是随机选择的,这就造成了算法很不稳定。比如选择了最小的数或者最大的数,就导致这次划分没什么用处。所以我们尝试确定一种方法来确定一个随机数,使得这个随机数处在一个恰当的位置。
具体方法为:
将n个输入元素划分成⌈ n/5 ⌉个组,每组5个元素,用任意一种排序算法对每组中的元素排序,然后取出每组的中位数,共⌈ n/5 ⌉个元素,找出这⌈ n/5 ⌉个元素的中位数,如果⌈ n/5 ⌉为偶数,则选择2个中位数中较大的一个,以这个选出的元素作为划分基准。
例如:
按递增顺序,找出下面29个元素的第18个元素:8,31,60,33,17,4,51,57,49,35,11,43,37,3,13,52,6,19,25,32,54,16,5,41,7,23,22,46,29.
(1) 把前面29个元素分为6(=ceil(29/5))组; (8,31,60,33,17),(4,51,57,49,35),(11,43,37,3,13),(52,6,19,25,32),(54,16,5,41,7),(23,22,46,29).
(2) 提取每一组的中值元素,构成集合{31,49,13,25,16,29};
(3) 递归地使用算法求取该集合的中值,得到m=29;
(4) 根据m=29, 把29个元素划分为3个子数组:
P={8,17,4,11, 3,13,6,19, 25,16,5,7,23,22}
Q={29}
R={31,60,33,51,57,49,35,43,37,52,32,54,41,46}
(5) 由于|P|=14,|Q|=1,k=18,所以放弃P,Q,使k=18-14-1=3,对R递归地执行本算法;
(6) 将R划分成3(ceil(14/5))组:{31,60,33,51,57},{49,35,43,37,52},{32,54,41,46}
(7) 求取这3组元素的中值元素分别为:{51,43,46},这个集合的中值元素是43;
(8) 根据43将R划分成3组:
{31, 33, 35,37,32, 41},{43},{60, 51,57, 49, 52,54, 46}
(9) 因为k=3,第一个子数组的元素个数大于k,所以放弃后面两个子数组,以k=3对第一个子数组递归调用本算法;
(10) 将这个子数组分成5个元素的一组:{31,33,35,37,32}、{41},取其中值元素为33;
(11) 根据33,把第一个子数组划分成{31,32},{33},{35,37};
(12) 因为k=3,而第一、第二个子数组的元素个数为3,所以33即为所求取的第18个小元素。
具体代码如下:
5、循环赛日程表
设有n=2^k个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表:
(1)每个选手必须与其他n-1个选手各赛一次;
(2)每个选手一天只能参赛一次;
(3)循环赛在n-1天内结束。
请按此要求将比赛日程表设计成有n行和n-1列的一个表。在表中的第i行,第j列处填入第i个选手在第j天所遇到的选手。其中1≤i≤n,1≤j≤n-1。8个选手的比赛日程表如下图:
#define _CRT_SECURE_NO_DEPRECATE;
#define _CRT_SECURE_NO_WARNINGS;
#include<stdio.h>
#define N 100
int a[N][N];
void Scheduled(int i, int size)
{
int x, y;
if (size == 2)
{
a[i][1] = i;
a[i + 1][1] = i + 1;
}
else {
Scheduled(i, size / 2);
Scheduled(i + size / 2, size / 2);
}
for (x = i;x<i + size / 2;x++)
for (y = size / 2 + 1;y <= size;y++) {
a[x][y] = a[x + size / 2][y - size / 2];
}
for (x = i + size / 2;x<i + size;x++)
for (y = size / 2 + 1;y <= size;y++) {
a[x][y] = a[x - size / 2][y - size / 2];
}
}
void Print(int n) {
int p, q;
FILE *fp;
fp = fopen("D:\\222017321062109_循环赛.txt", "wb");
for (p = 1;p <= n;p++) {
for (q = 1;q <= n;q++) {
printf("%d\t", a[p][q]);
fprintf(fp, "%d\t", a[p][q]);
}
printf("\n");
fprintf(fp, "\r\n");
}
fclose(fp);
}
int main() {
int n;
printf("请输入n的值(请确保N为2的K次方,K>=0):\n");
scanf_s("%d", &n);
Scheduled(1, n);
Print(n);
return 0;
}