2013年41题

强化讲义 · 原始转写 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照

## 2013年41题

(13 分) 已知—个整数序列 A = (a0, a1, . . . , an−1),其中 0≤ai < n (0≤i < n)。若存在 ap1 == x 且 m > n/2 (0≤pk < n, 1≤k≤m),则称 x 为 A 的主元素。例如 A = (0, 5, 5, 3,5, 7, 5, 5),则 5 为主元素;又如 A = (0, 5, 5, 3, 5, 1, 5, 7),则 A 中没有主元素。假设 A 中的n个元素保存在一个一维数组中,请设计一个尽可能高效的算法,找出A的主元素。若存在主元素,则输出该元素;否则输出-1。要求:(1)给出算法的基本设计思想。(2)根据设计思想,采用C、C++或Java 语言描述算法,关键之处给出注释。(3)说明你所设计算法的时间复杂度和空间复杂度。

<table><tr><td rowspan=1 colspan=1></td><td rowspan=1 colspan=1>做题思路</td><td rowspan=1 colspan=1>时间复杂度</td><td rowspan=1 colspan=1>空间复杂度</td><td rowspan=1 colspan=1>预计得分</td></tr><tr><td rowspan=1 colspan=1>暴力解</td><td rowspan=1 colspan=1>枚举</td><td rowspan=1 colspan=1>O(n2)</td><td rowspan=1 colspan=1>0(1)</td><td rowspan=1 colspan=1>11</td></tr><tr><td rowspan=1 colspan=1>暴力解</td><td rowspan=1 colspan=1>快速排序</td><td rowspan=1 colspan=1>O(nlogn)</td><td rowspan=1 colspan=1>O(logn)</td><td rowspan=1 colspan=1>13</td></tr><tr><td rowspan=1 colspan=1>优化解</td><td rowspan=1 colspan=1>散列表</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>14</td></tr><tr><td rowspan=1 colspan=1>特殊解</td><td rowspan=1 colspan=1>技巧</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>0(1)</td><td rowspan=1 colspan=1>15</td></tr></table>

## 【解法1】暴力解:枚举,分别判断每个数是不是主元素

(1)两层循环:外层循环枚举数组中每个候选元素,内层循环统计该元素在数组中出现的次数,若次数大于n/2则为主元素。(2)伪代码如下:

