习题2-16

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

## 习题2-16

(15 分)在数组中,某个数字减去它右边的数字得到一个数对之差。求所有数对之差的最大值。例如,在数组{2,4,1,16,7,5,11,9}中,数对之差的最大值是11,是16 减去5的结果。设计一个在时间上尽可能高效的算法输出所有数对之差的最大值。

(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(n²)</td><td rowspan=1 colspan=1>0(1)</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>0(1)</td><td rowspan=1 colspan=1>15</td></tr></table>

<!-- page 52 -->

(1)使用两层循环,外层i遍历数组,内层j再从i+1开始遍历一次整个数组(默认i<j),A[i]是数对中的前一个元素,A[j]即是数对中的后一个元素,求出m=A[i]和A[j]之差的绝对值,输出最大的m。

(2) 伪代码如下:

int ans(int A[], int n)
{
    int m; // m 是数对之差的绝对值
    int m_max = INT_MIN; // m_max 初值为最小 int
    for (int i = 0; i &lt; n-1; i++) // 枚举前一个元素
    for (int j = i+1; j &lt; n; j++) // 枚举后一个元素
    {
        m = A[i] - A[j];  // A[i] 和 A[j] 之差
        m_max = max(m_max, m); // 如果 m 大则更新 m_max
    }
    return m_max; // 输出最大 m 值
}
(3) // 时间复杂度: O(n²) 空间复杂度:O(1)
【解法2】优化解:预处理,保存i之前的最大元素 (1)j遍历一遍数组,t保存A[0\~j-1]的最大元素,相当于枚举j作为数对后一个元素,而t作为数对第一个元素,数对差为 m = t-A[j],保存最大数对差 m\_max。 (2) 伪代码如下:
int ans(int A[], int n)
{
    int m_max = INT_MIN; // m_max初值为最小int
    int m;
    //预处理,数组B保存最大值
    int B[n];
    B[0] = A[0];
    for (int i = 1; i &lt; n; i++)
    B[i] = max(A[i], B[i-1]);
    for (int i = 1; i&lt;n; i++)
    m_max = max(m_max, B[i-1]-A[i]); //如果B[i]-A[i]更大则更新m_max
    return m_max; //输出最大m值
}
进一步优化可以发现只需要维护一个最大值变量,无需额外数组,空间复杂度可以降到O(1): <!-- page 53 -->
int ans(int A[], int n)
{
    int m_max = INT_MIN; // m_max 初值为最小 int
    int m;
    int t = A[0]; // t 来保存 A[i] 之前的最大值,初值为
    A[0]
    for (int j = 1; j &lt; n; j++) //后一个元素下标从 1 开始
    {
        m = t - A[j];
        m_max = max(m_max, m); // 如果 m 大则更新 m_max
        t=max(t, A[j]); // 如果 A[j] 大则更新 t
        return m_max; // 输出最大 m 值
    }
}
(3) 时间复杂度:O(n) 空间复杂度:O(1) (这里保存状态只用了一个变量,所以空间复杂度仍然是O(1)。) ## 习题2-17 (15 分)已知一个整数升序序列 A =(a0, a1……, an-1)。设计一个尽可能高效的算法返回整数x 出现的次数。函数原型为:int countOccurrences(int A[], int n, int x)。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用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(n)</td><td rowspan=1 colspan=1>O(1)</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(logn)</td><td rowspan=1 colspan=1>0(1)</td><td rowspan=1 colspan=1>15</td></tr></table> 【解法1】暴力解:枚举,枚举所有元素,判断是否等于x (1)从头到尾完整地遍历一次数组A。在遍历的过程中,设置一个计数器,每当遇到一个值等于目标整数x的元素时,就将计数器加一。遍历结束后,计数器的值就是x在数组中出现的总次数。 (2) C语言算法描述 <!-- page 54 -->
int countoccurrences(int A[], int n, int x)
{
    int count = 0; //初始化计数器
    //枚举,遍历整个数组 A
    for (int i = 0; i &lt; n; i++)
    {
        //如果当前元素等于 x,计数器加一
        if (A[i] == x) count++;
    }
    return count; // 返回最终计数值
}
(3) // 时间复杂度:O(n) 空间复杂度:O(1)
## 【解法2】优化解:折半查找, (1)如果数组A是有序的,我们可以利用二分查找来快速定位元素。这个方法的思想是: 1. 定位左边界:首先,折半查找第一个大于或等于x的元素下标,记为left。这确定了x首次出现的位置。 2. 有效性判断:检查A[left] 是否等于x。如果不等,说明数组中根本不存在x,直接返回0。 3. 定位右边界:接着,再次折半查找第一个大于或等于x+1 的元素下标,记为 right。这个下标需要进行一次调整来精确定位最后一个x 的位置:。 如果 A[right]的值大于x(即查找成功),那么最后一个x实际在 right 的前一个位置,因此我们将 right 减一。。 如果A[right]的值等于x(即你查找失败,这只在x是数组末尾元素时发生),那么 right 本身就已经是最后一个x的位置,无需变动。 E矿H 4. 计算次数:此时,left 和 right 已分别指向第一个和最后一个 x 的下标。因此,x 出现的总次数就是 right - left + 1。(2) an
//折半查找模板二:查找第一个大于或等于 x 的元素的下标
int B_Search(int A[], int L, int R, int x)
{
    int mid;
    while (L &lt; R)
    mid = (L + R) / 2;
    if (A[mid] &gt;= x) R = mid;
    else L = mid + 1;
}
return L;
}
int countoccurrences(int A[], int n, int x) {
    // 1.查找最左侧的 x 下标
    int left = B_Search(A, 0, n-1, x);
<!-- page 55 -->
//输出元素重新放置后的数组
// 2.如果它不等于 x 则说明数组中没有 x
if (A[left] != x) return 0;
// 3.查找最左侧的 x 下标
int right = B_Search(A, 0, n-1, x+1);
if (A[right] &gt;= x+1)
right--;
// 4. 次数 = right - left + 1
return right - left + 1;
(3) // 时间复杂度:O(logn) an 空间复杂度:O(1)
## 2.6.2 真题 ## 2010年42题 (13 分)设将n(n>1)个整数存放到一维数组R中。试设计一个在时间和空间两方面都尽可能高效的算法。将R中保存的序列循环左移p(0<p<n)个位置,即将 R 中的数据由(X₀,X₁,……,Xn-1)变换为(Xp,Xp+1…,Xn-1,X0,X₁,…,Xp-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(n)</td><td rowspan=1 colspan=1>O(n)</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(n)</td><td rowspan=1 colspan=1>O(1)</td><td rowspan=1 colspan=1>13</td></tr></table> ## 【解法1】暴力解:枚举,另设一个数组,枚举每个元素再移动 (1)原序列保存在数组A中,设另一个数组B,将A[0],A[1],…,A[p-1]放到B[n-p],B[n-p+1]...,B[n-1]中,将A[p],A[p+1],…,A[n-1]放到B[0],B[1],….,B[n-p-1]中,数组B即是所求。 (2) 伪代码如下:
void ans(int A[], int n, int p)
{
    int B[n];
    for (int i=0; i&lt;p; i++)
    B[i+n-p]=A[i];
    for (int i=p; i&lt;n; i++)
    B[i-p]=A[i];
    for (int i=0; i&lt;n; i++)
    printf(&quot;%d &quot;, B[i]);
}
(3) // 时间复杂度:O(n) 空间复杂度:O(n)
<!-- page 56 --> ## 【解法2】特殊解:技巧,数组翻转 (1)原序列保存在数组A中,将数组 A的前 p 个元素 A[0]\~A[p-1]翻转,再将后面的 A[p]\~A[n-1] 翻转,最后整体翻转 A[0] \~ A[n-1]。 (2) 伪代码如下:
void Reverse(int A[], int L, int R)
{
    int num=(R-L+1)/2;
    for (int i=0; i&lt;num; i++)
    swap(A[L+i], A[R-i]);
}
void ans(int A[], int n, int p)
{
    Reverse(A, 0, p-1); //翻转A[0~p-1]
    Reverse(A, p, n-1); // 翻转A[p~n-1]
    Reverse(A, 0, n-1); //翻转A[0~n-1]
    for (int i=0; i&lt;n; i++)
    printf(&quot;%d &quot;, A[i]); // 输出翻转后的数组
}
(3) 时间复杂度:O(n) 空间复杂度:O(1) 三次翻转顺序很关键:先左段、再右段、最后整体。 ## 2011年42题 (15 分)一个长度为L(L≥1)的升序序列S,处在第[L/2]个位置的数称为S的中位数。例如,若序列S1 =(11,13,15,17,19),则 S1 的中位数是15,两个序列的中位数是含它们所有元素的升序序列的中位数。例如,若S2 =(2,4,6,8,20),则 S1 和S2的中位数是11。现在有两个等长升序序列 A和B,试设计一个在时间和空间两方面都尽可能高效的算法,找出两个序列A和B的中位数。要求: (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(nlogn)</td><td rowspan=1 colspan=1>O(n)</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(n)</td><td rowspan=1 colspan=1>O(n)</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(1)</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(logn)</td><td rowspan=1 colspan=1>O(1)</td><td rowspan=1 colspan=1>15</td></tr></table> ## 【解法1】暴力解:都放入新数组中快速排序 (1)将数组A和数组B中的数据都存入数组C中,C数组长度为2n,对数组C进行快速排序,输出排序后数组中的C[n-1]。 (2) 伪代码如下:
void Qsort(int A[], int L, int R)
{
    if (L&gt;=R)
    return; // 当前区间元素个数&lt;=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)
        j--;
        while (i&lt;j &amp;&amp; A[i]&lt;=pivot)
        i++;
        if (i&lt;j)
        // A[j]&lt;pivot, A[i]&gt;pivot
    }
    swap(A[L], A[i]); //将基准值放入他的最终位置
    Qsort(A, L, i-1); // 递归处理左区间
    Qsort(A, i+1, R); // 递归处理右区间
}
int ans(int A[], int B[], int n)
{
    int C[2*n];
    for (int i=0; i&lt;n; i++)
    {
        C[i]=A[i];
        C[n+i]=B[i];
    }
    Qsort(C, 0, 2*n-1); //快速排序处理C[0]~C[2n-1]
    return C[n-1]; // 中位数输出C[n-1]
}
<!-- page 57 -->
(3) // 时间复杂度:O(nlogn) 空间复杂度:O(n)
这种解法完全不利用A、B原本已升序的特性,因此效率较低。 ## 【解法2】优化解:多指针法,归并两个数组 (1)采用归并的方式合并数组A和B到新的数组C中,给A、B数组设置变量i和i保存数组下标,初始时都为0,每次比较A[i]和B[i],数组C的下一个空位保存小的那个数,并且对应指针后移,直到一个数组遍历完,将另一个数组中剩下的元素依次保存到C空位中。 (2) 伪代码如下: <!-- page 58 -->
int ans(int A[], int B[], int n)
{
    int C[2*n];
    int i=0,j=0,k=0;
    while (i&lt;n &amp;&amp; j&lt;n)
    if (A[i]&lt;B[j])
    C[k++]=A[i++];
    else
    C[k++]=B[j++];
    while (i&lt;n) C[k++]=A[i++];
    while (j&lt;n) C[k++]=B[j++];
    return C[n-1]; // 输出C[n-1]
}
(3) // 时间复杂度:O(n) 空间复杂度:O(n)
## 【解法3】优化解:数组指针后移 1)给A、B数组设置变量i和j保存数组下标,计数k表示此时是第几小的元素,初始时都为0,每次比较A[i]和B[j],取较小的那个数对应指针后移且k++,直到k等于n,输出这个数。 2)伪代码如下:
int ans(int A[], int B[], int n)
{
    int i=0, j=0, k;
    for (k=1; k&lt;n; k++)
    即为中位数。
    if (A[i]&lt;B[j]) i++;
    else j++;
    return min(A[i], B[j]);
}
(3) // 时间复杂度:O(n) 空间复杂度:O(1)
归并两个有序数组时,中位数一定是整个合并序列第n小的元素,因此只需要知道该位置的元素,无需完整合并后的数组。