『算法笔记』- 01 - C++ 实现:快速排序、归并排序 & 二分



📘At first:一个初学算法的萌新,如果文中有误,还请指正🤓
🎗️专栏介绍:本专栏目前基于AcWing算法基础课进行笔记的记录,包括及课上大佬讲的一些算法的模板还有自己的一些心得和理解
🕶️个人博客地址:https://blog.csdn.net/m0_73352841?spm=1010.2135.3001.5343

一、快速排序

快速排序的思想是分治


1.1 步骤

1、先确定分界点:可以是左边界,中间值、右边界或者任意位置都行

2、调整区间:小于等于x的在左半边,大于等于x的在右半边(不用纠结那个中间的位置),『难点』💣💣💣

3、递归处理左右两段


1.2 代码实现

void quick_sort(int q[], int l, int r)
//q[]为传入的数组
//l为传入的做边界值,一般是0
//r是传入的右边界的值,一般是n-1,n为数组中总共的个数,由于下标的因素,故r为n-1
{
	if (l == r) return;
	//当数组中就一个数时,就不用排了,直接结束该函数,条件也可以是l >= r,效果是一样的
	
	int x = q[l + r + 1 >> 1], i = l - 1,  j = r + 1;
	//由于后面代码的因素,这里i和j分别在l和r的基础上往外再移1个位
	
	while (i < j)
	{
		do i ++ ; while (q[i] <x);
		//只要q[i]的值小于定义的分界点的值,i的位置就往右移,直到大于它
		
		do j -- ; while (q[j] >x);
		//只要q[j]的值大于定义的分界点的值,j的位置就往左移,直到小于它
		
		if (i < j) swap(q[i], q[j]) ;
		//如果此时,下标i小于j,它们两个的值就交换,也就是我们步骤2中的“调整区间”
	} 
	
	quick_sort(q, l, j);
	//递归处理左边
	
	quick_sort(q, j + 1, r);
	//递归处理右边
	//其实这里的j也可以替换成i,因为边界问题,要做相应的调整
	
	//为什么是j和j+1,请看以下图示
}

1.3 图示

以下面的数据为例

5 3 4 1 2

在这里插入图片描述

确定分界点及左右起始指针

在这里插入图片描述

进行区间调整

在这里插入图片描述

递归处理左右两段

二、归并排序

归并排序的思想同样是分治


2.1 步骤

1、确定分界点:中点

2、递归排序:分别排序,中点两侧

3、归并:合二为一,『难点』💣💣💣


2.2 代码实现

void merge_sort(int q[], int l, int r)
{
	if (l == r) return;
	当数组中就一个数时,就不用排了,直接结束该函数,条件也可以是l >= r,效果是一样的
	
	int mid = l + r >> 1;
	//确定中间点
	
	merge_sort(q, l, mid), merge_sort(q, mid + 1, r);
	//递归排序左边、递归排序右边
	
	//以下为归并的过程
	int k = 0, i = l, j = mid + 1;
	//i指向左半边已排好序的起点
	//j指向右半边已排好序的起点
	
	while (i <= mid && j <= r)
	{
		//条件自然是直到循环完左右两个数组为止
		
		if (q[i] < q[j]) tmp[k ++ ] = q[ i ++ ];
		else tmp [k ++ ] = q[j ++ ];
		//这两句是每次把小的那个数放入tmp临时数组中
	}

	while (i <= mid) tmp[k ++ ] = q[i ++ ];
	while (j <= r) tmp[k ++ ] = q[j ++];
	//这两句是左右两边如果没有循环完的话,把剩下的元素直接接到这个tmp临时数组中,因为都是已经排好序的
	
	for (i = l, j = 0; i <= r; i ++ , j ++ ) q[i] = tmp[j];
}
	//将tmp临时数组中的数据存回原数组q中去

2.3 图示

以下面的数据为例

5 3 4 1

1 - 整个排序过程

在这里插入图片描述


2 - 其内部排序过程,每一次迭代都会排序
(下图中只显示出最后一次分完数组内的元素的排序)

在这里插入图片描述


3 - 编译器验证

在这里插入图片描述

1、图中第一个输入的数是来表示输入几个数的,即4个

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);
	
	
	printf("#\n");//添加标记
	
	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];
	
}