int ans(int A[], int n)
{
    int m; // m统计当前枚举元素出现次数
    for (int i=0; i&lt;n; i++)
    {
        m=0; // 每次换元素的时候m要清零
        for (int j=0; j&lt;n; j++)
        if (A[i]==A[j])
        m++;
        if (m&gt;n/2)
        return A[i]; //找到了主元素
    }
    return -1; // 未找到主元素
}
<!-- page 88 --> (3) 时间复杂度:O(n²) 空间复杂度:O(1) ## 【解法2】暴力解:快速排序,然后查找主元素 (1)先快速排序得到升序序列,权值相同的元素都会相邻,扫描一趟数组,L是之前相邻的相同元素最左边的元素下标,i是此时正在扫描的元素下标,如果A[i]不等于A[L]侧说明碰到了新的元素。(2)伪代码如下: <!-- suspect-code --> > [!warning] 这段代码需要核对(原文第 88 页) > OCR 体检未通过:花括号不平衡。扫描件的两栏排版(代码 + 右侧注释)有时会被交错读取,产生看起来合理但语义错误的代码。背之前请对照原文。
void Qsort(int A[], int L, int R)
{
    if (L&gt;=R)
    return; //区间≤1,退出
    int pivot, i=L, j=R; // i, j 为左右指针
    pivot=A[L]; // pivot 为基准
    while (i&lt;j){
        while (i&lt;j &amp;&amp; A[j]&gt;=pivot)
        while (i&lt;j &amp;&amp; A[i]&lt;=pivot) j--;
        i++;
        if (i&lt;j)
        swap(A[i], A[j]); // A[j]&lt;pivot, A[i]&gt;pivot
    }
    swap(A[L], A[i]); //基准归位
    Qsort(A, L, i-1); //递归处理左区间
    Qsort(A, i+1, R); // 递归处理右区间
}
Qsort(A, 0, n-1);
int t=A[(n-1)/2]; // 主元素只可能是A[(n-1)/2]
int m=0; // m统计当前枚举元素出现次数
for (int i=0; i&lt;n; i++)
if (A[i]==t)
m++;
if (m&gt;n/2)
return t; //找到了主元素
return -1; // 未找到主元素
}
## (3)时间复杂度:O(nlogn)空间复杂度:O(logn) <!-- page 89 --> ## 【解法3】优化解:散列表 (1) 设置一个数组count[0...n-1]统计每个数字出现的次数,比如count[i]=1表示数字i出现了1次,遍历一次A数组统计所有数字出现的次数,处理完后扫描一次count数组找到是否有主元素,如果有则输出主元素,否则输出-1。(2)伪代码如下:
int ans(int A[], int n)
{
    int count[n]={0};// count统计数字出现频率
    for (int i=0; i&lt;n; i++)
    count[A[i]]++; // 0&lt;=A[i]&lt;=n,不会越界
    for (int j=0; j&lt;n; j++)
    if (count[j]&gt;n/2)
    return j;//找到了主元素,输出主元素
    return -1; ///未找到主元素,输出-1
}
(3)时间复杂度:O(n)空间复杂度:O(n) ## 【解法4】特殊解:技巧 (1)利用抵消法(摩尔投票法):扫描数组,用变量j记录当前候选主元素,t记录其票数。遇到与j相同的元素时票数加1,不同则减1,票数为0时更新候选元素。第一轮结束后,j为可能主元素,再次遍历确认其出现次数是否超过n/2,若超过则为主元素,否则不存在主元素。(2)伪代码如下: <!-- suspect-code --> > [!warning] 这段代码需要核对(原文第 89 页) > OCR 体检未通过:花括号不平衡。扫描件的两栏排版(代码 + 右侧注释)有时会被交错读取,产生看起来合理但语义错误的代码。背之前请对照原文。
int ans(int A[], int n)
{
    int j=A[0], t=1; //取A[0]作为可能主元素
    for (int i=1; i&lt;n; i++)
    else
    {
        if(t&lt;0)
        j=A[i]; t=1;
    }
}
int m=0; //m统计j出现的次数
for (int i=0; i&lt;n; i++)
if (A[i]==j) m++;
if (m&gt;n/2) return j; //j是主元素,输出j
else return -1; //j不是主元素,输出-1
}
## (3) 时间复杂度:O(n) 空间复杂度:O(1) <!-- page 90 --> ## 摩尔投票法 摩尔投票法最早由 Boyer 和 Moore 在 1981 年提出,原本用于处理在线序列中寻找主元素问题。主元素定义为:在长度为 n的序列中,出现次数严格超过 n/2的元素。 其核心思想是“抵消法”把不同的元素互相抵消掉,主元素由于数量超过一半,最终必然“存活”下来。 摩尔投票法正确性证明: 主元素出现次数> n/2,非主元素出现次数< n/2。 每次遇到不同元素,抵消一对“主元素-非主元素”。 • 最多能抵消掉 n-m次(m是主元素出现次数)。 剩余主元素个数为 m - (n-m) = 2m - n > 0,说明主元素一定存活到最后。 最后留下的候选人若存在主元素,必然是主元素。 再次扫描确认其出现次数是否> n/2 即可。 ## 2016年43题 (13分)已知由n(n≥2)个正整数构成的一个集合A= {ak|0≤k< n},将其划分为两个不相交的子集A1和A2,元素个数分别是n1和n2,A1和A2中元素之和分别为S1和S2。设计一个尽可能高效的划分算法,满足|n1-n2|最小且|S1-S2|最大。要求:(1)给出算法的基本设计思想。(2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。(3)说明你所设计算法的平均时间复杂度和空间复杂度。 <table><tr><td rowspan=1 colspan=1></td><td rowspan=1 colspan=1>做题思路</td><td rowspan=1 colspan=1>时间复杂度</td><td rowspan=1 colspan=1>空间复杂度</td><td rowspan=1 colspan=1>预计得分</td></tr><tr><td rowspan=1 colspan=1>暴力解</td><td rowspan=1 colspan=1>快速排序</td><td rowspan=1 colspan=1>O(nlogn)</td><td rowspan=1 colspan=1>O(logn)</td><td rowspan=1 colspan=1>9</td></tr><tr><td rowspan=1 colspan=1>优化解</td><td rowspan=1 colspan=1>多指针法</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>O(logn)</td><td rowspan=1 colspan=1>12</td></tr><tr><td rowspan=1 colspan=1>优化解</td><td rowspan=1 colspan=1>多指针法</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>0(1)</td><td rowspan=1 colspan=1>13</td></tr></table> 【解法1】暴力解:快速排序1)对数组 A[0...n-1]进行快速排序得到升序,排序后集合S1 是A[0...n/2-1](向下取整),集合S2是A[n/2...n-1](向下取整)。2)伪代码如下: <!-- suspect-code --> > [!warning] 这段代码需要核对(原文第 90 页) > OCR 体检未通过:中文出现在代码行但缺注释符。扫描件的两栏排版(代码 + 右侧注释)有时会被交错读取,产生看起来合理但语义错误的代码。背之前请对照原文。
void Qsort(int A[], int L, int R, int )
{
    if (L&gt;=R)
    return; // 区间≤1,退出
    int pivot, i=L, j=R; j为左右指针
    pivot=A[L]; // pivot 为基准
    while (i&lt;j){
        while (i&lt;j &amp;&amp; A[j]&gt;=pivot)
        j--;
        while (i&lt;j &amp;&amp; A[i]&lt;=pivot)
        i++;
        if (i&lt;j)
        swap(A[i], A[j]); // A[j]&lt;pivot, A[i]&gt;pivot
    }
    swap(A[L], A[i]); //基准归位 首
    Qsort(A, L, i-1); //递归处理左区间
    Qsort(A, i+1, R); // 递归处理右区间
}
void ans(int A[], int n)
{
    Qsort(A, 0, n-1);
    输出A1:A[0~n/2-1]
    输出A2:A[n/2~n-1] //奇数时中间数分给A2
}
<!-- page 91 --> (3)时间复杂度:O(nlogn)空间复杂度:O(logn ## 【解法2】特殊解:多指针法(递归) (1)对数组A[0\~n-1] 进行类似快速排序的做法,在处理左右区间时只处理可能包含中位数的区间,即如果区间的范围是[L,R],则只有L<=n/2-1<=R 才会处理该区间。对快排的递归调用稍作修改即可。(2)伪代码如下: <!-- suspect-code --> > [!warning] 这段代码需要核对(原文第 91 页) > OCR 体检未通过:中文出现在代码行但缺注释符。扫描件的两栏排版(代码 + 右侧注释)有时会被交错读取,产生看起来合理但语义错误的代码。背之前请对照原文。
void Quick_Select(int A[], int L, int R, int n)
{
    if (L&gt;=R)
    return; //区间≤1,退出
    int pivot, i=L, j=R; // i,j 为左右指针
    while (i&lt;j) pivot=A[L]; // pivot 为基准
    {
        while (i&lt;j &amp;&amp; A[j]&gt;=pivot)
        while (i&lt;j &amp;&amp; A[i]&lt;=pivot)
        i++;
        swap(A[i], A[j]); // A[j]&lt;pivot, A[i]&gt;pivot
        swap(A[L], A[i]); //基准归位
        if (n/2-1==i) break;
        if (n/2-1&gt;=L &amp;&amp; n/2-1&lt;=i-1)
        Quick_Select(A,L,i-1,n); // n/2-1在左区间范围中,选择左区间递归处理
        else
        Quick_Select(A,i+1,R,n); // n/2-1在右区间范围中,选择右区间递归处理
    }
    void ans(int A[], int n)
    {
        Quick_Select(A, 0, n-1, n);
        输出A1:A[0~n/2-1]
        输出A2:A[n/2~n-1]
    }
}
<!-- page 92 --> (3)时间复杂度:O(n)空间复杂度:O(logn)(注:关于此方法的时间复杂度记住即可,不需要掌握,《算法导论》上有完整证明) ## 时间复杂度证明 这是一个「快速选择」(Quick Select)的递归算法,类似快排。 每次划分时,仍然执行一次 partition(类似快排的分区),其时间是 O(区间长度),第一次是O(n),下一次是 O(n/2),再下一次是O(n/4),以此类推。 但与快排不同的是: 快排每次递归处理两个子区间; 而这里的 Quick Select 只递归处理包含第 n/2 个元素的那一侧子区间— 只递归一个子区间。 因此,总时间复杂度满足如下递归关系: 这是典型的「二分递归+线性工作量」,根据递推公式展开: 注意这样求的其实是平均时间复杂度是O(n),但对于和快速排序有关的方法,我们都可以写平均时间复杂度来。 ## 【解法3】特殊解:多指针法(非递归) (1) 对数组A[0\~n-1]进行类似快速排序的做法,在处理左右区间时只处理可能包含中位数的区间,即如果区间的范围是[1,r],则只有|<=n/2-1<=r才会处理该区间,可以不用递归来写,每次划分后修改I和r的范围即可。(2)伪代码如下: <!-- suspect-code --> > [!warning] 这段代码需要核对(原文第 92 页) > OCR 体检未通过:中文出现在代码行但缺注释符。扫描件的两栏排版(代码 + 右侧注释)有时会被交错读取,产生看起来合理但语义错误的代码。背之前请对照原文。
void ans(int A[], int n)
{
    int L=0, R=n-1;
    while (L&lt;R){ // 类快排划分过程
        int pivot, i=L, j=R; // i, j 为左右指针
        pivot=A[L]; // pivot 为基准
        while (i&lt;j){
            while (i&lt;j &amp;&amp; A[j]&gt;=pivot)
            j--;
            i++;
            if (i&lt;j)
            swap(A[i], A[j]); // A[j]&lt;pivot, A[i]&gt;pivot
        }
        swap(A[L], A[i]); //基准归位
        if (n/2-1==i) break; //分成所需要的两个区间
        if(n/2-1&gt;=L &amp;&amp; n/2-1&lt;=i-1) R=i-1; // n/2-1在左,改右边界
        else L=i+1; // n/2-1在右,改左边界
    }
    输出A1:A[0~n/2-1]
    输出A2:A[n/2~n-1] /1奇数时中间数分给A2
}
<!-- page 93 --> (3)时间复杂度:O(n) 空间复杂度:O(1) ## 2018年41题 (13分)给定一个含n(n≥1)个整数的数组,请设计一个在时间上尽可能高效的算法,找出 数组中未出现的最小正整数。例如,数组{-5,3,2,3}中未出现的最小正整数是1;数组{1,2,3} 中未出现的最小正整数是4。要求:(1)给出算法的基本设计思想。(2)根据设计思想, 采用C或C++语言描述算法,关键之处给出注释。(3)说明你所设计算法的时间复杂度和空 间复杂度。1 <table><tr><td rowspan=1 colspan=1></td><td rowspan=1 colspan=1>做题思路</td><td rowspan=1 colspan=1>时间复杂度</td><td rowspan=1 colspan=1>空间复杂度</td><td rowspan=1 colspan=1>预计得分</td></tr><tr><td rowspan=1 colspan=1>暴力解</td><td rowspan=1 colspan=1>枚举</td><td rowspan=1 colspan=1></td><td rowspan=1 colspan=1>0(1)</td><td rowspan=1 colspan=1>10</td></tr><tr><td rowspan=1 colspan=1>暴力解</td><td rowspan=1 colspan=1>快速排序</td><td rowspan=1 colspan=1>O(nlogn)</td><td rowspan=1 colspan=1>O(logn)</td><td rowspan=1 colspan=1>12</td></tr><tr><td rowspan=1 colspan=1>优化解</td><td rowspan=1 colspan=1>散列表</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>13</td></tr></table> ## 【解法1】暴力解:枚举最小正整数 (1)可以证明最小未出现的正整数t一定≤n+1(因为如果t>n+1,则1\~t-1都会出现一次,但只有n个整数,所以t-1≤n,即t≤n+1),枚举所有可能的未出现最小正整数,扫描一次数组判断是否符合。(2)伪代码如下:
int ans(int A[], int n)
{
    for (int i=1; i&lt;=n+1; i++)
    {
        bool flag=false; //先假设i未出现
        for (int j=0; j&lt;n; j++)
        if (i==A[j])
        flag=true; // 此时i出现了,flag变成true
        if (flag==false) // 若i未出现过则输出i
        return i;
    }
}
<!-- page 94 --> <!-- code-audit: false_positive --> > [!info] 已人工审读:这是复杂度说明正文,不是代码。 ```c (3)时间复杂度:O(n²)空间复杂度:O(1) ```