5. 查找
408 学习笔记:5. 查找。保留原有知识内容、公式与图示;有对应原件时提供查看和下载入口。
本页目录
ABSTRACT · 本节范围 本笔记逐页覆盖原 PDF 的 7 页、7 道代码题:有序表查找/交换/插入、单链表结点前移、递归与非递归折半查找、BST 合法性、BST 最值、BST 结点层次、输出所有大于关键字的结点。代码统一为 C++17,原稿的查找方向、返回值、空表和空树语义均已订正。
0. 公共定义
#include <algorithm>
#include <cstddef>
#include <limits>
#include <optional>
#include <utility>
#include <vector>
struct LNode {
int data{};
LNode* next = nullptr;
};
struct BSTNode {
int data{};
BSTNode* left = nullptr;
BSTNode* right = nullptr;
};
- 顺序表和向量下标统一从 0 开始,查找失败返回
-1。 - BST 采用严格次序:左子树关键字 $<$ 根关键字 $<$ 右子树关键字,不允许重复关键字。
- BST 根结点层号为 1;空树合法,空树没有最大值、最小值或结点层号。
1. 有序顺序表:查找后交换,否则插入
1.1 题目
顺序表严格递增。以尽可能低的时间复杂度查找 $x$:
- 若找到且它有后继元素,将它与后继交换;
- 若找到但它已是表尾,不交换;
- 若未找到,按递增次序插入 $x$。
1.2 算法
先用折半思想求第一个“不小于 $x$”的位置 pos。若该位置等于 $x$,执行相邻交换;否则 pos 正是插入位置。
std::size_t lowerBoundIndex(const std::vector<int>& values, int x) {
std::size_t low = 0;
std::size_t high = values.size(); // 半开区间 [low, high)
while (low < high) {
const std::size_t mid = low + (high - low) / 2;
if (values[mid] < x) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
void findSwapOrInsert(std::vector<int>& values, int x) {
const std::size_t position = lowerBoundIndex(values, x);
if (position < values.size() && values[position] == x) {
if (position + 1 < values.size()) {
std::swap(values[position], values[position + 1]);
}
return;
}
values.insert(values.begin()
+ static_cast<std::ptrdiff_t>(position), x);
}
查找为 $O(\log n)$;找到时交换为 $O(1)$,未找到时顺序表移动元素不可避免,插入最坏为 $O(n)$。
WARNING · 操作后的有序性 当 $x$ 与更大的后继交换后,局部次序变成“后继、$x$”,顺序表不再递增。这是题目明确要求的结果,因此本算法不能在不重新排序的情况下连续依赖原有序性再次执行。
DANGER · 原稿错误 原稿在空表或查找失败时仍可能读取未初始化的
mid;插入循环的下标在循环后使用也不清楚。半开区间写法直接得到合法插入位置,空表同样成立。
2. 单链表目标结点与前驱交换
2.1 题目与边界
带头结点单链表中,找到第一个值为 key 的数据结点,将它与前一个数据结点交换。若目标是首元结点或不存在,则无法交换,返回 false。
2.2 可运行实现
bool swapWithPredecessor(LNode* head, int key) {
if (head == nullptr || head->next == nullptr) {
return false;
}
LNode* beforePrevious = head;
LNode* previous = head->next;
if (previous->data == key) {
return false; // 首元结点没有数据前驱。
}
LNode* current = previous->next;
while (current != nullptr && current->data != key) {
beforePrevious = previous;
previous = current;
current = current->next;
}
if (current == nullptr) {
return false;
}
previous->next = current->next;
current->next = previous;
beforePrevious->next = current;
return true;
}
时间复杂度 $O(n)$,辅助空间 $O(1)$。交换尾结点同样成立。
DANGER · 原稿错误 原稿在空表、单结点表上会直接解引用
list->next或list->next->next;遍历与交换后的判断还会把“找到最后一个结点”误判为失败。
3. 折半查找:非递归与递归
3.1 前提与区间
数组必须递增并支持随机访问。闭区间 [low, high] 有效时条件为 low <= high;失败条件为 low > high。
3.2 可运行实现
int binarySearch(const std::vector<int>& values, int key) {
int low = 0;
int high = static_cast<int>(values.size()) - 1;
while (low <= high) {
const int mid = low + (high - low) / 2;
if (values[mid] == key) {
return mid;
}
if (values[mid] < key) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
int binarySearchRecursiveImpl(const std::vector<int>& values,
int key,
int low,
int high) {
if (low > high) {
return -1;
}
const int mid = low + (high - low) / 2;
if (values[mid] == key) {
return mid;
}
if (values[mid] < key) {
return binarySearchRecursiveImpl(
values, key, mid + 1, high);
}
return binarySearchRecursiveImpl(
values, key, low, mid - 1);
}
int binarySearchRecursive(const std::vector<int>& values, int key) {
return binarySearchRecursiveImpl(
values, key, 0, static_cast<int>(values.size()) - 1);
}
两种方法时间均为 $O(\log n)$;非递归辅助空间 $O(1)$,递归调用栈 $O(\log n)$。
DANGER · 原稿错误 原稿在
values[mid] > key时错误地令low = mid + 1,在values[mid] < key时又缩小右边界,方向完全相反;递归版还没有返回递归调用结果,并存在函数声明为void却返回整数等语法错误。
4. 判断一棵树是否为 BST
4.1 全局约束
仅比较“当前结点与直接孩子”不够,因为 BST 要求左子树所有关键字都小于根、右子树所有关键字都大于根。递归传递当前结点允许的开区间 $(lower, upper)$:
- 左子树上界收紧为当前值;
- 右子树下界收紧为当前值。
4.2 可运行实现
bool isBSTImpl(const BSTNode* root,
long long lower,
long long upper) {
if (root == nullptr) {
return true;
}
if (root->data <= lower || root->data >= upper) {
return false;
}
return isBSTImpl(root->left, lower, root->data)
&& isBSTImpl(root->right, root->data, upper);
}
bool isBST(const BSTNode* root) {
return isBSTImpl(
root,
std::numeric_limits<long long>::lowest(),
std::numeric_limits<long long>::max());
}
时间 $O(n)$,递归栈 $O(h)$。使用 long long 边界使合法的 INT_MIN、INT_MAX 结点不会与哨兵冲突。
DANGER · 原稿错误 原稿使用全局
pre = -INT_MAX:它不是INT_MIN,会错误拒绝最小整型关键字;全局状态在多次调用前若不重置,也会让后一次判断受到前一次影响。
5. BST 的最小值与最大值
BST 最小结点是沿左孩子走到尽头的结点,最大结点是沿右孩子走到尽头的结点。空树返回空指针,不能返回整数 0,因为 0 可能是合法关键字。
const BSTNode* minimumNode(const BSTNode* root) {
if (root == nullptr) {
return nullptr;
}
while (root->left != nullptr) {
root = root->left;
}
return root;
}
const BSTNode* maximumNode(const BSTNode* root) {
if (root == nullptr) {
return nullptr;
}
while (root->right != nullptr) {
root = root->right;
}
return root;
}
时间复杂度 $O(h)$,辅助空间 $O(1)$。
6. 求指定结点在 BST 中的层次
沿 BST 查找路径移动,每下降一层将层号加 1;命中时返回层号,不存在时返回 -1。
int bstLevel(const BSTNode* root, int key) {
int level = 1;
while (root != nullptr) {
if (root->data == key) {
return level;
}
root = key < root->data ? root->left : root->right;
++level;
}
return -1;
}
时间 $O(h)$,辅助空间 $O(1)$。原稿命中后错误地返回 key,并对未声明变量 n 自增,因而没有真正返回层号。
7. 输出 BST 中所有大于 key 的结点
为了按递增次序输出,采用中序顺序。若当前值不大于 key,它的整棵左子树也不可能满足条件,可直接剪枝,只搜索右子树。
void collectGreaterThan(const BSTNode* root,
int key,
std::vector<int>& result) {
if (root == nullptr) {
return;
}
if (root->data <= key) {
collectGreaterThan(root->right, key, result);
return;
}
collectGreaterThan(root->left, key, result);
result.push_back(root->data);
collectGreaterThan(root->right, key, result);
}
std::vector<int> greaterThan(const BSTNode* root, int key) {
std::vector<int> result;
collectGreaterThan(root, key, result);
return result;
}
设输出结点数为 $k$,平衡 BST 中可做到 $O(h+k)$;最坏退化树仍为 $O(n)$。原稿无条件遍历左右子树,虽然结果正确,但没有利用 BST 次序剪枝。
8. 七题覆盖与纠错速查
| 题号 | 主题 | 关键边界或修正 |
|---|---|---|
| 1 | 折半定位后交换/插入 | 空表不读 mid;插入位置直接取 lower_bound |
| 2 | 链表目标与前驱交换 | 空表、首元、尾元、不存在均有确定行为 |
| 3 | 折半查找 | 大小方向纠正;递归结果必须 return |
| 4 | 判断 BST | 用全局值域,不用有状态的全局 pre |
| 5 | BST 最小/最大 | 空树返回空指针,避免 0 歧义 |
| 6 | BST 层次 | 命中返回 level,不是返回关键字 |
| 7 | 输出大于 key |
利用 BST 性质剪去不可能的左子树 |
相关知识
9. 本节复盘
- 折半查找的核心不是背代码,而是始终维护“答案仍在当前区间内”的不变式。
- 顺序表查找可为 $O(\log n)$,但中间插入仍需移动元素,最坏 $O(n)$。
- 链表交换相邻结点需要同时保存前前驱、前驱和当前结点。
- BST 判断传递的是祖先给出的全局范围,不能只比较父子。
- BST 的极值、层次和范围输出都应利用“左小右大”剪枝。