第三周总结
1.排序算法
- 快速排序
- 归并排序
2.二分查找
1.1 快速排序
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + 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);
}
机制:1.将一个区间二分(分治)。
2.通过双指针的查找让左右区间相对有序(左永远大于右/右永远大于左)。
3.对两边分别执行相同的操作。(递归)
1.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);
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];
}
机制:1.将一个区间二分(分治)。
2.将左右两个区间按照一定的顺序进行合并。
3.对两边分别执行相同的操作。(递归)
注意因为我们是先进行递归操作,所以我们每一个区间内的数据可以默认是排好序了的。
example:
给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。
输入格式
第一行包含整数 n,表示数列的长度。
第二行包含 n 个整数,表示整个数列。
输出格式
输出一个整数,表示逆序对的个数。
数据范围
1≤n≤100000,
数列中的元素的取值范围 [1,1000000000]。
输入样例:
6
2 3 4 5 6 1
输出样例:
5
思路:
逆序对的出现有三种情况:
1.逆序对在mid的左边。(红)
2.逆序对在mid的右边。(绿)
3.左边和右边各有一个。(黄)
1和2我们可以用不断对区间进行对分的方法使它变为第三种情况。
现在我们着重讨论第三种情况。
我们使用了归并排序的方法,所以我们默认在左右两个区间内的数据分别有序。
我们可以设置一个指针来指向mid左边的一个数据,另一个指针指向mid右边的数据。我们默认左边的区间最大的数字小于右边,这样最优的情况就是指针从左到右扫描区间并且左指针扫描到mid后右指针才会开始扫描。
但是也有可能出现以下情况:
如图所示,左边的指针遇到了比右指针更大的数据,导致右指针要优先放置数据。这也就出现了第二种情况的逆序对。
那么一共有多少个这样的逆序对呢?
答案是 mid - i + 1个
由单调性可知从i到mid的所有数都大于j处的数字。
这样的判断可以放入每一次的归并中。
代码实现如下。
#include<iostream>
using namespace std;
const int N = 10e6+10;
int q[N],tmp[N];
typedef long long LL;
LL cnt = 0;//记录逆序对的个数
LL merge_sort(int q[],int l,int r)
{
if( l >= r) return 0;
int mid = l + r >> 1,i = l ,j = mid + 1,k = 0;
cnt = merge_sort(q ,l ,mid) + merge_sort(q ,mid + 1,r );
while(i <= mid && j <= r)
{
if(q[i] <= q[j]) tmp[k ++] = q[i ++];
else//出现第二种情况 {
tmp[k ++] = q[j ++];
cnt += 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 cnt;
}
int main()
{
int n;
cin >> n;
for(int i = 0;i < n;i ++) scanf("%d",&q[i]);
cout << merge_sort(q ,0 ,n-1) << endl;
}
2.二分查找
二分查找的本质就是找到可以将结果转化为两种不重叠的情况的问题的边界。
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_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;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
example:
给定一个长度为 n+1 的数组nums,数组中所有的数均在 1∼n 的范围内,其中 n≥1。
请找出数组中任意一个重复的数,但不能修改输入的数组。
数据范围
1≤n≤1000
样例
给定
nums = [2, 3, 5, 4, 3, 2, 6, 7]。
返回
2 或 3。
这个问题可以利用抽屉原理来考虑。
桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里
面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”。 抽屉原理的一般含义为:“如果每个抽
屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定
有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。它是组合数学中一个重要的原
理 。
给定一个长度为 n+1 的数组nums,数组中所有的数均在 1∼n 的范围内,其中 n≥1。
所以我们可以把1~n个数字看做抽屉,因为有n+1个数字,所以我们一定可以找到一个重复的数字。
我们可以尝试把一段数据分成两个区间,让mid成为我们想要找到的重复数字。
然后我们可以扫描整个数组,记录数字在1到mid之间的数字的个数。
可以看到数字的个数比mid要大。
如果我们把1~mid这几个数看成抽屉,把nums内符合的数字看成苹果。符合条件的数字大于抽屉数说明重复的数字一定在[1,mid]中。所以我们要把右边界左移到mid处。
重复过程一定可以找到这个数。
代码如下:
class Solution {
public:
int duplicateInArray(vector<int>& nums) {
int l = 1, r = nums.size() - 1 , s = 0//计数器;
while(l < r)
{
int mid = l + r >> 1;
int s = 0;
for(auto x : nums) s += x >= l && x <= mid//扫描符合的数;
if (s > mid - l + 1 ) r = mid;
else l = mid + 1//移动边界;
}
return l;
}
};