数据结构之线性表(1)

单项选择题

01. 线性表是具有n个( )的有限序列。

  • A. 数据表
  • B. 字符
  • C. 数据元素
  • D. 数据项

答案:C

解析:
线性表是由具有相同数据类型的有限数据元素组成的。每个数据元素是线性表的基本单位,而数据项是组成数据元素的部分,因此正确答案为 数据元素


02. 以下( )是一个线性表。

  • A. 由n个实数组成的集合
  • B. 由100个字符组成的序列
  • C. 所有整数组成的序列
  • D. 邻接表

答案:B

解析:
线性表要求是相同数据类型的有限序列。选项 A 是集合,集合中的元素没有前后关系,选项 C 的元素个数是无穷个,不符合线性表的有限性,选项 D 属于邻接表,是一种存储结构,不是线性表。因此,选项 B 符合线性表的定义,即由 100 个字符组成的有限序列。


03. 在线性表中,除开始元素外,每个元素( )

  • A. 只有唯一的前驱元素
  • B. 只有唯一的后继元素
  • C. 有多个前驱元素
  • D. 有多个后继元素

答案:A

解析:
线性表中的元素是按线性顺序排列的。除第一个元素外,每个元素都只有一个前驱元素;同样地,除最后一个元素外,每个元素都只有一个后继元素。因此,正确答案是 只有唯一的前驱元素


04. 下述 ( ) 是顺序存储结构的优点。

  • A. 存储密度大
  • B. 插入运算方便
  • C. 删除运算方便
  • D. 方便地运用于各种逻辑结构的存储表示

答案:A

解析:
顺序表没有指针域,因此每个元素只占用其数据空间,这使得存储密度较大。插入和删除操作在顺序存储中通常需要移动大量元素,因此并不方便。顺序存储结构不适用于树、图等复杂逻辑结构,因此选项 D 也是错误的。


05. 线性表的顺序存储结构是一种 ( )。

  • A. 随机存取的存储结构
  • B. 顺序存取的存储结构
  • C. 索引存取的存储结构
  • D. 散列存取的存储结构

答案:A

解析:
顺序表的存取方式是随机存取,因为通过起始地址和元素序号可以直接访问任意元素。选项 B 是错误的,因为它是链表的特点。


06. 一个顺序表所占用的存储空间大小与 ( ) 无关。

  • A. 表的长度
  • B. 元素的存放顺序
  • C. 元素的类型
  • D. 元素各字段的类型

答案:B

解析:
顺序表所占的存储空间=表长*sizeof(元素的类型),元素的类型显然会影响存储空间的大小。对于同一元素类型的顺序表,表越长,所占存储空间就越大。


07. 若线性表最常用的操作是存取第 i 个元素及其前驱和后继元素的值,为了提高效率,应采用 ( ) 的存储方式。

  • A. 单链表
  • B. 双向链表
  • C. 单循环链表
  • D. 顺序表

答案:D

解析:
顺序表支持随机存取,可以直接通过下标访问元素,时间复杂度为 O(1),而链表结构的存取需要顺序查找,时间复杂度为 O(n)。因此,顺序表更适合快速存取指定元素及其前驱和后继元素。


08. 一个线性表最常用的操作是存取任一指定序号的元素并在最后进行插入、删除操作,则利用 ( ) 存储方式可以节省时间。

  • A. 顺序表
  • B. 单链表
  • C. 双链表
  • D. 带头结点的双循环链表

答案:A

解析:
顺序表支持按序号随机存取,时间复杂度为 O(1),且在最后进行插入和删除时不需要移动大量元素。因此顺序表在这种场景下可以节省时间。


09. 在 $ n $ 个元素的线性表的数组表示中,时间复杂度为 $ O(1) $ 的操作是()

  • I. 访问第 $ i $ 个结点 (\(1 \leq i \leq n\)) 和求第 $ i $ (\(2 \leq i \leq n\)) 个结点的直接前驱

  • II. 在最后一个结点后插入一个新的结点

  • III. 删除第 1 个结点

  • IV. 在第 $ i $ (\(1 \leq i \leq n\)) 个结点后插入一个结点

  • A. I、II

  • B. I、II、III

  • C. I、II、IV

  • D. I、II、III、IV

答案:A

解析:

  • I. 访问任意第 $ i $ 个元素和求其前驱可以通过数组下标直接实现,时间复杂度为 $ O(1) $。
  • II. 在最后一个结点后插入新结点时,不需要移动其他元素,因此时间复杂度为 $ O(1) $。
  • III. 删除第 1 个结点需要移动后续的元素,时间复杂度为 $ O(n) $。
  • IV. 在第 $ i $ 个结点后插入一个结点时,需要移动后续元素,时间复杂度为 $ O(n) $。

因此,I 和 II 操作的时间复杂度为 $ O(1) $,而 III 和 IV 的时间复杂度为 $ O(n) $。


10. 设线性表有 $ n $ 个元素,严格说来,以下操作中,() 在顺序表上实现要比在链表上实现的效率高。

  • I. 输出第 $ i $ (\(1 \leq i \leq n\)) 个元素值

  • II. 交换第 3 个元素与第 4 个元素的值

  • III. 顺序输出这 $ n $ 个元素的值

  • A. I

  • B. I、II

  • C. I、II、III

  • D. II、III

