一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。若输入数据所占空间只取决于问题本身,和算法无关,则只需分析除输入和程序之外的额外空间。例如,若算法中新建了几个与输入数据规模 n 相同的辅助数组,则空间复杂度为 O(n)。
算法原地工作是指算法所需的辅助空间为常量,即 O(1)。
1.2.3 本节试题精选
一、单项选择题
一个算法应该具有 ( ) 等重要特性。
A. 可维护性、可读性和可行性
B. 可行性、确定性和有穷性
C. 确定性、有穷性和可靠性
D. 可读性、正确性和可行性
01.B
一个算法应具有五个重要特性:有穷性、确定性、可行性、输入和输出。选项 A、C 和 D 中提到的特性(如可维护性、可读性、可靠性、正确性等)是很重要的,但它们并不是算法定义的重要特性,更多的是关于软件开发中的附加要求。
下列关于算法的说法中,正确的是 ( )。
A. 算法的时间效率取决于算法执行所花的 CPU 时间
B. 在算法设计中不允许用牺牲空间效率的方式来换取好的时间效率
C. 算法必须具备有穷性、确定性等五个特性
D. 通常用时间效率和空间效率来衡量算法的优劣
02.C
算法的时间效率是指算法的时间复杂度,即执行算法所需的计算工作量,选项 A 错误。算法设计会综合考虑时间效率和空间效率两个方面,若某些应用场景对时间效率要求很高,而对空间效率要求不高,则可用牺牲空间效率的方式来换取好的时间效率,选项 B 错误。评价一个算法的 "优劣" 不仅要考虑算法的时空效率,还要从正确性、可读性、健壮性等方面来综合评价。
某算法的时间复杂度为 O(n^2),则表示该算法的 ( )。
A. 问题规模是 n^2
B. 执行时间等于 n^2
C. 执行时间与 n^2 成正比
D. 问题规模与 n^2 成正比
03.C
时间复杂度为 O(n^2),说明算法的时间复杂度 T(n) 满足 T(n)≤cn^2(其中 c 为比例常数),即 T(n)=O(n^2),时间复杂度 T(n) 是问题规模 n 的函数,其问题规模仍然是 n 而不是 n^2。
通过递归树来分析,该递归树的高度近似为 O(2^n) (因为每一层的节点数近似是上一层的 2 倍 ),所以递归算法的时间复杂度为 O(2^n) 。这是因为随着 n 的增大,计算量呈指数级增长。
非递归算法时间复杂度分析
非递归算法实现斐波那契数列的代码(C++ 示例)如下:
cpp复制代码
int FibonacciIterative(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
int a = 0, b = 1, c;
for (int i = 2; i <= n; i++) {
c = b;
b = a + b;
a = c;
}
return b;
}
在这个非递归算法中,使用了一个 for 循环,循环次数为 n−1 次(从 i=2 到 i=n ) ,每次循环内执行的操作都是固定的几个赋值语句,时间复杂度为 O(1) 。根据算法时间复杂度的计算方法,当循环次数为 n ,每次循环内操作时间复杂度为常数时,整个算法的时间复杂度为 O(n) 。所以非递归算法计算斐波那契数列的时间复杂度为 O(n) 。
InitList(&L):初始化表。构造一个空的线性表。
Length(L):求表长。返回线性表 L 的长度,即 L 中数据元素的个数。
LocateElem(L, e):按值查找操作。在表 L 中查找具有给定关键字值的元素。
GetElem(L, i):按位查找操作。获取表 L 中第 i 个位置的元素的值。
ListInsert(&L, i, e):插入操作。在表 L 中的第 i 个位置上插入指定元素 e。
ListDelete(&L, i, &e):删除操作。删除表 L 中第 i 个位置的元素,并用 e 返回删除元素的值。
PrintList(L):输出操作。按前后顺序输出线性表 L 的所有元素值。
Empty(L):判空操作。若 L 为空表,则返回 true,否则返回 false。
DestroyList(&L):销毁操作。销毁线性表,并释放线性表 L 所占用的内存空间。
注意:
① 基本操作的实现取决于采用哪种存储结构,存储结构不同,算法的实现也不同。② 符号 "&" 表示 C++ 语言中的引用调用,在 C 语言中采用指针也可达到同样的效果。
2.1.3 本节试题精选
单项选择题
线性表是具有 n 个 ( ) 的有限序列。
A. 数据表
B. 字符
C. 数据元素
D. 数据项
C
线性表是由具有相同数据类型的有限数据元素组成的,数据元素是由数据项组成的。
下列几种描述中,( ) 是一个线性表。
A. 由 n 个实数组成的集合
B. 由 100 个字符组成的序列
C. 所有整数组成的序列
D. 邻接表
B
线性表定义的要求为:相同数据类型、有限序列。选项 C 的元素个数是无穷个,错误;选项 A 集合中的元素没有前后驱关系,错误;选项 D 属于一种存储结构,本题要求选出的是一个具体的线性表,不要将二者混为一谈。只有选项 B 符合线性表定义的要求。
对 n 个元素进行排序的时间复杂度最小也要 O(n)(初始有序时),通常为 O(nlog2n) 或 O(n2),通过第 8 章学习后会更理解。选项 B 和 D 显然错误。顺序表支持按序号的随机存取方式。
13.若长度为 n 的非空线性表采用顺序存储结构,在表的第 i 个位置插入一个数据元素,则 i 的合法值应该是 ( ) 。
A. 1≤i≤n
B. 1≤i≤n+1
C. 0≤i≤n−1
D. 0≤i≤n
13.B
线性表元素的序号是从 1 开始,而在第 n+1 个位置插入相当于在表尾追加。
14.顺序表的插入算法中,当 n 个空间已满时,可再申请增加分配 m 个空间,若申请失败,则说明系统没有 ( ) 可分配的存储空间。
A. m 个
B. m 个连续
C. n+m 个
D. n+m 个连续
14.D
顺序存储需要连续的存储空间,在申请时需申请 n+m 个连续的存储空间,然后将线性表原来的 n 个元素复制到新申请的 n+m 个连续的存储空间的前 n 个单元。
15.【2023 统考真题】在下列对顺序存储的有序表(长度为 n )实现给定操作的算法中,平均时间复杂度为 O(1) 的是 ( ) 。
A. 查找包含指定值元素的算法
B. 插入包含指定值元素的算法
C. 删除第 () 个元素的算法
D. 获取第 () 个元素的算法
15.D
对于顺序存储的有序表,查找指定值元素可以采用顺序查找法或折半查找法,平均时间复杂度最少为 O(log2n)。插入指定值元素需要先找到插入位置,然后将该位置及之后的元素依次后移一个位置,最后将指定值元素插入到该位置,平均时间复杂度为 O(n)。删除第 i 个元素需要将该元素之后的全部元素依次前移一个位置,平均时间复杂度为 O(n)。获取第 i 个元素只需直接根据下标读取对应的数组元素即可,时间复杂度为 O(1)。
为 (bn,bn−1,bn−2,⋯,b1,am,am−1,am−2,⋯,a1),然后对前 n 个元素和后 m 个元素分别使用逆置算法,即可得到 (b1,b2,b3,⋯,bn,a1,a2,a3,⋯,am),
从而实现顺序表的位置互换。
本题代码如下:
cpp复制代码
typedef int DataType;
void Reverse(DataType A[], int left, int right, int arraySize) {
//逆转(a[left],a[left+1],a[left+2],...,a[right])为(a[right],a[right-1],...,a[left])
if (left > right || right >= arraySize)
return;
int mid = (left + right) / 2;
for (int i = 0; i <= mid - left; i++) {
DataType temp = A[left + i];
A[left + i] = A[right - i];
A[right - i] = temp;
}
}
void Exchange(DataType A[], int m, int n, int arraySize) {
/*数组 A[m+n]中,从 0到m-1存放顺序表(a1,a2,a3,⋯,am),从m到m+n-1存放顺序表
(b1,b2,b3,⋯,bn),算法将这两个表的位置互换*/
Reverse(A, 0, m + n - 1, arraySize);
Reverse(A, 0, n - 1, arraySize);
Reverse(A, n, m + n - 1, arraySize);
}
08.线性表 (a1,a2,a3,⋯,an) 中的元素递增有序且按顺序存储于计算机内。要求设计一个算法,完成用最少时间在表中查找数值为 x 的元素,若找到,则将其与后继元素位置相交换,若找不到,则将其插入表中并使表中元素仍递增有序。
08.【解答】
算法思想:顺序存储的线性表递增有序,可以顺序查找,也可以折半查找。题目要求 "用最少的时间在表中查找数值为 x 的元素",这里应使用折半查找法。
本题代码如下:
cpp复制代码
void SearchExchangeInsert(ElemType A[], ElemType x) {
int low = 0, high = n - 1, mid; //low和high指向顺序表下界和上界的下标
while (low <= high) {
mid = (low + high) / 2; //找中间位置
if (A[mid] == x) break; //找到x,退出while循环
else if (A[mid] < x) low = mid + 1; //到中点mid的右半部去查
else high = mid - 1; //到中点mid的左半部去查
//下面两个if语句只会执行一个
}
if (A[mid] == x && mid != n - 1) { //若最后一个元素与x相等,则不存在与其后
//继交换的操作
ElemType t = A[mid]; A[mid] = A[mid + 1]; A[mid + 1] = t;
}
if (low > high) { //查找失败,插入数据元素x
for (int i = n - 1; i > high; i--) A[i + 1] = A[i]; //后移元素
A[high + 1] = x; //插入x
}
}
本题的算法也可写成三个函数:查找函数、交换后继函数与插入函数。写成三个函数的优点是逻辑清晰、易读。
09.给定三个序列 A、B、C,长度均为 n,且均为无重复元素的递增序列,请设计一个时间上尽可能高效的算法,逐行输出同时存在于这三个序列中的所有元素。例如,数组 A 为 {1,2,3},数组 B 为 {2,3,4},数组 C 为 {−1,0,2},则输出 2。要求:
void samekey(int A[], int B[], int C[], int n) {
int i = 0, j = 0, k = 0; //定义三个工作指针
while (i < n && j < n && k < n) { //相同则输出,并集体后移
if (A[i] == B[j] && B[j] == C[k]) {
printf("%d\n", A[i]);
i++; j++; k++;
}
else {
int maxNum = max(A[i], max(B[j], C[k]));
if (A[i] < maxNum) i++;
if (B[j] < maxNum) j++;
if (C[k] < maxNum) k++;
}
}
}
3)每个指针移动的次数不超过 n 次,且每次循环至少有一个指针后移,所以时间复杂度为 O(n),算法只用到了常数个变量,空间复杂度为 O(1)。
10.【2010 统考真题】设将 () 个整数存放到一维数组 R 中。设计一个在时间和空间两方面都尽可能高效的算法。将 R 中保存的序列循环左移 () 个位置,即将 R 中的数据由 (X0,X1,⋯,Xn−1) 变换为 (Xp,Xp+1,⋯,Xn−1,X0,X1,⋯,Xp−1)。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用 C 或 C++ 或 Java 语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。
10.【解答】
1)算法的基本设计思想:
可将问题视为把数组 ab 转换成数组 ba(a 代表数组的前 p 个元素,b 代表数组中余下的 n−p 个元素),先将 a 逆置得到 a^−1b,再将 b 逆置得到 a^−1b^−1,最后将整个 a^−1b^−1 逆置得到 (a^−1b^−1)^−1=ba。设 Reverse 函数执行将数组逆置的操作,对 abcdefgh 向左循环移动 3(p=3)个位置的过程如下:
Reverse(0, p - 1) 得到 cbadefgh;
Reverse(p, n - 1) 得到 cbahgfed;
Reverse(0, n - 1) 得到 defghabc。
注:在 Reverse 中,两个参数分别表示数组中待转换元素的始末位置。
2)使用 C 语言描述算法如下:
cpp复制代码
void Reverse(int R[], int from, int to) {
int i, temp;
for (i = 0; i < (to - from + 1) / 2; i++)
{ temp = R[from + i]; R[from + i] = R[to - i]; R[to - i] = temp; }
}
void Converse(int R[], int n, int p) {
Reverse(R, 0, p - 1);
Reverse(R, p, n - 1);
Reverse(R, 0, n - 1);
}
【另解】对两个长度为 n 的升序序列 A 和 B 中的元素按从小到大的顺序依次访问,这里访问的含义只是比较序列中两个元素的大小,并不实现两个序列的合并,因此空间复杂度为 O(1)。按照上述规则访问第 n 个元素时,这个元素为两个序列 A 和 B 的中位数。
12.【2013 统考真题】已知一个整数序列 A=(a0,a1,⋯,an−1),其中 ()。若存在 ap1=ap2=⋯=apm=x 且 (),则称 x 为 A 的主元素。例如 A={0,5,5,3,5,7,5,5},则 5 为主元素;又如 A={0,5,5,3,5,1,5,7},则 A 中没有主元素。假设 A 中的 n 个元素保存在一个一维数组中,请设计一个尽可能高效的算法,找出 A 的主元素。若存在主元素,则输出该元素;否则输出 −1。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用 C 或 C++ 或 Java 语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。
【解答】
1)算法的基本设计思想:算法的策略是从前向后扫描数组元素,标记出一个可能成为主元素的元素 Num。然后重新计数,确认 Num 是否是主元素。
算法可分为以下两步:
① 选取候选的主元素。依次扫描所给数组中的每个整数,将第一个遇到的整数 Num 保存到 c 中,记录 Num 的出现次数为 1;若遇到的下一个整数仍等于 Num,则计数加 1,否则计数减 1;当计数减到 0 时,将遇到的下一个整数保存到 c 中,计数重新记为 1,开始新一轮计数,即从当前位置开始重复上述过程,直到扫描完全部数组元素。
② 判断 c 中元素是否是真正的主元素。再次扫描该数组,统计 c 中元素出现的次数,若大于 n/2,则为主元素;否则,序列中不存在主元素。
2)算法实现如下:
cpp复制代码
int Majority(int A[], int n) {
int i, c, count = 1; //c用来保存候选主元素,count用来计数
c = A[0]; //设置A[0]为候选主元素
for (i = 1; i < n; i++) //查找候选主元素
if (A[i] == c)
count++; //对A中的候选主元素计数
else
if (count > 0)
count--; //处理不是候选主元素的情况
else { //更换候选主元素,重新计数
c = A[i];
count = 1;
}
if (count > 0)
for (i = count = 0; i < n; i++) //统计候选主元素的实际出现次数
if (A[i] == c)
count++;
if (count > n / 2) return c; //确认候选主元素
else return -1; //不存在主元素
}
要求在时间上尽可能高效,因此采用空间换时间的办法。分配一个用于标记的数组 B[n],用来记录 A 中是否出现了 1∼n 中的正整数,B[0] 对应正整数 1,B[n - 1] 对应正整数 n,初始化 B 中全部为 0。A 中含有 n 个整数,因此可能返回的值是 1∼n+1,当 A 中 n 个数恰好为 1∼n 时返回 n+1。当数组 A 中出现了小于或等于 0 或大于 n 的值时,会导致 1∼n 中出现空余位置,返回结果必然在 1∼n 中,因此对于 A 中出现了小于或等于 0 或大于 n 的值,可以不采取任何操作。
经过以上分析可以得出算法流程:从 A[0] 开始遍历 A,若 0<A[i]<=n,则令 B[A[i] - 1] = 1;否则不做操作。对 A 遍历结束后,开始遍历数组 B,若能查找到第一个满足 B[i] == 0 的下标 i,返回 i + 1 即为结果,此时说明 A 中未出现的最小正整数在 1 和 n 之间。若 B[i] 全部不为 0,返回 i + 1(跳出循环时 i = n,i + 1 等于 n + 1),此时说明 A 中未出现的最小正整数是 n + 1。
2)算法实现:
cpp复制代码
int findMissMin(int A[], int n) {
int i, *B; //标记数组
B = (int *)malloc(sizeof(int) * n); //分配空间
memset(B, 0, sizeof(int) * n); //赋初值为0
for (i = 0; i < n; i++)
if (A[i] > 0 && A[i] <= n) //若A[i]的值介于1~n,则标记数组B
B[A[i] - 1] = 1;
for (i = 0; i < n; i++) //扫描数组B,找到目标值
if (B[i] == 0) break;
return i + 1; //返回结果
}
3)时间复杂度:遍历 A 一次,遍历 B 一次,两次循环内操作步骤为 O(1) 量级,因此时间复杂度为 O(n)。空间复杂度:额外分配了 B[n],空间复杂度为 O(n)。
由 D 的表达式可知,事实上决定 D 大小的关键是 a 和 c 之间的距离,于是问题就可以简化为每次固定 c 找一个 a,使得 L3=∣c−a∣ 最小。
1)算法的基本设计思想:
① 使用 D_min 记录所有已处理的三元组的最小距离,初值为一个足够大的整数。
② 集合 S1、S2 和 S3 分别保存在数组 A、B、C 中。数组的下标变量 i=j=k=0,当 i<∣S1∣,j<∣S2∣ 且 k<∣S3∣ 时(∣S∣ 表示集合 S 中的元素个数),循环执行下面的 a)~c)。
a)计算 (A[i], B[j], C[k]) 的距离 D;(计算 D)
b)若 D < D_min,则 D_min = D;(更新 D)
c)将 A[i]、B[j]、C[k] 中的最小值的下标 +1;(对照分析:最小值为 a,最大值为 c,这里 c 不变而更新 a,试图寻找更小的距离 D)
③ 输出 D_min,结束。
2)算法实现:
cpp复制代码
#define INT_MAX 0x7fffffff
int abs_(int a) { //计算绝对值
if (a < 0) return -a;
else return a;
}
bool xls_min(int a, int b, int c) { //a是否是三个数中的最小值
if (a <= b && a <= c) return true;
return false;
}
int findMinTrip(int A[], int n, int B[], int m, int C[], int p) {
//D_min用于记录三元组的最小距离,初值赋为INT_MAX
int i = 0, j = 0, k = 0, D_min = INT_MAX, D;
while (i < n && j < m && k < p && D_min > 0) {
D = abs_(A[i] - B[j]) + abs_(B[j] - C[k]) + abs_(C[k] - A[i]); //计算D
if (D < D_min) D_min = D; //更新D
if (xls_min(A[i], B[j], C[k])) i++; //更新a
else if (xls_min(B[j], C[k], A[i])) j++;
else k++;
}
return D_min;
}
**线性表的链式存储也称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。**单链表结点结构如图2.3所示,其中 data 为数据域,存放数据元素;next 为指针域,存放其后继结点的地址。
通常用头指针 L(或 head 等)来标识一个单链表,头指针为 NULL 时表示一个空表。此外,为了操作上的方便,在单链表第一个数据结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,但也可以记录表长等信息。单链表带头结点时,头指针 L 指向头结点,如图2.4(a)所示。单链表不带头结点时,头指针 L 指向第一个数据结点,如图2.4(b)所示。表尾结点的指针域为 NULL(用 "^" 表示)。