1. 顺序表
Chinese-language Computer Science 408 note: 1. 顺序表. Original source available where applicable.
On this page
This note is currently available in Chinese. The original wording, formulas and diagrams are preserved to avoid an unverified technical translation.
本资料共 11 道顺序表代码题。核心是明确下标含义、容量上限和循环不变量:筛选题通常让
i负责读取、k负责写入;区间题统一采用闭区间;所有访问都必须先证明下标合法。
1.0 统一定义与约定
以下代码采用 C++ 引用语法;数组下标从 0 开始,合法数据区为 data[0..length-1]。
constexpr int MAXSIZE = 100;
using ElemType = int;
struct SqList {
ElemType data[MAXSIZE];
int length = 0;
};
- 涉及插入或合并时必须检查容量,不能写入
data[MAXSIZE]。 - 需要返回元素且可能失败时,使用
bool表示成功与否,用引用参数带回结果,避免用某个合法整数充当失败哨兵。 - 除非特别说明,“递增”按非递减理解,即允许相等关键字。
1.1 代码题 1:递增有序表插入元素
题目
在非递减顺序表中插入元素 x,使插入后仍保持非递减。
bool OrderedInsert(SqList &L, ElemType x) {
if (L.length >= MAXSIZE) return false;
int pos = 0;
// 跳过所有 <= x 的元素,使 x 插到已有相等元素之后。
while (pos < L.length && L.data[pos] <= x) ++pos;
for (int j = L.length; j > pos; --j)
L.data[j] = L.data[j - 1];
L.data[pos] = x;
++L.length;
return true;
}
- 循环结束时,
[0,pos)中元素均不大于x,[pos,length)中元素均大于x。 - 从尾部向前移动,避免尚未复制的元素被覆盖。
- 时间 $O(n)$、空间 $O(1)$;插在已有相等元素之后可保持稳定性。
- 原 PDF 使用了未定义变量
index,且比较方向不能正确找到插入点;上面已修正。
1.2 代码题 2:用末元素覆盖最小元素
题目
删除顺序表中的最小元素,用最后一个元素填补其位置,并返回被删除的最小值。
bool DeleteMin(SqList &L, ElemType &minValue) {
if (L.length == 0) return false;
int minIndex = 0;
for (int i = 1; i < L.length; ++i)
if (L.data[i] < L.data[minIndex])
minIndex = i;
minValue = L.data[minIndex];
L.data[minIndex] = L.data[L.length - 1];
--L.length;
return true;
}
- 时间 $O(n)$、空间 $O(1)$。
- 题目明确要求“末元素覆盖”,因此删除后不保证原有次序;若必须保持次序,应把后继元素依次前移。
- 空表时不能读取
data[0],所以必须先判空。
1.3 代码题 3:原地逆置顺序表
void Reverse(SqList &L) {
for (int i = 0, j = L.length - 1; i < j; ++i, --j) {
ElemType temp = L.data[i];
L.data[i] = L.data[j];
L.data[j] = temp;
}
}
- 循环不变量:
i左侧和j右侧的元素已经到达最终位置。 - 奇数长度时中间元素无须处理;空表时
j=-1,条件i<j直接为假。 - 时间 $O(n)$、空间 $O(1)$。
1.4 代码题 4:交换两个连续子序列
题目
将
$$ (a_1,a_2,\ldots,a_m,b_1,b_2,\ldots,b_n) $$
原地变为
$$ (b_1,b_2,\ldots,b_n,a_1,a_2,\ldots,a_m) $$
void ReverseRange(SqList &L, int left, int right) { // 闭区间
while (left < right) {
ElemType temp = L.data[left];
L.data[left++] = L.data[right];
L.data[right--] = temp;
}
}
bool ExchangeBlocks(SqList &L, int m, int n) {
if (m < 0 || n < 0 || m + n != L.length) return false;
ReverseRange(L, 0, m - 1);
ReverseRange(L, m, m + n - 1);
ReverseRange(L, 0, m + n - 1);
return true;
}
- 三次逆置的中间状态为 $A^RB^R\rightarrow( A^RB^R)^R=BA$。
m、n是两个子序列的长度,所以第二段末下标是m+n-1,不是n-1;原 PDF 在此混淆了长度与下标。- 本题交换的是构成整个顺序表的两个连续块,因此还必须满足
m+n==L.length。若只想交换表的某个前缀,应另行传入区间边界并在接口名中说明。 - 时间 $O(m+n)$、空间 $O(1)$。
1.5 代码题 5:删除所有值为 x 的元素
方法一:写指针保存保留元素
void DeleteX(SqList &L, ElemType x) {
int k = 0; // 下一次写入位置
for (int i = 0; i < L.length; ++i)
if (L.data[i] != x)
L.data[k++] = L.data[i];
L.length = k;
}
方法二:累计已删除数量
void DeleteXByCount(SqList &L, ElemType x) {
int removed = 0;
for (int i = 0; i < L.length; ++i) {
if (L.data[i] == x)
++removed;
else
L.data[i - removed] = L.data[i];
}
L.length -= removed;
}
- 两种方法都保持剩余元素的相对次序,时间 $O(n)$、空间 $O(1)$。
- 不要每发现一个
x就把全部后继前移,否则最坏时间会变成 $O(n^2)$。
1.6 代码题 6:删除值在闭区间 s 到 t 内的元素
bool DeleteRange(SqList &L, ElemType s, ElemType t) {
if (s > t) return false;
int k = 0;
for (int i = 0; i < L.length; ++i)
if (L.data[i] < s || L.data[i] > t)
L.data[k++] = L.data[i];
L.length = k;
return true;
}
- 保留条件是区间补集
value < s || value > t;把||错写成&&会导致没有元素能被保留。 s>t属于非法区间,本实现返回false且不修改顺序表。- 时间 $O(n)$、空间 $O(1)$。
1.7 代码题 7:有序表原地去重
void DeleteDuplicates(SqList &L) { // L 已非递减有序
if (L.length <= 1) return;
int k = 0; // 最后一个已保留元素的下标
for (int i = 1; i < L.length; ++i)
if (L.data[i] != L.data[k])
L.data[++k] = L.data[i];
L.length = k + 1;
}
k指向最后一个已保留元素,而不是下一写入位置,所以最终长度是k+1。- 有序性保证相同元素连续;无序表不能直接使用本算法。
- 时间 $O(n)$、空间 $O(1)$,且保留每个值第一次出现的记录。
1.8 代码题 8:合并两个递增顺序表
bool Merge(const SqList &A, const SqList &B, SqList &C) {
if (A.length + B.length > MAXSIZE) return false;
int i = 0, j = 0, k = 0;
while (i < A.length && j < B.length) {
if (A.data[i] <= B.data[j])
C.data[k++] = A.data[i++];
else
C.data[k++] = B.data[j++];
}
while (i < A.length) C.data[k++] = A.data[i++];
while (j < B.length) C.data[k++] = B.data[j++];
C.length = k;
return true;
}
A、B必须已按同一规则有序,且输出表C不能与输入表共用同一存储区。- 相等时先取
A中记录可保持两输入序列之间的稳定次序。 - 一方耗尽后,另一方剩余元素直接复制;时间 $O(m+n)$,输出空间 $O(m+n)$。
- 原 PDF 的返回类型、变量名和
l.length均不一致,且缺少容量检查;上面已统一。
1.9 代码题 9:两个递增序列合并后的中位数
本题必须先确认“中位数”的口径。以下按总长度 $N=m+n$ 的下中位数处理,其 0 下标为:
$$ target=\left\lfloor\frac{N-1}{2}\right\rfloor $$
方法一:完整合并
bool LowerMedianByMerge(const SqList &A, const SqList &B,
ElemType &median) {
SqList C;
if (!Merge(A, B, C) || C.length == 0) return false;
median = C.data[(C.length - 1) / 2];
return true;
}
方法二:只归并到目标位置
bool LowerMedian(const SqList &A, const SqList &B,
ElemType &median) {
const int total = A.length + B.length;
if (total == 0) return false;
const int target = (total - 1) / 2;
int i = 0, j = 0;
for (int k = 0; k <= target; ++k) {
if (j >= B.length ||
(i < A.length && A.data[i] <= B.data[j]))
median = A.data[i++];
else
median = B.data[j++];
}
return true;
}
- 方法一时间 $O(m+n)$、另需 $O(m+n)$ 输出空间;方法二最坏时间 $O(m+n)$、额外空间 $O(1)$。
- 原 PDF 的
GetMid返回了中位数下标,不是该下标处的元素,属于实质性错误。 - 若题目定义上中位数或偶数长度取两中间值平均数,应相应改变目标下标和返回类型。
1.10 代码题 10:最小未出现正整数
关键结论
长度为 $n$ 的数组中,最小未出现正整数一定属于 $[1,n+1]$。大于 $n$ 的值、0 和负数都不会影响答案。
int FirstMissingPositive(const int A[], int n) {
bool *seen = new bool[n + 1]{}; // 只使用 seen[1..n]
for (int i = 0; i < n; ++i)
if (A[i] >= 1 && A[i] <= n)
seen[A[i]] = true;
int answer = 1;
while (answer <= n && seen[answer]) ++answer;
delete[] seen;
return answer;
}
- 时间 $O(n)$、空间 $O(n)$;若
1..n全出现,循环自然返回n+1。 - 记录数组必须零初始化,映射前必须检查
1<=A[i]<=n,否则会越界。 - 原 PDF 在寻找答案时扫描了原数组
a而非标记数组record,还混用了minNumber、maxNumber与大小写不同的释放变量;上面已全部修正。
1.11 代码题 11:寻找主元素
题目
若某元素出现次数严格大于 $n/2$,称为主元素。找到它;不存在则报告失败。
最优空间:Boyer-Moore 投票并复核
bool MajorityElement(const int A[], int n, int &majority) {
if (n <= 0) return false;
int candidate = A[0];
int votes = 1;
for (int i = 1; i < n; ++i) {
if (votes == 0) {
candidate = A[i];
votes = 1;
} else if (A[i] == candidate) {
++votes;
} else {
--votes;
}
}
int count = 0;
for (int i = 0; i < n; ++i)
if (A[i] == candidate) ++count;
if (count <= n / 2) return false;
majority = candidate;
return true;
}
- 两个不同元素可以成对抵消;若主元素存在,抵消后它必是候选者。
- “候选者”不等于“必然是主元素”,所以第二趟必须统计并验证
count>n/2。 - 时间 $O(n)$、空间 $O(1)$。
- 原 PDF 利用
0<a_i<n建立计数数组也能做到 $O(n)$ 时间,但需要 $O(n)$ 空间,且依赖值域前提;投票法不依赖值域。
1.12 原 PDF 代码错误清单
| 题号 | 原代码主要问题 | 修正原则 |
|---|---|---|
| 1 | index 未定义、查找条件方向错误、无满表检查 |
明确插入位置并从尾部移动 |
| 2 | 空表直接访问 data[0] |
先判空,再返回删除值 |
| 3 | 语句缺分号、交换语句位置被 OCR/排版打乱 | 以 i<j 为循环条件原地交换 |
| 4 | 把后段长度 n 误作结束下标 |
第二段为 [m,m+n-1] |
| 5~7 | 类型名拼写不一、写指针语义不统一 | 明确 k 是下一写位或最后保留位 |
| 8 | 返回类型、变量名、最终长度对象错误 | 三指针合并并检查总容量 |
| 9 | 返回中位数下标而非中位数 | 扫描到目标位置后返回元素值 |
| 10 | 扫描错数组、变量名混乱、释放对象大小写错误 | 扫描 seen 并统一变量 |
| 11 | 只给计数数组方案 | 投票得到候选者后必须复核 |
相关知识
- 科目导航:数据结构导航
- 阅读顺序:上一篇:重点算法汇总 · 下一篇:链表代码
- 关联专题:顺序表原理
1.13 本节复盘
- 插入:先检查容量,找位置后必须从后向前搬移。
- 压缩删除:
i读、k写;去重时先判断k表示“下一写入位置”还是“最后保留位置”。 - 交换相邻块:三个闭区间依次逆置,长度不能误当下标。
- 有序表合并:三指针、复制剩余段、相等时固定取值侧,并检查输出容量。
- 中位数函数必须返回元素而不是下标;题目未说明时要先明确下中位数、上中位数或平均值口径。
- 标记数组题先证明答案范围再映射;主元素题必须验证候选者。