注意事项:

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

## 注意事项:

快速排序不是万能的,有如下情况下不能使用:

题目明示或暗示不能改变顺序表元素的次序。

要采用稳定的排序算法。

如果题目需要得到降序,有两种方法:

方法1:修改快速排序代码(适合基础好的同学)。

方法2:快速排序代码不变,主过程段新增 for 循环将数组逆置。for (int i=0;i < n/2;
i++) swap(A[i], A[n-i-1]);

正常情况下快速排序代码可以直接默写,但如果数组的类型不是int 就需要修改。

<!-- page 12 -->

## 2.3 判断能否优化

写出暴力解后,下一步就是思考优化的可能性。从以下两个角度自问,往往能发现突破口:

1.“条件浪费”:题目中是否给了一些我没用上的重要条件?

例如:数组是“升序”的,但我却从头到尾线性扫描,浪费了有序性。

例如:元素值范围是“0到n-1”,但我却用了排序,浪费了数据范围的特性。

2.“目标超额”:我为了解决问题,是不是做了很多额外的不必要工作?

例如:题目只要求“找出最小值”,我却把整个数组都排序了。

例如:题目只要求“把小于k的放左边,大于等于k的放右边”,我却把左右两边的元素内部都排好序了。

最好的算法,就是把题目的所有条件都榨干,并且不做多余的操作。

## 思考:下面几种暴力解分别符合哪个标准

例题 2-1:对升序数组 A,求是否存在值为 x的元素,存在返回1,不存在返回-1。暴力解:枚举 A

例题 2-2:将两个升序数组 A 和 B 合并成一个新的升序数组 C。暴力解:快速排序

例题2-3:数组 A 中有 n 个非负元素,每个元素绝对值< n,其中只有一个数重复出现过,请输出这个数。暴力解:枚举

例题2-4:求含n个数(n为奇数)的数组A的中位数。暴力解:快速排序

例题 2-5:已知数组 A 含有n 个整数,现在要将小于 k的元素放在数组左侧,大于等于 k的元素放在右侧,如何实现这个功能。暴力解:快速排序

例题 2-6:已知数组 A 含有 n 个整数,现在要求另一个数组 res,res[i] 表示长度为 i+1 的连续子序列中元素和的最大值,求res数组。暴力解:枚举 人

<!-- page 13 -->

## 2.4 优化解

针对前文分析,我们介绍几种核心的优化思想。

## 2.4.1 折半查找

核心要素:

使用条件:有序的顺序表(数组)。

核心思想:通过不断与区间中点比较,每次都将查找范围缩小一半。

•复杂度:时间 O(logn),空间 O(1)。

当题目出现“有序数组”(且是顺序表,不是链表)时,你的脑海里应该立刻响起“折半查找”的警报,真题中频繁考察。 当计

## 两种经典模板

<table><tr><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>查找满足条件的第一个或最后一个元素,元素可以重复。</td></tr><tr><td rowspan=1 colspan=1>查找成功</td><td rowspan=1 colspan=1>A[mid] == x 判断。</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>区间缩小为只有一个元素,需额外判断。</td></tr><tr><td rowspan=1 colspan=1>循环条件</td><td rowspan=1 colspan=1>while(L &lt;= R)(区间没有元素时退出循环)</td><td rowspan=1 colspan=1>while (L &lt; R) (区间收敛为一个元素时退出循环)</td></tr><tr><td rowspan=1 colspan=1>边界更新逻辑</td><td rowspan=1 colspan=1>R = mid - 1 或 L = mid+ 1 (直接排除 mid)</td><td rowspan=1 colspan=1>R = mid 或 L = mid + 1(可能不能排除 mid)</td></tr></table>

## 模板一:『等值判断』

这是最基础、最常用的模板,其核心是在循环内部通过 if(A[mid] == x)直接判断是否找到了目标值。

