【解法2】优化解:多指针法

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

## 【解法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]&gt;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) ```