习题课1
1.第k个数
这道题虽我做出来了,并且通过了,但是你老师永远是你老师,我还是太弱了。
我的想法很简单,先快排,然后直接输出那个位置即可,时间复杂度是O( nlogn)
但是老师提出了一个新思路,可以说叫做快选,时间复杂度是O(n)
我们快排思路是每次将区间分成两个子区间然后递归处理。其实我们可以记录下k
的位置,那么我们每次只需要递归k
所在的那个区间就可以了,另一个区间中的数一定是集体大于或者小于k
的,不影响结果。
这样处理,我们第一次时间复杂度为n
,第二次递归时间复杂度为n/2
(理想状态下,也就是期望下),第三次n/4
....知道区间长度为1,这时候这个区间内的数就是我们要找的k
。综上,总体时间复杂度为n (1+1/2+1/4+1/8+......) < 2n
,所以时间复杂度就是O(n)
代码如下
#include <iostream>
using namespace std;
const int N=1e6+10;
int a[N];
int quick_sort(int l, int r, int k) {
// 两指针相遇时,说明已经部分快排完毕,di
if(l >= r) return a[k];
// 初始化
int x = a[l], i = l - 1, j = r + 1;
while (i < j) {
// 另外一种写法,可以代替do-while,也是至少会进行一次
while(a[++i] < x);
while(a[--j] > x);
if (i < j) swap(a[i], a[j]);
}
// 判断排好序后第k个数在哪个区间里,只递归那个区间
if (k <= j) return quick_sort(l, j, k);
else return quick_sort(j + 1, r, k);
}
int main() {
int n, k;
cin >> n >> k;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
// 我们默认处理a数组,所以可以不用写,但是参数要记录第k个数
//第k个数在数组里的位置其实是第k-1位,所以传参为k-1
cout << quick_sort(0, n - 1, k - 1) << endl;
return 0;
}
2.逆序对的数量
写这道题之前,一定要先明白,归并排序是先递归后处理
我们先一直递归到区间长度为1或0的时候,这时候return 0
,这个值会给到上一层的res
里
每一层的res
值都是他下面两层的res
结果之和,倒数第一层的res
都是0,所以这样就相当于初始化res=0
我们先不管递归的过程,来分析一下逆序对的情况,如果我们此时把大区间分为了两个小区间,那么逆序对两个数位置可能的情况有:
- 两个都在左区间
- 两个都在右区间
- 一个在左一个在右
看似有三种情况,其实在真正递归处理时,我们只需要处理第三种情况:我们知道,归并排序是分治的过程,在传递完后开始处理,我们的处理其实是从最后一层往第一层走的。所以每一层我们都是拿到了下一层两个已经处理好的区间,他们已经按照从小到大的顺序排列好了,所以就不会存在情况一和二。
其实可以这样理解,如果拿到的两个区间没有被处理,那么情况一和情况二是有可能存在的,但是现在被处理好了,也就意味着他们其实对于下面的某一层来说属于情况三,底下的那一层把他们已经作为逆序数记录下来了,所以现在就没有必要记录他们了
所以,我们只需要处理情况三即可,任何情况一或二都能在他下面的某一层中作为情况三被处理:
先说一下最简单的暴力方法:左区间遍历,每一个数都去遍历右区间,去看看是否满足,时间复杂度O(n^2)
但这样其实很傻,因为两个区间都是有序而且从小到大的,如果左区间中a
大于右区间中的b
,那么其实这时候已经表明a
后面的所有数(左区间内)都大于b
,这样会快很多。
因此,我们先使用if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
,当他停止的时候,说明我们在左区间找到了一个大于右区间的数,那么此时这个数以及他后面所有左区间的数总和为mid - i + 1
(左区间的右边界为mid,当前指到i),然后再使用tmp[k ++ ] = q[j ++ ];
去看看右区间的下一个数如何。如此往复。
我们从最底层开始传res
,不断地把当前层的两个(一层一般是两个子区间)res
传回给上层,最后在第一层完成汇总,这就是答案了。
代码如下:
#include <iostream>
using namespace std;
// 给long long起别名,方便使用
typedef long long LL;
const int N = 1e5 + 10;
int a[N], tmp[N];
LL merge_sort(int q[], int l, int r)
{
// 如果区间为1,那么必然此时无法得出逆序对,返回0
// 这一步也是res的初始化
if (l >= r) return 0;
int mid = l + r >> 1;
// 归并排序先进行递归
LL res = 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
{
// 关键节点
res += mid - i + 1;
tmp[k ++ ] = q[j ++ ];
}
// 扫尾工作
while (i <= mid) tmp[k ++ ] = q[i ++ ];
while (j <= r) tmp[k ++ ] = q[j ++ ];
// 将处理好的结果给到a数组
for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
//返回res
return res;
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
cout << merge_sort(a, 0, n - 1) << endl;
return 0;
}
3.数的三次方根
这个简单,直接二分。注意这里即使没有负数,也不能将初始区间定为[ 0 ,x ]
,因为小数开完根会更大,所以右边界应该为max( 1 , x )
。还有一点补充:一般要求小数精度为x
,那么我们应该至少让他运行到x+2
精度再停止,这样的数据精度更高
#include <iostream>
using namespace std;
int main()
{
double x;
cin >> x;
// 这里直接根据答案范围写区间,无脑但简单
double l = -100, r = 100;
while (r - l > 1e-8)
{
double mid = (l + r) / 2;
if (mid * mid * mid >= x) r = mid;
else l = mid;
}
printf("%.6lf\n", l);
return 0;
}