答案:B

解析:

  • I. 在顺序表中,访问任意第 $ i $ 个元素只需通过下标访问,时间复杂度为 $ O(1) $,而链表需要从头查找,时间复杂度为 $ O(n) $。
  • II. 在顺序表中,交换两个元素只需简单的值交换操作,时间复杂度为 $ O(1) $,而链表需要找到两个元素的前驱后进行交换,时间复杂度更高。
  • III. 顺序输出 $ n $ 个元素在顺序表和链表中的时间复杂度都是 $ O(n) $,没有明显差别。

因此,选项 B 正确。

11. 在一个长度为 $ n $ 的顺序表中删除第 $ i $ (\(1 \leq i \leq n\)) 个元素时,需向前移动()个元素。

  • A. $ n $
  • B. $ i-1 $
  • C. $ n-i $
  • D. $ n-i+1 $

答案:C

解析:
删除第 $ i $ 个元素后,顺序表中的元素从第 $ i+1 $ 到第 $ n $ 个元素都需要向前移动一位,因此一共需要移动 $ n-i $ 个元素。


12. 对于顺序表,访问第 $ i $ 个位置的元素和在第 $ i $ 个位置插入一个元素的时间复杂度为()

  • A. $ O(n) \(,\) O(n) $
  • B. $ O(n) \(,\) O(1) $
  • C. $ O(1) \(,\) O(n) $
  • D. $ O(1) \(,\) O(1) $

答案:C

解析:

  • 访问第 $ i $ 个元素: 在顺序表中,可以通过数组下标直接访问,时间复杂度为 $ O(1) $。
  • 插入第 $ i $ 个元素: 需要将 $ i $ 之后的元素全部向后移动,时间复杂度为 $ O(n) $。

因此,访问的时间复杂度是 $ O(1) $,插入的时间复杂度是 $ O(n) $。


13. 若长度为 $ n $ 的非空线性表采用顺序存储结构,在表的第 $ i $ 个位置插入一个数据元素,则 $ i $ 的合法值应该是()。

  • A. $ 1 i n $
  • B. $ 1 i n+1 $
  • C. $ 0 i n-1 $
  • D. $ 0 i n $

答案:B

解析:
插入元素的位置可以是从第 1 个位置到第 $ n+1 $ 个位置,$ n+1 $ 表示在表尾追加元素。因此,合法的 $ i $ 值范围是 $ 1 i n+1 $。


14. 顺序表的插入算法中,当 $ n $ 个空间已满时,可再申请增加分配 $ m $ 个空间,若申请失败,则说明系统没有()可分配的存储空间。

  • A. $ m $ 个
  • B. $ m $ 个连续
  • C. $ n+m $ 个
  • D. $ n+m $ 个连续

答案:D

解析:
顺序表存储要求申请的空间必须是连续的。如果 $ n $ 个元素的空间已经用完,当我们想扩展 $ m $ 个空间时,系统必须能分配 $ n+m $ 个连续的存储空间。如果申请失败,说明系统无法提供 $ n+m $ 个连续的空间。

综合应用题

01.从顺序表中删除具有最小值的元素(假设唯一)并由函数返回被删除元素的值。空出的位置由最后一个元素填补,若顺序表为空,则显示出错信息并退出运行。

解答:

算法思想:

  1. 搜索整个顺序表,找到最小值元素,并记住其位置。
  2. 搜索结束后,将最后一个元素填补到空出的最小值元素的位置。
  3. 返回被删除的最小值元素。如果顺序表为空,输出错误信息并退出。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bool DelMin(sqList &L, ElemType &value) {
// 删除顺序表中最小值元素节点,并通过引用参数 value 返回其值
// 若删除成功,则返回 true,否则返回 false
if (L.length == 0) {
// 顺序表为空,无法删除元素,返回 false
return false;
}

value = L.data[0]; // 假定 0 号元素是最小值
int pos = 0; // 记录最小值元素的位置

// 遍历整个顺序表,找到最小值
for (int i = 1; i < L.length; i++) {
if (L.data[i] < value) {
value = L.data[i]; // 更新最小值
pos = i; // 更新最小值的位置
}
}

// 将最后一个元素移到最小值元素的位置
L.data[pos] = L.data[L.length - 1];

// 顺序表长度减 1
L.length--;

return true;
}

代码说明:

  1. 初始化: 假设第 0 号元素为最小值,并记录其位置为 pos = 0
  2. 遍历顺序表: 从第 1 个元素开始,逐个比较,更新 value 为最小值,pos 记录最小值的索引。
  3. 填补空缺: 找到最小值后,用最后一个元素填补空缺的位置。
  4. 减少顺序表长度: 删除元素后,顺序表长度减 1。
  5. 返回值: 通过引用参数 value 返回被删除的最小值,并返回操作成功的布尔值。

时间复杂度:

  • 搜索最小值需要遍历整个顺序表,因此时间复杂度为 $ O(n) $,其中 $ n $ 为顺序表的长度。

出错处理:

  • 当顺序表为空时,函数直接返回 false,表示无法执行删除操作。

02. 设计一个高效算法,将顺序表的所有元素逆置,要求算法的空间复杂度为 $ O(1) $

解答:

