第一章 基础算法(一)—— 快排,归并与二分

文章目录

    • 快排
    • 归并排序
    • 二分
      • 整数二分
      • 浮点数二分
    • 快速排序练习题
      • 785. 快速排序
      • 786. 第k个数
    • 归并排序练习题
      • 787. 归并排序
      • 788. 逆序对的数量
    • 二分练习题
      • 789. 数的范围
      • 790. 数的三次方根

有些累了,把这两天做的笔记整理发出

快排

快排的思路:

  1. 确定分界点
  2. 根据分界点,调整区间中的数
  3. 递归左右区间

其中第2点是快排的关键,采用双指针法。

  1. i从0开始往右走,j从n - 1开始往左走
  2. nums[i] > xi停下,否则继续往右走
  3. nums[j] < xj停下,否则继续往左走
  4. 当两个指针都停下,交换两指针指向的数,接着重复2,3步,直到ij相遇

当两个指针都停下时,
i左边的所有数小于xi的右边以及i的位置上所有的数大于等于x
同理j右边的所有数大于xj的左边以及j的位置上所有的数小于等于x
此时可以选择i或者j分割nums数组,分割后的数组为[l, j][j + 1, r]

快排模板:

void quick_sort(int nums[], int l, int r)
{
	if (l >= r) return;
	int x = nums[(l + r) >> 1], i = l - 1, j = r + 1;
	while (i < j)
	{
		do ++i; while (nums[i] < x);
		do --j; while (nums[j] > x);
		if (i < j) swap(nums[i], nums[j]);
	}
	quick_sort(nums, l, j);
	quick_sort(nums, j + 1, r);
}

需要注意选择分界点时,必须选择(l + r) / 2 ,不能除3,不能除4
以及递归子区间时,不能选择i作为分界点,因为会导致死循环

关于死循环的例子:1, 2
当区间划分为[l, j][j + 1, r]时,x = mid[r]会出现死循环问题
当区间划分为[l, i - 1][i, r]时,x = mid[l]会出现死循环问题


归并排序

归并排序的思路:

  1. 确定分界点:mid = (l + r) / 2
  2. 递归排序左右两个子区间
  3. 归并区间,合二为一

其中最重要的是第三步,涉及到两个有序区间的合并

合并两个有序区间的算法:双指针

  • 用两个指针指向数组中的开始位置
  • 将较小的数放入tmp数组中,并向后移动该指针
  • 直到有一个指针遍历完一个数组,此时将未遍历完的数组直接拼接到tmp数组中
  • 最后将tmp数组中的数,拷贝回nums数组

归并模板:

const int N = 1e6 + 10;
int nums[N], tmp[N};
				 
void merge_sort(int nums[], int l, int r)
{
    if (l >= r) return;
    int mid = l + r >> 1;
    
    merge_sort(nums, l, mid), merge_sort(nums, mid + 1, r);
    
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
    {
        if (nums[i] <= nums[j]) tmp[k++] = nums[i++];
        else tmp[k++] = nums[j++];
    }
    
    while (i <= mid) tmp[k++] = nums[i++];
    while (j <= r) tmp[k++] = nums[j++];
    for (i = l, k = 0; i <= r; ++i, ++k) nums[i] = tmp[k];
}

快排和归并的时间复杂度都是nlogn,在最坏的情况下,快排可能达到n * n。而归并总是nlogn


二分

整数二分

二分的本质不是单调性,有单调性的题目一定可以二分,但是可以二分的题目不一定具有单调性

二分的本质是边界。确定一个性质,使左右区间中其中一个区间满足该性质,另一个区间不满足该性质

因此,找到一个性质,左右区间的边界点就可以二分出来。
第一章 基础算法(一)—— 快排,归并与二分_第1张图片

红绿区间的交界处,其中一个交界就是要查找的数x的位置。
举个例子,整个数组为nums,左边区间中的数满足nums[k] <= x的性质,右边区间中的数满足nums[k] > x的性质,此时左边区间的边界就是要查找的x的位置。

对于mid的计算:mid = (l + r) / 2,有时mid = (l + r + 1) / 2

check函数连接了nums数组中的数与要查找的数x,它是一个关于x的性质。

check函数的更新:
check函数检查的是:mid是否满足左边区间的性质时

  • if (check(mid))
    • truemid落在左边区间中,要查找的数落在[mid, r]中,更新l = mid
    • falsemid落在右边区间中,要查找的数落在[l, mid - 1]中,更新r = mid - 1
      check函数检查的是:mid是否满足右边区间的性质时
  • if (check(mid))
    • truemid落在右边区间中,要查找的数落在[l, mid]中,更新r = mid
    • flasemid落在左边区间中,要查找的数落在[mid + 1, r]中,更新l = mid + 1