//折半查找
int B_Search(int A[], int L, int R, int x)
{
    int mid;
    while (L &lt;= R) // 区间非空则继续
    {
        mid = (R + L) / 2; //求中点
        if (A[mid] == x)
        return mid; // 查找成功
        else
        if (x &lt; A[mid])
        R = mid - 1; // x 在左半区间,L ~ mid-1
        else L = mid + 1; // x 在右半区间, mid+1
    }
    return -1; //查找失败
}
<!-- page 14 --> ## 模板二:『区间收敛』 这个模板通过不断收敛(逼近)区间来锁定一个边界,专门用于解决边界查找问题。
// 折半查找第一个等于 x 元素的下标
int B_Search(int A[], int L, int R, int x)
{
    int mid;
    while (L &lt; R) // 收敛至单元素
    {
        mid = (R + L) / 2;
        if (A[mid] &gt;= x) R = mid; // x 在 L ∼ mid
        else L = mid + 1; // x 在 mid+1 ∼ R
    }
    return L; // 需额外判断是否等于
}
## 注意 循环条件与边界更新的匹配:L<= R和 L< R分别对应着不同的区间定义和边界更新逻辑,必须配套使用,否则容易导致死循环或漏解。 边界更新的细节:在模板二中,如果将 R = mid 误写为 R = mid - 1,若 A[mid] 正好是你要找的那个边界值时,就会把它错误地排除掉。 学会灵活应用:很多时候折半查找的代码并不能直接默写使用,而是需要根据题目要求来修改,例如题目中给的是降序序列。 例题2-1:对升序数组 A,求是否存在值为x的元素,存在返回 1,不存在返回-1。 ## 暴力解:枚举 优化解:折半查找 解析:关键字“升序数组”,可使用折半查找优化。本题只是查找一个确切的值,使用最直观的模板一即可。 N
//折半查找模板一
int B_Search(int A[], int L, int R, int x)
{
    int mid;
    while (L &lt;= R) // 区间非空则继续
    {
        mid = (R + L) / 2; //求中点
        if (A[mid] == x)
        return mid; //查找成功
        else
        if (x &lt; A[mid])
        R = mid - 1; // x 在左半区间 ~ mid-1
        else L = mid + 1; // x 在右半区间, mid+1 ∼ R
    }
    return -1; //查找失败
}
//在升序数组A中判断是否存在
int ans(int A[], int n, int x)
{
    // 直接调用折半查找函数
    if (B_Search(A, 0, n - 1, x) == -1)
    return -1;
    else return 1;
}
<!-- page 15 --> 而如果改一下题目,就适合模板二了:在一个升序数组A中,查找第一个等于 x的元素的位置,如果存在则返回下标,如果不存在则返回-1。(数组中可能存在多个x) 解析:此题可能有重复元素,因此适用于模板二。因为数组中可能存在多个x(例如[1,2,3,3,3,4,4],查找 3),如果使用模板一,当 A[mid] == x时直接返回,可能会返回中间的那个 3,而不是第一个。我们需要找到满足 A[i] == x的最小的i。 这正是模板二的应用场景。我们使用模板二找到这个位置pos 后,还需要额外进行一次判断:检查 A[pos] 是否真的等于 x。如果等于,pos 就是答案;如果不等(或者pos 超出数组范围),则说明数组中不存在x。
//折半模板二
int B_Search(int A[], int L, int R, int x)
{
    int mid;
    while (L &lt; R)
    {
        mid = (L + R) / 2;
        if (A[mid] &gt;= x) R = mid;
        else L = mid + 1;
    }
    return L;
}
//在升序数组 A 中查找第一个等于 x的元素的位置
int ans(int A[], int n, int x)
{
    int pos = B_Search(A, 0, n - 1, x);
    if (A[pos] == x) return pos; //查找成功
    else return -1; //查找失败
}
<!-- page 17 --> ## 2.4.2 散列表 使用条件:数据范围有限 使用条件:数据范围有限,可以用数组下标直接表示数据的值。 核心思想:用空间换时间。创建一个辅助数组 count[],count[i] 用于记录数值 i 出现的相关信息(如次数)。 复杂度:时间 O(n),空间 O(M),其中 M 是数据范围。 当题目明示或者通过分析确定“元素值为0到n-1”、“元素绝对值小于n”等等,此时我们要马上考虑使用散列表。 例题2-3:数组A 中有 n个非负元素,每个元素绝对值< n,其中只有一个数重复出现过,请输出这个数。 如:n=6, A={1, 2, 5, 2, 4, 3} 暴力解:枚举 优化解:散列表 解析:关键信息“绝对值<n”,可使用散列表
void Hash(int A[], int n)
{
    int count[n] = {0}; // 需要初始化
    for (int i = 0; i &lt; n; i++)
    {
        //A[i] 的值在0到n-1 之间,可以直接用作 count 数组的下标
        if (count[A[i]] &gt; 0)
        输出 A[i]; // 说明 A[i] 之前出现过
        count[A[i]]++; // 标记 A[i] 出现
    }
}
2018年41题
(13分)给定一个含n(n≥1)个整数的数组,请设计一个在时间上尽可能高效的算法,找出数组中未出现的最小正整数。例如,数组{-5, 3, 2,3} 中未出现的最小正整数是1;数组 {1,2,3} 中未出现的最小正整数是4。 解析:可以证明最小未出现的正整数t一定≤n+1(因为如果t>n+1,则1\~t-1都会出现一次,但只有n个整数,所以t-1≤n,即t≤n+1)。因此虽然题目中没有明确给出取值范围,但是通过我们的分析,只有0\~n+1这个范围的整数,所以可以使用散列表法。
int ans(int A[], int n)
{
    int count[n+2] = {0}; //0~n+1
    for (int i=0; i&lt;n; i++) // 扫描数组A
    if (A[i]&gt;0 &amp;&amp; A[i]&lt;=n+1) // 当0&lt;A[i]&lt;=n+1时才修改
    count[A[i]]++;
    for (int j=1; j&lt;=n+1; j++) //扫描count数组
    if (count[j]==0) //j未出现过,是最小未出现正整数
    return j; //输出j
}
<!-- page 18 --> ## 注意 使用散列表时一定要考虑 A[i] 的大小是否会超出 count 下标的范围。 新建辅助数组(散列表)需要初始化。 当题目限制元素的范围时可以考虑使用散列表,条件可以是直接给出也可能是暗示。 <!-- page 19 --> ## 2.4.3 预处理 使用条件:需要频繁计算区间和、积、最值等。 核心思想:类似散列表,以空间换时间。提前计算并存储一些结果,供后续快速查询。 考试中常见的预处理分为两类:求区间之和(前缀和)、求区间最值。 ## 前缀和 如果需要多次查询不同区间的和,可以先用O(n)的时间预处理出前缀和数组 sum,其中sum[i] 表示原数组前1 个元素的和(下标 0~下标 i-1)。之后查询任意区间 [L,R] 的和,只需要O(1)的时间。 HN 如果数组 A下标从0 开始存放数据, ,A[L\~R] 之间所有元素之和 = sum[R+1] - sum[L]。 只需要提前处理一次 sum 数组,之后就能在 O(1) 的时间快速计算元素和。 例题 2-6:已知数组 A 含有 n 个整数,现在要求另一个数组 res,res[i] 表示长度为 i+1 的连续子序列中元素和的最大值,求 res 数组,函数原型为 void ans(int A, int n)。 例:n=4,A={1,-2, 3, 5} <table><tr><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>i=0</td><td rowspan=1 colspan=1>{1}、{-2}、{3}、{5}</td><td rowspan=1 colspan=1>{5}→5</td></tr><tr><td rowspan=1 colspan=1>i=1</td><td rowspan=1 colspan=1>{1, -2}、{-2, 3}、{3, 5}</td><td rowspan=1 colspan=1>{3, 5}→8</td></tr><tr><td rowspan=1 colspan=1>i=2</td><td rowspan=1 colspan=1>{1, -2, 3}、{-2, 3, 5}</td><td rowspan=1 colspan=1>{-2, 3, 5}→6</td></tr><tr><td rowspan=1 colspan=1>i=3</td><td rowspan=1 colspan=1>{1, -2, 3, 5}</td><td rowspan=1 colspan=1>{1, -2, 3, 5}→7</td></tr></table> 则处理之后的 res 数组为 {5, 8, 6, 7} 暴力解:枚举,时间复杂度O(n3)
void ans(int A[], int n)
{
    //创建将res数组初值设为最小负数
    int res[n] = {0};
    for (int i = 0; i &lt; n; i++)
    res[i] = INT_MIN;
    // 枚举每种A[i~j]的连续子序列
    for (int i = 0; i &lt; n; i++)
    for (int j = i; j &lt; n; j++)
    {
        int temp = 0;
        for (int k = i; k &lt;= j; k++)
        temp += A[k];
        if (temp &gt; res[j-i])
        res[j-i] = temp;
    }
    输出res数组
}
<!-- page 20 --> 优化解:预处理,时间复杂度O(n²) 解析:在枚举的过程中我们发现需要大量重复计算,可以预处理前缀和数组,然后每次计算子序列和的时间复杂度就变成了O(1)
void ans(int A[], int n)
{
    //创建并初始化前缀和数组
    int res[n];
    for (int i = 0; i &lt; n; i++)
    res[i] = INT_MIN;
    //创建并初始化前缀和数组
    int sum[n + 1] = {0};
    for (int i = 1; i &lt;= n; i++)
    sum[i] = sum[i-1] + A[i-1];
    //枚举每种A[i~j]的连续子序列
    for (int i = 0; i &lt; n; i++)
    for (int j = i; j &lt; n; j++)
    {
        int temp = sum[j+1] - sum[i];
        if (temp &gt; res[j-i])
        res[j-i] = temp;
    }
    输出res数组
}
当然本题还有其他写法,甚至有时间复杂度为O(n)的方法,但是考研可能用不到,所以此处跳过。 X算 <!-- page 21 --> ## 区间最值 如果要频繁求出数组某一区间内的最小值或最大值,也可以通过预处理来实现,比如说设置数组B,B[i]表示i下标及之前元素的最值,或者是下标及之后的最值,根据题意来决定。 例题 2-7:已知数组 A 含有 n 个正整数,现在要求另一个数组 res,计算 A[i]与 A](0 ≤i ≤j ≤ n-1)乘积的最大值,并将其保存到 res] 中。求 res 数组,函数原型为 void ans(int A,int n)。 若 A[] = {3, 1, 2, 7, 6},则得到 res[ = {9, 3, 6, 49, 42}。 暴力解:枚举,时间复杂度为O(n²)
void ans(int A[], int n)
{
    int res[n];
    for (int j = 0; j &lt; n; j++)
    {
        res[j] = INT_MIN;
        for (int i = 0; i &lt;= j; i++)
        if (A[i] * A[j] &gt; res[j]) res[j] = A[i] * A[j];
    }
    输出res数组
}
优化解:预处理,时间复杂度为O(n) 解析:要快速确定 A[i]与 A[]]的最大乘积,我们可以预处理,使用数组 B 提前维护,B]]表示 A[0\~j]之间的最大值,res[i]=B[j]×A[i]。
void ans(int A[], int n)
{
    int res[n], B[n];
    int B[0] = A[0];
    for (int i = 1; i &lt; n; i++)
    B[i] = max(B[i-1], A[i]);
    for (int j = 0; j &lt; n; j++)
    res[j] = B[j] * A[j];
    输出res数组
}
通过预处理每个位置之前的最大值,我们只需O(1)的时间即可确定当前位置的最值,使得整个算法的时间复杂度降为O(n)。 当然,细心的同学可能会发现,其实本题的预处理并不需要数组B,使用一个变量B,然后把两次循环合并即可。
void ans(int A[], int n)
{
    int res[n];
    int B = A[0];
    for (int j = 0; j &lt; n; j++)
    {
        B = max(B, A[j]);
        res[j] = B * A[j];
    }
    输出res数组
}
<!-- page 23 --> ## 2.4.4 多指针法 适用条件:线性表(顺序表、链表),主要是有序线性表。 核心思想:在一个或多个线性表上设置两个或多个下标变量(指针),根据特定规则移动它们,协同完成任务。“规则”是多指针法的灵魂。它指的是根据题目要求和数据特性(如顺序性)制定出的指针移动策略。算法的每一步都依据这个规则来判断:应该移动哪个指针?移动后执行什么操作?一个设计精良的规则能确保在指针的移动过程中,我们总能做出局部最优决策,并最终导向全局最优解,同时避免了对无效状态的搜索。 人 多指针法可适用于以下两种场景: 多个线性表:每个线性表设置一个指针,如合并多个有序线性表。 单个线性表:在一个线性表中设置两个指针,例如一前一后或一快一慢,用于查找或调整元素顺序。 经典应用场景包括合并多个有序线性表、寻找第k个元素、归并排序等。注意:不可机械套用模板,应深入理解后灵活应用。