算法思想:

  1. 使用双指针方法,分别指向顺序表的首部和尾部元素。
  2. 依次交换这两个指针所指向的元素,并将指针向中间移动,直到指针相遇或越过为止。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
void Reverse(Sqlist &L) {
// 定义临时变量用于交换元素
ElemType temp;
// 双指针法,i从头开始,j从尾开始
for (int i = 0; i < L.length / 2; i++) {
// 交换 L.data[i] 与 L.data[L.length - i - 1]
temp = L.data[i];
L.data[i] = L.data[L.length - i - 1];
L.data[L.length - i - 1] = temp;
}
}

代码说明:

  • i 指向顺序表的前半部分元素,L.length - i - 1 指向对应的后半部分元素。
  • 每次循环交换两者,直到遍历完前半部分的所有元素。

时间复杂度:

  • 每次交换两个元素,总共需要 $ n/2 $ 次交换操作,时间复杂度为 $ O(n) $。

空间复杂度:

  • 只使用一个临时变量 temp 进行交换操作,空间复杂度为 $ O(1) $。

03. 对长度为 $ n $ 的顺序表 $ L $,编写一个时间复杂度为 $ O(n) $、空间复杂度为 $ O(1) $ 的算法,该算法删除线性表中所有值为 $ x $ 的数据元素

解答:

解法一:

  • k 记录顺序表中不等于 $ x $ 的元素个数,遍历顺序表时将不等于 $ x $ 的元素移动到下标 $ k $ 的位置,并更新 $ k $ 值。
  • 最后将顺序表的长度更新为 $ k $。

代码实现:

1
2
3
4
5
6
7
8
9
10
void DelX1(Sqlist &L, ElemType x) {
int k = 0; // 记录不等于 x 的元素个数
for (int i = 0; i < L.length; i++) {
if (L.data[i] != x) {
L.data[k] = L.data[i]; // 将不等于 x 的元素移到下标 k 的位置
k++; // 记录不等于 x 的元素个数
}
}
L.length = k; // 更新顺序表的长度
}

时间复杂度:

  • 遍历顺序表一次,时间复杂度为 $ O(n) $。

空间复杂度:

  • 只使用一个变量 k,空间复杂度为 $ O(1) $。

解法二:

  • k 记录顺序表中等于 $ x $ 的元素个数,在遍历过程中统计出等于 $ x $ 的元素数,并将不等于 $ x $ 的元素前移 $ k $ 个位置。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
void DelX2(Sqlist &L, ElemType x) {
int k = 0; // 记录值等于 x 的元素个数
for (int i = 0; i < L.length; i++) {
if (L.data[i] == x) {
k++; // 统计等于 x 的元素个数
} else {
L.data[i - k] = L.data[i]; // 将不等于 x 的元素前移 k 个位置
}
}
L.length -= k; // 更新顺序表的长度
}

时间复杂度:

  • 需要遍历整个顺序表,时间复杂度为 $ O(n) $。

空间复杂度:

  • 只使用一个变量 k,空间复杂度为 $ O(1) $。

解法三(优化版):

  • 使用两个指针,i 从表头开始,j 从表尾开始。遇到最左端值为 $ x $ 的元素时,将最右端不等于 $ x $ 的元素移到左边。
  • 这种方法可以减少不必要的遍历,但会改变原顺序表中元素的相对位置。

时间复杂度:

  • 双指针从两端向中间移动,时间复杂度仍然为 $ O(n) $。

空间复杂度:

  • 不需要额外的存储空间,空间复杂度为 $ O(1) $。

04. 从有序顺序表中删除其值在给定值 $ s $ 与 $ t $ 之间的所有元素 (要求 $ s < t $),若 $ s $ 或 $ t $ 不合理或顺序表为空,则显示出错信息并退出运行。

解答:

算法思想:

  • 因为顺序表是有序的,因此所有需要删除的元素一定是连续的一段。
  • 首先找到第一个值大于等于 $ s $ 的元素位置,然后找到第一个值大于 $ t $ 的元素位置(即需要保留的元素)。
  • 删除这些元素,只需将位于值大于 $ t $ 的元素向前移动,覆盖删除段的位置。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
bool Del_s_t(SqList &L, ElemType s, ElemType t) {
if (s >= t || L.length == 0) {
// 输入不合法,或者顺序表为空
return false;
}

int i = 0, j = 0;

// 找到第一个值大于等于 s 的元素位置
for (i = 0; i < L.length && L.data[i] < s; i++);
if (i >= L.length) {
// 若所有元素都小于 s,无需删除
return false;
}

// 找到第一个值大于 t 的元素位置
for (j = i; j < L.length && L.data[j] <= t; j++);

// 前移元素,覆盖删除的元素段
for (; j < L.length; i++, j++) {
L.data[i] = L.data[j];
}

// 更新顺序表的长度
L.length = i;

return true;
}

代码说明:

  • 第一部分检查输入是否合法,如 $ s t $ 或顺序表为空,直接返回 false
  • 然后通过两个 for 循环找到需要删除的起始和终止位置,再将删除段后面的元素前移。
  • 最后更新顺序表的长度。

时间复杂度:

  • 由于是线性扫描表,时间复杂度为 $ O(n) $。

