chapter3-排序和查找

1.排序

1.1基础排序

排序就是把一组无序的数据变成有序的过程。对于机试而言,直接使用C++封装好的sort函数就可以了。sort函数内部采用快速排序实现,因此非常高效。使用sort函数,需要引用#include <algorithm>头文件,sort(first_address, last_address, compare)有三个参数,first和last待排序序列的起始地址和结束地址;compare为比较方式。如果不规定第三个参数,则默认是从小到大升序方式。
注意,1、sort函数的地址是左闭右开区间的。2、如果要对给定的数组进行降序排列或以自定义的方式排列,那么第三个参数会起作用。可以通过编写比较函数的方式来实现想要的自定义排序方式。

排序,既包括对内部已经定义的基本数据类型的排序,如整型、字符型等,也包括对自定义数据类型的排序,如结构体或者类。所以要注意,整数是有大小关系的,而结构体变量本身是没有大小关系的,sort函数的默认排序方式无法满足要求,必须依据题面描述写一个自定义的函数才能完成上述要求。
考生在遇到新的排序规则时,只需记住如下这条黄金法则:当比较函数的返回值为true时,表示的是比较函数的第一个参数将会排在第二个参数前面。
所有的比较,最终还是要转换成为对于大小的比较,奇偶性就是考察模2之后值的大小比较,一切比较都是基于两个数之间如何定义它们的大小关系,找出内部的大小关系,设计出合格的Compare比较函数。

1.1整数奇偶排序

奇偶排序_代码实现
//排序-习题3.2 整数奇偶排序 2024-02-09
#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int MAXN=10+2;
int arr[MAXN];
//自定义比较函数,确定谁在前谁在后
bool compare(int a, int b)
{
    if(a%2 == 1 && b%2 == 1)
    {
        return b < a;
    }else if (a%2 == 0 && b%2 == 0)
    {
        return a < b;
    }else if (a%2 == 1 && b%2 == 0)
    {
        return true;
    }else{//情况4:a为偶数,b为奇数
        return false;
    }
}

int main()
{
    while(scanf("%d", &arr[0]) != EOF)
    {
        for(int i=1; i<10; i++)
        {
            scanf("%d", &arr[i]);
        }
        sort(arr, arr + 10, compare);//注意:sort函数的地址参数是左闭右开的

        for(int i=0; i<10; i++)
        {
            printf("%d", arr[i]);
            if(i != 9)
                printf(" ");
        }
        printf("\n");
    }

    return 0;
}

1.2扩展排序——如何根据一些排序算法的特性去求解特殊的问题。

1.2.1问题1:线性排序

即在线性O(n)时间内实现对一个序列的排序,这就需要用到计数排序的特性。
我们知道,基于比较实现的排序时间复杂度下届是O(nlogn),那么要实现线性时间排序,就不能基于比较,而是用计数排序。注意使用计数排序的前提是,给定序列的所有数据是有一个范围的,开一个和数据范围一样大的辅助数组进行计数。

计数排序
//计数排序-杭州电子科技大学1425
//2024-02-14
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

const int MAXN = 1e6+10;
const int RANGE = 5e5;

int arr[MAXN]={0};//有多少个数据的范围
int tmp[MAXN]={0};//数据本身的范围。辅助数组,用于计数

int main()
{
    int n,m;
    while(scanf("%d%d", &n, &m) != EOF)
    {
        memset(tmp, 0, sizeof(tmp)); //因为有多组测试数据,每次要清空辅助数组
        for(int i=0; i<n; i++)
        {
            scanf("%d", &arr[i]);
            tmp[arr[i]+RANGE]++; //偏移5e5计数
        }
        int index = 0;
        for(int i=0; i < MAXN; i++)
        {
            while(tmp[i]--)//把计数数组对应下标i存回原数组
            {
                arr[index++] = i - RANGE;
            }
        }
        
        for(int i=n-1; i >= n-m; i--) //从后往前输出前m大的数
        {
            if(i == n - m)//输出格式控制,不错
            {
                printf("%d\n", arr[i]);
            }
            else{
                printf("%d ", arr[i]);
            }
        }
    }
    return 0;
}
注意,1、因为有多组测试数据,所以辅助数组每组新的数据输入前要清空置0,用memset函数,memset()可以应用在将一段内存初始化为某个值。例如:

