文章目录
- 快排
- 归并排序
- 二分
- 整数二分
- 浮点数二分
- 快速排序练习题
- 785. 快速排序
- 786. 第k个数
- 归并排序练习题
- 787. 归并排序
- 788. 逆序对的数量
- 二分练习题
- 789. 数的范围
- 790. 数的三次方根
有些累了,把这两天做的笔记整理发出
快排
快排的思路:
- 确定分界点
- 根据分界点,调整区间中的数
- 递归左右区间
其中第2点是快排的关键,采用双指针法。
- i从0开始往右走,j从n - 1开始往左走
- 若
nums[i] > x
,i
停下,否则继续往右走 - 若
nums[j] < x
,j
停下,否则继续往左走 - 当两个指针都停下,交换两指针指向的数,接着重复2,3步,直到
i
与j
相遇
当两个指针都停下时,
i
左边的所有数小于x
,i
的右边以及i
的位置上所有的数大于等于x
同理j
右边的所有数大于x
,j
的左边以及j
的位置上所有的数小于等于x
此时可以选择i
或者j
分割nums
数组,分割后的数组为[l, j]
和[j + 1, r]
,
快排模板:
void quick_sort(int nums[], int l, int r)
{
if (l >= r) return;
int x = nums[(l + r) >> 1], i = l - 1, j = r + 1;
while (i < j)
{
do ++i; while (nums[i] < x);
do --j; while (nums[j] > x);
if (i < j) swap(nums[i], nums[j]);
}
quick_sort(nums, l, j);
quick_sort(nums, j + 1, r);
}
需要注意选择分界点时,必须选择(l + r) / 2
,不能除3
,不能除4
以及递归子区间时,不能选择i
作为分界点,因为会导致死循环
关于死循环的例子:1, 2
当区间划分为[l, j]
和[j + 1, r]
时,x = mid[r]
会出现死循环问题
当区间划分为[l, i - 1]
和[i, r]
时,x = mid[l]
会出现死循环问题
归并排序
归并排序的思路:
- 确定分界点:
mid = (l + r) / 2
- 递归排序左右两个子区间
- 归并区间,合二为一
其中最重要的是第三步,涉及到两个有序区间的合并
合并两个有序区间的算法:双指针
- 用两个指针指向数组中的开始位置
- 将较小的数放入
tmp
数组中,并向后移动该指针 - 直到有一个指针遍历完一个数组,此时将未遍历完的数组直接拼接到
tmp
数组中 - 最后将
tmp
数组中的数,拷贝回nums
数组
归并模板:
const int N = 1e6 + 10;
int nums[N], tmp[N};
void merge_sort(int nums[], int l, int r)
{
if (l >= r) return;
int mid = l + r >> 1;
merge_sort(nums, l, mid), merge_sort(nums, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
{
if (nums[i] <= nums[j]) tmp[k++] = nums[i++];
else tmp[k++] = nums[j++];
}
while (i <= mid) tmp[k++] = nums[i++];
while (j <= r) tmp[k++] = nums[j++];
for (i = l, k = 0; i <= r; ++i, ++k) nums[i] = tmp[k];
}
快排和归并的时间复杂度都是nlogn
,在最坏的情况下,快排可能达到n * n
。而归并总是nlogn
二分
整数二分
二分的本质不是单调性,有单调性的题目一定可以二分,但是可以二分的题目不一定具有单调性
二分的本质是边界。确定一个性质,使左右区间中其中一个区间满足该性质,另一个区间不满足该性质
因此,找到一个性质,左右区间的边界点就可以二分出来。
红绿区间的交界处,其中一个交界就是要查找的数x
的位置。
举个例子,整个数组为nums
,左边区间中的数满足nums[k] <= x
的性质,右边区间中的数满足nums[k] > x
的性质,此时左边区间的边界就是要查找的x
的位置。
对于mid
的计算:mid = (l + r) / 2
,有时mid = (l + r + 1) / 2
check
函数连接了nums
数组中的数与要查找的数x
,它是一个关于x
的性质。
check函数的更新:
当check
函数检查的是:mid是否满足左边区间的性质时
if (check(mid))
true
:mid
落在左边区间中,要查找的数落在[mid, r]
中,更新l = mid
false
:mid
落在右边区间中,要查找的数落在[l, mid - 1]
中,更新r = mid - 1
当check
函数检查的是:mid是否满足右边区间的性质时
if (check(mid))
true
:mid
落在右边区间中,要查找的数落在[l, mid]
中,更新r = mid
flase
:mid
落在左边区间中,要查找的数落在[mid + 1, r]
中,更新l = mid + 1
需要注意的是:对于check
,若mid
满足某个区间的性质,可能mid
就是要查找的x
。因此区间更新时不应该舍弃mid
二分的两个模板:
// 检查左边区间是否满足性质时使用
// 也就是区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用
int bsearch_1(int l, int r)
{
if (l >= r) return;
while (l < r)
{
int mid = (l + r + 1) >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
// 检查右边区间是否满足性质时使用
// 也就是区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用
int bsearch_2(int l, int r)
{
if (l >= r) return;
while (l < r)
{
int mid = (l + r) >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
当更新方式是r = mid
时,mid = (l + r) >> 1
当更新方式是l = mid
时,mid = (l + r + 1) >> 1
为什么此时的mid
要加1
?当l = r - 1
时,变形一下:r = l + 1
由于语言是向0
取整的,若不加1
,此时mid = (2 * l + 1) = l
若check
为true
,更新l = mid = l
,l
作为区间的左端点,没有变化。而区间的右端点r
也没有变化,此时区间没有更新,将陷入死循环
思路:
- 思考
check
函数(性质) - 根据
check
函数思考区间如何更新 - 根据区间的更新方式,决定
mid
是否要+1
二分一定有解,但题目可能无解。根据二分出来的答案,判断答案是否满足题目要求,进而判断题目是否有解
浮点数二分
浮点数二分没有边界问题,所以不会存在死循环的情况,也就不需要思考是否要+1-1
简化整数二分的模板即可:
// 假设题目要求保留6位小数
int bsearch_3(int l, int r)
{
if (l >= r) return;
while (r - l > 1e-8)
{
int mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
return l;
}
check
函数中:检查区间的不同,l和r
的更新也不同。例如,以下是将x
开方的两种写法
#include <iostream>
using namespace std;
int main()
{
double x = 0;
cin >> x;
double l = 0, r = x;
while (r - l > 1e-8)
{
double mid = (l + r) / 2;
if (mid * mid <= x) l = mid;
else r = mid;
}
printf("%lf", l);
return 0;
}
#include <iostream>
using namespace std;
int main()
{
double x = 0;
cin >> x;
double l = 0, r = max(1, 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;
}
tips:若题目要求保留实数的n
位小数,则二分的精度需要为n + 2
,这样可以保证结果是准确的
浮点数二分还可以写for循环
,循环100
次,也就是将区间二分2的100次方
次,此时得到的结果一定是准确的
#include <iostream>
using namespace std;
int main()
{
double x = 0;
cin >> x;
double l = 0, r = x;
for (int i = 0; i < 100; ++i)
{
double mid = (l + r) / 2;
if (mid * mid <= x) l = mid;
else r = mid;
}
printf("%lf", l);
return 0;
}
快速排序练习题
785. 快速排序
785. 快速排序 - AcWing题库
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int nums[N];
void quick_sort(int nums[], int l, int r)
{
if (l >= r) return;
int x = nums[(l + r) / 2], i = l - 1, j = r + 1;
while (i < j)
{
do ++i; while (nums[i] < x);
do --j; while (nums[j] > x);
if (i < j) swap(nums[i], nums[j]);
}
quick_sort(nums, l, j);
quick_sort(nums, j + 1, r);
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; ++i) scanf("%d", &nums[i]);
quick_sort(nums, 0, n - 1);
for (int i = 0; i < n; ++i) printf("%d ", nums[i]);
return 0;
}
需要注意选择分界点时,必须选择(l + r) / 2
,不能除3,不能除4
以及递归子区间时,不能选择i
作为分界点,因为会导致死循环
786. 第k个数
786. 第k个数 - AcWing题库
快排后,返回第k个数即可。
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int nums[N];
int n, k;
void quick_sort(int nums[], int l, int r)
{
if (l >= r) return;
int x = nums[l + r >> 1], i = l - 1, j = r + 1;
while (i < j)
{
do ++i; while (nums[i] < x);
do --j; while (nums[j] > x);
if (i < j) swap(nums[i], nums[j]);
}
quick_sort(nums, l, j), quick_sort(nums, j + 1, r);
}
int main()
{
scanf("%d%d", &n, &k);
for (int i = 0; i < n; ++i) scanf("%d", &nums[i]);
quick_sort(nums, 0, n - 1);
printf("%d\n", nums[k - 1]);
return 0;
}
归并排序练习题
787. 归并排序
787. 归并排序 - AcWing题库
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int nums[N], tmp[N];
int n;
void merge_sort(int nums[], int l, int r)
{
if (l >= r) return;
int mid = l + r >> 1;
merge_sort(nums, l, mid), merge_sort(nums, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
{
if (nums[i] <= nums[j]) tmp[k++] = nums[i++];
else tmp[k++] = nums[j++];
}
while (i <= mid) tmp[k++] = nums[i++];
while (j <= r) tmp[k++] = nums[j++];
for (i = l, k = 0; i <= r; ++i, ++k) nums[i] = tmp[k];
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; ++i) scanf("%d", &nums[i]);
merge_sort(nums, 0, n - 1);
for (int i = 0; i < n; ++i) printf("%d ", nums[i]);
return 0;
}
788. 逆序对的数量
788. 逆序对的数量 - AcWing题库
直接暴力搜索?直接超时了。
利用归并的性质,合并有序数组时,两个有序数组其实是两个左右区间,左区间的数字下标小于右区间的所有数字。当左区间中的某个数字nums[i]
大于右区间的某个数字nums[j]
时,说明该数字之后的所有数字都大于nums[j]
。此时将包括nums[i]
在内,向后的所有数字与nums[j]
都能构成逆序对。将答案加上这个数,即mid - i + 1
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
int n;
int nums[N], tmp[N];
LL merge_sort(int l, int r)
{
if (l >= r) return 0;
int mid = l + r >> 1;
LL res = merge_sort(l, mid) + merge_sort(mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
{
if (nums[i] <= nums[j]) tmp[k++] = nums[i++];
else
{
tmp[k++] = nums[j++];
res += mid - i + 1;
}
}
while (i <= mid) tmp[k++] = nums[i++];
while (j <= r) tmp[k++] = nums[j++];
for (k = 0, i = l; i <= r; ++i, ++k) nums[i] = tmp[k];
return res;
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; ++i) scanf("%d", &nums[i]);
printf("%ld\n", merge_sort(0, n - 1));
return 0;
}
二分练习题
789. 数的范围
789. 数的范围 - AcWing题库
查询有序数组中,某个数所在的区间范围
首先确定check
函数,整个数组可以划分成三个区间,>= x
,<= x
和== x
当然,这些区间之间有交集,先将check
函数设置为>= x
,找到第一个>= x
的数.若该数不是x
,则说明nums
中没有x
,题目无解,输出-1 -1
若找到了第一个>= x
的数,且该数为x
,那么继续查找第一个<= x
的数。此时将check
设置为<= x
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int nums[N];
int n, x, count;
int main()
{
scanf("%d%d", &n, &count);
for (int i = 0; i < n; ++i) scanf("%d", &nums[i]);
while (count--)
{
scanf("%d", &x);
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r >> 1;
if (nums[mid] >= x) r = mid;
else l = mid + 1;
}
if (nums[l] != x) printf("-1 -1\n");
else
{
printf("%d ", l);
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (nums[mid] <= x) l = mid;
else r = mid - 1;
}
printf("%d\n", l);
}
}
return 0;
}
790. 数的三次方根
790. 数的三次方根 - AcWing题库
#include <iostream>
using namespace std;
int main()
{
double x;
scanf("%lf", &x);
double l = -10000.0, r = 10000.0;
while (r - l > 1e-8)
{
double mid = (l + r) / 2;
if (mid * mid * mid >= x) r = mid;
else l = mid;
}
printf("%lf\n", l);
return 0;
}
注意,r
的值不能取x
,若x
为0.01时,正确答案为0.1。而二分的区间为[0, 0.01]
此时取不到正确答案