数据结构/算法笔记(1)-两种排序 & 二分查找

快速排序

  1. 确定分界点:在数组中选一个元素的值 x x x 作为分界,(某人说)可任取
  2. 重点)调整区间:以 x x x 为准,将数组分为左右两段,通过换位保证左边都≤x,右边都≥x,即可
  3. 递归分别处理左右两部分,直到排完

暴力做法:分拣到两个新数组中,再合起来。 硬开数组,空间复杂度过于友好了(笑)

文明做法:设计一个函数,传入待排序数列的两端序号和数组首地址。(若全局数组则不需要地址)

void quick_sort(int q[], int l, int r)

设计函数是方便递归。由于数列结构简单,指针直接用数组下标实现,可专注于数据本身

**双指针算法:**指针 i , j i,j ij先后从两边出发,直到各发现应属于另一方的数(即第一个不小于/不大于 x x x 的数,等于无需单独处理),接着将两个数对调。一次可处理两个数,之后 i , j i,j ij继续走自己的路,直到都停下时已相遇或擦肩而过,函数主体结束

因此可保证 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

补充

  1. scanf读入一般比cin更快

补充: c++中为了保证与c的兼容,将输入输出与c的方式绑定,导致 cin 效率降低。

  1. ios::sync_with_stdio(false); ——关闭 cinstdio 的兼容,提高 cin 效率
    但使用之后要注意,不能将 cin coutscanf printf 混用

  2. cin.tie() ——可人为设置输入输出流的绑定
    其中无参数时 cin 只绑定 cout ,也可以加速输入

  3. 对于简单数据可以一律用 scanf

  1. 用异或运算交换两个数(联想基因传递和表达x
a ^= b;
//用异或标记a和b二进制中不同的位。结果保存给a
b ^= a;
//照着a的密码,1对应位取反,0对应位不变。翻译成了原先的a
a ^= b;
//以自己为密码,由现在的b翻译出原先的b

特点是快!而且省空间!能用的场合可以无脑。

但要避免指针导致的自交换,会清零数据。必要时还是用第三者赋值法,保险


归并排序

快速排序:先粗排,再递归细化

归并排序:先递归二分,再边排序边合并(归并)

  1. 分界点:mid = (l + r) / 2

  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 都行的
}

小数二分:一般出现在数学问题中,原理相同,而且不需要考虑边界问题,更简单,略

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值