memset( the_array, '\0', sizeof(the_array) );这是将一个数组的所有分量设置成零的很便捷的方法。使用memset要引用头文件#include <string.h>(C语言)或者#include <cstring>(C++中使用)。
2、辅助数组计数完之后,为了后续的使用,也为了输出的便捷性,可以把辅助数组下标(即数据)再存回原数组。
3、杭电OJ很奇怪,我用G++编译运行会报超时错误,同样的代码在C++编译运行就通过了,不过注意一下初始化一个1e6次的数组是占用一定时间的,所以对于大的数组可以不初始化。

1.2.2问题2:求逆序数对

即如何快速求出一个序列的逆序数,这个问题需要用到归并排序的特点。
对一个无序序列归并成有序序列,如图所示,先拆分直至只剩一个元素,这时仅含单个元素的子序列就是有序的,然后合并,不断地把两个有序子序列合成一个更大的有序子序列,最终完成一整个序列的排序。在这个过程中,每一趟拆分或者合并都是线性时间O(n)完成的,一共2logn趟。算法时间复杂度最好、最坏都是O(nlogn)。
mergesort2.jpg
合并的过程如图。当后一个有序子序列的元素比前一个子序列的某个元素小时,就产生了逆序数,值为middle + 1 - i
combine.jpg

归并排序+计算逆序数对
//归并排序的同时附带求逆序数对 2024-02-14
#include <iostream>
#include <cstdio>

using namespace std;

const int MAXN = 1000 + 10;

int arr[MAXN];
int temp[MAXN];
int number; //用于计数,记录逆序数个数

//把两个有序子序列,合并成为一个更大的有序序列
void Combine(int left, int middle, int right)
{
    int i = left,j = middle + 1;
    int k = left; //temp数组的索引
    while(i <= middle && j <= right)
    {
        if(arr[i] <= arr[j]){
            temp[k++] = arr[i++];
        }
        else{
            number += middle + 1 - i;
            temp[k++] = arr[j++];
        }
    }
    while(i <= middle)
    {
        temp[k++] = arr[i++];
    }
    while(j <= right)
    {
        temp[k++] = arr[j++];
    }
    //最后把合并完的有序序列存回原始数组arr
    for(k= left; k <= right; k++)
    {
        arr[k] = temp[k];
    }
}

void MergeSort(int left, int right)
{
    //注意:不能写while(left < right),这样递归没有出口,永远不会跳出函数栈,陷入死循环!!!
    if(left < right)//拆分直至单个元素必为有序子序列
    {
        // middle = (left + right) / 2; //这种写法存在溢出问题
        int middle = left + (right - left) / 2;
        MergeSort(left, middle);
        MergeSort(middle+1, right);
        Combine(left, middle, right);
    }
}

int main()
{
    int n;
    scanf("%d", &n);
    number = 0;
    for(int i=0; i<n; i++)
    {
        scanf("%d", &arr[i]);
    }
    MergeSort(0,n-1);
    for(int i=0; i<n; i++)//输出排序后的结果
    {
        if(i == n-1){
            printf("%d\n", arr[i]);
        }
        else{
            printf("%d ", arr[i]);
        }
    }
    //实现在归并排序中附带计算逆序数的大小
    printf("该序列的逆序数为:%d\n", number);
    return 0;    
}

1.2.3问题3:第K大数