需要注意的是:对于check,若mid满足某个区间的性质,可能mid就是要查找的x。因此区间更新时不应该舍弃mid

二分的两个模板:

// 检查左边区间是否满足性质时使用
// 也就是区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用
int bsearch_1(int l, int r)
{
	if (l >= r) return;
	while (l < r)
	{
		int mid = (l + r + 1) >> 1;
		if (check(mid)) l = mid;
		else r = mid - 1;
	}
	return l;
}

// 检查右边区间是否满足性质时使用
// 也就是区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用
int bsearch_2(int l, int r)
{
	if (l >= r) return;
	while (l < r)
	{
		int mid = (l + r) >> 1;
		if (check(mid)) r = mid;
		else l = mid + 1;
	}
	return l;
}

当更新方式是r = mid时,mid = (l + r) >> 1
当更新方式是l = mid时,mid = (l + r + 1) >> 1
为什么此时的mid要加1?当l = r - 1时,变形一下:r = l + 1
由于语言是向0取整的,若不加1,此时mid = (2 * l + 1) = l
checktrue,更新l = mid = ll作为区间的左端点,没有变化。而区间的右端点r也没有变化,此时区间没有更新,将陷入死循环

思路:

  1. 思考check函数(性质
  2. 根据check函数思考区间如何更新
  3. 根据区间的更新方式,决定mid是否要+1

二分一定有解,但题目可能无解。根据二分出来的答案,判断答案是否满足题目要求,进而判断题目是否有解

浮点数二分

浮点数二分没有边界问题,所以不会存在死循环的情况,也就不需要思考是否要+1-1
简化整数二分的模板即可:

// 假设题目要求保留6位小数
int bsearch_3(int l, int r)
{
	if (l >= r) return;
	while (r - l > 1e-8)
	{
		int mid = (l + r) / 2;
		if (check(mid)) l = mid;
		else r = mid;
	}
	return l;
}

check函数中:检查区间的不同,l和r的更新也不同。例如,以下是将x开方的两种写法

#include 
using namespace std;

int main()
{
    double x = 0;
    cin >> x;
    double l = 0, r = x;
    while (r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if (mid * mid <= x) l = mid;
        else r = mid;
    }
    printf("%lf", l);
    return 0;
}

#include 
using namespace std;

int main()
{
    double x = 0;
    cin >> x;
    double l = 0, r = max(1, x);
    while (r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if (mid * mid >= x) r = mid;
        else l = mid;
    }
    printf("%lf", l);
    return 0;
}

tips:若题目要求保留实数的n位小数,则二分的精度需要为n + 2,这样可以保证结果是准确的

浮点数二分还可以写for循环,循环100次,也就是将区间二分2的100次方次,此时得到的结果一定是准确的

#include 
using namespace std;

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

快速排序练习题

785. 快速排序

785. 快速排序 - AcWing题库
第一章 基础算法(一)—— 快排,归并与二分_第2张图片

#include 
using namespace std;

const int N = 1e6 + 10;
int nums[N];

void quick_sort(int nums[], int l, int r)
{
    if (l >= r) return;
    int x = nums[(l + r) / 2], i = l - 1, j = r + 1;
    while (i < j)
    {
        do ++i; while (nums[i] < x);
        do --j; while (nums[j] > x);
        if (i < j) swap(nums[i], nums[j]);
    }
    quick_sort(nums, l, j);
    quick_sort(nums, j + 1, r);
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) scanf("%d", &nums[i]);
    
    quick_sort(nums, 0, n - 1);
    
    for (int i = 0; i < n; ++i) printf("%d ", nums[i]);
    return 0;
}

需要注意选择分界点时,必须选择(l + r) / 2 ,不能除3,不能除4
以及递归子区间时,不能选择i作为分界点,因为会导致死循环


786. 第k个数

786. 第k个数 - AcWing题库
第一章 基础算法(一)—— 快排,归并与二分_第3张图片
快排后,返回第k个数即可。

#include 
using namespace std;

const int N = 1e6 + 10;
int nums[N];
int n, k;

void quick_sort(int nums[], int l, int r)
{
    if (l >= r) return;
    int x = nums[l + r >> 1], i = l - 1, j = r + 1;
    while (i < j)
    {
        do ++i; while (nums[i] < x);
        do --j; while (nums[j] > x);
        if (i < j) swap(nums[i], nums[j]);
    }
    quick_sort(nums, l, j), quick_sort(nums, j + 1, r);
}

