2-1众数问题

(一)题目

问题描述

给定含有 n n n个元素的多重集合 S S S,每个元素在 S S S中出现的次数称为该元素的重数。多重集 S S S中重数最大的元素称为众数。例如: S = 1 , 2 , 2 , 3 , 5 S={1,2,2,3,5} S=1,2,2,3,5。多重集 S S S的众数是2,其重数为3。

算法设计

对于给定的由 n n n个自然数组成的多重集 S S S,计算 S S S的众数及其重数。

数据输入

输入数据由文件名为input.txt的文本文件提供。文件的第一行为多重集 中元素个数 n n n,接下来的 n n n行中,每行有一个自然数。

结果输出

将计算结果输出到文件output.txt。输出文件有两行,第1行是众数,第2行是重数。

(二)解法

采用分治的思想,用递归实现,两种方法主要是排序与不排序的区别

方法1:分治递归(先经过排序)
算法思路

先把数组排序,对于一个排好序的长度为 n n n数组 a a a,我们可以通过中位数将数组分成3部分——中位数左边的部分、中位数、中位数右边的部分;

用两个指针 l l l r r r分别指向中位数第一次出现的位置和最后一次出现的下一个位置, r − l r-l rl即为该中位数的重数 c n t cnt cnt,如果 c n t cnt cnt大于当前最大的重数 m a x c n t maxcnt maxcnt,我们就将该中位数更新为众数, c n t cnt cnt更新为 m a x c n t maxcnt maxcnt

如果中位数左边部分的长度大于 m a x c n t maxcnt maxcnt,说明该部分可能存在众数,将其递归;

如果中位数右边部分的长度大于 m a x c n t maxcnt maxcnt,说明该部分可能存在众数,将其递归;

当递归不再进行时,说明整个数组的众数已找到。

举例

在这里插入图片描述

源代码
#include<iostream>
#include<cstdio>
#include<fstream>
#include<algorithm>

using namespace std;

//数组长度
int n;
//数组
int a[1000];
//众数的下标
int num;
//重数的重数
int maxcnt;

//读取
void read(); 
//写入
void write();
//将长度为n的一段数组分为中位数左边的部分、中位数、中位数右边的部分
void split(int a[], int n, int &l, int &r);
//求众数及其重数
void findMaxCnt(int &num, int &maxcnt, int a[], int n);

int main()
{
    //读取
    read();
    //对数组排序
    sort(a, a + n);
    //求众数及其重数
    findMaxCnt(num, maxcnt, a, n);
    //写入
    write();
    return 0;
}

void read()
{
    ifstream ifs;
    //打开输入文件
    ifs.open("G:\\algorithm\\data\\2_1_in.txt", ios::in);
    //读取数据
    ifs>>n;
    for (int i = 0; i < n; ++i)
    {
        ifs>>a[i];
    }
    //关闭输入文件
    ifs.close();
}

void write()
{
    ofstream ofs;
    //创建输出文件
    ofs.open("G:\\algorithm\\data\\2_1_1out.txt", ios::out);
    //写入数据
    ofs<<a[num]<<endl;
    ofs<<maxcnt<<endl;
    //关闭输出文件
    ofs.close();
}

void split(int a[], int n, int &l, int &r)
{
    int mid = n / 2;
    for (l = 0; l < n; ++l)
    {
        if (a[l] == a[mid])
        {
            //此时l为中位数第一次出现的位置
            break;
        }
    }
    for (r = l + 1; r < n; ++r)
    {
        if (a[r] != a[mid])
        {
            //此时r为中位数最后一次出现的下一个位置
            break;
        }
    }
}

void findMaxCnt(int &num, int &maxcnt, int a[], int n)
{
    int l, r;
    //将长度为n的一段数组分为中位数左边的部分、中位数、中位数右边的部分
    split(a, n, l, r);
    //此时的中位数
    int mid = n / 2;
    //此时中位数的重数
    int cnt = r - l;

    //若该中位数的重数大于最大的重数,将该中位数更新为众数,该中位数的重数更新为最大重数
    if (cnt > maxcnt)
    {
        maxcnt = cnt;
        num = mid;
    }

    //如果中位数左边的元素个数大于最大的重数,则继续递归
    if (l > maxcnt)
    {
        findMaxCnt(num, maxcnt, a, l);
    }

    //如果中位数右边的元素个数大于最大的重数,则继续递归
    if (n - r > maxcnt)
    {
        findMaxCnt(num, maxcnt, a + r, n - r);
    }
}
方法2:分治递归(不经过排序)
算法思路