出现了3次“#”。可见,排序进行了3次,对应图中5与3、4与1、3-5与1-4的3次排序合并


三、二分

3.1 整数二分

二分的本质不是单调性。它们的关系是,如果有单调性的话一定可以二分,但是可以二分的题目不一定非得有单调性

二分的本质是边界

假设给定一个区间,在这个区间上定义了某种性质,设为x

假设右半边是满足性质的,我们用绿色表示;左半边是不满足性质的,用红色表示。由于是整数二分,不能有交点

在这里插入图片描述

如果我们能找到这样一个性质的话,把整个区间一分为二,一半满足,一半不满足,那么二分就能寻找性质的边界。既可以寻找红色的边界a,也可以寻找绿色的边界b


3.1.1 两段查找代码

寻找左边界

int bserch_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;
	}
}

寻找右边界

int bsearch_2(int l, int r)
{
	while (l < r)
	{
		int mid = l + r + 1 >> 1;
		if (check(mid)) l = mid;
		//check()判断mid是否满足性质
		
		else r = mid - 1;
	}
	return l;
}

问题
Q1:为什么寻找右边界的时候mid需要多加1?
A1:假设,l(left)和r(right)相距1(见上图左右端点),mid=(l+r加1)/2=l,区间更新后,还是(l, r),会陷入死循环

Q2:为什么最后输出l而不是r
A2:其实都可以,循环结束时,lr指向同一元素,见下“图示”


3.1.2 图示

下面以一道例题为例

题目描述
给定一个按照升序排列的长度为n的整数数组,以及q个查询。

对于每个查询,返回一个元素k的起始位置和终止位置(位置从0开始计数)。

如果数组中不存在该元素,则返回“-1 -1”.

输入格式
第一行包含整数nq,表示数组长度和询问格式。

第二行包含n个整数(均在1~10000范围内),表示完整数组。

接下来q行,每行包含一个整数k,表示一个询问元素。

输出格式
q行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回“-1 -1”.

数据范围
1 <= n <= 100000,
1 <= q <= 10000,
1 <= k <= 100000

输入样例:

6 3
1 2 2 3 3 4
3
4
5

输出样例:

3 4
5 5
-1 -1

完整代码

#include <iostream>

using namespace std;

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) cout << "-1 -1" << endl;
		else
		{
			cout << 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;
			}
			
			cout << l <<endl;
		}
	}
	
	
}

由于是给定了按照升序排列的数组,暗含了其单调性。我们就以所给样例数据为例

确定左边界:用mid=(l+r)/2,进行循环判断,最终l和r指向同一元素(x是要寻找的数的第一个位置和最后一个位置,这里默认为3,即x=3,因为是样例中第一个输入的数,我们以样例为例)

在这里插入图片描述


确定右边界:用mid=(l+r+1)/2,进行循环判断,最终l和r指向同一元素

在这里插入图片描述


一点想法

其实可以看看两次得到"3"的地址,来判断我们的两段代码是否真的取到的是左边界和右边界

在这里插入图片描述

可以看到,第一个“3”的地址大于第二个“3”的地址,说明第一段代码得到的就是左边界,后者得到的就是右边界,一个在前一个在后


3.2 浮点数二分

浮点数二分和整数二分本质上差不多,都是探讨边界,但是要比前者简单些,少考虑一些边界。这里就以一个求解一个数的平方根为例,简单说明

一种写法

#include <iostream>

using namespace std;

int main()
{
    double x;
    cin >> x;
    
    double l = 0, r = x;
    while (r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if (mid * mid >= x) r = mid;
		//mid^2大于x说明mid在x^1/2的右边,将右边界向左移动即可

        else l = mid;
		//反之,将左边界向右移动
    }
    
    printf("%lf\n", l);
    
    return 0;
}


另一种写法

#include <iostream>

using namespace std;

int main()
{
    double x;
    cin >> x;
    
    double l = 0, r = x;
    for (int i = 0; i < 100; i ++ )
    {
        double mid = (l + r) / 2;
        if (mid * mid >= x) r = mid;
        else l = mid;
    }
    
    printf("%d", l);
    
    return 0;
}

不用精度来表示迭代,直接迭代100次。两者类似,前者是当精度足够小的时候停止,后者是将整个区间的长度除以2^100,除完之后就很小了


Ending

在这里插入图片描述

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Rainbow_Criss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值