算法基础_基础算法【快速排序 + 归并排序 + 二分查找】
算法基础_基础算法【快速排序 + 归并排序 + 二分】
- ---------------快速排序---------------
- 785.快速排序
- 题目介绍
- 方法一:
- 代码片段解释
- 片段一:
- 片段二:
- 解题思路分析
- 方法二:
- 代码片段解释
- 片段一:
- 解题思路分析
- 786.第k个数
- 题目介绍
- 方法一:
- 代码片段解释
- 片段一:
- 片段二:
- 片段三:
- 解题思路分析
- ---------------归并排序---------------
- 787.归并排序
- 题目介绍
- 方法一:
- 代码片段解释
- 片段一:
- 解题思路分析
- 788.逆序对的数量
- 题目介绍
- 方法一:
- 解题思路分析
- ---------------二分查找---------------
- 789.数的范围
- 题目介绍
- 方法一:
- 解题思路分析
- 790.数的三次方根
- 题目介绍
- 方法一:
- 解题思路分析
---------------快速排序---------------
785.快速排序
题目介绍
方法一:
#include <iostream>
#include <vector>
using namespace std;
//实现快速排序的函数
void quick_sort(vector<int>& q, int l, int r)
{
//1.排除
if (l >= r) return;
//2.找点 + 更界
int mid = q[(l + r) >> 1], i = l - 1, j = r + 1;
//3.while循环
while (i < j)
{
//4.移动指针i
do i++; while (q[i] < mid);
//5.移动指针j
do j--; while (q[j] > mid);
//6.交换
if (i < j) swap(q[i], q[j]);
}
//7.递归
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
int main()
{
//获取数据:
//1.需要快速排序的整数的数量n ---》 一个int变量
//2.n个整数 ---> 一个一维数组
int n;
cin >> n;
vector<int> q(n);
for (int i = 0; i < n; i++) cin >> q[i];
//处理数据: 对数组中的数据进行快速排序
quick_sort(q, 0, n - 1);
//输出数据:数组快速排序的结果
for (int i = 0; i < n; i++) cout << q[i];
return 0;
}
代码片段解释
片段一:
if (l >= r) return;
疑问:在快速排序的递归终止条件中,写成
if (l >= r) return;
而不是if (l == r) return;
这是为什么?
快速排序
:是一个分治算法,它将数组分成两个子数组,然后递归地对子数组进行排序。
快速排序的递归终止条件
:是当子数组的长度为 1 或 0 时,不再需要排序。
l
和r
的含义:
l
是当前区间的左边界(起始索引)r
是当前区间的右边界(结束索引)区间长度:
- 区间长度为
r - l + 1
。- 当
l == r
时,区间长度为 1,表示只有一个元素,不需要排序。- 当
l > r
时,区间长度为 0,表示没有元素,也不需要排序。
片段二:
if (i < j) swap(q[i], q[j]);
if (i < j)
这个条件在快速排序的分区过程中非常重要,它的作用是避免不必要的交换,同时确保分区逻辑的正确性。
分区过程的逻辑 :
在快速排序的分区过程中,我们使用两个指针
i
和j
:
i
从左向右移动,寻找第一个大于等于基准值的元素。j
从右向左移动,寻找第一个小于等于基准值的元素。当
i
和j
都停止移动时,如果i < j
,说明这两个元素的位置是“错位”的(即:左边的元素大于基准值,右边的元素小于基准值),这时需要交换它们的位置。
疑问:为什么需要
if (i < j)
?情况 1:
i < j
- 当
i < j
时,说明i
和j
还没有相遇或交叉。- 此时:
q[i]
大于基准值,q[j]
小于基准值,交换它们是合理的,可以确保左边的元素都小于等于基准值,右边的元素都大于等于基准值。情况 2:
i >= j
- 当
i >= j
时,说明i
和j
已经相遇或交叉,分区过程已经完成。- 如果此时仍然执行
swap(q[i], q[j])
,会导致以下问题:
- 如果
i == j
,交换同一个元素是没有意义的。- 如果
i > j
,交换已经分好区的元素会破坏分区的正确性。
解题思路分析
快速排序的思路步骤:(使用数组中的中间值作为分界点)
第一步:使用if条件语句判断待排序的数组中的数组是不是空数组第二步:使用数组中的中间值更新边界 + 获取分界点
第三步:使用while循环
- 第四步:使用do - while循环更新指针i
- 第五步:使用do - while循环更新指针j
- 第六步:使用if条件语句判断在满足条件的情况下交换指针i与指针j所指向的元素
第七步:使用递归调用quick_sort函数快排:l~j 和 j+1~r 区间的元素
方法二:
#include <iostream>
#include <vector>
#include <ctime>
using namespace std;
void quick_sort(vector<int>& q, int l, int r)
{
if (l >= r) return;
int rnd_idx = rand() % (r - l + 1) + l;
swap(q[l], q[rnd_idx]);
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);
}
int main()
{
srand(time(nullptr));
int n;
scanf("%d", &n);
vector<int> q(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) cout << q[i] << " ";
return 0;
}
代码片段解释
片段一:
int rnd_idx = rand() % (r - l + 1) + l;
swap(q[l], q[rnd_idx]);
这行代码的作用是随机选择一个基准值(pivot),用于快速排序的分区操作。
具体来说:
- 它生成一个随机的索引
rnd_idx
- 然后将该索引对应的元素与当前区间的第一个元素交换,作为基准值
代码解析:
int rnd_idx = rand() % (r - l + 1) + l;
rand()
:
rand()
是 C++ 标准库中的一个函数,用于生成一个伪随机数- 它的返回值范围是
0
到RAND_MAX
(通常是 32767)
(r - l + 1)
:
r
是当前区间的右边界,l
是当前区间的左边界r - l + 1
表示当前区间的长度(包含的元素个数)
- 例如:如果
l = 2
,r = 5
,那么区间长度是5 - 2 + 1 = 4
rand() % (r - l + 1)
:
rand() % (r - l + 1)
的作用是生成一个0
到r - l
之间的随机数
- 例如:如果
r - l + 1 = 4
,那么rand() % 4
的结果可能是0
、1
、2
或3
+ l
:
rand() % (r - l + 1)
生成的是相对于区间左边界l
的偏移量- 加上
l
后,rnd_idx
的值范围就是l
到r
,即:当前区间的有效索引
疑问:为什么需要随机选择基准值?
在快速排序中,基准值的选择对算法的性能有很大影响。
如果每次选择的基准值都是固定的(比如:总是选择第一个元素),在某些情况下(例如:数组已经有序或接近有序),快速排序的性能会退化为 O ( n 2 ) O(n^2) O(n2)
通过随机选择基准值,可以避免这种最坏情况的发生,使得快速排序的期望时间复杂度保持在 O ( n log n ) O(n \log n) O(nlogn)
分区流程示例:假设当前区间
[l, r]
是[2, 5]
,数组内容如下:索引: 2 3 4 5 值: 7 3 9 2
计算区间长度:
r - l + 1 = 5 - 2 + 1 = 4
生成随机索引:
rand() % 4
可能生成0
、1
、2
或3
- 假设生成
2
,则rnd_idx = 2 + 2 = 4
交换元素:
- 将
q[2]
(值为7
)与q[4]
(值为9
)交换。- 交换后数组为:
索引: 2 3 4 5 值: 9 3 7 2
分区操作:
- 以
q[2]
(值为9
)为基准值进行分区。
解题思路分析
快速排序的思路步骤:(使用数组中的随机值作为分界点)
第零步:使用if条件语句判断待排序的数组中的数组是不是空数组第一步:寻找数组中的随机下标并将其对应的值与数组中的左端值交换
第二步:使用数组中的左端值更新边界 + 获取分界点
第三步:使用while循环
- 第四步:使用do - while循环更新指针i
- 第五步:使用do - while循环更新指针j
- 第六步:使用if条件语句判断在满足条件的情况下交换指针i与指针j所指向的元素
第七步:使用递归调用quick_sort函数快排:l~j 和 j+1~r 区间的元素
786.第k个数
题目介绍
方法一:
#include <iostream>
using namespace std;
const int N = 100010; //定义一个常量的目的:为了使用全局数组
int n, k;
int q[N];
int quick_select(int l, int r, int k)
{
if (l == r) return q[l];
int x = q[l], 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 sl = j - l + 1;
if (k <= sl) return quick_select(l, j, k);
return quick_select(j + 1, r, k - sl);
}
int main()
{
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> q[i];
cout << quick_select(0, n - 1, k) << endl;
return 0;
}
代码片段解释
片段一:
if (l == r) return q[l];
if (l == r)
和if (l >= r)
的区别在于它们处理递归终止条件的方式不同。
if (l >= r)
的使用场景:在快速排序中,通常使用
if (l >= r)
作为递归终止条件,这是因为:
l > r
的情况:
- 在快速排序的分区过程中可能会出现
l > r
的情况,尤其是在数组中有重复元素或分区不均匀时
- 例如:如果基准值
x
是数组中的最小值,分区后左子区间可能为空,导致l > r
l == r
的情况:
- 当区间长度为 1 时,
l == r
,表示只有一个元素,不需要再排序。因此:
if (l >= r)
可以同时处理l == r
和l > r
的情况,确保递归在所有情况下都能正确终止。
if (l == r)
的使用场景:在快速选择算法中,通常使用
if (l == r)
作为递归终止条件,这是因为:
- 分区逻辑:
- 快速选择的分区逻辑确保每次递归都会缩小搜索范围。
- 如果
k <= s1
,递归查找左子区间;否则,递归查找右子区间。- 这种逻辑保证了最终一定会缩小到
l == r
的情况。l > r
的情况:
- 在快速选择中,
l > r
的情况不会发生,因为每次递归都会根据k
和s1
调整搜索范围。因此:
if (l == r)
足以作为快速选择的递归终止条件。
快速排序的递归终止条件:
if (l >= r)
:
- 用于快速排序,处理
l == r
和l > r
的情况。- 适用于完全排序的场景。
快速选择的递归终止条件:
if (l == r)
:
- 用于快速选择,处理
l == r
的情况。- 适用于查找第
k
小元素的场景。
片段二:
int sl = j - l + 1;
if (k <= sl) return quick_select(l, j, k);
return quick_select(j + 1, r, k - sl);
在这段代码中,
int sl = j - l + 1;
的作用是计算分区后左子区间的长度,也就是基准值x
左侧的元素个数。这个值用于判断第
k
小的数是否在左子区间中,从而决定递归的方向。
快速选择算法
:用于在未排序的数组中找到第k
小的数。快速选择算法的核心思想是:
- 选择一个基准值
x
(这里选择的是q[l]
)- 将数组分为两部分:
- 左子区间:所有小于等于
x
的元素。- 右子区间:所有大于
x
的元素。- 根据
k
和左子区间的长度sl
,决定递归查找左子区间还是右子区间。
sl
的含义
sl
表示左子区间的长度,即从l
到j
的元素个数。- 计算公式:
sl = j - l + 1
j
:是分区后左子区间的右边界l
:是当前区间的左边界j - l + 1
:计算的是从l
到j
的元素个数(包括l
和j
)
sl
的作用
- 如果
k <= sl
,说明第k
小的数在左子区间中,递归查找左子区间- 如果
k > sl
,说明第k
小的数在右子区间中,递归查找右子区间,并将k
减去sl
(因为左子区间已经有sl
个数比右子区间小)
片段三:
解题思路分析
快速选择的思路步骤:
第一步:使用if条件语句书写递归出口第二步:使用数组中的左端值更新分界点 + 获取边界
第三步:使用while循环
- 第四步:使用do - while循环更新指针i
- 第五步:使用do - while循环更新指针j
- 第六步:使用if条件语句判断在满足条件的情况下交换指针i与指针j所指向的元素
第七步:定义变量k其值为分界点的左侧区域的长度
第八步:使用if条件语句根据k在l~j 或 j+1~r 哪个区间而在哪个区间递归调用quick_select函数快选
---------------归并排序---------------
787.归并排序
题目介绍
方法一:
#include <iostream>
using namespace std;
const int N = 100010;
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 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];
}
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]);
return 0;
}
代码片段解释
片段一:
for (i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
for (i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
和for (i = 0, j = 0; i < n; i++, j++) q[i] = tmp[j];
的区别在于它们复制的范围和目标位置不同。
在归并排序中,合并两个有序子数组的过程如下:
- 将左子数组
[l, mid]
和右子数组[mid + 1, r]
合并到一个临时数组tmp
中。- 将临时数组
tmp
中的内容复制回原数组q
的[l, r]
区间。
疑问:为什么第一种写法是正确的?
局部性:
- 归并排序的合并操作只影响当前递归层的
[l, r]
区间。- 因此:只需要将
tmp
中的内容复制回q
的[l, r]
区间。正确性:
for (i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
确保只复制当前递归层的[l, r]
区间,不会影响其他部分。疑问:为什么第二种写法是错误的?
- 全局覆盖:
for (i = 0, j = 0; i <= n; i++, j++) q[i] = tmp[j];
会覆盖整个数组q
,而不仅仅是当前递归层的[l, r]
区间。- 数据破坏:
tmp
只存储了当前递归层的[l, r]
区间的内容,而第二种写法会将这些内容复制到整个数组q
,破坏其他部分的数据。
解题思路分析
归并排序的思路步骤:
第一步:使用if条件语句书写递归出口第二步:使用数组中的中间值更新分界下标
第三步:使用递归调用merge_sort函数快排:l~mid 和 mid+1~r 区间的元素
第四步:定义一个变量k赋值为0作为临时数组的下标 + 获取边界
第五步:使用while循环 (i <= mid && j <= r)
- 第六步:使用if分支语句判断现在指针i与指针j所指向的数组元素谁小
- 第七步:将值小的元素添加到临时数组中
第八步:使用while循环判断i是否还<=mid —> 说明i遍历的数组中的元素还未都添加到临时数组中 --> 使用while循环将剩余的都添加进去
第九步:使用while循环判断j是否还<=r —> 说明j遍历的数组中的元素还未都添加到临时数组中 -->使用while循环将剩余的都添加进去
第十步:使用for循环将临时数组中的元素都添加到原数组中
788.逆序对的数量
题目介绍
方法一:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int q[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 (q[i] <= q[j]) tmp[k++] = q[i++];
else
{
tmp[k++] = q[j++];
res += mid - i + 1;
}
}
// 扫尾
while (i <= mid) tmp[k++] = q[i++];
while (j <= r) tmp[k++] = q[j++];
// 物归原主
for (int i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
return res;
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++) cin >> q[i];
cout << merge_sort(0, n - 1) << endl;
return 0;
}
解题思路分析
使用归并排序统计逆序对的数量:
第一步:使用if条件语句书写递归出口
第二步:使用数组中的中间值更新分界下标
第三步:使用递归调用merge_sort函数快排:l~mid 和 mid+1~r 区间的元素
第四步:定义一个变量k赋值为0作为临时数组的下标 + 获取边界
第五步:使用while循环 (i <= mid && j <= r)
- 第六步:使用if分支语句判断现在指针i与指针j所指向的数组元素谁小,同时将值小的元素添加到临时数组中
- 第七步:核心操作(res += mid - i + 1;)
第八步:使用while循环判断i是否还<=mid —> 说明i遍历的数组中的元素还未都添加到临时数组中 --> 使用while循环将剩余的都添加进去
第九步:使用while循环判断j是否还<=r —> 说明j遍历的数组中的元素还未都添加到临时数组中 -->使用while循环将剩余的都添加进去
第十步:使用for循环将临时数组中的元素都添加到原数组中
使用归并排序统计逆序对的数量与单纯的使用归并排序对数组中的元素进行排序的的不同之处有以下几点:
- 函数传参的不同:
- 归并排序数组元素:
merge_sort(q, 0, n - 1);
- 归并统计逆序对:
merge_sort(0, n - 1)
- 函数的返回类型不同:
- 归并排序数组元素:
void
- 归并统计逆序对:
基本数据类型
- 递归调用归并函数的使用不同:
- 归并排序数组元素:
merge_sort(q, l, mid), merge_sort(q, mid + 1, r);
- 归并统计逆序对:
LL res = merge_sort(l, mid) + merge_sort(mid + 1, r);
- 归并过程中对于q[i] <= q[j]的else情况的处理不同
- 归并排序数组元素:
tmp[k++] = q[j++];
- 归并统计逆序对:
tmp[k++] = q[j++]; res += mid - i + 1;
---------------二分查找---------------
789.数的范围
题目介绍
方法一:
#include <stdio.h>
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) printf("-1 -1\n");
else
{
printf("%d ", 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;
}
printf("%d\n", l);
}
}
return 0;
}
解题思路分析
整数二分的思路步骤:
第一步:定义二分区间的边界
第二步:使用while循环(while (l < r))
- 第三步:定义二分中点的下标
- 第四步:使用if分支语句判断中点下标对应的值与目标值之间的大小关系
- 第五步:对于不同的大小关系选择不同的方式更新区间边界
对于二分不同的情况应该怎么去更新区间的边界值:
第一种情况:
q[mid] >= x
:下标mid对应的值比目标值大 —> 目标值在mid的左边 —> 修改右边界
if(真):
r = mid
else(假):
l = mid + 1
第二种情况:
q[mid] <= x
:下标mid对应的值比目标值小 —> 目标值在mid的右边 —> 修改左边界
if(真):
l = mid
else(假):
r = mid - 1
790.数的三次方根
题目介绍
方法一:
#include <iostream>
using namespace std;
int main()
{
double x;
cin >> x;
double l = -1e4, r = 1e4;
while (r - l > 1e-8)//这道题要求保留六位的有效数组,所以我们将区间最小设置为1e-8
{//注意:这里的1e-8的意思是:10的-8次方,也就是0.000000001
double mid = (l + r) / 2;
if (mid * mid * mid >= x) r = mid;
else l = mid;
}
printf("%lf\n", l);
return 0;
}
解题思路分析
浮点数二分的思路步骤:
第一步:定义二分区间的边界
第二步:使用while循环(while (r - l > 1e-8))
- 第三步:定义二分中点的值
- 第四步:使用if分支语句判断中点的值与目标值之间的大小关系
- 第五步:对于不同的大小关系选择不同的方式更新区间边界
整数二分与浮点数二分的不同之处有以下几点:
- while循环结束的条件不同:
- 整数二分:
l < r
- 浮点数二分:
r - l > 1e-8
- mid的含义不同:
- 整数二分:
下标
- 浮点数二分:
值
- 区间边界的更新方式不同:
- 整数二分:
if (q[mid] >= x) r = mid; else l = mid + 1;
- 浮点数二分:
if (mid * mid * mid >= x) r = mid; else l = mid;