【解法2】优化解:多指针法
← 内容总览 07 / 24 下一节 →
原始讲义 · 非增强校订版 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照
## 【解法2】优化解:多指针法
(1)用指针i,j维护一个窗口,其和 current\_sum <= k。固定左端点i,向右移动右端点j直到窗口和将要超过 k。此时,以i为起点的所有合法子序列有i-i+1个。然后将i右移一位,继续该过程。
(2) 伪代码如下:
<!-- verified-by-hand -->
> [!success] 已结合原文第 44 页及 Markdown 上下文人工校对
> m 和‘章 s ka’为水印,while 的两条语句需同属一个块。
int ans ( int A [], int n , int k )
{
int count = 0 , sum = 0 , i = 0 ;
for ( int j = 0 ; j & lt ; n ; j ++ )
{
sum += A [ j ];
while ( sum & gt ; k )
{
sum -= A [ i ];
i ++ ;
}
count += j - i + 1 ;
}
return count ;
}
<!-- page 45 -->
<!-- verified-by-hand -->
> [!success] 已结合原文第 45 页及 Markdown 上下文人工校对
> 一个函数被跨页错拆,中间又插入了复杂度文字;此条在 p45 的实际承载块写入完整函数。
int ans ( int A [], int B [], int n )
{
int pair_count = 0 ;
int count [ 5 * n + 1 ], s [ 5 * n + 1 ];
for ( int i = 0 ; i & lt ; = 5 * n ; i ++ ) count [ i ] = 0 ;
for ( int i = 0 ; i & lt ; n ; i ++ )
if ( A [ i ] & lt ; = 5 * n ) count [ A [ i ]] ++ ;
s [ 0 ] = count [ 0 ];
for ( int i = 1 ; i & lt ; = 5 * n ; i ++ )
s [ i ] = s [ i - 1 ] + count [ i ];
for ( int j = 0 ; j & lt ; n ; j ++ )
if ( B [ j ] & lt ; = 5 * n ) pair_count += s [ 5 * n - B [ j ]];
return pair_count ;
}
## 习题2-12
(13 分)已知两个非负整数序列 A = {a0, a1,…, an-1}、B = {b0, b₁,……,bn-1},求出有多少个数对 {ai, bj}使得 a₁+bj <=5n,设计一个在时间上尽可能高效的算法,有多少个这样的数对。例:A = <2,10,8>,B = <8,12,1>,n = 3,<2,8>、<2,12>、<2,1>、<10,1>、<8,1> 共满足要求的数对,返回 5。 例:A=<2,10,8>,B=<8,12,1>,n=3,<2,8>、<2,12>、<2,1>、<10,1>、<8,1>共满足要求的数对,返回 5。
(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>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(n)</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>13</td></tr></table>
## 【解法1】暴力解:枚举
(1)两层循环i和 j遍历数组(保证i < j),如果 A[i]+ B[j]<=5n 则计数器t+1,遍历完后返回 t。
(2) 伪代码如下:
int ans ( int A [], int B [], int n )
{
int t = 0 ;
for ( int i = 0 ; i & lt ; n ; i ++ )
for ( int j = 0 ; j & lt ; n ; j ++ )
if ( A [ i ] + B [ j ] & lt ; = 5 * n ) t ++ ;
return t ;
}
( 3 ) // 时间复杂度:O(n²) 空间复杂度:O(1)
## 【解法2】优化解:散列表
(1)使用 count 数组保存A数组中每个数出现的次数,count[i] 表示i出现的次数,循环统计完 count 后计算出数组S,S[i]表示小于等于i的元素有多少个。然后循环扫描 B 数组看<= 5n - B[i]的数有多少个,加到sum 中,循环完后返回sum。 68
(2) 伪代码如下:
<!-- page 46 -->
<!-- verified-by-hand -->
> [!success] 已结合原文第 46 页及 Markdown 上下文人工校对
> 该块是已合并到 p45 完整函数的跨页续段,应清除重复代码。
( 3 ) // 时间复杂度:O(n) 空间复杂度:O(n)
## \*习题2-13
(13 分)已知数组A 中有 n 个非负元素,每个元素 A[i] < 2n。请设计一个时间上尽可能高效的算法,求出数组 A 中第 k
小的元素。 W
例:A = {1,2,1,2,3, 3},n = 6,k = 3,按照升序得到 {1,1,2, 2,3,3},所以第 k小的元素是 2(第 3、4 大元素都是
2),输出2。
(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(logn)</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></table>
## 【解法1】暴力解:快速排序
(1)对数组快速排序,然后输出A[k-1]。
(2) 伪代码如下:
//快速排序 void Qsort(int A[], int L, int R)
{
if ( L & gt ; = R ) return ; //区间内元素个数为0或 1,自然有序,递归
终止
int i = L , j = R ;
int pivot = A [ L ]; //选取区间第一个元素作为基准
// 不断交换左边比 pivot 大的数和右边比 pivot 小的数
while ( i & lt ; j )
{
while ( i & lt ; j & amp ; & amp ; A [ j ] & gt ; = pivot ) j -- ; // 从右向左找第一个小于 pivot 的数
三 while ( i & lt ; j & amp ; & amp ; A [ i ] & lt ; = pivot ) i ++ ; // 从左向右找第一个大于 pivot的数
if ( i & lt ; j ) swap ( A [ i ], A [ j ]);
}
//将基准值 A[L] 与 A[i] 交换
swap ( A [ L ], A [ i ]);
//递归处理左右两个子区间
Qsort ( A , L , i - 1 );
Qsort ( A , i + 1 , R );
}
int ans ( int A [], int n , int k )
{
Qsort ( A , 0 , n -1 );
return A [ k -1 ];
}
<!-- page 47 -->
(3)快排选择枢值时会加上优化不会退化成最坏情况,代码中省略优化部分。
<!-- code-audit: false_positive -->
> [!info] 已人工审读:整块只是复杂度结论,不是代码,checker 对中文的告警在此不代表算法错误。
时间复杂度 : O ( nlogn ) 空间复杂度 : O ( logn )
## 【解法2】暴力解:散列表
(1)用 count数组保存每个元素出现的次数,count[i] 表示i出现的次数。统计完后再从0 开始扫描一遍 count 数组,k减去 count[i],如果 k<=0 了则输出 i。
(2) 伪代码如下:
int ans ( int A [], int n , int k )
{
int count [ 2 * n ];
for ( int i = 0 ; i & lt ; 2 * n ; i ++ ) count [ i ] = 0 ; // 初始化count数组
for ( int i = 0 ; i & lt ; n ; i ++ ) count [ A [ i ]] ++ ;
for ( int i = 0 ; i & lt ; 2 * n ; i ++ )
{
k = k - count [ i ];
if ( k & lt ; = 0 ) return i ;
} }
( 3 ) // 时间复杂度:O(n) 空间复杂度:O(n)
## 习题2-14
(13 分)已知数组A中有 n个元素(可能重复),每个元素2n ≤A[i]< 3n。请设计一个尽可能高效的算法,从小到大输出A数组中的元素(重复元素只输出一次)。
例:A={13,17,16,14,13,13},n=6,出现的元素是13、17、16、14,所以从小到大依次输出:13、14、16、17。
(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(logn)</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></table>
<!-- page 48 -->
【解法1】暴力解:快速排序
(1)对数组快速排序,然后从前往后遍历,只输出和前一个元素不一样的元素,这样就能够保证每个元素只输出一次。(2) 伪代码如下: VIWN
//快速排序
void Qsort ( int A [], int L , int R )
{
if ( L & gt ; = R ) return ; //区间内元素个数为 0或 1,自然有序,递归
终止
int i = L , j = R ;
int pivot = A [ L ]; //选取区间第一个元素作为基准
// 不断交换左边比 pivot 大的数和右边比 pivot 小的数
while ( i & lt ; j )
{
while ( i & lt ; j & amp ; & amp ; A [ j ] & gt ; = pivot ) j -- ; // 从右向左找第一个小于 pivot 的数
while ( i & lt ; j & amp ; & amp ; A [ i ] & lt ; = pivot ) i ++ ; // 从左向右找第一个大于 pivot 的数
if ( i & lt ; j ) swap ( A [ i ], A [ j ]);
}
//将基准值 A[L] 与 A[i] 交换
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 ); // 览机考研
for ( int i = 0 ; i & lt ; n ; i ++ )
if ( i == 0 | l A [ i ] != A [ i -1 ]) // A[i]这个元素第一次出现则输出
printf ( & quot ; % d & quot ;, A [ i ]);
}
( 3 ) // 时间复杂度:O(nlogn) 空间复杂度:O(logn)
## 【解法2】优化解:散列表
(1)count数组表示某个元素是否出现,count[x]表示x+2n出现的次数(0≤x<2n),遍历A数组统计每个元素出现的次数,即count[A[i]+2n]++。遍历完后依次扫描count数组判断有哪些元素出现过,从小到大输出这些元素。
(2) 伪代码如下:
<!-- page 49 -->
void ans ( int A [], int n )
{
int count [ n ] = { 0 };
for ( int i = 0 ; i & lt ; n ; i ++ )
count [ A [ i ] -2 * n ] ++ ; // 统计 A 中每个元素出现次数
for ( int i = 0 ; i & lt ; n ; i ++ )
if ( count [ i ] & gt ; 0 )
printf ( & quot ; % d & quot ;, i + 2 * n ); // 若 count[i]>0 说明 i+2n 出现过
}
(3)时间复杂度:O(n) 空间复杂度:O(n)
## 习题2-15
(15 分)已知一个整数序列 A =(a0,a1…, an-1),其中 0 ≤ a < n(0 ≤i < n)。从数组中选择一些数,要求选择的数中任意两数差的绝对值<=k。设计一个在时间上尽可能高效的算法输出最多可能选择的元素个数。要求:
(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>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>15</td></tr></table>
## 【解法1】暴力解:枚举,枚举最小元素
(1)假设我们选择的数中最小的那个为A[i],那么我们可以选择的数范围就是A[i]\~A[i]+k,只要确定了A[i]再访问一遍数组就能够算出有多少个可选元素m,所以使用两层循环,外层i遍历数组,内层i再遍历一次整个数组判断可选元素个数,选择最多可能的可选元素,输出m。 IN
(2) 伪代码如下:
int ans ( int A [], int n , int k )
{
int m ; // m是本次遍历选择元素数
int m_max = 0 ; // m_max初值为0
for ( int i = 0 ; i & lt ; n ; i ++ ){ // A[i]为本次枚举最小元素
m = 0 ; // 每次枚举时都需要清空m
for ( int j = 0 ; j & lt ; n ; j ++ )
if ( A [ j ] & gt ; = A [ i ] & amp ; & amp ; A [ j ] & lt ; = A [ i ] + k )
m ++ ; // A[j]处于选择范围则m++
m_max = max ( m_max , m ); //如果m大则更新m_max
}
return m_max ; //输出最大m值
}
<!-- page 50 -->
( 3 ) // 时间复杂度:O(n²) 空间复杂度:O(1)
## 【解法2】优化解:快速排序,然后数组指针后移
(1)先快速排序将A数组变成升序,设置数组指针i和i(初始为0),枚举i从0...n-1,从上一轮i的i开始往后移动,如果A[j+1]<=A[i]+k说明A[j+1]仍然处于A[i]...A[i]+k这个范围,此时j后移,直到j移动到最后一个元素说明已经不会有更大的m=j-i+1。
(2) 伪代码如下:
<!-- verified-by-hand -->
> [!success] 已结合原文第 50 页及 Markdown 上下文人工校对
> Qsort 中有裸字‘终止’,ans 末尾有裸字‘道’且缺少函数闭合。
int ans ( int A [], int n , int k )
{
Qsort ( A , 0 , n - 1 );
int m_max = 0 , j = 0 ;
for ( int i = 0 ; i & lt ; n ; i ++ )
{
if ( j & lt ; i ) j = i ;
while ( j + 1 & lt ; n & amp ; & amp ; A [ j + 1 ] & lt ; = A [ i ] + k ) j ++ ;
m_max = max ( m_max , j - i + 1 );
}
return m_max ;
}
<!-- page 51 -->
## 【解法3】优化解:散列表
(1)数组中每个元素大小都在0...n-1,可以使用数组B[n]表示每个值出现的次数,扫描一次整个数组统计每个数出现的次数,然后R从0扫描到n-1,维持一个队列,队头L,队尾R,保证这个队列长度不超过k,m表示队列中元素出现次数的总和,此时可取数的范围就是[L...R],一个新元素R 入队后(m+= B[R]),如果队列长度超过k了则将L出队(m-= B[L]),统计最大的 m 即最大队列中元素出现的次数和。
(2) 伪代码如下:
int ans ( int A [], int n , int k )
{
int m = 0 ; // m 是本次取值范围为 L~R 选择元素数
om
int m_max = 0 ; // m_max 初值为 0
int B [ maxn ]; an
memset ( B , 0 , sizeof ( B ));
for ( int i = 0 ; i & lt ; n ; i ++ ) B [ A [ i ]] ++ ; // A[i]出现次数+1
int L = 0 , R = 0 ;
for ( R = 0 ; R & lt ; n ; R ++ )
{
m += B [ R ]; // m 加上 B[R]
if ( R - L & gt ; k )
{ // 如果范围差大于 k 则 L 右移
m -= B [ L ]; // 此时范围是[L+1,R],m 减去 B[L]
L ++ ;
}
m_max = max ( m_max , m ); // 如果 m 大则更新 m_max
}
return m_max ; // 输出最大m值
}
```c
(3)时间复杂度:O(n) // 空间复杂度:O(n)
```
← 内容总览 07 / 24 下一节 →