1. 排序
1.1 快速排序 —— 分治
(一)步骤
- 确定分界点 x :
(1)q[ l ]
(2)q[ ( l + r ) / 2 ]
(3)q[ r ]
(4)随机
- 调整区间 :保证区间左边的数小于等于 x , 区间右边的数大于等于 x
(1)暴力调整区间
① 创建两个数组,a[ ] 、b[ ]
② 扫描 q[ l ~ r ] ,
q [ i ] <= x , 放入 a[ ] 中
q [ i ] > x , 放入 b[ ] 中
③ 先将 a[ ] 中数放入 q [ ] 中,再将 b[ ] 放入 q[ ] 中
(2)优美解法
使用两个指针,i 指针在左边,j 指针在右边,两个指针同时往中间走。
如果 i 指针指向的数小于 x,i 指针继续向中间(右)移动 ,直到某次 i 指针指向的数大于等于 x , 这个数应该放到区间右边,此时 i 停下,移动 j 。
如果 j 指针指向的数大于 x,j 指针继续向中间(左)移动,直到 j 指针指向的数小于等于 x ,这个数应该放到左半边。
此时,i 指向的数应该放到右边,j 指向的数应该放到左边,交换 i 、 j 指向的数。
交换完后,i 、j 指针各向中间移动一位。
i ,j 继续往中间走,直到 i,j 相遇,区间一分为二。
(任意时刻都保证 i 指针左边的数小于 x, j 指针右边的数大于 x)
- 递归处理左右两端
(二)代码模板
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int q[N];
void quick_sort(int q[], int l, int r)
{
if(l >= r) return; // 如果区间没有数或者只有一个数,不需要排序直接返回
int x = q[l],i = l - 1,j = r + 1; // 分界点取左边界
while(i<j)
{
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if(i < j) swap(q[i],q[j]);
}
quick_sort(q,l,j);
quick_sort(q,j+1,r);
/*
分界点取左边界,后面的递归取j
*/
}
int main()
{
scanf("%d",&n);
for(int i = 0;i < n;i++)
scanf("%d",&q[i]);
quick_sort(q, 0, n-1);
for( i = 0;i < n; i++)
printf("%d",q[i]);
return 0;
}
(三)快速排序基本思想
从待排序列中任意选择一个记录,以该记录的关键字作为“枢轴”,凡其关键字小于枢轴的记录均移至枢轴之前;凡关键字大于枢轴的记录均移至枢轴之后。
使一趟排序之后,记录的无序序列q将分割成左右两个子序列,然后分别对分割所得到的两个子序列递归地进行快速排序,直至每个子序列中只含有一个记录为止。
1.2 归并排序——分治
(一)步骤
-
确定分界点,mid = (l+r)/2
-
先递归排序左右两边,排完序之后左右两边都是有序的序列
-
归并:将两个有序的数组合为一个有序的数组(合二为一)
有两个数组a,b,a,b都按照从小到大的顺序排列,现在要将a,b合二为一,要求得到的新数组数据也是由小到大。
创建一个新数组c,指针 i,j分别指向a,b的最左端,比较a[i],a[j],将最小的数放到c中,并将该数所在的数组指针继续向下移。当a,b中的任何一个数组遍历完毕,将另一个数组中的数据直接插到c的后边。
*O(logN)底数是多少?
算法中log级别的时间复杂度都是由于使用了分治思想,这个底数直接由分治的复杂度决定。如果采用二分法,那么就会以2为底数,三分法就会以3为底数,其他亦然。
O(log n),而不论对数的底是多少,是对数时间算法的标准记法。
(二)代码模板
#include<iostream>
using namespace std;
const int N = 1000010;
int n;
int q[N],tmp[N];
void merge_sort(int q[],int l,int r)
{
if(l>=r)
return;
int mid = l+r >> 1; // 1.确定分界点(l+r)/2
merge_sort(q,l,mid);
merge_sort(q,mid+1,r); // 2.递归排序左右两边(q 不是一个全局变量吗,为什么还要作为参数在这里传递)
//3.合并
int k=0; // tmp数组中元素个数
int i=l,j=mid+1; // i指向左半边的起点,j指向右半边的起点
while(i <= mid && j<=r)//i <= 左半边边界,j <= 右半边边界
{
if(q[i] <= q[j])tmp[k++] = q[i++];
else tmp[k++] = q[j++];
}
while(i <= mid)tmp[k++]=q[i++]; // 左边没有取完(右边已取完)将左边所有元素直接加到tmp后边
while(j <= r)tmp[k++] = q[j++];
for(i = l,j = 0;i <= r;i++,j++) q[i] = tmp[j];
}
int main()
{
scanf("%d",&n);
for(int i = 0;i < n;i++) scanf("%d",&q[i]);
merge_sort(q,0,n-1);
for( i = 0;i < n;i++) printf("%d",q[i]);
return 0;
}
1.3 整数二分
有单调性一定可以二分,可以二分的题目不一定非要有单调性。二分的本质并不是单调性。
在一个区间中如果可以找到一个性质,使右半边满足,左半边不满足,(左右半边没有交点),我们可以将边界点二分出来。
二分的本质是边界。
给定一个区间,在区间上定义某种性质,左半边满足性质,右半边不满足,左右两边没有交点。二分就可以寻找左右两边的边界。
(一)模板
-
如何二分出左边边界点:
找中间值,判断中间值是否满足左半边性质。
mid=(l+r**+1**)/2
满足:左边 边界点在[mid,r]中,更新:l=mid(mid在左半边区域,左半边区域分界点在mid右边)
不满足:[l,mid-1],更新 r=mid-1
【注】这里的mid为什么等于(l+r+1)/2?
如果l=r-1,如果mid这里不加1,那么mid=(l+r)/2=(2l+1)/2=l,如果此时check(mid)结果为true,更新:l=mid,更新后的区间为[l,r],如此会死循环下去。
如果mid这里加1,那么mid=(l+r+1)/2=(r-1+r+1)/2=r,更新后的区间为[r,r],循环停止。
-
如何二分出右边边界点:
找中间值,判断中间值是否满足右半边性质。
mid=(l+r )/2
满足:[l,mid] r=mid
不满足:[mid+1,r] l=mid+1
给我们一个二分问题,如何选择用模板1还是2,mid应该如何写?
二分问题先写一个 check函数,想一下check(mid)的true和false情况如何更新。根据l,r的更新情况选择mid。
(二)代码模板
#include<iostream>
using namespace std;
const int N = 100010;
int m,n;
int q[N];
int main()
{
scanf("%d%d",&n,&m);
for(int i = 0;i < n;i++) scanf("%d",&q[i]);
while(m--)
{
int x;
scanf("%d",&x);
int l=0,r=n-1;
while(l<r)
{
int mid = l + r >> 1;
if(q[mid]>=x) r=mid; // check(mid)应该如何写
else l=mid+1;
}
if(q[l] != x)cout<<"-1 -1"<<endl;
else{
cout<<l<<" ";
int l=0,r=n-1;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(q[mid] <= x)l = mid;
else r = mid - 1;
}
cout<<l<<endl;
}
}
return 0;
}
输入:
5 1
2 2 2 3 4
2
输出:
0 2
check(mid)应该如何写?
1.4 浮点数二分
#include<iostream>
using namespace std;
int main()
{
// 求平方根
double x;
cin>>x;
double l=0,r=x;
while(r-l>1e-8)
{
double mid = (l+r)/2;
if(mid*mid >= x)r=mid;
else l=mid;
}
printf("%lf",l);
return 0;
}