空间复杂度:

  • 不使用额外的存储空间,空间复杂度为 $ O(1) $。

05. 从顺序表中删除其值在给定值 $ s $ 与 $ t $ 之间(包含 $ s $ 和 $ t $)的所有元素,若 $ s $ 或 $ t $ 不合理或顺序表为空,则显示出错信息并退出运行。

解答:

算法思想:

  • 从头到尾扫描顺序表,用变量 $ k $ 记录在区间 $ [s, t] $ 内的元素个数(即需要删除的元素个数)。
  • 对于当前扫描的元素,若其值不在区间 $ [s, t] $ 内,则将其前移 $ k $ 个位置;否则,计数 $ k $ 增加。
  • 这样每个不在区间内的元素只需移动一次,因此该算法效率较高。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool Del_s_t(SqList &L, ElemType s, ElemType t) {
if (L.length == 0 || s >= t) {
// 输入不合法,或者顺序表为空
return false;
}

int k = 0; // 记录区间内需要删除的元素个数

for (int i = 0; i < L.length; i++) {
if (L.data[i] >= s && L.data[i] <= t) {
k++; // 统计在区间 [s, t] 内的元素个数
} else {
L.data[i - k] = L.data[i]; // 不在区间内的元素前移 k 个位置
}
}

// 更新顺序表长度
L.length -= k;

return true;
}

代码说明:

  • 首先检查输入是否合法,如 $ s t $ 或顺序表为空,直接返回 false
  • 用一个 for 循环扫描顺序表,如果当前元素不在 $ [s, t] $ 区间内,则将其前移 $ k $ 个位置(根据已删除的元素数量)。
  • 最后更新顺序表的长度。

时间复杂度:

  • 线性扫描,时间复杂度为 $ O(n) $。

空间复杂度:

  • 只用到了一个变量 $ k $,空间复杂度为 $ O(1) $。

对比与说明:

  • 04题 的算法专门针对有序顺序表,因为有序性可以简化删除操作,只需要找到开始和结束位置,然后统一删除一段元素。
  • 05题 的算法则适用于一般的顺序表,通过扫描并前移非区间内的元素来实现删除操作。

06. 从有序顺序表中删除所有重复的元素,使表中所有元素的值均不同。

解答:

算法思想:

  • 由于顺序表是有序的,相同值的元素必然是相邻的。通过顺序扫描顺序表,用两个指针来维护:
    • 一个指针 $ i $ 负责指向非重复的元素。
    • 另一个指针 $ j $ 负责顺序扫描表中的每一个元素。
  • 若发现当前元素与前面的元素不同,则将其加入到非重复的部分。重复的元素则被跳过。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool DeleteSame(SeqList &L) {
if (L.length == 0) {
// 若顺序表为空
return false;
}

int i = 0; // i 作为工作指针,指向非重复元素
for (int j = 1; j < L.length; j++) {
if (L.data[i] != L.data[j]) {
// 如果当前元素和上一个非重复元素不同,存入下一个位置
L.data[++i] = L.data[j];
}
}

// 更新顺序表的长度
L.length = i + 1;

return true;
}

代码说明:

  • 首先检查顺序表是否为空,若为空返回 false
  • 使用两个指针 $ i $ 和 $ j $,其中 $ i $ 用于存放非重复元素,$ j $ 用于顺序扫描整个表。
  • 当 $ j $ 指向的元素与 $ i $ 指向的不同,则将其前移至非重复部分。最终调整顺序表的长度。

时间复杂度:

  • 由于只需扫描整个顺序表一次,时间复杂度为 $ O(n) $。

空间复杂度:

  • 只使用了固定数量的额外变量,空间复杂度为 $ O(1) $。

07. 将两个有序顺序表合并为一个新的有序顺序表,并由函数返回结果顺序表。

解答:

算法思想:

  • 由于两个顺序表都是有序的,因此可以使用归并的思想来合并:
    • 使用两个指针分别指向两个表的当前元素,比较它们的值,小的元素先加入结果顺序表。
    • 当其中一个表的元素全部插入结果顺序表后,将另一个表剩余的部分直接添加到结果顺序表的末尾。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bool Merge(SeqList &A, SeqList &B, SeqList &C) {
// 检查结果顺序表是否有足够的空间
if (A.length + B.length > C.maxSize) {
return false;
}

int i = 0, j = 0, k = 0;

// 归并过程:依次比较 A 和 B 中的元素,较小的放入 C 中
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++];
}
}

// 如果 A 中还有剩余元素,全部加入 C 中
while (i < A.length) {
C.data[k++] = A.data[i++];
}

// 如果 B 中还有剩余元素,全部加入 C 中
while (j < B.length) {
C.data[k++] = B.data[j++];
}

// 更新结果顺序表的长度
C.length = k;

return true;
}

代码说明:

  • 首先检查合并后的顺序表是否有足够的空间存放所有元素,若空间不足则返回 false
  • 使用两个指针 $ i $ 和 $ j $ 分别指向两个顺序表的当前元素,通过比较大小,较小的元素先加入结果顺序表。
  • 当其中一个顺序表的元素全部合并完后,直接将另一个顺序表的剩余元素追加到结果顺序表末尾。
  • 最后更新结果顺序表的长度。