即如何快速求出一个序列中第K大的数,需要用到快速排序的特性。
使得能够在线性O(n)的时间内获得结果。而朴素的想法是先通过排序,将无序序列变为有序序列,再定位数组arr[n-k]即为第K大的数,时间复杂度为O(nlogn)。
快速排序和归并排序一样同样通过递归实现。通过取枢轴元素,将整个无序序列一分为二,枢轴左边的元素都比它小,枢轴右边的元素都比它大;在左边的子序列中重复操作,即再取一个子序列的枢轴元素,一分为二,枢轴左边的元素都比它小,枢轴右边的元素都比它大;右边子序列同样;子序列的子序列只要还有1个以上的元素,就继续上述操作,直至序列中只剩一个元素,此时必定有序,也就是到了递归的出口条件left==right
qsort.jpg
依据上图,写出快排的实现。

快速排序
//快速排序实现 2024-02-14
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>

using namespace std;

const int MAXN = 1000 + 10;

int arr[MAXN]; 

//确定序列的枢轴元素,并遍历使枢轴左边的元素都比它小,右边都比它大
int Partition(int left, int right)
{
    int pos = left + rand() % (right - left + 1);//得到[left, right]区间内的一个随机数
    swap(arr[left], arr[pos]); //保证快排性能

    int pivot = arr[left];//枢轴

    while(left < right)
    {
        while(left < right && arr[left] < arr[right])
        {
            right--;
        }
        swap(arr[left], arr[right]);

        while(left < right && arr[left] < arr[right])
        {
            left++;
        }
        swap(arr[left], arr[right]);
    }
    return left;
}

void QuickSort(int left, int right)
{
    if(left < right)
    {
        int position = Partition(left, right);
        QuickSort(left, position -1);
        QuickSort(position + 1, right);
    }
}

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

进一步,我们如何利用快排的特性,在O(n)线性时间内找到无序序列中第K大的数呢?设无序序列总共有n个元素,第K大的数在有序序列中的数组下标就应该是n-k,而在每一轮的快排中,我们随机指定的枢轴元素都能找到它的最终位置,如图所示。故假如该枢轴元素刚好下标就是n-k,那么我们就找到了第K大的数,算法结束;假如此轮枢轴元素下标比n-k小,那么我们也能判断出第K大的数必定在枢轴的右边,从而排除掉近一半的元素(根据快排性能);假如枢轴下标比n-k大,则说明要找的第K大的数在枢轴的左边,继续只对左边的子序列进行快排即可,排除右边子序列。
qsort2.jpg

O(n)时间内找到第K大的数
//利用快排逻辑实现O(n)找到无序序列第K大的数 2024-02-14
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>

using namespace std;

const int MAXN = 1000 + 10;

int arr[MAXN]; 

//确定序列的枢轴元素,并遍历使枢轴左边的元素都比它小,右边都比它大
int Partition(int left, int right)
{
    int pos = left + rand() % (right - left + 1);//得到[left, right]区间内的一个随机数
    swap(arr[left], arr[pos]); //保证快排性能

    int pivot = arr[left];//枢轴

    while(left < right)
    {
        while(left < right && arr[left] < arr[right])
        {
            right--;
        }
        swap(arr[left], arr[right]);

        while(left < right && arr[left] < arr[right])
        {
            left++;
        }
        swap(arr[left], arr[right]);
    }
    return left;
}

int QuickSort(int left, int right, int m)//m表示n-k,即要找的元素在有序序列中最终的下标
{
    if(left < right)
    {
        int position = Partition(left, right);
        if(position == m)
        {
            return position; // 枢轴元素最终位置,对应数组下标
        }
        else if(position < m)
        {
            return QuickSort(position + 1, right, m);
        }
        else{
            return QuickSort(left, position - 1, m);
        }
    }//只要K比n小,就一定能够找到,不会找不到 
}

int main()
{
    int n;
    int k;//找到第K大的数
    scanf("%d%d", &n, &k);
    for(int i=0; i<n; i++)
    {
        scanf("%d", &arr[i]);
    }
    int ret = QuickSort(0, n-1, n-k);

    printf("第%d大的数为:%d\n", k, arr[ret]);

    return 0;    
}
posted @ 2024-02-09 20:09  paopaotangzu  阅读(25)  评论(0)    收藏  举报