快速排序
- 确定分界点:在数组中选一个元素的值 x x x 作为分界,(某人说)可任取
- (重点)调整区间:以 x x x 为准,将数组分为左右两段,通过换位保证左边都≤x,右边都≥x,即可
- 递归分别处理左右两部分,直到排完
暴力做法:分拣到两个新数组中,再合起来。硬开数组,空间复杂度过于友好了(笑)
文明做法:设计一个函数,传入待排序数列的两端序号和数组首地址。(若全局数组则不需要地址)
void quick_sort(int q[], int l, int r)
设计函数是方便递归。由于数列结构简单,指针直接用数组下标实现,可专注于数据本身
**双指针算法:**指针 i , j i,j i,j先后从两边出发,直到各发现应属于另一方的数(即第一个不小于/不大于 x x x 的数,等于无需单独处理),接着将两个数对调。一次可处理两个数,之后 i , j i,j i,j继续走自己的路,直到都停下时已相遇或擦肩而过,函数主体结束
因此可保证 i i i 左边是小于等于, j j j 右边是大于等于(等于可随意)。
于是基本结构是循环嵌套,再整体递归
while(i < j) //在这之前,判断若i >= j,直接跳出函数
{
do {i++;} while(q[i] < x); //小讲究,do while比while更接近汇编逻辑,或许更快
do {j--;} while(q[j] > x); //这里规定等于的时候也停(防止x是极值时,停不下来
if(i < j) swap(q[i], q[j]); //如果已经相遇过,说明所有数都扫描完毕,无需交换了
}
quick_sort(q, l, i - 1); //换成j和j+1也行,因为i j最后都停在了另一侧,所以递归时属于另一侧
quick_sort(q, i, r);
注意:采用 do while 循环使得代码精简,起步前先将 i i i, j j j下标外移一格,每次先移动再判定。
一般从数组中取基准 x x x ,示例取了左边界
q[l]
。但取边界 x x x 可能有如下问题:
因为规定等于也停,所以第一轮必有一边停在起点。
如果脸不好,对面直接贴脸了,循环结束。于是两个递归中,一个边界错位直接退回,另一个边界不变,无限循环。
这样就充分解释了为何递归时传 i i i 的话 x x x 不能取 q[l],传 j j j 的话 x x x 不能取 q[r]。
因此干脆取中间值就行了。
int x = q[l + r + 1>> 1]; //+-优先级大于>>,后者比 /2 快几十倍
//为防止数组只有2个,递归取i要+1 取j不+1
补充:
scanf
读入一般比cin
更快
补充: c++中为了保证与c的兼容,将输入输出与c的方式绑定,导致
cin
效率降低。
ios::sync_with_stdio(false);
——关闭cin
与stdio
的兼容,提高cin
效率
但使用之后要注意,不能将cin
cout
与scanf
printf
混用
cin.tie()
——可人为设置输入输出流的绑定
其中无参数时cin
只绑定cout
,也可以加速输入对于简单数据可以一律用
scanf
- 用异或运算交换两个数(联想基因传递和表达x
a ^= b;
//用异或标记a和b二进制中不同的位。结果保存给a
b ^= a;
//照着a的密码,1对应位取反,0对应位不变。翻译成了原先的a
a ^= b;
//以自己为密码,由现在的b翻译出原先的b
特点是快!而且省空间!能用的场合可以无脑。
但要避免指针导致的自交换,会清零数据。必要时还是用第三者赋值法,保险
归并排序
快速排序:先粗排,再递归细化
归并排序:先递归二分,再边排序边合并(归并)
-
分界点:
mid = (l + r) / 2
-
二分
**重点:**快排在于分的时候完成排序,而归并在于合的时候完成排序(合并数组或链表)
双指针算法:(递归结束后)定义两个指针分别指向两个数组片段左端
每次选较小数填入新数组 tmp[N]
,将指针后移。一边结束后,另一边剩余数字(已排序)填入列尾。相等数字,优先填前一个
稳定性,相等数字不变序。因为是从同一个原数组中取得的两个有序片段,相同数字,原本在前的继续在前
而快排不具有稳定性。因为数字频繁换位,只能通过额外区分解决
宏观上就是在
q[N]
从最小的片段开始,逐级获得更大的有序片段。随着有序片段越来越长,在 l o g n logn logn 次后完成排序
int q[N], tmp[N];
void merge_sort(int q[], int l, int r)
{
if(l >= r)
return; //判断递归结束
int mid = l + r >> 2;
merge_sort(q, l, mid), merge_sort(q, mid + 1, r); //先排完小的,再排大的
int k = 0, i = l, j = mid + 1; //k是tmp的指针,i j是q的片段指针
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]; //最后将tmp内容填回q中的原位
}
时间复杂度:归并和快排都是递归+线性比较,每层递归中,数据只扫描一次。其中快排根据数值不同,平均为O( n l o g n nlogn nlogn),最坏是O( n 2 n^2 n2)。而归并是按位置二分,一共分 l o g 2 n log_2n log2n次,变成长度为1的数组,因此稳定在O( n l o g n nlogn nlogn)。
之所以快就是归功于二分法,相比线性方法可以说是降维打击。另外,时间复杂度忽略常系数
另:代码长度、时间复杂度、空间复杂度都不是正相关的,需要自己取舍。
二分
一般是缩小范围用于查找等操作。假设数列是有序的
整数二分:根据查找的性质将集合一分为二,因为是整数数组,中点 mid
可有两种取法。举例:
n个整数组成的有序数列,其中有重复的数字。要求输入一个数,并输出这个数出现最左端和最右端的位置。若没有,输出-1 -1
int q[N], x; //省略输入,N>n。x是查找的数,假设数列是升序的
int l = 0, r = n - 1, mid;
while(l < r)
{
mid = l + r >> 1; //这里是否+1,原理和快排相同
if(q[mid] >= x) r = mid; //为了防止这一行因重合而死循环
else l = mid + 1;
} //第一次二分查找,通过判断等于或偏大,将范围从两边向 x 左端点缩小,最后l = mid + 1总能保证l r汇合且落在x边界上
if(q[l] != x) cout << "-1 -1" endl;
//l r已经汇合,一般单个元素查找到此结束了。整数数组在这里判断 x 是否存在,而小数数轴(比如算平方根)会输出一个近似值
else{
cout << l << endl;
l = 0, r = n - 1;
while(l < r) //求两个边界相当于查找两次,本质上查找条件也不同(因为取等于的方向不同)
{
mid = l + r + 1 >> 1;
if(q[mid] <= x) l = mid;
else r = mid -1; //两种二分还是有区别的。这一次将范围向 x 右端点缩小,最后一步保证r落在右边界
}
cout << l << endl; //这里和上面一样,输出l 和 r 都行的
}
小数二分:一般出现在数学问题中,原理相同,而且不需要考虑边界问题,更简单,略