时间复杂度:

  • 每次从两个顺序表中选择一个最小元素加入结果表,时间复杂度为 $ O(n) $,其中 $ n $ 是两个顺序表的总长度。

空间复杂度:

  • 使用了一个结果顺序表来存储合并结果,空间复杂度为 $ O(n) $。

08. 将一维数组中两个顺序表的位置互换

解答:

算法思想:

  • 一维数组 \(A[m+n]\) 中依次存放两个线性表 $ (a_1, a_2, ..., a_m) $ 和 $ (b_1, b_2, ..., b_n) $。
  • 要将两个顺序表的位置互换,即 $ (b_1, b_2, ..., b_n) $ 置于前面,$ (a_1, a_2, ..., a_m) $ 置于后面。
  • 使用逆置算法来实现:
    • 先将整个数组逆置。
    • 然后分别逆置前 \(n\) 个元素和后 \(m\) 个元素,即可完成两个顺序表的互换。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef int DataType;

// 逆置函数,将数组 A 的 [left, right] 区间元素逆置
void Reverse(DataType A[], int left, int right) {
while (left < right) {
DataType temp = A[left];
A[left] = A[right];
A[right] = temp;
left++;
right--;
}
}

// 交换两个顺序表的位置
void Exchange(DataType A[], int m, int n) {
// 逆置整个数组
Reverse(A, 0, m + n - 1);

// 逆置前 n 个元素
Reverse(A, 0, n - 1);

// 逆置后 m 个元素
Reverse(A, n, m + n - 1);
}

代码说明:

  1. Reverse 函数用于将数组中指定区间的元素逆置。
  2. Exchange 函数先将整个数组逆置,然后再分别对前 $ n $ 个和后 $ m $ 个元素进行逆置,从而实现位置互换。

执行过程:

  • 逆置整个数组:
    $ (a_1, a_2, ..., a_m, b_1, b_2, ..., b_n) (b_n, b_{n-1}, ..., b_1, a_m, a_{m-1}, ..., a_1) $
  • 逆置前 $ n $ 个元素:
    $ (b_n, b_{n-1}, ..., b_1) (b_1, b_2, ..., b_n) $
  • 逆置后 $ m $ 个元素:
    $ (a_m, a_{m-1}, ..., a_1) (a_1, a_2, ..., a_m) $

最终得到结果:
$ (b_1, b_2, ..., b_n, a_1, a_2, ..., a_m) $


09. 在递增有序的线性表中查找数值并进行相应操作

解答:

算法思想:

  • 对于递增有序的线性表,最优查找方法是 折半查找(二分查找)。
  • 若找到目标值 $ x $,则与其后继元素交换位置。
  • 若未找到,则将 $ x $ 插入适当位置,保持顺序表的有序性。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void SearchExchangeInsert(int A[], int &n, int x) {
int low = 0, high = n - 1, mid;

// 折半查找
while (low <= high) {
mid = (low + high) / 2;
if (A[mid] == x) {
break;
} else if (A[mid] < x) {
low = mid + 1;
} else {
high = mid - 1;
}
}

// 如果找到了 x,并且不是最后一个元素
if (A[mid] == x && mid != n - 1) {
// 交换 x 和其后继元素
int temp = A[mid];
A[mid] = A[mid + 1];
A[mid + 1] = temp;
}

// 如果没有找到 x
if (low > high) {
// 将 x 插入到有序表中
for (int i = n - 1; i > high; i--) {
A[i + 1] = A[i]; // 后移元素
}
A[high + 1] = x; // 插入 x
n++; // 表长加 1
}
}

代码说明:

  1. 折半查找:通过比较中间值与 $ x $ 的大小,逐步缩小查找区间。
  2. 若找到 $ x $,并且 $ x $ 不是最后一个元素,则将 $ x $ 与后继元素交换。
  3. 若未找到 $ x $,则将 $ x $ 插入到合适位置,保证顺序表有序。

执行过程:

  • 查找过程
    • 通过折半查找,快速找到目标元素 $ x $ 的位置。
  • 交换操作
    • 如果找到 $ x $ 且 $ x $ 不是最后一个元素,则将其与后继元素交换。
  • 插入操作
    • 如果未找到 $ x $,则找到 $ x $ 应该插入的位置,后移表中的元素,将 $ x $ 插入,并更新顺序表的长度。

时间复杂度:

  • 折半查找 的时间复杂度为 $ O(n) $。
  • 交换或插入操作 的时间复杂度为 $ O(n) $,因为插入时需要移动元素。

优化建议:

  • 在未找到 $ x $ 时,使用二分查找的结果 $ high $ 来确定插入位置,从而避免顺序查找插入点。

10. 【2010统考真题】

问题描述
给定一个包含 \(n\) 个整数的数组 \(R\)\(n > 1\)),设计一个在时间和空间方面都尽可能高效的算法,将数组 \(R\) 循环左移 \(p\) 个位置(其中 \(0 < p < n\))。即将数组 \(R\) 中的数据由 \((X_0, X_1, X_2, ..., X_{n-1})\) 转变为 \((X_p, X_{p+1}, ..., X_{n-1}, X_0, X_1, ..., X_{p-1})\)