对于一个无序的数组 a a a,参考快速排序确定基准数位置的方法,我们可以在一趟排序中把小于等于基准数的数放在基准数左边,把大于等于基准数的数放在基准数右边,同时记录下基准数出现的次数 c n t cnt cnt,如果 c n t cnt cnt大于当前最大的重数 m a x c n t maxcnt maxcnt,我们就将该基准数更新为众数, c n t cnt cnt更新为 m a x c n t maxcnt maxcnt

如果小于等于基准数的部分长度大于 m a x c n t maxcnt maxcnt,说明该部分可能存在众数,将其递归;

如果大于等于基准数的部分长度大于 m a x c n t maxcnt maxcnt,说明该部分可能存在众数,将其递归;

当递归不再进行时,说明整个数组的众数已找到。

举例

在这里插入图片描述

源代码
#include<iostream>
#include<cstdio>
#include<fstream>
#include<algorithm>

using namespace std;

//数组长度
int n;
//数组
int a[1000];
//众数的下标
int num;
//重数的重数
int maxcnt;

//读取
void read(); 
//写入
void write();
//交换数组中的两个元素
void swap(int a[], int i, int j);
//将一段数组分为小于等于基准数的部分、基准数(只有一个)、大于基准数的部分
int partition(int a[], int l, int h, int &cnt);
//求众数及其重数
void findMaxCnt(int &num, int &maxcnt, int a[], int l, int h);

int main()
{
    //读取
    read();
    //求众数及其重数
    findMaxCnt(num, maxcnt, a, 0, n - 1);
    //写入
    write();
    return 0;
}

void read()
{
    ifstream ifs;
    //打开输入文件
    ifs.open("G:\\algorithm\\data\\2_1_in.txt", ios::in);
    //读取数据
    ifs>>n;
    for (int i = 0; i < n; ++i)
    {
        ifs>>a[i];
    }
    //关闭输入文件
    ifs.close();
}

void write()
{
    ofstream ofs;
    //创建输出文件
    ofs.open("G:\\algorithm\\data\\2_1_2out.txt", ios::out);
    //写入数据
    ofs<<a[num]<<endl;
    ofs<<maxcnt<<endl;
    //关闭输出文件
    ofs.close();
}


void swap(int a[], int i, int j)
{
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
}

int partition(int a[], int l, int h, int &cnt)
{
    //假设基准数pivot为数组的第一个元素
    int pivot = a[l];
    //povit出现一次
    cnt = 1; 
    while (l < h)
    {
        //h指针左移找到第一个小于povit的数,交换l、h指针所指的值
        while (l < h && a[h] >= pivot)
        {
            if (a[h] == pivot)
            {
                cnt++;
            }
            h--;
        }
        swap(a, l, h);
        //l指针右移找到第一个大于povit的数,交换l、h指针所指的值
        while (l < h && a[l] <= pivot)
        {
            if (a[l] == pivot)
            {
                cnt++;
            }
            l++;
        }
        swap(a, l, h);
    }
    //返回基准数的位置,此时l=h
    return l;
}

void findMaxCnt(int &num, int &maxcnt, int a[], int l, int h)
{
    //基准数的重数
    int cnt;
    //将一段数组分为小于等于基准数的部分、基准数(只有一个)、大于基准数的部分
    int pos = partition(a, l, h, cnt);

    //若该基准数的重数大于最大的重数,将该基准数数更新为众数,该基准数数的重数更新为最大重数
    if (cnt > maxcnt)
    {
        maxcnt = cnt;
        num = pos;
    }

    //如果基准数左边的元素个数大于最大的重数,则继续递归
    if (pos > maxcnt)
    {
        findMaxCnt(num, maxcnt, a, 0, pos - 1);
    }

    //如果基准数右边的元素个数大于最大的重数,则继续递归
    if (n - pos > maxcnt)
    {
        findMaxCnt(num, maxcnt, a, pos + 1, n - 1);
    }
}
结果示例

输入:
在这里插入图片描述
输出:
在这里插入图片描述

(三)总结

两种方法本质上差别不大,时间复杂度也相同

方法1:分治递归(先经过排序)
时间复杂度

read()函数的时间复杂度为 O ( n ) \Omicron(n) O(n)

write()函数的时间复杂度为 O ( 1 ) \Omicron(1) O(1)

sort()函数的时间复杂度为 O ( n l o g n ) \Omicron(nlogn) O(nlogn) (sort函数结合了快速排序-插入排序-堆排序 三种排序算法)

split()函数的时间复杂度为 O ( n ) \Omicron(n) O(n)

