文章目录
基础算法(一)
本节讲解的是排序和二分,排序讲解了快排和归并,二分讲解了整数二分和浮点数二分。
排序
快排
quick_sort(int q[], int l, int r)
q
是待排序数组,l
是待排序区间的左边界,r
是右边界
- 基本思路
- 选取一个基准值
x
可以取左边界的值q[l]
,或右边界的值q[r]
,或者中间位置的值q[l + r >> 1]
- ⭐️根据基准值,调整区间,使得左半边区间的值全都
≤x
,右半边区间的值全都≥x
采用双指针,左指针i
从左边界l
开始,往右扫描,右指针j
从右边界r
开始,往左扫描。
当满足条件q[i] < x
时,i
右移;直到不满足条件时,i
停下;开始移动j
当满足条件q[j] > x
时,j
左移;直到不满足条件时,j
停下;交换q[i]
和q[j]
;
将i
右移一位,j
左移一位。
重复上面的操作,直到i
和j
相遇。此时左半区间的数都满足≤x
,且左半区间的最后一个数的下标为j
,右半区间的数都满足≥x
,且右半区间的第一个数的下标为i
。i
和j
之间的关系为:i = j + 1
或i = j
- 对左右两边的区间,做递归操作
递归操作[l, j]
,[j + 1, r]
区间,或者[l, i - 1]
,[i, r]
区间即可
- 算法题目
Acwing - 785 快速排序#include<iostream> using namespace std; const int N = 100010; int n; int q[N]; void quick_sort(int q[], int l, int r) { if(l >= r) return; // 递归退出条件, 不要写成 l == r, 可能会出现 l > r 的情况,可以尝试用例[1,2] int x = q[l + r >> 1], i = l - 1, j = r + 1; // 这里 i 置为 l - 1, j 置为 r + 1, 是因为下面更新i, j 时采用了do while循环, while(i < j) { // 这里不要写成 i <= j do i++; while(q[i] < x);// 不能写为 <= x , 那样写 i 可能会越界, 考虑 [1,1,1]。 // 因为基准值x是数组中的一个数,i从左往右移动的过程中,一定会遇到这个数x,此时不满足小于条件, i 一定会停下,也就变相保证了 i 不会越界。 do j--; while(q[j] > x);// 不能写为 >= x , 因为 j 可能会越界, 原因同上 if(i < j) swap(q[i], q[j]); // 若 i 和 j 还未相遇, 则交换2个数 } quick_sort(q, l, j); quick_sort(q, j + 1, r); } int main() { scanf("%d", &n); for(int i = 0; i < n; i++) scanf("%d", &q[i]); quick_sort(q, 0, n - 1); for(int i = 0; i < n; i++) printf("%d ", q[i]); }
- 注意要点当区间划分结束后,左指针
i
和右指针j
的相对位置,只有2种情况 -
i
= j + 1
-
i
= j
(此时i
和j
指向的元素,恰好等于基准值x
)
- 若用
j
来作为区间的分界,则[l, j]
都是≤x
,[j + 1, r]
都是≥x
若用i
来作为区间的分界,则[l, i - 1]
都是≤x
,[i, r]
都是≥x
当取i
作为分界的话,基准值x
不能取到左边界q[l]
,否则会出现死循环,比如用例[1,2]
。此时基准值可以取q[r]
,或者q[l + r + 1 >> 1]
,注意取中间位置的数时,要加个1
,避免l + r >> 1
的结果为l
当取j
作为分界的话,基准值x
不能取到右边界q[r]
,否则会出现死循环。此时基准值可以取q[l]
,或者q[l + r >> 1]
- 算法模板
// 任选一种模板进行记忆即可, 下面采用: 基准值取中间位置, 递归时使用j作分界 void quick_sort(int q[], int l, int r) { if(l >= r) return; int x = q[l + r >> 1], 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); }
- 思想总结
- 选一个值
x
- 以该值为分界点,将数组分为左右两部分,左边的全都
≤x
,右边的全都≥x
- 对左右两部分进行递归操作
衍生题目:求第k个数
练习题:Acwing - 786 第k个数
#include<iostream>
using namespace std;
const int N = 1e5 +10;
int n, k;
int q[N];
// 选取[l, r]区间内数组q第k小的数
int quick_select(int q[], int l, int r, int k) {
if(l == r) return q[l]; // 找到答案
int x = q[l + r >> 1], i = l - 1, j = r + 1;
while(i < j) {
while(q[++i] < x);
while(q[--j] > x);
if(i < j) swap(q[i], q[j]);
}
int left = j - l + 1;
if(k <= left) return quick_select(q, l, j, k);
else return quick_select(q, j + 1, r, k - left);
}
int main() {
scanf("%d%d", &n, &k);
for(int i = 0; i < n; i++) scanf("%d", &q[i]);
printf("%d", quick_select(q, 0, n - 1, k));
return 0;
}
归并
merge_sort(int q[], int l, int r)
- 基本思路
- 确定分界点,一般是中间位置
- 从分界点将数组切成两半,对左右两部分做递归排序
- ⭐️将左右两部分区间合并成一个区间(将2个有序数组,合并成1个有序数组,使用双指针即可)
- 算法题目
Acwing - 787 归并排序#include<iostream> using namespace std; const int N = 1e6 + 10; 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; merge_sort(q, l, mid); merge_sort(q, mid + 1, r); // 合并 int i = l, j = mid + 1, k = 0; 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]; } int main() { scanf("%d", &n); for(int i = 0; i < n; i++) scanf("%d", &q[i]); merge_sort(q, 0, n - 1); for(int i = 0; i < n; i++) printf("%d ", q[i]); }
衍生题目:逆序对的数量
练习题:Acwing - 788: 逆序对的数量
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n;
int q[N], tmp[N];
// 返回区间[l, r]中的逆序对的数量
LL merge_sort(int q[], int l, int r) {
if(l >= r) return 0;
int mid = l + r >> 1;
LL cnt = merge_sort(q, l, mid) + merge_sort(q, mid + 1, r); // 计算左右区间内各自的逆序对数量
// 合并时, 计算左右两个区间中的数组成的逆序对
int i = l, j = mid + 1, k = 0;
while(i <= mid && j <= r) {
if(q[i] <= q[j]) tmp[k++] = q[i++];
else {
cnt += mid - i + 1;
tmp[k++] = q[j++];
}
}
while(i <= mid) tmp[k++] = q[i++];
while(j <= r) tmp[k++] = q[j++];
for(int i = l, k = 0; i <= r; i++, k++) q[i] = tmp[k];
return cnt;
}
int main() {
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", &q[i]);
LL cnt = merge_sort(q, 0, n - 1);
printf("%lld", cnt);
return 0;
}
二分
整数二分
- 算法模板
int binary_search_1(int l, int r) { while(l < r) { int mid = l + r >> 1; if(check(mid)) r = mid; else l = mid + 1; } return l; } int binary_search_2(int l, int r) { while(l < r) { int mid = l + r + 1 >> 1; // 当下面是 l = mid 这样来更新的话,这里计算mid时要多加1,否则会出现边界问题 if(check(mid)) l = mid; else r = mid - 1; } return l; }
- 二分的本质
注意:二分的本质不是单调性。单调性可以理解为函数单调性,如一个数组是升序排列或降序排列,此时可以用二分来查找某一个数的位置。
有单调性一定可以二分,但没有单调性,也有可能能够二分。
二分的本质是边界。假设给定一个区间,如果能够根据某个条件,将区间划分为左右两部分,使得左半边满足这个条件,右半边不满足这个条件(或者反之)。此时就可以用二分来查找左右两部分的边界点。
注意左右两半部分的边界不是同一个点,而是相邻的2个点,因为是整数二分(离散的)。上面的2个算法模板,就分别对应了求左半部分的边界(上图红色区域最右边的点),和求右半部分的边界(上图绿色区域最左边的点)
比如,我们要找上图中左边红色部分的边界点,我们取mid = l + r >> 1
,判断一下q[mid]
是否满足条件x,若满足,说明mid
位置在红色区域内,我们的答案在mid
右侧(可以取到mid
),即[mid, r]
,则此时更新l = mid
;若q[mid]
不满足条件x,则说明mid
位置在右边的绿色区域,我们的答案在mid
左侧(不能取到mid
),即[l, mid - 1]
,此时更新r = mid - 1
。
注意,当采用l = mid
和r = mid - 1
这种更新方式时,计算mid
时,要加上1(向上取整),即mid = l + r + 1 >> 1
。否则,在l = r - 1
时,计算mid
时若不加1,则mid = l + r >> 1 = l
,这样更新l = mid
,就是l = l
,会导致死循环。所以要向上取整,采用mid = l + r + 1 >> 1
。
同理,当采用r = mid
和 l = mid + 1
这种更新方式时,计算mid
时不能加1,在l = r - 1
时,若计算mid
时加1,则mid = l + r + 1 >> 1 = r
,这样更新r = mid
。就是r = r
,会导致死循环。
简单的记忆就是,仅当采用l = mid
这种更新方式时,计算mid
时需要加1。
练习题:Acwing-789:数的范围
#include<iostream>
using namespace std;
const int N = 100010;
int arr[N];
int n,q;
int main() {
scanf("%d%d", &n, &q);
for(int i = 0; i < n; i++) scanf("%d", &arr[i]);
while(q--) {
int k;
scanf("%d", &k);
int l = 0, r = n - 1;
while(l < r) {
int mid = l + r >> 1;
if(arr[mid] >= k) r = mid;
else l = mid + 1;
}
if(arr[l] != k) printf("-1 -1\n");
else {
printf("%d ", l);
l = 0, r = n - 1;
while(l < r) {
int mid = l + r + 1 >> 1;
if(arr[mid] <= k) l = mid;
else r = mid - 1;
}
printf("%d\n", l);
}
}
}
浮点数二分
相比整数二分,浮点数二分无需考虑边界问题,比较简单。
当二分的区间足够小时,可以认为已经找到了答案,如当r - l < 1e-6
,停止二分。
或者直接迭代一定的次数,比如循环100次后停止二分。
练习题:Acwing-790:数的三次方根
#include<iostream>
using namespace std;
int main() {
double n;
scanf("%lf", &n);
double l = -10000, r = 10000;
while(r - l > 1e-8) {
double mid = (l + r) / 2;
if(mid * mid * mid >= n) r = mid;
else l = mid;
}
printf("%.6f", l);
}