习题2-16
← 内容总览 08 / 24 下一节 →
原始讲义 · 非增强校订版 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照
## 习题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 ; // 当前区间元素个数<=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]<pivot, A[i]>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小的元素,因此只需要知道该位置的元素,无需完整合并后的数组。
← 内容总览 08 / 24 下一节 →