类型1:多个有序顺序表
← 内容总览 03 / 24 下一节 →
原始讲义 · 非增强校订版 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照
## 类型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]<pivot, A[i]>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)说明你所设计算法的时间复杂度和空间复杂度。
← 内容总览 03 / 24 下一节 →