- 具有n个元素的线性表采用顺序存储结构,在其第i个位置插入一个新元素的算法间复杂度为
( )(1≤i≤n+1) 。
A.O(1) B.O(i)
C.O(n) D.O(n²)
解析:
线性表的顺序存储结构
线性表的顺序存储结构意味着表中的元素是在连续的存储单元里存放的,类似于数组。每个元素都可以通过起始地址加上偏移量直接访问。因此,访问任何一个位置的元素的时间复杂度是O(1),即常数时间复杂度。
插入操作
当我们在顺序存储的线性表中的第i个位置插入一个新元素时,需要进行以下步骤:
- 检查有效性:首先确认插入位置i是否有效,即检查1≤i≤n+1(n为线性表当前的元素数量)。
- 腾出位置:从最后一个元素开始,将从第i个位置开始的所有元素向后移动一个位置,以腾出位置i。
- 插入元素:将新元素放入第i个位置。
- 调整表的大小:线性表的元素数量n增加1。
算法的时间复杂度
时间复杂度是衡量算法执行时间随输入数据规模增长而增长的速度或者增长顺序的一种度量。在本题中,我们关心的是插入操作的时间复杂度:
- 插入操作的关键在于移动元素。假设要在第i个位置插入新元素,需要移动从位置i到位置n的所有元素(即n-i+1个元素)。每次移动操作可以视为常数时间操作,但总的操作次数与i的位置有关。最坏的情况是i=1,此时需要移动所有n个元素。
- 因此,插入操作的时间复杂度依赖于i的值,具体来说,是O(n)------随着线性表长度n的增长,最坏情况下的时间开销线性增加。
选项分析
- A. O(1):这表示插入操作的时间复杂度是常数,这显然不适用于我们需要移动多个元素的情况。
- B. O(i):虽然移动的元素数量与i有关,但时间复杂度应表示为与线性表总长度n相关的最坏情况,而不是i。
- C. O(n):这是正确的选项,因为最坏的情况下我们需要移动n个元素,时间复杂度是线性的。
- D. O(n²):这表示时间复杂度是n的平方,通常用于双层循环等情况,与本题情景不符。
因此,正确答案是 C. O(n)。这代表在顺序存储结构的线性表中进行插入操作时,最坏情况下的时间复杂度是线性的,与表中元素的数量成线性关系。
笔记:
- 顺序存储结构:线性表的元素在内存中连续存放,类似数组。
- 插入操作 :
- 位置i:在第i个位置插入新元素(1≤i≤n+1)。
- 步骤:将第i个位置及之后的元素向后移动,腾出空间插入新元素。
- 时间复杂度 :O(n)
- 原因:最坏情况下(i=1),需要移动整个表的所有元素。
- 解释:时间复杂度与线性表的长度n线性相关。
- 线性表采用顺序存储结构时,其元素地址( )。
A.必须是连续的
B.部分地址必须是连续的
C. 一定是不连续的
D. 连续不连续都可以
解析:
元素的地址计算
在顺序存储结构中,元素的存储地址可以通过下面的公式计算: 地址(�[�])=基地址+�×元素大小地址(A[i])=基地址+i×元素大小 其中,�[�]A[i] 表示数组中的第 �i 个元素,基地址是数组第一个元素的存储地址,元素大小是存储每个元素所需的字节数。这个公式表明,每个元素的存储位置仅依赖于它的索引,这保证了元素之间物理地址上的连续性。
现在,让我们来逐一看看每个选项的意义和是否符合顺序存储结构的特点:
-
A. 必须是连续的 这个选项正确。在顺序存储结构中,所有元素都是连续存放的,这是顺序存储的定义和特点。每个元素的地址直接依赖于其前一个元素的地址和元素本身的大小。
-
B. 部分地址必须是连续的 这个选项不正确。顺序存储不仅是部分连续,而是完全连续。所有元素的地址从头到尾都是连续的,没有间断。
-
C. 一定是不连续的 这个选项完全错误。顺序存储结构的一个基本特征就是地址连续性,选项C与顺序存储的定义相矛盾。
-
D. 连续不连续都可以 这个选项也不正确。在顺序存储结构中,元素地址的连续性是必须的,不可能是不连续的。
正确答案是 A. 必须是连续的。这反映了顺序存储结构的核心特性,即在物理内存中元素之间的连续性。这种存储方式使得访问速度快,因为可以直接通过计算偏移来定位任何元素,但这也意味着插入和删除操作可能会需要移动多个元素以保持这种连续性。理解这一点对于初学者来说是基础且重要的。
笔记:
- 顺序存储结构:类似数组,线性表中的元素在内存中连续存放。
- 地址连续性:在顺序存储结构中,每个元素的地址都是连续的。
- 地址计算公式:地址(A[i])=基地址+i×元素大小,显示了如何根据第一个元素的位置和元素索引计算任何元素的地址。
- 给定一个含m(n≥1)个整数的数组,请设计一个在时间上尽可能高效的算法,找出数组中未出现的最小正整数。例如,数组{-5,3,2,3}中未出现的最小正整数是1;数组{1,2,3)中未出现的最小正整数是4。要求
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++语言描述算法,关健之处给出往释。
(3)说明你所设计算法的时间复杂度和空间复杂度。
解析:
真题的的解析看不明白,自己的版本,至少能实现。。
(1)
-
顺序搜索:从1开始,逐个检查每个正整数是否存在于数组中。这种顺序搜索方法保证了第一个未在数组中找到的正整数即是我们要找的答案。
-
全数组遍历 :对于每一个正整数
x
,遍历整个数组,检查x
是否作为元素存在。如果存在,则增加x
的值继续检查下一个整数;如果不存在,立即返回该数作为未出现的最小正整数。 -
停止条件 :这种方法没有事先设定的停止条件,它会继续增加
x
的值,直到找到一个不在数组中的正整数为止。
(2)
#include <bits/stdc++.h> //万能库
using namespace std; // 使用 std 命名空间
// 定义一个函数,用于找出数组中未出现的最小正整数
int findMissingPositive(int nums[], int size) {
int x = 1; // 初始化x为1,从1开始寻找未出现的最小正整数
while (true) { // 开始一个无限循环
bool found = false; // 初始化found为false,用于标记x是否在数组中找到
for (int i = 0; i < size; ++i) { // 遍历数组
if (nums[i] == x) { // 如果数组中的某个元素等于x
found = true; // 设置found为true
break; // 退出循环
}
}
if (!found) { // 循环结束后,如果没有找到x
return x; // 返回x作为未出现的最小正整数
}
x++; // 如果x在数组中被找到,x加1,检查下一个数字
}
}
// 主函数
int main() {
int nums[] = {1, -5, 3, 2, 3}; // 定义一个数组
int size = sizeof(nums) / sizeof(nums[0]); // 计算数组的大小
cout << "未出现的最小正整数是: " << findMissingPositive(nums, size) << endl; // 输出结果
int nums2[] = {1, 2, 3}; // 定义第二个数组
int size2 = sizeof(nums2) / sizeof(nums2[0]); // 计算第二个数组的大小
cout << "未出现的最小正整数是: " << findMissingPositive(nums2, size2) << endl; // 输出第二个结果
return 0; // 主函数返回0,程序结束
}
- 使用了C++内置的静态数组。
- 数组的大小通过
sizeof(nums) / sizeof(nums[0])
获得。 - 使用了一个基本的循环和线性搜索检查数组中是否存在某个数。
(3)
- 时间复杂度: 与前面相同,为O(n*m),其中n是数组的大小,m是未出现的最小正整数。每次增加x都要遍历整个数组检查是否存在。
- 空间复杂度: 为O(1),除了输入的数组外,只使用了固定的额外空间。
4.
解析:
题目不讲人话,其实就是,在一个数组中找出一个数,这个数的出现次数超过数组长度的一半。
(1)
1. 双重遍历(暴力法)
- 逐个检查:算法通过外层循环逐个选取数组中的每一个元素。
- 计数匹配:内层循环再次遍历整个数组,计数当前选中元素的出现次数。
2. 检查条件满足
- 超过半数判定:每次内层循环结束后,检查当前元素的出现次数是否超过数组长度的一半。
- 立即返回:如果发现某个元素的出现次数满足超过半数的条件,则立即返回该元素作为主元素。
3. 无主元素处理
- 结束遍历:如果外层循环结束后,没有任何一个元素的出现次数超过数组长度的一半,则返回 -1,表示数组中不存在主元素。
(2)
#include <iostream> // 包含标准输入输出流库,用于输入输出操作
using namespace std; // 使用标准命名空间,简化类型名称
// 定义一个函数,用于找出数组中的主元素
int findMajorityElement(int nums[], int size) {
// 外层循环遍历数组中的每个元素
for (int i = 0; i < size; ++i) {
int count = 0; // 初始化计数器,用于统计当前元素的出现次数
// 内层循环再次遍历数组,用于计数当前外层元素的出现次数
for (int j = 0; j < size; ++j) {
if (nums[j] == nums[i]) { // 如果内外循环的元素相同
++count; // 增加计数器
}
}
// 检查当前元素的出现次数是否超过数组长度的一半
if (count > size / 2) {
return nums[i]; // 如果是,返回当前元素作为主元素
}
}
return -1; // 如果循环结束没有找到任何主元素,返回-1
}
// 主函数,程序入口点
int main() {
// 初始化第一个数组和计算其大小
int nums[] = {0, 5, 5, 3, 5, 7, 5, 5};
int size = sizeof(nums) / sizeof(nums[0]); // 计算数组大小,即元素数量
// 输出第一个数组的主元素结果
cout << "主元素是: " << findMajorityElement(nums, size) << endl;
// 初始化第二个数组和计算其大小
int nums2[] = {0, 5, 5, 3, 5, 1, 5, 7};
int size2 = sizeof(nums2) / sizeof(nums2[0]); // 计算第二个数组的大小
// 输出第二个数组的主元素结果
cout << "主元素是: " << findMajorityElement(nums2, size2) << endl;
return 0; // 程序正常结束
}
- 数组初始化和大小计算 : 使用
sizeof
方法来计算数组的元素数量。 - 嵌套循环: 外层循环遍历数组的每个元素,内层循环遍历整个数组来计数当前元素的出现次数。
- 主元素的检查: 如果一个元素的出现次数超过了数组长度的一半,则它是主元素。
(3)
- 时间复杂度: O(n^2),其中 n 是数组的长度。对每个元素,我们需要再次遍历整个数组来计算其出现的次数。
- 空间复杂度: O(1),因为我们没有使用除输入数组和少数几个变量外的任何额外空间。
5.
解析:
(1)
算法的基本思想是先将两个升序数组合并成一个大的升序数组,然后直接从中取出中位数。合并后的数组长度为两个输入数组长度之和,合并过程中保持数组的排序特性。
(2)
#include <iostream>
#include <algorithm>
using namespace std;
// 合并两个升序数组并找到中位数的函数
int findMedianSortedArrays(int A[], int B[], int size) {
int *C = new int[2 * size]; // 动态分配合并后数组的空间
// 合并两个数组
int i = 0, j = 0, k = 0;
while (i < size && j < size) {
if (A[i] < B[j]) {
C[k++] = A[i++];
} else {
C[k++] = B[j++];
}
}
// 如果还有剩余,继续添加
while (i < size) {
C[k++] = A[i++];
}
while (j < size) {
C[k++] = B[j++];
}
// 计算中位数
int midIndex = (2 * size) / 2;
int median = C[midIndex];
delete[] C; // 释放动态分配的内存
return median;
}
int main() {
int A[] = {11, 13, 15, 17, 19};
int B[] = {2, 4, 6, 8, 20};
int size = sizeof(A) / sizeof(A[0]);
cout << "中位数是: " << findMedianSortedArrays(A, B, size) << endl;
return 0;
}
(3)
- 时间复杂度: 合并两个数组的过程是线性的,即O(n),因为每个元素都正好被访问一次。这是因为两个数组都已经排序。
- 空间复杂度: 额外使用了一个大小为 2n 的数组来存储合并后的结果,因此空间复杂度是 O(n)。
解析:
"左移两个位置"意味着最开始的两个数字被移到了列表的末尾,而其他数字都向前移动了两个位置。
(1)
具体来说,我们可以创建一个新的数组,将原数组从指定位置开始的元素复制到新数组的开头,然后再把原数组开头到指定位置的元素复制到新数组的后面。
(2)
#include <iostream>
using namespace std;
void rotate(int arr[], int n, int p) {
int* temp = new int[n]; // 动态分配一个临时数组
// 将原数组的 p 到 n-1 位置的元素复制到新数组的开头
for (int i = p; i < n; i++) {
temp[i - p] = arr[i];
}
// 将原数组的 0 到 p-1 位置的元素复制到新数组的后面
for (int i = 0; i < p; i++) {
temp[n - p + i] = arr[i];
}
// 将新数组的值复制回原数组
for (int i = 0; i < n; i++) {
arr[i] = temp[i];
}
delete[] temp; // 释放临时数组内存
}
int main() {
int arr[] = {1, 2, 3, 4, 5}; // 示例数组
int n = sizeof(arr) / sizeof(arr[0]); // 计算数组大小
int p = 2; // 左移的位置数
rotate(arr, n, p);
// 输出移动后的数组
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
(3)
时间复杂度: O(n)。这是因为算法中涉及到三次遍历整个数组的操作:一次是填充临时数组,一次是复制临时数组回原数组。
空间复杂度: O(n)。算法使用了一个与原数组同样大小的临时数组来辅助移动数据。
补充声明,由于笔者为初学者,故,算法类练习全部采用了暴力解法,而实际情况下暴力只要正确,也能拿到8/11的分数。其中某题的明确评分法子。