int main()
{
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; ++i) scanf("%d", &nums[i]);
    
    quick_sort(nums, 0, n - 1);
    
    printf("%d\n", nums[k - 1]);
    return 0;
}

归并排序练习题

787. 归并排序

787. 归并排序 - AcWing题库
第一章 基础算法(一)—— 快排,归并与二分_第4张图片

#include 
using namespace std;

const int N = 1e6 + 10;
int nums[N], tmp[N];
int n;

void merge_sort(int nums[], int l, int r)
{
    if (l >= r) return;
    int mid = l + r >> 1;
    
    merge_sort(nums, l, mid), merge_sort(nums, mid + 1, r);
    
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
    {
        if (nums[i] <= nums[j]) tmp[k++] = nums[i++];
        else tmp[k++] = nums[j++];
    }
    
    while (i <= mid) tmp[k++] = nums[i++];
    while (j <= r) tmp[k++] = nums[j++];
    for (i = l, k = 0; i <= r; ++i, ++k) nums[i] = tmp[k];
}

int main()
{   
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) scanf("%d", &nums[i]);
    
    merge_sort(nums, 0, n - 1);
    
    for (int i = 0; i < n; ++i) printf("%d ", nums[i]);
    
    return 0;
}

788. 逆序对的数量

788. 逆序对的数量 - AcWing题库
第一章 基础算法(一)—— 快排,归并与二分_第5张图片
直接暴力搜索?直接超时了。
利用归并的性质,合并有序数组时,两个有序数组其实是两个左右区间,左区间的数字下标小于右区间的所有数字。当左区间中的某个数字nums[i]大于右区间的某个数字nums[j]时,说明该数字之后的所有数字都大于nums[j]。此时将包括nums[i]在内,向后的所有数字与nums[j]都能构成逆序对。将答案加上这个数,即mid - i + 1

#include 
using namespace std;

const int N = 1e6 + 10;
typedef long long LL;
int n;
int nums[N], tmp[N];

LL merge_sort(int l, int r)
{
    if (l >= r) return 0;
    int mid = l + r >> 1;
    LL res = merge_sort(l, mid) + merge_sort(mid + 1, r);
    
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
    {
        if (nums[i] <= nums[j]) tmp[k++] = nums[i++];
        else 
        {
            tmp[k++] = nums[j++];
            res += mid - i + 1;
        }
    }
    while (i <= mid) tmp[k++] = nums[i++];
    while (j <= r) tmp[k++] = nums[j++];
    for (k = 0, i = l; i <= r; ++i, ++k) nums[i] = tmp[k];
    return res;
}

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) scanf("%d", &nums[i]);
    
    printf("%ld\n", merge_sort(0, n - 1));
    
    return 0;
}

二分练习题

789. 数的范围

789. 数的范围 - AcWing题库
第一章 基础算法(一)—— 快排,归并与二分_第6张图片

查询有序数组中,某个数所在的区间范围
首先确定check函数,整个数组可以划分成三个区间,>= x<= x== x
当然,这些区间之间有交集,先将check函数设置为>= x,找到第一个>= x的数.若该数不是x,则说明nums中没有x,题目无解,输出-1 -1
若找到了第一个>= x 的数,且该数为x,那么继续查找第一个<= x的数。此时将check设置为<= x

#include 
using namespace std;

const int N = 1e6 + 10;
int nums[N];
int n, x, count;

int main() 
{
    scanf("%d%d", &n, &count);
    for (int i = 0; i < n; ++i) scanf("%d", &nums[i]);
    
    while (count--)
    {
        scanf("%d", &x);
        int l = 0, r = n - 1;
        while (l < r)
        {
            int mid = l + r >> 1;
            if (nums[mid] >= x) r = mid;
            else l = mid + 1;    
        }
       
        if (nums[l] != x) printf("-1 -1\n");
        else
        {
            printf("%d ", l);
            int l = 0, r = n - 1;
            while (l < r)
            {
                int mid = l + r + 1 >> 1;
                if (nums[mid] <= x) l = mid;
                else r = mid - 1;
            }
            printf("%d\n", l);
        }
        
    }
    return 0;
}

790. 数的三次方根

790. 数的三次方根 - AcWing题库

#include 
using namespace std;

int main()
{
    double x;
    scanf("%lf", &x);
    double l = -10000.0, r = 10000.0;
    
    while (r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if (mid * mid * mid >= x) r = mid;
        else l = mid;
    }
    printf("%lf\n", l);
    return 0;
}

注意,r的值不能取x,若x为0.01时,正确答案为0.1。而二分的区间为[0, 0.01]此时取不到正确答案

你可能感兴趣的:(AcWing算法课,课程记录,算法,数据结构,java)