findMaxCnt()函数的时间复杂度递推式为:
T 1 ( n ) = { O ( 1 ) ,           n = 1 2 T 1 ( n 2 ) + O ( n ) ,      n > 1 T1(n)=\begin{cases} \Omicron(1),\;\qquad\;\;\ \qquad \quad \qquad n=1\\ 2T1(\frac{n}{2})+\Omicron(n),\;\;\quad\qquad n>1\\ \end{cases} T1(n)={O(1), n=12T1(2n)+O(n),n>1
因此,
T 1 ( n ) = 2 T 1 ( n 2 ) + n O ( 1 ) = 2 ( 2 T 1 ( n 2 2 ) + n 2 O ( 1 ) ) + n O ( 1 ) = 2 2 T 1 ( n 2 2 ) + 2 ⋅ n O ( 1 ) = . . . = n T 1 ( 1 ) + l o g n ⋅ n O ( 1 ) = O ( n l o g n ) \begin{aligned}T1(n)&=2T1(\frac{n}{2})+n\Omicron(1)\\&=2(2T1(\frac{n}{2^2})+\frac{n}{2}\Omicron(1))+n\Omicron(1)\\&=2^2T1(\frac{n}{2^2})+2\cdot n\Omicron(1)\\&=...\\&=nT1(1)+logn\cdot n\Omicron(1)\\&=\Omicron(nlogn)\end{aligned} T1(n)=2T1(2n)+nO(1)=2(2T1(22n)+2nO(1))+nO(1)=22T1(22n)+2nO(1)=...=nT1(1)+lognnO(1)=O(nlogn)
整个算法的时间复杂度为:
T ( n ) = O ( n ) + O ( n l o g n ) + T 1 ( n ) + O ( 1 ) = O ( n l o g n ) \begin{aligned}T(n)&=\Omicron(n)+\Omicron(nlogn)+T1(n)+\Omicron(1)\\&=\Omicron(nlogn)\end{aligned} T(n)=O(n)+O(nlogn)+T1(n)+O(1)=O(nlogn)

方法2:分治递归(不经过排序)
时间复杂度

read()函数的时间复杂度为 O ( n ) \Omicron(n) O(n)

write()函数的时间复杂度为 O ( 1 ) \Omicron(1) O(1)

swap()函数的时间复杂度为 O ( 1 ) \Omicron(1) O(1)

partition()函数的时间复杂度为 O ( n ) \Omicron(n) O(n) (每个元素要遍历一次)

findMaxCnt()函数的时间复杂度递推式为:
T 1 ( n ) = { O ( 1 ) ,           n = 1 2 T 1 ( n 2 ) + O ( n ) ,      n > 1 T1(n)=\begin{cases} \Omicron(1),\;\qquad\;\;\ \qquad \quad \qquad n=1\\ 2T1(\frac{n}{2})+\Omicron(n),\;\;\quad\qquad n>1\\ \end{cases} T1(n)={O(1), n=12T1(2n)+O(n),n>1
因此,
T 1 ( n ) = 2 T 1 ( n 2 ) + n O ( 1 ) = 2 ( 2 T 1 ( n 2 2 ) + n 2 O ( 1 ) ) + n O ( 1 ) = 2 2 T 1 ( n 2 2 ) + 2 ⋅ n O ( 1 ) = . . . = n T 1 ( 1 ) + l o g n ⋅ n O ( 1 ) = O ( n l o g n ) \begin{aligned}T1(n)&=2T1(\frac{n}{2})+n\Omicron(1)\\&=2(2T1(\frac{n}{2^2})+\frac{n}{2}\Omicron(1))+n\Omicron(1)\\&=2^2T1(\frac{n}{2^2})+2\cdot n\Omicron(1)\\&=...\\&=nT1(1)+logn\cdot n\Omicron(1)\\&=\Omicron(nlogn)\end{aligned} T1(n)=2T1(2n)+nO(1)=2(2T1(22n)+2nO(1))+nO(1)=22T1(22n)+2nO(1)=...=nT1(1)+lognnO(1)=O(nlogn)
整个算法的时间复杂度为:
T ( n ) = O ( n ) + O ( n l o g n ) + T 1 ( n ) + O ( 1 ) = O ( n l o g n ) \begin{aligned}T(n)&=\Omicron(n)+\Omicron(nlogn)+T1(n)+\Omicron(1)\\&=\Omicron(nlogn)\end{aligned} T(n)=O(n)+O(nlogn)+T1(n)+O(1)=O(nlogn)

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