要求:

  1. 给出算法的基本设计思想。
  2. 根据设计思想,采用 C 或 C++ 或 Java 语言描述算法,关键之处需给出注释。
  3. 说明你设计的算法的时间复杂度和空间复杂度。

1) 算法的基本设计思想:

将数组视为两部分:前 \(p\) 个元素为 \(a\),剩余的 \(n - p\) 个元素为 \(b\)
问题可以转化为:

  • 将数组 \(a\)\(b\) 互换位置,即将 \(R = a + b\) 转换为 \(R = b + a\)

具体步骤如下:

  1. 先将前 \(p\) 个元素 \(a\) 逆置,得到 \(a'\)
  2. 再将剩下的 \(n - p\) 个元素 \(b\) 逆置,得到 \(b'\)
  3. 最后将整个数组逆置,得到 \(b + a\)

例如: 原数组为 \(R = [a_1, a_2, ..., a_p, b_1, b_2, ..., b_{n-p}]\)
循环左移 \(p\) 个位置后的数组变为 \(R = [b_1, b_2, ..., b_{n-p}, a_1, a_2, ..., a_p]\)

2) 算法实现(C 语言):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>

// 逆置数组 R 的 from 到 to 区间的元素
void Reverse(int R[], int from, int to) {
int temp;
for (int i = 0; i < (to - from + 1) / 2; i++) {
temp = R[from + i];
R[from + i] = R[to - i];
R[to - i] = temp;
}
}

// 将数组 R 的元素左移 p 个位置
void Converse(int R[], int n, int p) {
// 逆置前 p 个元素
Reverse(R, 0, p - 1);

// 逆置剩下的 n - p 个元素
Reverse(R, p, n - 1);

// 逆置整个数组
Reverse(R, 0, n - 1);
}

int main() {
int R[] = {1, 2, 3, 4, 5, 6, 7};
int n = 7; // 数组长度
int p = 3; // 左移的位数

Converse(R, n, p);

// 打印结果
for (int i = 0; i < n; i++) {
printf("%d ", R[i]);
}

return 0;
}

3) 时间复杂度和空间复杂度:

  • 时间复杂度
    • 逆置操作的时间复杂度为 \(O(n)\),因为每次逆置都需要线性时间扫描数组的某个部分。
    • 整个算法执行三次逆置操作,总的时间复杂度为 \(O(n)\)
  • 空间复杂度
    • 该算法只使用了常数级别的额外空间 \(temp\),因此空间复杂度为 \(O(1)\)

11. 【2011统考真题】

问题描述
给定两个等长的升序序列 \(A\)\(B\),设计一个在时间和空间方面都尽可能高效的算法,找出这两个序列的中位数。中位数的定义为:在一个升序序列中,位于 \(\left[\frac{L}{2}\right]\) 位置的数;对于两个序列 \(A\)\(B\),它们的中位数是将两个序列的所有元素合并后得到的升序序列的中位数。

要求:

  1. 给出算法的基本设计思想。
  2. 根据设计思想,采用 C 或 C++ 或 Java 语言描述算法,关键之处需给出注释。
  3. 说明你设计的算法的时间复杂度和空间复杂度。

1) 算法的基本设计思想:

  1. 基本步骤
    • 首先,计算两个升序序列 \(A\)\(B\) 的中位数,设为 \(a\)\(b\)
    • 比较 \(a\)\(b\)
      • 如果 \(a = b\),则 \(a\)(或 \(b\))即为所求的中位数,算法结束。
      • 如果 \(a < b\),则丢弃序列 \(A\) 中较小的一半,同时丢弃序列 \(B\) 中较大的一半,保持剩下的两个序列长度相等。
      • 如果 \(a > b\),则丢弃序列 \(A\) 中较大的一半,同时丢弃序列 \(B\) 中较小的一半,同样保持剩下的两个序列长度相等。
  2. 迭代过程
    • 重复上述步骤,直到两个序列中均只含一个元素,此时较小者即为所求的中位数。

2) 算法实现(Java 语言):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class MedianFinder {

// 查找两个升序数组的中位数
public double findMedianSortedArrays(int[] A, int[] B) {
int n = A.length; // 假设 A 和 B 的长度相等
int leftA = 0, rightA = n - 1;
int leftB = 0, rightB = n - 1;

while (leftA < rightA) {
// 计算 A 和 B 的中位数索引
int midA = (leftA + rightA) / 2;
int midB = (leftB + rightB) / 2;

// 中位数值
int aMid = A[midA];
int bMid = B[midB];

// 情况 1: 中位数相等
if (aMid == bMid) {
return aMid; // 或 bMid
}

// 情况 2: aMid < bMid,丢弃 A 的左半部分和 B 的右半部分
if (aMid < bMid) {
leftA = midA + 1; // 更新 A 的左边界
rightB = midB; // 更新 B 的右边界
}
// 情况 3: aMid > bMid,丢弃 A 的右半部分和 B 的左半部分
else {
rightA = midA; // 更新 A 的右边界
leftB = midB + 1; // 更新 B 的左边界
}
}

// 返回中位数,较小的一个
return Math.min(A[leftA], B[leftB]);
}

public static void main(String[] args) {
MedianFinder finder = new MedianFinder();
int[] A = {1, 3, 8};
int[] B = {7, 9, 10, 11};

double median = finder.findMedianSortedArrays(A, B);
System.out.println("中位数是: " + median); // 输出中位数
}
}

