类型1:多个有序顺序表

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

## 类型1:多个有序顺序表

这是多指针法最经典的应用场景。通过在每个有序表上设置一个指针(可能在头或尾),并根据特定规则移动它们,可以高效地解决多种问题。我们从以下题目中感受:

例题2-2:给定升序数组A、B,长度分别为n、m,合并成一个新的升序数组C并输出。

例如: A = {1, 2, 7} , B = {3, 4, 5} 。

枚举法:最暴力的枚举是,为了确定新数组 C 的下一个元素,我们需要比较 A 和 B 中还剩下的所有元素,然后找出最小的。确定第二个元素时,又要重复这个过程。这显然是低效的,时间复杂度为O((m+n)²)。

多指针法:在枚举法的基础上,发现升序条件没有用到,数组A、B、C都会是升序数组,因此数组C的下一个元素一定是数组A中最小元素及数组B中最小元素其中较小的那个。最小元素在升序数组中是最左侧元素,所以设置两个指针分别指向A、B现存最小元素,每次比较指针所指元素并移动即可,时间复杂度O(m+n)。

• 分析:数组C 需要是升序,元素是从小到大存放,下一个元素一定是数组A 中最小元素及数组B 中最小元素其中较小的那个。所以每个数组的指针都应该分别指向他们还未处理的最小元素(即最左侧的下标)。

• 起始位置:数组A、B、C的指针都是从左侧开始向右移动。

• 规则:比较数组A、B对应指针所指的元素,将较小的元素复制到结果数组中,然后将该较小元素所在的数组的指针、最终数组的指针各移动一位。不断重复这个过程。这个规则保证了结果数组始终是升序的,因为我们每次都选取了当前所有待选元素中的最小值。

• 停止处理:当其中一个数组的指针移出数组(即遍历完该数组的所有元素)时,退出循环。此时,另一个数组中可能还有剩余的元素,由于原数组是有序的,这些剩余元素必然都大于等于已合并的所有元素。因此,只需将这个数组的剩余部分依次复制到结果数组的末尾即可。

