目录
2.1递归
- 递归的概念
直接或间接的调用自身的算法成为递归算法。用函数自身给出定义的函数称为递归函数。例如我们首先接触的汉诺塔问题,以及我们很早接触的未使用递归解Fibonacci数问题。
2.2分治
- 分支的基本思想
分治法的基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原问题相同。递归的解这些子问题,然后将各个子问题的解合并得到原问题的解。
- 适用条件
- 该问题的规模缩小到一定程度就可以很容易的解决。
- 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构。
- 利用该问题分解出的子问题的解可以合并为该问题的解。
- 原问题分解出的各个子问题是相互独立的,即子问题直接不包含公共的子问题。
- 求解过程
- 分解:将原问题分解为若干个规模较小的子问题。
- 求解:直接解决子问题或递归求解。
- 合并:将各个子问题的解合并及为原问题的解。
- 主定理计算分治算法的时间复杂性
如果在递推关系式中T(n) = a*T(n/b)+f(n),f(n)∈Θ(n^d)(d>=0)
则当a<b^d时,T(n) =Θ(n^d) ,当a = b^d时,T(n) = Θn^d*logn,当a<b^d时,T(n) =Θ(n^logba)(以b为低a的对数) 。
2.3重要代码及经典例题
-
二分查找
基本思路:将n个元素一分为二,取a[n/2]与x作比,如果相等,终止,如若x<a[n/2],则在数组a的左半部分继续搜索,相反,则在右半部分继续搜索。
/**
二分查找
*/
int BinarySearch(int a[], const int& x,int left, int right){
int left = 0;int right = n-1;
while(left<=right)
{
int middle = (left + right) / 2;
if (x == a[middle])
return middle;
if (x > a[middle])
left = middle + 1;
else
right = middle - 1;
}
return -1;
}
-
合并排序(归并排序)
基本思路:合并排序的基本思路是用分治策略实现对n个元素进行排序的算法,其基本思想是:将待排序的元素分成大小大致相同的两个子集合,分别对两个子集合进行排序,最终将排好序的子集合合并成要求的排好序的集合。代码如下:
#include<stdio.h>
void merge(int arr[],int temparr[],int left,int mid,int right)
{
int l_pos = left;
int r_pos = mid+1;
//临时元素下标
int pos = left;
while(l_pos <=mid&&r_pos<=right)
{
if(arr[l_pos]<arr[r_pos])
temparr[pos++] = arr[l_pos++];
else
temparr[pos++] = arr[r_pos++];
}
//合并左边剩余
while(l_pos<=mid)
temparr[pos++] = arr[l_pos++];
//合并右边剩余
while(r_pos<=right)
temparr[pos++] = arr[r_pos++];
while(left <= right)
{
arr[left] = temparr[left];
left++;
}
}
//归并排序
void msort(int arr[],int temparr[],int left,int right)
{
//只有一个数字只需要归并
if(left<right)//left小于right肯定有数
{
int mid = (left+right)/2;
//递归划分左半区
msort(arr,temparr,left,mid);
//划分右半区
msort(arr,temparr,mid+1,right);
//合并已经排序的部分
merge(arr,temparr,left,mid,right);
}
}
//归并排序入口
void merg_sort(int arr[],int n)
{
int temarr[1000];
msort(arr,temarr,0,n-1);
}
int main()
{
int a[10] = {3,6,7,10,8,1,5,4,9,2};
int n = 10;
merg_sort(a,n);
for(int i = 0;i<10;i++)
{
printf("%d ",a[i]);
}
}
算法分析:
归并排序算法的时间复杂度为O(nlogn)。当n=1时:T(n)=O(1)
当n>1时:T(n)=2T(n/2)+O(n)。
-
快速排序
基本思路:1分解(Divide):以a[p]为基准元素将a[p:r]划分成3段alp:q-1], a[q]和 a[q+1:r],使a[p:q-1]中任何一个元素小于等于a[q],而a[q+1:r]中任何一个元素大于等于 a[q]。下标q在划分过程中确定。
2递归求解(Conquer):通过递归调用快速排序算法,分别对a[p:q-1]和a[q+1:r]进行排序。
3合并(Merge): 由于对a[p:q-1]和al[q+1:r]的排序是就地进行的,因此在a[p:q-1]和a[q+ 1:r]都已排好的序后,不需要执行任何计算,a[p:r]则已排好序。
给出的代码以第一个元素为pivot(注释掉的为最后一个元素为pivot):
#include<stdio.h>
void quick_sort(int a[],int len);
void qsort(int a[],int low,int high);
int partition(int a[],int low,int high);
int main(){
int a[10] = {3,6,7,10,8,1,5,4,9,2};
int n = 10;
quick_sort(a,10);
for(int i = 0;i<10;i++)
{
printf("%d ",a[i]);
}
}
void quick_sort(int a[],int len)
{
qsort(a,0,len-1);
}
void qsort(int a[],int low,int high)
{
if(low<high)
{
int mid = partition(a,low,high);
qsort(a,low,mid-1);
qsort(a,mid+1,high);
}
}
//int partition(int a[],int low,int high)
//{
//
// int pivot = a[high];
// int i = low;
// for(int j = low;j<high;j++)
// {
// if(a[j]<pivot)
// {
// int temp;
// temp = a[j];
// a[j] = a[i];
// a[i] = temp;
// i++;
// }
// }
// int temp;
// temp = a[high];
// a[high] = a[i];
// a[i] = temp;
// return i;
//}
int partition(int a[],int low,int high)
{
int pivot = a[low];
int i = high;
for(int j = high-1;j>=low;j--)
{
if(a[j]>pivot)
{
int temp;
temp = a[j];
a[j] = a[i];
a[i] = temp;
i--;
}
}
int temp;
temp = a[low];
a[low] = a[i];
a[i] = temp;
return i;
}
//void quick_sort(int a[],int len)
//{
// q_sort(a,0,len-1);
//}
//void q_sort(int a[],int low,int high)
//{
// if(low<high)
// {
// int mid = partition(a,low,high);
// q_sort(a,low,mid-1);
// q_sort(a,mid+1,high);
// }
//
//}
//
//int partion(int a[],int low,int high)
//{
// int pivot = a[high];
// int i = low;
// for(int j = low;j<high;j++)
// {
// if(a[j]<pivot)
// swap(a[i++],a[j]);
// }
// swap(a[i],a[high]);
// return i;
//}
算法分析:
快速排序是在冒泡排序的基础上改进而来的,冒泡排序每次只能交换相邻的两个元素,而快速排序是跳跃式的交换,交换的距离很大,因此总的比较和交换次数少了很多,速度也快了不少。
但是快速排序在最坏情况下的时间复杂度和冒泡排序一样,是 O(n2),实际上每次比较都需要交换,但是这种情况并不常见。我们可以思考一下如果每次比较都需要交换,那么数列的平均时间复杂度是 O(nlogn),事实上在大多数时候,排序的速度要快于这个平均时间复杂度。这种算法实际上是一种分治法思想,也就是分而治之,把问题分为一个个的小部分来分别解决,再把结果组合起来。
快速排序只是使用数组原本的空间进行排序,所以所占用的空间应该是常量级的,但是由于每次划分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,在一般情况下的空间复杂度为 O(logn),在最差的情况下,若每次只完成了一个元素,那么空间复杂度为 O(n)。所以我们一般认为快速排序的空间复杂度为 O(logn)。
快速排序是一个不稳定的算法,在经过排序之后,可能会对相同值的元素的相对位置造成改变。
快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的。
下面给出快排随机标杆算法:
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
int Partition(int a[],int p,int r)
{
int i = 0,j = 0,temp = 0,x = 0;
x = a[r];
i = p-1;
for(j = p;j<r;j++)
{
if(a[j]<=x)
{
i++;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
temp = a[i+1];
a[i+1] = a[r];
a[r] = temp;
return i+1;
}
int RandomPartition(int a[],int p,int r)
{
int i = 0;
int temp = 0;
srand((unsigned)time(NULL));
i = rand()%(r-p+1)+p;
temp = a[i];
a[i] = a[r];
a[r] = temp;
return Partition(a,p,r);
}
void RandomQuickSort(int a[],int p,int r)
{
int q = 0;
if(p<r)
{
q = RandomPartition(a,p,r);
RandomQuickSort(a,p,q-1);
RandomQuickSort(a,q+1,r);
}
}
int main(){
int a[10] = {3,6,7,10,8,1,5,4,9,2};
int n = 10;
RandomQuickSort(a,0,n-1);
for(int i = 0;i<10;i++)
{
printf("%d ",a[i]);
}
return 0;
}
-
选择第k小
基本思路:本算法时在快速排序的基础上做的修改而得到的。基本思路是因为在快速排序时每进行一次快排,都有一个点的位置被确定了下来,利用这个被确定下来的位置判断k点的位置,如果小了,就往左找,大了向右找。
给个例题:邮局选址问题:
问题描述:
在一个按照东西和南北方向划分成规整街区的城市里,n个居民点散乱地分布在不同的街区中。用x 坐标表示东西向,用y坐标表示南北向。各居民点的位置可以由坐标(x,y)表示。街区中任意2 点(x1,y1)和(x2,y2)之间的距离可以用数值∣x1−x2∣+∣y1−y2∣度量。
居民们希望在城市中选择建立邮局的最佳位置,使n个居民点到邮局的距离总和最小。
编程任务:
给定n 个居民点的位置,编程计算邮局的最佳位置。
输入格式:
第1 行是居民点数n,1≤n≤5000000。接下来的n行是居民点的位置,每行两个整数x和y,分别为居民点位置的x坐标和y坐标,−109≤x,y≤109
输出格式:
输出只有一行,表示邮局的最佳位置,形如:(x,y)
输入样例:
在这里给出一组输入。例如:
5
1 2
2 2
1 3
3 -2
3 3
输出样例:
在这里给出相应的输出。例如:
(2,2)
解决代码:
#include<iostream>
using namespace std;
const int N = 1e7+5;
int x[N];
int y[N];
int qsort(int q[],int l,int r,int k)
{
if(l == r) return q[l];
int m=( l + r ) /2;
int x = q[m],i,j;
i = l ,j = r + 1;
swap(q[m],q[l]);
while(1)
{
while(q[++i] < x&&i<r);
while(q[--j] > x);
if(i>=j)break;
swap(q[i],q[j]);
}
q[l]=q[j];
q[j]=x;
int sl = j - l + 1;
if(sl==k)return q[j];
if(k < sl) qsort(q,l,j-1,k);
else qsort(q,j + 1,r,k - sl);
}
int main(){
int n;
scanf("%d",&n);
for(int i = 1;i<=n;i++)
{
scanf("%d%d",&x[i],&y[i]);
}
int xmid = qsort(x,1,n,(n+1)/2);
int ymid = qsort(y,1,n,(n+1)/2);
printf("(%d,%d)",xmid,ymid);
return 0;
}
-
大整数乘法
令X = A*2^n/2+B,Y = C*2^n/2+D
可将X*Y写成X*Y = AC*2^n+((A-B)*(D-C)+A*C+B*D)*2^n/2+B*D;
时间复杂度分析:
- Strassen矩阵乘法
七次乘法:
时间复杂度分析:
-
棋盘覆盖
在一个2^k×2^k个方格组成的棋盘中,若恰有一个方格与其他方格不同,则称该方格为一特殊方格,且称该棋盘为一特殊棋盘。显然,特殊方格在棋盘上出现的位置有4^k种情形。因而对任何k≥0,有4^k种特殊棋盘。图2-4中的特殊棋盘是k=2时16个特殊棋盘中的一个。
在棋盘覆盖问题中,要用图2-5所示的4种不同形态的L型骨牌覆盖一个给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。易知,在任何一个2^k×2^k的棋盘覆盖中,用到的L型骨牌个数恰为(4^k-1)/3
用分治策略,可以设计解棋盘覆盖问题的一个简捷的算法。当k>0时,将2^k×2^k棋盘分割为4个2^k-1×2^k-1子棋盘,如图2-6(a)所示。特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,如图2-6(b)所示,这3个子棋盘上被L型骨牌覆盖的方格就成为该棋盘上的特殊方格,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为1×1棋盘。
代码
时间复杂度分析:
-
最近点对问题
分治法:1.将点集分成大致相等的两个部分A和B
2.分别递归求解A和B中的最近点对Da和Db。
3.再求出一点在A中?零一点在B中的最近点对Dab。
4.合并得到原问题的解d = min{Da,Db,Dab}
步骤:
-
循环赛日程表
思路:观察此表,我们发现在比赛还有两个人时比赛顺序是固定的,所以 按分治的策略,将所有选手分为两半,n个选手的比赛日程表就可以通过为n/2个选手设计的比赛日程表来决定。递归的执行选手的分割,知道剩下两个选手时,只要这两个选手进行比赛
#include<stdio.h>
int a[10000][10000];
void Table(int n);
void copyToLB(int k);
void copyToRB(int k);
void copyToRT(int k);
int main()
{
int n;
scanf("%d",&n);
Table(n);
for(int i = 0;i<n;i++)
{
for(int j = 0;j<n;j++)
{
printf("%d ",a[i][j]);
}
printf("\n");
}
}
void Table(int n)
{
if(n == 2)
{
a[0][0] = 1;
a[1][1] = 1;
a[0][1] = 2;
a[1][0] = 2;
}
else
{
Table(n/2);
copyToLB(n/2);
copyToRT(n/2);
copyToRB(n/2);
}
}
void copyToLB(int k)
{
for(int i = 0;i<k;++i)
{
for(int j = 0;j<k;++j)
a[i+k][j] = a[i][j]+k;
}
}
void copyToRT(int k)
{
for(int i = 0;i<k;++i)
{
for(int j = 0;j<k;++j)
a[i][j+k] = a[i][j]+k;
}
}
void copyToRB(int k)
{
for(int i = 0;i<k;++i)
{
for(int j = 0;j<k;++j)
a[i+k][j+k] = a[i][j];
}
}