3) 算法的时间复杂度和空间复杂度:

  • 时间复杂度
    • 每次迭代操作都将一个数组的搜索范围缩小一半,因此时间复杂度为 \(O(\log n)\)
  • 空间复杂度
    • 该算法只使用了常数级别的额外空间(如几个变量),因此空间复杂度为 \(O(1)\)

12. 【2013 统考真题】

问题描述
已知一个整数序列 \(A=(a_0, a_1, \ldots, a_{n-1})\),若存在元素 \(a_{p1}=a_{p2}=\ldots=a_{pm}=x\)\(m > \frac{n}{2}\)\(0 \leq p_k < n\)\(1 \leq k \leq m\)),则称 \(x\)\(A\) 的主元素。例如,对于 \(A=(0, 5, 5, 3, 5, 7, 5, 5)\),5 为主元素;而对于 \(A=(0, 5, 5, 3, 5, 1, 5, 7)\),则没有主元素。

要求:

  1. 给出算法的基本设计思想。
  2. 根据设计思想,采用 C 或 C++ 或 Java 语言描述算法,关键之处需给出注释。
  3. 说明你设计的算法的时间复杂度和空间复杂度。

1) 算法的基本设计思想:

算法的策略是使用摩尔投票算法(Boyer-Moore Voting Algorithm)来找到主元素。主要包括以下步骤:

  1. 选取候选的主元素
    • 遍历数组中的每个整数,将第一个遇到的整数保存为候选主元素 Num,并初始化计数为 1。
    • 如果遇到的下一个整数等于 Num,计数加 1;如果不等于,计数减 1。
    • 当计数减到 0 时,更新候选主元素为当前元素,并重置计数为 1。
  2. 确认候选主元素
    • 重新遍历数组,统计候选主元素出现的次数。
    • 如果出现次数大于 \(n/2\),则返回该元素作为主元素;否则,返回 -1。

2) 算法实现(Java 语言):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MajorityElement {

// 查找主元素
public int findMajorityElement(int[] A) {
int count = 0; // 计数器
int candidate = 0; // 候选主元素

// 选取候选主元素
for (int num : A) {
if (count == 0) {
candidate = num; // 更新候选主元素
count = 1; // 重置计数
} else if (num == candidate) {
count++; // 计数加1
} else {
count--; // 计数减1
}
}

// 确认候选主元素
count = 0; // 重新初始化计数
for (int num : A) {
if (num == candidate) {
count++; // 统计候选主元素的出现次数
}
}

// 返回主元素或-1
return (count > A.length / 2) ? candidate : -1;
}

public static void main(String[] args) {
MajorityElement majorityElement = new MajorityElement();
int[] A = {0, 5, 5, 3, 5, 7, 5, 5}; // 示例数组
int result = majorityElement.findMajorityElement(A);
System.out.println("主元素是: " + result); // 输出主元素
}
}

3) 算法的时间复杂度和空间复杂度:

  • 时间复杂度
    • 遍历数组两次:第一次用于找出候选主元素,第二次用于确认候选主元素的出现次数,因此时间复杂度为 \(O(n)\)
  • 空间复杂度
    • 只使用了常数级别的额外空间(如几个变量),因此空间复杂度为 \(O(1)\)

13. 【2018 统考真题】

问题描述
给定一个含有 \(n\)\(n > 1\))个整数的数组,请设计一个在时间上尽可能高效的算法,找出数组中未出现的最小正整数。例如,在数组 \(1, 2, 3, 3\) 中,未出现的最小正整数是 4;而在数组 \(1, 2, 3\) 中,未出现的最小正整数是 4。

要求:

  1. 给出算法的基本设计思想。
  2. 根据设计思想,采用 C 或 C++ 语言描述算法,关键之处需给出注释。
  3. 说明你设计的算法的时间复杂度和空间复杂度。

1) 算法的基本设计思想:

为了实现高效的时间复杂度,采用空间换时间的策略。设计思路如下:

  • 标记数组:创建一个大小为 \(n\) 的布尔数组 \(B\),用于标记数组 \(A\) 中是否出现了正整数 1 到 \(n\)。其中 \(B[0]\) 对应正整数 1,\(B[n-1]\) 对应正整数 \(n\)
  • 初始化:将标记数组 \(B\) 中的所有元素初始化为 0。
  • 填充标记数组:遍历数组 \(A\),对于每个元素,如果它的值在 1 到 \(n\) 之间,则在标记数组 \(B\) 中标记相应位置为 1。对于小于等于 0 或大于 \(n\) 的值不做任何操作。
  • 查找最小未出现正整数:再次遍历标记数组 \(B\),找出第一个值为 0 的位置 \(i\),则返回 \(i + 1\) 作为结果。如果所有位置的值都为 1,说明数组 \(A\) 中的正整数 1 到 \(n\) 都出现过,此时返回 \(n + 1\)