void Merge(int A[], int n, int B[], int m)
{
    int C[n + m];
    int i = 0, j = 0, k = 0;
}
// i,j,k 分别是 A,B,C 的指针 <!-- page 18 -->
//直到有一个数组遍历完退出循环
while (i &lt; n &amp;&amp; j &lt; m)
if (A[i] &lt; B[j])
c[k++] = A[i++];
else
c[k++] = B[j++];
// 复制 A 中剩余的元素
while (i &lt; n)
c[k++] = A[i++];
// 复制 B 中剩余的元素
while (j &lt; m)
c[k++] = B[j++];
从左到右输出数组 C 中的元素
例题2-8 改编:给定升序数组 A、B,数组A 中都是负整数,数组 B 中都是非负整数,将所有元素平方后按降序排序,存入新数组C 并输出。 <!-- code-audit: false_positive --> > [!info] 已人工审读:这是例题的输入输出示例,不是 C 代码;仅因 Markdown 语言标注误用 c 而触发 checker。
例如: A = {-4, -2} B = {0, 1, 3} → C = {16, 9, 4, 1, 0} 。
暴力解:将所有元素平方后存入数组C,对数组C 快速排序,时间复杂度O((m+n)²)。 优化解:多指针法,时间复杂度O(m+n)。 • 分析:目标数组C需要是降序,元素应从大到小存放。C的下一个元素必然是 A和B当前剩余元素平方后的较大者。 。对于 A(负数升序),其绝对值是降序的,所以平方后的值也是降序((-4)²>(-2)²)。最大值在 A[0]。 。对于B(非负数升序),其绝对值是升序的,所以平方后的值是升序(0²<1²< 3²)。最大值在 B 的末尾。 起始位置:数组A、C的指针都是从左侧开始向右移动,数组B的指针是从右向左移动。 • 规则:比较 |A[i]| 和 |B[j]|,将绝对值较大者的平方值放入 C[k],然后移动被选中的指针(i 向右或j 向左)和结果指针k。 • 停止处理:当其中一个指针(i 或 j)越界时,主循环停止。此时,将另一个数组中所有剩余元素的平方值依次复制到C 的末尾。 <!-- verified-by-hand --> > [!success] 已结合原文第 18 页及 Markdown 上下文人工校对 > OCR 水印词块 om、道、NW W 混入代码,导致非法 C;按本页双指针文字说明还原。
void mergeAndSquare(int A[], int n, int B[], int m)
{
    int C[n + m];
    int i = 0, j = m - 1, k = 0;
    while (i &lt; n &amp;&amp; j &gt;= 0)
    {
        if (abs(A[i]) &gt; abs(B[j]))
        {
            C[k++] = A[i] * A[i];
            i++;
        }
        else
        {
            C[k++] = B[j] * B[j];
            j--;
        }
    }
    while (i &lt; n)
    {
        C[k++] = A[i] * A[i];
        i++;
    }
    while (j &gt;= 0)
    {
        C[k++] = B[j] * B[j];
        j--;
    }
    // 输出 C
}
<!-- page 19 --> ## 类型2:单个有序顺序表 在处理单个有序顺序表时,我们常常可以根据题目的特点,将这个单一的表看作是从某个“切点”分成的两个逻辑上的有序子序列。指针则分别置于这些逻辑子序列的端点,然后像处理多个有序表一样进行操作。首尾双指针(相向而行)就是这种思想最常见的体现,首尾的指针最终会在中间相遇。 有计 ao 例题2-8:升序数组A 中包含n 个整数,将其元素平方后按降序排序,存入新数组B 并输出。 <!-- code-audit: false_positive --> > [!info] 已人工审读:这是讲义例子文字,不是程序;应视为文本块的语言标注误报。
例如: A = {-4, -2, 0, 1, 3} → B = {16, 9, 4, 1, 0} 。
暴力解:平方后快速排序,时间复杂度O(nlogn)。 优化解:多指针法,时间复杂度O(n)。 思路分析:对于升序序列,平方后的最大值一定产生于原数组的“两端”(绝对值最大的地方)。因此,可以用两个指针i,j 分别指向 A 的头和尾,比较 |A[i]| 和 |A[j]|,将较大的平方值从后往前放入结果数组 B。 • 分析:对于一个包含正负数的升序序列,其平方后的最大值一定产生于原数组的“两端”(绝对值最大的地方)。这相当于将原数组以0为“切点”,分成了两个逻辑序列:一个负数序列(绝对值降序)和一个正数序列(绝对值升序)。我们可以设置两个指针,一个在数组头,一个在数组尾,它们分别指向这两个逻辑序列中绝对值最大的元素。 • 起始位置:头指针 i 指向 A[0],尾指针 j 指向 A[n-1] 。结果数组的指针 k 应指向 B 的首位B[0]。 规则:比较 |A[i]| 和 |A[j]|,将绝对值较大者的平方值放入 B[k],然后移动相应的指针(i 向右或j向左)和结果指针k(向右)。 • 停止处理:当头尾指针相遇或交错(i > j )时,循环结束。 <!-- page 20 --> 有的同学会疑惑为什么这里的判断条件只是i<=i,而不需要考虑正负发生变化时的情况,这里可以证明:当有正负发生变化时,那剩下的所有元素正负都相同,也就是说剩下的元素都是单调的,那在单调的情况下继续比较头尾一定能够保证只有一端移动。所以这种情况下仍然是可以继续执行直到i<=j才结束,并且没有元素剩下。 <!-- verified-by-hand --> > [!success] 已结合原文第 20 页及 Markdown 上下文人工校对 > 函数末尾缺失右花括号,注释尾还混有水印。
void squareAndSort(int A[], int n)
{
    int B[n];
    int i = 0, j = n - 1, k = 0;
    while (i &lt;= j)
    {
        if (abs(A[i]) &gt; abs(A[j]))
        {
            B[k++] = A[i] * A[i];
            i++;
        }
        else
        {
            B[k++] = A[j] * A[j];
            j--;
        }
    }
    // 输出 B
}
## \*类型3:单个无序顺序表 无序情况下的多指针法稍复杂,考察频率较低。学有余力的同学可以试着学习本节,掌握单个无序顺序表的处理即可,该方法常用于实现划分(Partition)操作。 10 X W 关于划分(Partition)操作,请读者回忆快速排序的过程,本节的方法都是基于快速排序的思路。 例题2-9:给定数组A 含有n个互异整数,以A[0]为基准,将小于A[0]的元素放左侧,大于 A[0]的元素放右侧。例如: A = {2, 5, 3, -1, -2} → 可能结果为 {-2, -1, 2, 3, 5}或{-1, -2, 2, 3, 5}或{-2, -1, 2,5, 3}或{-1, -2, 2, 5, 3} 。 • 暴力解:快速排序,时间复杂度 O(nlogn)。 • 优化解:多指针法,时间复杂度 O(n)。 思路分析:使用快速排序的单次划分思想,两个指针分别从数组两端向中间移动,分别找到大于和小于基准的元素进行交换,直到相遇后再将基准元素放入正确位置。 <!-- page 21 --> <!-- code-audit: false_positive --> > [!info] 已人工审读:整块是快排与快选的概念对比文字,其中的左花括号是排版噪声,不是应编译的代码。
例题2-9(按 A[0] 划分)的解法就是快排的单趟 Partition。
例题2-10(划分为大小两半)的解法,则是在 Partition 思想的基础上,不断调整,直到基准 pivot 的最终
位置恰好是 n/2。这被称为 快速选择(Quick Select) // 算法。
快速排序 (Quick Sort) vs. 快速选择 (Quick Select)
{
    • 目标不同:快排的目标是整个数组有序;快选的目标是找到第k小的元素。
    • 递归方式不同:
<!-- verified-by-hand --> > [!success] 已结合原文第 21 页及 Markdown 上下文人工校对 > 对照原文第21页核对:OCR 曾把 while 的 { 读成字母 s(花括号计数照样平衡、体检查不出来),已修正并补回末尾 };代码现与原文一致
void ans(int A[], int n)
{
    int i = 0, j = n - 1;                 // i和j是左右两个数组下标移动
    int pivot = A[0];
    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) swap(A[i], A[j]);      // A[j]&lt;pivot, A[i]&gt;pivot,交换
    }
    swap(A[0], A[i]);                     // 将基准值放入他的最终位置
}
例题2-10:给定数组A含有n个互异整数,将较小的一半元素放左侧,较大的一半放右侧。 例如: A = {2, 5,-1, -2} → 可能结果为 {-2, -1,2,5}或{-1,-2,2,5}或{-2,-1,5,2}或{-1,-2, 5,2}。。 • 暴力解:快速排序,时间复杂度O(nlogn)。 • 优化解:多指针法,时间复杂度 O(n)。 思路分析:在上一道例题的划分基础上改进,上一题会划分成左区间、x、右区间,x所在下标是i。但是本题要求两个区间各占一半,若i==n2,则说明正好平分区间,如果小于则说明右边的区间过大应该继续处理右边的区间,而如果大于则说明左边的区间过大应该继续处理左边的区间。4。快排:划分后,递归处理左右两个子区间。 T(n) = 2T(n/2) + 0(n) -> O(nlogn)
void ans(int A[], int n)
{
    int L = 0, R = n - 1; // L和R是当前处理区间的左右边界
    while (true)
    {
        int i = L, j = R;
        int pivot = A[L];
        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) swap(A[i], A[j]);
        }
        swap(A[L], A[i]);
        if (i == n / 2) return;
        if (i &lt; n / 2) L = i + 1; // 继续处理右区间
        else R = i - 1; // 继续处理左区间
    }
}
<!-- page 22 --> 。快选:划分后,根据划分点的位置,只递归处理其中一个子区间。T(n) = T(n/2) +0(n) -> O(n)这就是为什么“找中位数”比“完整排序”更快的原因。 ## 2.5 章节练习 ## 2.5.1 习题 ## 【习题2-1】 (13分)已知升序数组A含有n 个整数,求是否存在值为x的元素,存在返回数组下标,不存在则返回-1。设计一个在时间上尽可能高效的算法实现该功能。 例:A={1,2,3,7,8,11,15},n = 7,x = 8,则返回数组下标为 4。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C、C++或Java 语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-2】 (13 分)已知两个整数升序数组A和B.他们的元素数分别为 n和 m、请将他们合并成一个新的升序数组C.并输出 C数组。设计一个尽可能高效的算法实现该功能 例: A = {1, 2, 7}, n = 3, B = {3, 4, 5, 10}, m = 4, 则 C = {1, 2, 3, 4, 5, 7, 10}。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C、C++或Java 语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-3】 (15 分)已知数组A中有n个非负元素,每个元素绝对值<n,其中只有一个数重复出现过,请输出这个数,否则输出-1。设计一个时间上尽可能高效的算法实现该功能。 计 oy 例:n = 6,A = {1, 2, 5, 2, 4, 3},则 2 重复出现,输出 2。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C、C++或Java语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-4】 (15 分)已知整数数组A 含有n 个不相等的整数(n为奇数,且数组A 中每个元素的绝对值≤n)。设计一个时间上尽可能高效的算法,求数组A的中位数。例:n=5.A={-3.0.4.-2.3},则中位数为0。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C、C++或Java 语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-5】 (15分)已知数组A含有n个整数,现在要将小于k的元素放在数组右侧,大于等于k的元素放在左侧,设计一个时间上尽可能高效的算法实现这个功能。 <!-- page 23 --> 例:n = 7,A = {4, 5, 9, 3, 7, 10, 1},k = 6,则处理之后的数组可以为 {10, 7,9, 3, 5,4, 1},其中 10、7、9 比 6 大,在数组左侧;而3、5、4、1小于等于6,在数组右侧。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C、C++或Java 语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-6】 (13 分)有一个数组A由n个互不相同的正整数组成(n为奇数),该序列中的数先减后增,中位数是最小值,输入一个整数key。请设计一个时间和空间上尽可能高效的算法,判断key是否在该数组中,如果是则返回数组下标(下标从0开始),否则返回-1。例如对应 A = {10,7,5,2, 6,13,15},key = 5 时返回下标 2,若 key = 8 则返回 -1。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-7】 (13 分) 定义距离 d = x - yl,给定 2 个长度为 n 的升序数组 a、b,x 是数组 a 中的某个元素,y 是数组 b 中的某个元素。请设计一个尽可能高效的算法,计算并输出所有可能的的最小距离d。例如数组 a = {-16,-8,5,8,13},b ={-2,0,2, 6,10},则最小距离为1,相应的 x=5, y=6, d= |5 − 6|=1。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-8】 (15 分)已知升序数组A含有n个非负整数,没有重复元素,设计一个时间上尽可能高效的算法求是否存在两个数和为k(k>0),如果存在则返回1,不存在则返回0。 例:A={1,2,3,7,8,11,15},k=18,则7+11=18,返回1。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C、C++或Java 语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-9】 (13分)已知升序数组(无重复元素)A、B都含有n个整数,设计一个时间上尽可能高效的算法求新的数组C使得C中只存在A、B中都包含的元素,输出数组C。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C、C++或Java 语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-10】 (13分)将一个数组最开始的若干元素搬到数组的末尾,称之为数组的旋转。已知数组A为某个有序数组旋转后的结果(无重复值),设计一个时间上尽可能高效的算法求该旋转数组的最小元素。 例:数组 A = {3, 4, 5, 6,1,2} 为有序数组 {1, 2,3,4, 5, 6} 的一个旋转数组,该数组的最小值为 1。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C、C++或Java 语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-11】 <!-- page 24 --> (15 分) 已知一个整数升序序列 A = {a0, a₁…, an-1},其中 0 ≤ a < n(0 ≤ i < n)。连续子序列是从原序列中选取的一段连续的元素构成的序列(至少包含一个元素),如{a0,a₁}、{aρ}都是连续子序列。设计一个在时间上尽可能高效的算法,输出元素之和<=k的连续子序列个数。 E 例:A = {1,2, 3, 4, 5},n = 5,k = 6,则符合要求的连续子序列为 {1}、{2}、{3}、{4}、{5}、{1,2}、{2,3}、{1,2, 3},共有8个,返回8。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C、C++或Java语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。 ## 【习题2-12】 (13 分)已知两个非负整数序列 A = {a0,a1,…,an-1}、B = {b0, b1₁,……, bn-1},求出有多少个数对 {a,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。 (1)给出算法的基本设计思想。 (2)根据设计思想,采用C、C++或Java 语言描述算法,关键之处给出注释。 (3)说明你所设计算法的时间复杂度和空间复杂度。