目录
📘At first:一个初学算法的萌新,如果文中有误,还请指正🤓
🎗️专栏介绍:本专栏目前基于AcWing算法基础课进行笔记的记录,包括及课上大佬讲的一些算法的模板还有自己的一些心得和理解
🕶️个人博客地址:https://blog.csdn.net/m0_73352841?spm=1010.2135.3001.5343
一、快速排序
快速排序的思想是分治
1.1 步骤
1、先确定分界点:可以是左边界,中间值、右边界或者任意位置都行
2、调整区间:小于等于x的在左半边,大于等于x的在右半边(不用纠结那个中间的位置),『难点』💣💣💣
3、递归处理左右两段
1.2 代码实现
void quick_sort(int q[], int l, int r)
//q[]为传入的数组
//l为传入的做边界值,一般是0
//r是传入的右边界的值,一般是n-1,n为数组中总共的个数,由于下标的因素,故r为n-1
{
if (l == r) return;
//当数组中就一个数时,就不用排了,直接结束该函数,条件也可以是l >= r,效果是一样的
int x = q[l + r + 1 >> 1], i = l - 1, j = r + 1;
//由于后面代码的因素,这里i和j分别在l和r的基础上往外再移1个位
while (i < j)
{
do i ++ ; while (q[i] <x);
//只要q[i]的值小于定义的分界点的值,i的位置就往右移,直到大于它
do j -- ; while (q[j] >x);
//只要q[j]的值大于定义的分界点的值,j的位置就往左移,直到小于它
if (i < j) swap(q[i], q[j]) ;
//如果此时,下标i小于j,它们两个的值就交换,也就是我们步骤2中的“调整区间”
}
quick_sort(q, l, j);
//递归处理左边
quick_sort(q, j + 1, r);
//递归处理右边
//其实这里的j也可以替换成i,因为边界问题,要做相应的调整
//为什么是j和j+1,请看以下图示
}
1.3 图示
以下面的数据为例
5 3 4 1 2
二、归并排序
归并排序的思想同样是分治
2.1 步骤
1、确定分界点:中点
2、递归排序:分别排序,中点两侧
3、归并:合二为一,『难点』💣💣💣
2.2 代码实现
void merge_sort(int q[], int l, int r)
{
if (l == r) return;
当数组中就一个数时,就不用排了,直接结束该函数,条件也可以是l >= r,效果是一样的
int mid = l + r >> 1;
//确定中间点
merge_sort(q, l, mid), merge_sort(q, mid + 1, r);
//递归排序左边、递归排序右边
//以下为归并的过程
int k = 0, i = l, j = mid + 1;
//i指向左半边已排好序的起点
//j指向右半边已排好序的起点
while (i <= mid && j <= r)
{
//条件自然是直到循环完左右两个数组为止
if (q[i] < q[j]) tmp[k ++ ] = q[ i ++ ];
else tmp [k ++ ] = q[j ++ ];
//这两句是每次把小的那个数放入tmp临时数组中
}
while (i <= mid) tmp[k ++ ] = q[i ++ ];
while (j <= r) tmp[k ++ ] = q[j ++];
//这两句是左右两边如果没有循环完的话,把剩下的元素直接接到这个tmp临时数组中,因为都是已经排好序的
for (i = l, j = 0; i <= r; i ++ , j ++ ) q[i] = tmp[j];
}
//将tmp临时数组中的数据存回原数组q中去
2.3 图示
以下面的数据为例
5 3 4 1
1 - 整个排序过程
2 - 其内部排序过程,每一次迭代都会排序
(下图中只显示出最后一次分完数组内的元素的排序)
3 - 编译器验证
注
1、图中第一个输入的数是来表示输入几个数的,即4个
2、我们用“#”来表示排序过程进行了几次,即
void merge_sort(int q[], int l, int r)
{
if (l == r) return;
int mid = l + r >> 1;
merge_sort(q, l, mid), merge_sort(q, mid + 1, r);
printf("#\n");//添加标记
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (q[i] < q[j]) tmp[k ++ ] = q[ i ++ ];
else tmp [k ++ ] = q[j ++ ];
while (i <= mid) tmp[k ++ ] = q[i ++ ];
while (j <= r) tmp[k ++ ] = q[j ++];
for (i = l, j = 0; i <= r; i ++ , j ++ ) q[i] = tmp[j];
}
出现了3次“#”。可见,排序进行了3次,对应图中5与3、4与1、3-5与1-4的3次排序合并
三、二分
3.1 整数二分
二分的本质不是单调性。它们的关系是,如果有单调性的话一定可以二分,但是可以二分的题目不一定非得有单调性
二分的本质是边界
假设给定一个区间,在这个区间上定义了某种性质,设为x
假设右半边是满足性质的,我们用绿色表示;左半边是不满足性质的,用红色表示。由于是整数二分,不能有交点
如果我们能找到这样一个性质的话,把整个区间一分为二,一半满足,一半不满足,那么二分就能寻找性质的边界。既可以寻找红色的边界a,也可以寻找绿色的边界b
3.1.1 两段查找代码
寻找左边界
int bserch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
//check()判断mid是否满足性质
else l = mid + 1;
return l;
}
}
寻找右边界
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
//check()判断mid是否满足性质
else r = mid - 1;
}
return l;
}
问题
Q1:为什么寻找右边界的时候mid需要多加1?
A1:假设,l
(left)和r
(right)相距1(见上图左右端点),mid=(l+r加1)/2=l,区间更新后,还是(l, r)
,会陷入死循环
Q2:为什么最后输出l而不是r
?
A2:其实都可以,循环结束时,l
和r
指向同一元素,见下“图示”
3.1.2 图示
下面以一道例题为例
题目描述
给定一个按照升序排列的长度为n
的整数数组,以及q
个查询。
对于每个查询,返回一个元素k的起始位置和终止位置(位置从0开始计数)。
如果数组中不存在该元素,则返回“-1 -1”.
输入格式
第一行包含整数n
和q
,表示数组长度和询问格式。
第二行包含n
个整数(均在1~10000范围内),表示完整数组。
接下来q
行,每行包含一个整数k
,表示一个询问元素。
输出格式
共q
行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回“-1 -1”.
数据范围
1 <= n <= 100000,
1 <= q <= 10000,
1 <= k <= 100000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
完整代码
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
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;
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;
}
}
}
由于是给定了按照升序排列的数组,暗含了其单调性。我们就以所给样例数据为例
确定左边界:用mid=(l+r)/2,进行循环判断,最终l和r指向同一元素(x是要寻找的数的第一个位置和最后一个位置,这里默认为3,即x=3,因为是样例中第一个输入的数,我们以样例为例)
确定右边界:用mid=(l+r+1)/2,进行循环判断,最终l和r指向同一元素
一点想法
其实可以看看两次得到"3"的地址,来判断我们的两段代码是否真的取到的是左边界和右边界
可以看到,第一个“3”的地址大于第二个“3”的地址,说明第一段代码得到的就是左边界,后者得到的就是右边界,一个在前一个在后
3.2 浮点数二分
浮点数二分和整数二分本质上差不多,都是探讨边界,但是要比前者简单些,少考虑一些边界。这里就以一个求解一个数的平方根为例,简单说明
一种写法
#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;
//mid^2大于x说明mid在x^1/2的右边,将右边界向左移动即可
else l = mid;
//反之,将左边界向右移动
}
printf("%lf\n", l);
return 0;
}
另一种写法
#include <iostream>
using namespace std;
int main()
{
double x;
cin >> x;
double l = 0, r = x;
for (int i = 0; i < 100; i ++ )
{
double mid = (l + r) / 2;
if (mid * mid >= x) r = mid;
else l = mid;
}
printf("%d", l);
return 0;
}
不用精度来表示迭代,直接迭代100次。两者类似,前者是当精度足够小的时候停止,后者是将整个区间的长度除以2^100,除完之后就很小了