14天阅读挑战赛
努力是为了不平庸~
算法学习有些时候是枯燥的,这一次,让我们先人一步,趣学算法!欢迎记录下你的那些努力时刻(算法学习知识点/算法题解/遇到的算法bug/等等),在分享的同时加深对于算法的理解,同时吸收他人的奇思妙想,一起见证技术er的成长~
一、分治算法
分治算法采用分而治之的策略,将一个大规模的问题分解为若干个规模较小的相同子问题。
1.使用分治算法的条件
(1)原问题可分解为若干个规模较小的相同子问题。
(2)子问题相互独立。
(3)子问题的解可以合并为原问题的解。
2.分治法解题步骤
(1)分解:将要解决的问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题。
(2)治理:求解各个子问题。由于各个子问题与原问题形式相同,只是规模较小而已,而当子问题划分得足够小时,就可以用较简单的方法解决。
(3)合并:按原问题的要求,将子问题的解合并为原问题的解。
二、分治算法实例——二分搜索
1.问题描述
主持人在女嘉宾的手心上写一个10以内的整数,让女嘉宾的老公猜是多少,而女嘉宾只能提示大了,还是小了,并且只有3次机会。
主持人悄悄地在女嘉宾手心写了一个8。
老公:“2。”
老婆:“小了。”
老公:“3。”
老婆:“小了。”
老公:“10。”
老婆:“晕了!”
2.问题分析
猜数游戏是一个简单的二分搜索问题。
在有序序列中查找,每次和中间的元素比较,如果比中间元素小,则在前半部分查找,如果比中间元素大,则去后半部分查找。这种方法称为二分查找或折半查找,也称为二分搜索技术。
3.算法设计
- 初始化。令low=0,即指向有序数组S[]的第一个元素;high=n−1。
- 判定low≤high是否成立,如果成立,转向第3步,否则,算法结束。
- middle=(low+high)/2,即指向查找范围的中间元素。
- 判断x与S[middle]的关系。如果x=S[middle],则搜索成功,算法结束;如果x>S[middle],则令low=middle+1;否则令high=middle−1,转向第2步。
4.程序代码
//二分搜索,非递归算法
#include<iostream>
#include<algorithm>
using namespace std;
const int M=10005;
int s[M];
int BinarySearch(int s[],int n,int x){
int low=0,high=n-1; //low指向有序数组的第一个元素,high指向有序数组的最后一个元素
while(low<=high){
int middle=(low+high)/2; //middle为查找范围的中间值
if(x==s[middle]) //x等于中间元素,查找成功
return middle;
else if(x>s[middle]) //x大于中间元素,则在前半部分查找
low=middle+1;
else //x小于中间元素,则在后半部分查找
high=middle-1;
}
return -1;
}
int main(){
int t,n,x; //测试用例数,元素个数,待查找元素
cin>>t;
while(t--){
cin>>n;
for(int i=0;i<n;i++)
cin>>s[i];
sort(s,s+n); //升序排序
for(int i=0;i<n;i++) //输出有序序列
cout<<s[i]<<" ";
cout<<endl;
cin>>x; //输入要查找的元素
int k=BinarySearch(s,n,x);
if(k==-1)
cout<<"-1"<<endl;
else
cout<<k+1<<endl;
}
return 0;
}
5.算法分析
当n>1时,待查找元素和中间位置元素比较,需要O(1)时间,如果比较不成功,那么需要在前半部分或后半部分搜索,问题的规模缩小了一半,时间复杂度变为T(n/2)。
T
(
n
)
=
{
O
(
1
)
,n=1
T
(
n
/
2
)
+
O
(
1
)
,n>1
T(n)= \begin{cases} O(1)& \text{,n=1}\\ T(n/2)+O(1)& \text{,n>1} \end{cases}
T(n)={O(1)T(n/2)+O(1),n=1,n>1
T
(
n
)
=
T
(
n
/
2
)
+
O
(
1
)
=
T
(
n
/
2
2
)
+
2
O
(
1
)
=
T
(
n
/
2
3
)
+
3
O
(
1
)
=
.
.
.
.
.
.
=
T
(
n
/
2
x
)
+
x
O
(
1
)
T(n)=T(n/2)+O(1)=T(n/2^2)+2O(1)=T(n/2^3)+3O(1)=......=T(n/2^x)+xO(1)
T(n)=T(n/2)+O(1)=T(n/22)+2O(1)=T(n/23)+3O(1)=......=T(n/2x)+xO(1)
令
n
=
2
x
,
n=2^x,
n=2x,则
x
=
l
o
g
n
。
T
(
n
)
=
T
(
1
)
+
l
o
g
n
O
(
1
)
=
O
(
1
)
+
l
o
g
n
O
(
1
)
=
O
(
l
o
g
n
)
x=logn。T(n)=T(1)+lognO(1)=O(1)+lognO(1)=O(logn)
x=logn。T(n)=T(1)+lognO(1)=O(1)+lognO(1)=O(logn)
二分查找的时间复杂度的为O(logn)。
二分查找的空间复杂度的为O(1)。
三、分治算法实例——合并排序
1.基本思想
合并排序是采用分治的策略,将一个大的问题分成很多小问题,再通过小问题解决大问题。
把待排序元素分解成两个规模大致相等的子序列,如果不易解决,再将得到的子序列继续分解,直到子序列中包含1个元素。然后自底向上进行合并,得到一个有序序列。
2.排序步骤
1)分解—将待排序元素分成大小相当的两个子序列。
2)治理—对两个子序列进行合并排序。
3)合并—将排好序的有序子序列合并,得到有序序列。
3.程序代码
//合并排序
#include<iostream>
using namespace std;
const int M=10005;
int A[M];
void Merge(int A[],int low,int mid,int high){ //合并函数,合并有序序列A[low:mid]和A[mid+1:high]
int *B=new int[high-low+1];//申请一个辅助数组
int i=low,j=mid+1,k=0;
while(i<=mid&&j<=high) {
if(A[i]<=A[j]) //将较小元素存放到B[]中
B[k++]=A[i++];
else
B[k++]=A[j++];
}
while(i<=mid) B[k++]=A[i++]; //如果前半部分有剩余,将剩余元素放置到B[]中
while(j<=high) B[k++]=A[j++]; //如果后半部分有剩余,将剩余元素放置到B[]中
for(i=low,k=0;i<=high;i++) //将有序序列放回A[]数组
A[i]=B[k++];
delete []B;
}
void MergeSort(int A[],int low,int high){//合并排序
if(low<high){
int mid=(low+high)/2; //取中点
MergeSort(A,low,mid); //对A[low:mid]中的元素合并排序
MergeSort(A,mid+1,high); //对A[mid+1:high]中的元素合并排序
Merge(A,low,mid,high); //将两个有序序列A[low:mid]和A[mid+1:high]合并
}
}
int main(){
int t,n; //测试用例数,元素个数
cin>>t;
while(t--){
cin>>n;
for(int i=0;i<n;i++)
cin>>A[i];
MergeSort(A,0,n-1);
for(int i=0;i<n;i++)
cout<<A[i]<<" ";
cout<<endl;
}
return 0;
}
4.算法分析
时间复杂度:
• 分解:这一步仅仅计算出子序列的中间位置,需要O(1)时间。
• 解决:递归求解两个规模为n/2的子问题,所需时间为2T(n/2)。
• 合并:Merge算法可以在O(n)的时间内完成。
T
(
n
)
=
{
O
(
1
)
,n=1
2
T
(
n
/
2
)
+
O
(
n
)
,n>1
T(n)= \begin{cases} O(1)& \text{,n=1}\\ 2T(n/2)+O(n)& \text{,n>1} \end{cases}
T(n)={O(1)2T(n/2)+O(n),n=1,n>1
时间复杂度:O(nlogn)
空间复杂度:O(n)
四、分治算法实例——快速排序
1.基本思想
通过一组排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,最终使所有数据变成有序序列。
2.排序步骤
1)分解:先从数列中取出一个元素作为基准元素。以基准元素为标准,将问题分解为两个子序列,使小于或等于基准元素的子序列在左侧,使大于基准元素的子序列在右侧。
2)治理:对两个子序列进行快速排序。
3)合并:将排好序的两个子序列合并在一起,得到原问题的解。
2.1.基准元素选取有以下几种方法:
• 取第一个元素。
• 取最后一个元素。
• 取中间位置元素。
• 取第一个、最后一个、中间位置元素三者之中位数。
• 取第一个和最后一个之间位置的随机数k(low≤k≤high),选R[k]做基准元素。
3.算法步骤
1)取第一个元素作为基准元素pivot=R[low],i=low,j=high。
2)从右向左扫描,找小于等于pivot的数,则R[i]和R[j]交换,i++。
3)从左向右扫描,找大于pivot的数,则R[i]和R[j]交换,j− −。
4)重复2)和3),直到i和j重合,返回该位置mid=i,该位置的数正好是pivot元素。
5)至此完成一趟排序。此时以mid为界,将原数据分为两个子序列,左侧子序列元素都比pivot小,右侧子序列元素都比pivot大,再分别对这两个子序列进行快速排序。
4.程序代码
//快速排序
#include<iostream>
using namespace std;
const int M=10005;
int R[M];
int Partition(int R[],int low,int high){//划分函数
int i=low,j=high,pivot=R[low];//基准元素
while(i<j){
while(i<j&&R[j]>pivot) j--;//向左扫描
if(i<j)
swap(R[i++],R[j]);//R[i]和R[j]交换后,i+1右移一位
while(i<j&&R[i]<=pivot) i++;//向右扫描
if(i<j)
swap(R[i],R[j--]);//R[i]和R[j]交换后,j-1左移一位
}
return i;//返回划分完成后基准元素位置
}
int Partition2(int R[],int low,int high){//划分函数优化
int i=low,j=high,pivot=R[low];//基准元素
while(i<j){
while(i<j&&R[j]>pivot) j--;//向左扫描
while(i<j&&R[i]<=pivot) i++;//向右扫描
if(i<j)
swap(R[i++],R[j--]);//R[i]和R[j]交换
}
if(R[i]>pivot){
swap(R[i-1],R[low]);//R[i-1]和R[low]交换
return i-1;//返回划分完成后基准元素位置
}
swap(R[i],R[low]);//R[i]和R[low]交换
return i;//返回划分完成后基准元素位置
}
void QuickSort(int R[],int low,int high){//实现快排算法
if(low<high){
int mid=Partition(R,low,high); //基准位置
QuickSort(R,low,mid-1);//左区间递归快排
QuickSort(R,mid+1,high);//右区间递归快排
}
}
int main(){
int t,n; //测试用例数,元素个数
cin>>t;
while(t--){
cin>>n;
for(int i=0;i<n;i++)
cin>>R[i];
QuickSort(R,0,n-1);
for(int i=0;i<n;i++){
if(i!=0)
cout<<" ";
cout<<R[i];
}
cout<<endl;
}
return 0;
}
5.算法分析
5.1时间复杂度分析
1)分解:划分函数Partition需要扫描每个元素,每次扫描的元素个数不超过n,因此时间复杂度为O(n)。
2)治理:最好情况下,每次划分将问题分解为两个n/2的子问题,递归求解两个子问题,所需时间为2T(n/2) 。最坏情况下,每次划分将问题分解为1和n-1的子问题,所需时间为T(n-1) 。
3)合并:原地排序,合并操作不需要时间。
a.最好情况:
T
(
n
)
=
{
O
(
1
)
,n=1
2
T
(
n
/
2
)
+
O
(
n
)
,n>1
T(n)= \begin{cases} O(1)& \text{,n=1}\\ 2T(n/2)+O(n)& \text{,n>1} \end{cases}
T(n)={O(1)2T(n/2)+O(n),n=1,n>1
T
(
n
)
=
2
T
(
n
/
2
)
+
O
(
n
)
=
2
(
2
T
(
n
/
2
2
)
+
O
(
n
/
2
)
)
+
O
(
n
)
=
2
2
T
(
n
/
2
2
)
+
2
O
(
n
)
=
2
3
T
(
n
/
2
3
)
+
3
O
(
n
)
=
.
.
.
.
.
.
=
2
x
T
(
n
/
2
x
)
+
x
O
(
n
)
T(n)=2T(n/2)+O(n)=2(2T(n/2^2)+O(n/2))+O(n)=2^2T(n/2^2)+2O(n)=2^3T(n/2^3)+3O(n)=......=2^xT(n/2^x)+xO(n)
T(n)=2T(n/2)+O(n)=2(2T(n/22)+O(n/2))+O(n)=22T(n/22)+2O(n)=23T(n/23)+3O(n)=......=2xT(n/2x)+xO(n)
令
n
=
2
x
,
n=2^x,
n=2x,则
x
=
l
o
g
n
。
T
(
n
)
=
n
T
(
1
)
+
l
o
g
n
O
(
n
)
=
n
+
l
o
g
n
O
(
n
)
=
O
(
n
l
o
g
n
)
x=logn。T(n)=nT(1)+lognO(n)=n+lognO(n)=O(nlogn)
x=logn。T(n)=nT(1)+lognO(n)=n+lognO(n)=O(nlogn)
b.最坏情况:
T
(
n
)
=
{
O
(
1
)
,n=1
T
(
n
−
1
)
+
O
(
n
)
,n>1
T(n)= \begin{cases} O(1)& \text{,n=1}\\ T(n-1)+O(n)& \text{,n>1} \end{cases}
T(n)={O(1)T(n−1)+O(n),n=1,n>1
T
(
n
)
=
T
(
n
−
1
)
+
O
(
n
)
=
T
(
n
−
2
)
+
O
(
n
−
1
)
+
O
(
n
)
=
T
(
n
−
3
)
+
O
(
n
−
2
)
+
O
(
n
−
1
)
+
O
(
n
)
=
.
.
.
.
.
.
=
T
(
1
)
+
O
(
2
)
+
.
.
.
+
O
(
n
−
1
)
+
O
(
n
)
=
O
(
1
)
+
O
(
2
)
+
.
.
.
+
O
(
n
−
1
)
+
O
(
n
)
=
O
(
n
(
n
+
1
)
/
2
)
T(n)=T(n-1)+O(n)=T(n-2)+O(n-1)+O(n)=T(n-3)+O(n-2)+O(n-1)+O(n)=......=T(1)+O(2)+...+O(n-1)+O(n)=O(1)+O(2)+...+O(n-1)+O(n)=O(n(n+1)/2)
T(n)=T(n−1)+O(n)=T(n−2)+O(n−1)+O(n)=T(n−3)+O(n−2)+O(n−1)+O(n)=......=T(1)+O(2)+...+O(n−1)+O(n)=O(1)+O(2)+...+O(n−1)+O(n)=O(n(n+1)/2)
c.平均情况:
T
(
n
)
=
1
n
∑
k
=
1
n
(
T
(
n
−
k
)
+
T
(
k
−
1
)
)
+
O
(
n
)
=
1
n
(
T
(
n
−
1
)
+
T
(
0
)
+
T
(
n
−
2
)
+
T
(
1
)
+
.
.
.
+
T
(
1
)
+
T
(
n
−
2
)
+
T
(
0
)
+
T
(
n
−
1
)
)
+
O
(
n
)
=
2
n
∑
k
=
1
n
−
1
T
(
k
)
+
O
(
n
)
T(n)=\frac{1}{n}\sum_{k=1}^{n}(T(n-k)+T(k-1))+O(n)=\frac{1}{n}(T(n-1)+T(0)+T(n-2)+T(1)+...+T(1)+T(n-2)+T(0)+T(n-1))+O(n)=\frac{2}{n}\sum_{k=1}^{n-1}T(k)+O(n)
T(n)=n1k=1∑n(T(n−k)+T(k−1))+O(n)=n1(T(n−1)+T(0)+T(n−2)+T(1)+...+T(1)+T(n−2)+T(0)+T(n−1))+O(n)=n2k=1∑n−1T(k)+O(n)
5.2空间复杂度分析:
最好(平均)情况:
- 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
- 空间复杂度: O ( l o g n ) O(logn) O(logn)
最坏情况:
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( n ) O(n) O(n)