【解法1】暴力解:枚举,枚举数对中的两个元素
强化讲义 · 原始转写 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照
## 【解法1】暴力解:枚举,枚举数对中的两个元素
(1)使用两层循环,外层i遍历数组,内层j再从i+1开始遍历一次整个数组(默认i <j),A[i]是数对中的前一个元素,A[j]即是数对中的后一个元素,求出m=A[i]和A[i]之差的绝对值,输出最大的m。(2)伪代码如下:
int ans(int A[], int n)
{
int m; // m 是数对之差的绝对值
int m_max = INT_MIN; //m_max初值为最小 int
for (int i = 0; i < n-1; i++) // 枚举前一个元素
for (int j = i+1; j < 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;
int m;
// 预处理,数组B保存最大值
int B[n];
B[0] = A[0];
for (int i = 1; i < n; i++) //预处理
B[i] = max(A[i], B[i-1]);
for (int i = 1; i < n; i++)
m_max = max(m_max, B[i-1]-A[i]); // 更新 m max
return m_max; // 输出最大m值
}
<!-- page 80 -->
进一步优化可以发现只需要维护一个最大值变量,无需额外数组,空间复杂度可以降到O(1)
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 < 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>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(logn)</td><td rowspan=1 colspan=1>0(1)</td><td rowspan=1 colspan=1>15</td></tr></table>
【解法1】暴力解:枚举,枚举所有元素,判断是否等于x
<!-- page 81 -->
(1)从头到尾完整地遍历一次数组A。在遍历的过程中,设置一个计数器,每当遇到一个值等于目标整数x的元素时,就将计数器加一。遍历结束后,计数器的值就是x在数组中出现的总次数。(2)C语言算法描述
int countOccurrences(int A[], int n, int x)
{
int count = 0; // 初始化计数器
//枚举,遍历整个数组A
for (int i = 0; i < 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的位置,无需变动。
4. 计算次数:此时,left 和 right 已分别指向第一个和最后一个 x 的下标。因此,x 出现的总次数就是 right - left + 1。 (2)
//折半查找模板二:查找第一个大于或等于 x的元素的下标
int B_Search(int A[], int L, int R, int x)
{
int mid;
while (L < R)
{
mid = (L + R) / 2;
if (A[mid] >= x) R = mid;
else L = mid + 1;
}
return L;
}
<!-- page 82 -->
<!-- verified-by-hand -->
> [!success] 已结合原文第 82 页及 Markdown 上下文人工校对
> 函数缺少闭合花括号,且直接访问 A[left]/A[right] 未检查折半查找返回的边界。改用两个边界二分避免越界。
int countOccurrences(int A[], int n, int x)
{
int l = 0, r = n;
while (l < r) { int m = l + (r - l) / 2; if (A[m] < x) l = m + 1; else r = m; }
int left = l;
l = 0; r = n;
while (l < r) { int m = l + (r - l) / 2; if (A[m] <= x) l = m + 1; else r = m; }
return l - left;
}
(3) 时间复杂度:O(logn)空间复杂度:O(1)
<!-- page 83 -->
## 2.6.2 真题
## 2010年42题
(13 分)设将n(n>1)个整数存放到一维数组R 中。试设计一个在时间和空间两方面都尽可能高效的算法。将 R 中保存的序列循环左移p(0 < p < n)个位置,即将 R 中的数据由(X0, X1, . . . , Xn−1)变换为(Xp, Xp+1,. . . , Xn−1, X0, X1, . . . , 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<p; i++)
B[i+n-p]=A[i];
B[i-p]=A[i];
for (int i=0; i<n; i++)
printf("%d ", B[i]); //输出元素重新放置后的数组
}
(3) 时间复杂度:O(n)空间复杂度:O(n)
## 【解法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<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-11
for (int i=0; i<n; i++)
printf("%d ", A[i]); // 输出翻转后的数组
}
<!-- page 84 -->
(3) 时间复杂度:O(n) 空间复杂度:O(1)
三次翻转顺序很关键:先左段、再右段、最后整体。
## 2011年42题
(15 分)一个长度为L(L≥1)的升序序列S,处在第「L/21个位置的数称为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)说明你所设计算法的时间复杂度和空间复杂度。 道V
<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></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】暴力解:都放入新数组中快速排序
void Qsort(int A[], int L, int R)
{
if (L>=R)
return; //区间≤1,退出
int pivot, i=L, j=R; // i, j 为左右指针
pivot=A[L]; // pivot 为基准
while (i<j){
while (i<j && A[j]>=pivot)
j--;
while (i<j && A[i]<=pivot) // 材
i++;
if (i<j)
swap(A[i], A[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<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 85 -->
## (3)时间复杂度:O(nlogn)空间复杂度:O(n)
这种解法完全不利用A、B原本已升序的特性,因此效率较低。
## 【解法2】优化解:多指针法,归并两个数组
(1)采用归并的方式合并数组A和B到新的数组C中,给A、B数组设置变量i和j保存数组下标,初始时都为0,每次比较A[i]和Bi],数组C的下一个空位保存小的那个数,并且对应指针(2) 伪代码如下:
int ans(int A[], int B[], int n)
{
int C[2*n];
int i=0,j=0,k=0;
while (i<n && j<n)
if (A[i]<B[j])
C[k++]=A[i++];
else
C[k++]=B[j++];
while (i<n) C[k++]=A[i++];
while (j<n) C[k++]=B[j++];
return C[n-1]; // 输出C[n-1]
}
<!-- page 86 -->
(3)时间复杂度:O(n) 空间复杂度:O(n)
【解法3】优化解:数组指针后移1)给A、B数组设置变量i和j保存数组下标,计数k表示此时是第几小的元素,初始时都为0,每次比较A和Bi],取较小的那个数对应指针后移且k++,直到k等于n,输出这个数。2)伪代码如下:
int ans(int A[], int B[], int n)
{
int i=0, j=0, k;
for (k=1; k<n; k++) // 只需要移动n-1次,最后一次取当前较小值即为中位数。
if (A[i]<B[j]) i++;
else j++;
return min(A[i],B[j]);// 输出A[i]和B[j]中的小值
}
(3) 时间复杂度:O(n) 空间复杂度:O(1)
归并两个有序数组时,中位数一定是整个合并序列第n小的元素,因此只需要知道该位置的元素,无需完整合并后的数组。 人
## 【解法4】优化解:多有序数组折半查找
(1)对A、B数组进行折半查找,设L1、R1为数组A的左右查找边界,L2、R2为B的左右查找边界。数组A[L1至R1]和B[L2\~R2]的中位数,设为a和b,求序列A、B的中位数过程如下:①若a < b则舍弃序列A中较小的一半A[L1...mid-1],同时舍弃序列B中较大的一半B[mid+1...R1]。②若a>=b则舍弃序列A中较大的一半A[mid+1...R1],同时舍弃序列B中较小的一半B[L1...mid-1]。重复过程①、②,且需要保证数组A、B中的元素个数相同,如果不同则从长的数组中舍弃一个(因为我们取mid时都是向下取整,所以这里舍弃最小的那个值),直到两个序列中均只含一个元素时为止,较小者即为所求的中位数。本质上不断缩小搜索区间,每次抛弃较小半区,直至剩下的最后一个候选区间。(2)伪代码如下:
void ans(int A[], int B[], int n)
{
int mid1, mid2, L1=0, L2=0, R1=n-1, R2=n-1;
while (L1<R1) //如果L1>=R1则退出循环
{
mid1=(L1+R1)/2;
mid2=(L2+R2)/2; //取中间数,向下取整
if (A[mid1]<B[mid2]){
L1=mid1;
R2=mid2; // 更新查找范围
if (R1-L1!=R2-L2)
L1++;
}
else{
R1=mid1;
L2=mid2; // 更新查找范围
if (R1-L1!=R2-L2)
L2++; //保证R1-L1=R2-L2
}
}
printf("%d", min(A[L1], B[L2])); // 输出A[i]和B[j]中的小值
}
<!-- page 87 -->
(3)时间复杂度:O(logn)空间复杂度:O(1)
注意:本题的O(logn)解法思想深刻,但代码实现细节非常多,在考场上一次写对的难度很高。对于大部分同学,熟练掌握O(n)的解法(如解法3:多指针法)是更稳妥的策略可以确保拿到大部分分数。此O(logn)解法建议学有余力的同学深入理解。 炫价