2) 算法实现(C 语言):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int findMissMin(int A[], int n) {
// 标记数组
int *B = (int*)malloc(sizeof(int) * n); // 分配空间
memset(B, 0, sizeof(int) * n); // 赋初值为0

// 填充标记数组
for (int i = 0; i < n; i++) {
if (A[i] > 0 && A[i] <= n) {
B[A[i] - 1] = 1; // 标记 A[i] 的值出现
}
}

// 查找最小未出现的正整数
for (int i = 0; i < n; i++) {
if (B[i] == 0) {
free(B); // 释放内存
return i + 1; // 返回结果
}
}

free(B); // 释放内存
return n + 1; // 返回 n + 1
}

int main() {
int A[] = {1, 2, 3}; // 示例数组
int n = sizeof(A) / sizeof(A[0]);
int result = findMissMin(A, n);
printf("未出现的最小正整数是: %d\n", result); // 输出结果
return 0;
}

3) 算法的时间复杂度和空间复杂度:

  • 时间复杂度
    • 遍历数组 \(A\) 一次,标记数组 \(B\) 的填充也需要遍历一次,再次遍历 \(B\) 查找结果,因此时间复杂度为 \(O(n)\)
  • 空间复杂度
    • 额外分配了标记数组 \(B\) 的空间,大小为 \(n\),因此空间复杂度为 \(O(n)\)

总结:

该算法通过使用标记数组有效地找出了未出现的最小正整数,时间复杂度为 \(O(n)\),并且提供了清晰的实现代码,符合题目的要求。

14. 【2020 统考真题】

问题描述
定义三元组 \((a, b, c)\) 的距离 \(D = |a-b| + |b-c| + |c-a|\)\(a\)\(b\)\(c\)均为正数)。给定三个非空整数集合 \(S_1\)\(S_2\)\(S_3\),按升序分别存储在三个数组中。请设计一个尽可能高效的算法,计算并输出所有可能的三元组 \((a, b, c)\)\(a \in S_1\)\(b \in S_2\)\(c \in S_3\))中的最小距离。例如,当 $S_1 = \(-1, 0, 9\)\(,\)S_2 = \(-25, -10, 10, 11\)\(,\)S_3 = \(2, 9, 17, 30, 41\)$ 时,最小距离为 2,对应的三元组为 \((9, 10, 9)\)

解答

1) 算法的基本设计思想:

根据距离的定义,我们可以推导出以下几个要点:

  • 距离为非负\(D = |a-b| + |b-c| + |c-a| \geq 0\),当 \(a = b = c\) 时,距离最小。
  • 简化问题:不失一般性,假设 \(a \leq b \leq c\)。通过分析可以发现,距离 \(D\) 的关键在于 \(a\)\(c\) 之间的距离。因此,可以固定 \(c\),然后寻找一个 \(a\) 使得 \(L = |c - a|\) 最小。

算法步骤

  1. 初始化一个变量 \(D_{\text{min}}\) 用于记录最小距离,初值设为一个足够大的整数。
  2. 使用三个指针 \(i\)\(j\)\(k\) 分别遍历数组 \(A\)\(B\)\(C\),当 \(i < n\)\(j < m\)\(k < p\) 时循环执行以下步骤:
    • 计算当前三元组的距离 \(D\)
    • 如果 \(D < D_{\text{min}}\),更新 \(D_{\text{min}}\)
    • 根据 \(A[i]\)\(B[j]\)\(C[k]\) 的大小关系,调整指针,以期望找到更小的距离。

2) 算法实现(C 语言):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

// 计算绝对值
int abs(int a) {
return a < 0 ? -a : a;
}

// 查找最小的三元组距离
int findMinofTrip(int A[], int n, int B[], int m, int C[], int p) {
int i = 0, j = 0, k = 0;
int D_min = INT_MAX; // 用于记录最小距离
int D;

while (i < n && j < m && k < p) {
// 计算当前三元组的距离 D
D = abs(A[i] - B[j]) + abs(B[j] - C[k]) + abs(C[k] - A[i]);

// 更新最小距离
if (D < D_min) {
D_min = D;
}

// 根据当前元素的大小关系更新指针
if (A[i] < B[j]) {
i++; // 增加 a 的下标
} else if (B[j] < C[k]) {
j++; // 增加 b 的下标
} else {
k++; // 增加 c 的下标
}
}
return D_min; // 返回最小距离
}

int main() {
int S1[] = {-1, 0, 9};
int S2[] = {-25, -10, 10, 11};
int S3[] = {2, 9, 17, 30, 41};

int n = sizeof(S1) / sizeof(S1[0]);
int m = sizeof(S2) / sizeof(S2[0]);
int p = sizeof(S3) / sizeof(S3[0]);

int result = findMinofTrip(S1, n, S2, m, S3, p);
printf("最小距离为: %d\n", result); // 输出结果

return 0;
}

3) 算法的时间复杂度和空间复杂度:

  • 时间复杂度
    • 每个指针 \(i\)\(j\)\(k\) 最多各自遍历一次对应的数组,因此总体时间复杂度为 \(O(n + m + p)\),这里 \(n\)\(m\)\(p\) 分别是三个集合的大小。
  • 空间复杂度
    • 只使用了常数级别的额外空间(用于存储指针和距离变量),因此空间复杂度为 \(O(1)\)

总结:

该算法有效地找出了所有可能三元组的最小距离,利用了指针的方式实现了高效的查找,时间复杂度为 \(O(n + m + p)\),空间复杂度为 \(O(1)\),符合题目的要求。