1 基本操作
线性表(List)的抽象数据类型(ADT)定义

线性表(List)的抽象数据类型(ADT)定义 ,并非可直接运行的编程语言代码(如 Python、C、Java 等代码),而是对线性表 "数据结构 + 操作" 的抽象描述(类似 "设计蓝图")。
若要将这些操作转化为具体代码,需先选定编程语言 和存储结构 (比如用数组实现线性表,或用链表实现线性表),再手动编写函数。以下以 C 语言 + 顺序表(数组实现) 为例,给出这些操作的代码框架:
1. 定义线性表结构
cpp
#define MAXSIZE 100 // 线性表最大长度
typedef int ElemType; // 假设元素类型为 int,可根据需求修改
typedef struct {
ElemType data[MAXSIZE]; // 数组存储元素
int length; // 线性表当前长度
} SqList;
2. 初始化线性表(InitList
)
cpp
void InitList(SqList *L) {
L->length = 0; // 初始长度为 0
}
3. 获取线性表长度(ListLength
)
cpp
int ListLength(SqList L) {
return L.length;
}
4. 获取指定位置元素(GetElem
)
cpp
int GetElem(SqList L, int i, ElemType *e) {
if (i < 1 || i > L.length) {
return 0; // 位置不合法,返回 0 表示失败
}
*e = L.data[i - 1]; // 数组下标从 0 开始,所以第 i 个元素对应下标 i-1
return 1; // 成功,返回 1
}
5. 获取前驱元素(PriorElem
)
cpp
int PriorElem(SqList L, ElemType cur_e, ElemType *pre_e) {
int i;
for (i = 1; i < L.length; i++) {
if (L.data[i] == cur_e) { // 找到当前元素
if (i == 0) {
return 0; // 是第一个元素,无前驱
}
*pre_e = L.data[i - 1];
return 1;
}
}
return 0; // 未找到当前元素
}
6. 获取后继元素(NextElem
)
cpp
int NextElem(SqList L, ElemType cur_e, ElemType *next_e) {
int i;
for (i = 0; i < L.length - 1; i++) {
if (L.data[i] == cur_e) { // 找到当前元素
*next_e = L.data[i + 1];
return 1;
}
}
return 0; // 未找到当前元素
}
7. 定位元素(LocateElem
)
cpp
int LocateElem(SqList L, ElemType e) {
int i;
for (i = 0; i < L.length; i++) {
if (L.data[i] == e) {
return i + 1; // 返回元素位置(从 1 开始计数)
}
}
return 0; // 未找到元素
}
8. 插入元素(ListInsert
)
cpp
int ListInsert(SqList *L, int i, ElemType e) {
if (i < 1 || i > L->length + 1) {
return 0; // 插入位置不合法
}
if (L->length == MAXSIZE) {
return 0; // 线性表已满
}
// 元素后移,为新元素腾出位置
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j - 1];
}
L->data[i - 1] = e; // 插入新元素
L->length++;
return 1;
}
9. 删除元素(ListDelete
)
cpp
int ListDelete(SqList *L, int i, ElemType *e) {
if (i < 1 || i > L->length) {
return 0; // 删除位置不合法
}
*e = L->data[i - 1]; // 保存被删除的元素
// 元素前移,覆盖被删除元素的位置
for (int j = i; j < L->length; j++) {
L->data[j - 1] = L->data[j];
}
L->length--;
return 1;
}
【L->length = 0; // 等价于 (*L).length = 0,为什么,还是说没关系,这只是一种写法?】
L->length = 0
等价于 (*L).length = 0
是C 语言的语法规定,两者完全等效,只是写法不同。
这背后的原因与指针的特性有关:
L
是一个指向结构体的指针,存储的是结构体在内存中的地址*L
表示 "取指针所指向的结构体本身"(解引用操作)- 当拿到结构体本身后,就需要用
.
来访问其成员,即(*L).length
但这种写法 (*L).length
既麻烦又容易出错(比如忘记加括号),所以 C 语言专门设计了 ->
运算符作为简化写法,让指针访问结构体成员的语法更简洁。
可以理解为:
cpp
L->length ≡ (*L).length // 完全等价
这和 Java 有很大不同:
- Java 中没有显式指针,也就不需要这种区分
- Java 的引用变量虽然类似指针,但访问成员统一用
.
运算符 - C 语言需要明确区分 "直接访问"(.)和 "通过指针访问"(->)
简单说,->
就是 C 语言为了简化指针访问结构体成员而设计的语法糖,本质和 (*L).length
完全一样。
【length是一个内置的或者标准库的函数吗,需要自己额外写吗?】
在 C 语言中,length
既不是内置函数,也不是标准库中定义的函数 ------ 它通常是程序员自己定义的结构体成员变量,用于表示某种 "长度"(比如数组长度、链表长度等)。
具体说明:
-
length
是自定义的成员变量 你看到的L.length
或L->length
中的length
,是程序员在结构体中自己定义的,例如:cpp// 自定义一个链表结构体 struct LinkedList { int data; // 数据 struct LinkedList* next;// 下一个节点 int length; // 自己定义的"长度"变量 };
这里的
length
只是一个普通的 int 变量,用于记录链表中节点的数量,完全由程序员自己维护(需要手动更新)。 -
C 语言没有内置的 "长度" 函数
- 对于数组,C 语言没有像 Java 中
array.length
这样的内置属性,需要自己计算(例如sizeof(arr)/sizeof(arr[0])
)。 - 对于字符串(字符数组),标准库提供了
strlen()
函数计算长度,但这是针对字符串的特殊处理,并非通用的length
函数。
- 对于数组,C 语言没有像 Java 中
-
是否需要自己写?
- 如果你需要用
length
表示某种结构的长度,就必须自己在结构体中定义这个成员变量。 - 同时需要自己编写逻辑来维护它(例如添加元素时
length++
,删除时length--
)。
- 如果你需要用
和 Java 的对比:
- Java 中
length
(数组)或size()
(集合)是语言或标准库内置的,自动维护,无需手动定义。 - C 语言中完全需要手动实现,
length
只是程序员约定俗成的变量名(也可以叫len
、count
等)。
所以,length
在 C 中是 "自定义变量",不是现成的函数或属性,需要自己定义和维护。
【我知道了,其实就是对于指针变量的结构体访问其成员】
简单总结一下核心区别:
- 当
L
是结构体变量 时,用.
访问成员:L.length
- 当
L
是结构体指针 时,用->
访问成员:L->length
(本质是(*L).length
的简化写法)
这两种写法的目的都是访问结构体中的成员(比如你说的 length
),只是根据 L
的类型(变量还是指针)选择不同的运算符。
C 语言中这种区分是因为指针直接存储的是内存地址,需要先 "解引用" 才能访问到实际的结构体内容,而 ->
运算符就是为了简化这个过程而设计的语法。 这一点和 Java 确实不同,Java 会自动处理引用关系,所以统一用 .
即可。
【java里面length获取长度是一个内置函数还是什么】
在 Java 中,length
并不是函数(方法),而是数组的一个内置属性,用于表示数组的长度(即数组中元素的个数)。
它的特点是:
-
直接通过数组变量访问 :格式为
数组名.length
(注意没有括号,因为不是方法)javaint[] arr = new int[5]; System.out.println(arr.length); // 输出 5(数组长度为5)
-
数组创建时自动确定 :
length
的值在数组初始化时就已固定(由数组的容量决定),且无法修改(即使数组元素被修改,length
也不变)。 -
仅属于数组 :
length
是数组特有的属性,其他数据结构(如集合类)不能用length
获取长度,而是用size()
方法(这是真正的方法,需要加括号):javaList<String> list = new ArrayList<>(); System.out.println(list.size()); // 集合用 size() 方法(有括号)
总结:
- Java 中数组的
length
是内置属性 (不是函数),用于获取数组长度,格式为数组名.length
(无括号)。 - 集合类(如
List
、Set
)则通过size()
方法(有括号)获取元素数量,这是类中定义的方法。
2 线性表顺序结构的顺序存储结构
一、特点
- 存储单元连续:使用一组地址连续的存储单元,依次存放线性表中的元素。这意味着元素在内存中是紧密排列的,没有空隙。
- 逻辑关系通过物理位置体现:以元素在计算机内的 "物理位置相邻" 来表示数据之间的逻辑关系。若设线性表中第 i 个元素的存储位置为 LOC(a_i),每个元素占用 L 个存储单元,则有LOC(a_{i+1}) = LOC(a_i) + L,即后一个元素的存储位置是前一个元素存储位置加上单个元素占用的存储单元数。
- 随机存取:可以通过公式 LOC(a_i) = LOC(a_1) + (i - 1) * L 直接计算出线性表中第 i 个元素的存储位置,从而实现随机存取,能快速访问表中任意位置的元素。
- 插入 / 删除操作效率低:当进行插入或删除操作时,需要移动大量元素。例如,在第 i 个元素前插入一个新元素,需要将第 i 个及之后的所有元素向后移动一个位置;删除第 i 个元素时,需要将第 (i+1) 个及之后的所有元素向前移动一个位置。
3 线性表插入元素的时间复杂度

4 线性表顺序结构删除元素的时间复杂度

5 实现两个有序线性表合并

可以采用双指针从后往前遍历的方法来实现,这样能最大限度避免元素移动。具体步骤如下:
- 分别获取线性表 LA 和 LB 的长度,记为 lenLA 和 lenLB。
- 设定三个指针,i 指向 LA 中最后一个元素的位置(初始为 (lenLA - 1)),j 指向 LB 中最后一个元素的位置(初始为(lenLB - 1)),k 指向合并后 LA 中最后一个元素将要存放的位置(初始为 (lenLA + lenLB - 1))。
- 从后往前比较 (LA[i]) 和 (LB[j]) 的大小:
- 如果 (LA[i] ==LB[j]),就把 \(LA[i]\) 放到 (LA[k]) 的位置,然后 i 减 1,k 减 1。
- 如果 (LA[i] < LB[j]),就把 \(LB[j]\) 放到 (LA[k]) 的位置,然后 j 减 1,k 减 1。
- 重复步骤 3,直到 (j < 0)(此时 LB 中的元素已全部合并到 LA 中)。如果 i 还没到 -1,说明 LA 中剩下的元素已经在正确的位置上,不需要再处理。
cpp
#include <stdio.h>
#include <stdlib.h>
// 合并两个有序线性表(假设LA和LB均为非递减排序)
// 要求:LA有足够的空间容纳LA和LB的所有元素
void merge(int LA[], int lenLA, int LB[], int lenLB) {
int i = lenLA - 1; // LA的最后一个元素索引
int j = lenLB - 1; // LB的最后一个元素索引
int k = lenLA + lenLB - 1; // 合并后数组的最后一个位置索引
// 从后往前合并两个数组
while (i >= 0 && j >= 0) {
if (LA[i] >= LB[j]) {
LA[k--] = LA[i--];
} else {
LA[k--] = LB[j--];
}
}
// 如果LB中还有剩余元素,直接复制到LA中
while (j >= 0) {
LA[k--] = LB[j--];
}
// LA中剩余元素已在正确位置,无需处理
}
int main() {
// 示例:合并两个有序线性表
int LA[10] = {1, 3, 5, 7, 9}; // 预留足够空间
int lenLA = 5;
int LB[] = {2, 4, 6, 8, 10};
int lenLB = 5;
// 执行合并操作
merge(LA, lenLA, LB, lenLB);
// 输出合并结果
printf("合并后的线性表:");
for (int i = 0; i < lenLA + lenLB; i++) {
printf("%d ", LA[i]);
}
printf("\n");
return 0;
}
示例
假设我们有两个顺序存储的线性表:
- LA:元素为 \([1, 3, 5, 7, 9]\),长度 \(lenLA = 5\),并且我们预先给 LA 分配了足够的空间(比如可以容纳 10 个元素)。
- LB:元素为 \([2, 4, 6, 8, 10]\),长度 \(lenLB = 5\)。
我们的目标是将 LB 合并到 LA 中,使合并后的 LA 仍然保持非递减有序。
算法步骤详细分析
- 初始化指针
- 我们设置三个指针:
- i:指向 LA 中最后一个元素的位置,初始时 \(i = lenLA - 1 = 5 - 1 = 4\)(因为数组下标从 0 开始,\(LA[4] = 9\))。
- j:指向 LB 中最后一个元素的位置,初始时 \(j = lenLB - 1 = 5 - 1 = 4\)(\(LB[4] = 10\))。
- k:指向合并后 LA 中最后一个元素将要存放的位置,初始时 \(k = lenLA + lenLB - 1 = 5 + 5 - 1 = 9\)。
- 我们设置三个指针:
- 从后往前比较并合并元素
- 第一次比较:
- \(LA[i] = LA[4] = 9\),\(LB[j] = LB[4] = 10\)。
- 因为 \(9 < 10\),所以将 \(LB[j]\)(即 10)放到 \(LA[k]\) 的位置。此时 \(LA[9] = 10\)。然后 j 减 1(\(j = 3\)),k 减 1(\(k = 8\))。
- 第二次比较:
- \(LA[i] = 9\),\(LB[j] = LB[3] = 8\)。
- 因为 \(9 \geq 8\),所以将 \(LA[i]\)(即 9)放到 \(LA[k]\) 的位置。此时 \(LA[8] = 9\)。然后 i 减 1(\(i = 3\)),k 减 1(\(k = 7\))。
- 第三次比较:
- \(LA[i] = LA[3] = 7\),\(LB[j] = LB[3] = 8\)。
- 因为 \(7 < 8\),所以将 \(LB[j]\)(即 8)放到 \(LA[k]\) 的位置。此时 \(LA[7] = 8\)。然后 j 减 1(\(j = 2\)),k 减 1(\(k = 6\))。
- 第四次比较:
- \(LA[i] = 7\),\(LB[j] = LB[2] = 6\)。
- 因为 \(7 \geq 6\),所以将 \(LA[i]\)(即 7)放到 \(LA[k]\) 的位置。此时 \(LA[6] = 7\)。然后 i 减 1(\(i = 2\)),k 减 1(\(k = 5\))。
- 第五次比较:
- \(LA[i] = LA[2] = 5\),\(LB[j] = LB[2] = 6\)。
- 因为 \(5 < 6\),所以将 \(LB[j]\)(即 6)放到 \(LA[k]\) 的位置。此时 \(LA[5] = 6\)。然后 j 减 1(\(j = 1\)),k 减 1(\(k = 4\))。
- 第六次比较:
- \(LA[i] = 5\),\(LB[j] = LB[1] = 4\)。
- 因为 \(5 \geq 4\),所以将 \(LA[i]\)(即 5)放到 \(LA[k]\) 的位置。此时 \(LA[4] = 5\)。然后 i 减 1(\(i = 1\)),k 减 1(\(k = 3\))。
- 第七次比较:
- \(LA[i] = LA[1] = 3\),\(LB[j] = LB[1] = 4\)。
- 因为 \(3 < 4\),所以将 \(LB[j]\)(即 4)放到 \(LA[k]\) 的位置。此时 \(LA[3] = 4\)。然后 j 减 1(\(j = 0\)),k 减 1(\(k = 2\))。
- 第八次比较:
- \(LA[i] = 3\),\(LB[j] = LB[0] = 2\)。
- 因为 \(3 \geq 2\),所以将 \(LA[i]\)(即 3)放到 \(LA[k]\) 的位置。此时 \(LA[2] = 3\)。然后 i 减 1(\(i = 0\)),k 减 1(\(k = 1\))。
- 第九次比较:
- \(LA[i] = LA[0] = 1\),\(LB[j] = LB[0] = 2\)。
- 因为 \(1 < 2\),所以将 \(LB[j]\)(即 2)放到 \(LA[k]\) 的位置。此时 \(LA[1] = 2\)。然后 j 减 1(\(j = -1\)),k 减 1(\(k = 0\))。
- 第一次比较:
- 处理剩余元素
- 此时 \(j = -1\),说明 LB 中的元素已经全部合并到 LA 中。而 \(i = 0\),LA 中还剩下元素 1,它已经在正确的位置(\(LA[0]\)),不需要再处理。
- 最终结果合并后的 LA 为 \([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\),仍然保持非递减有序。
算法效率分析
- 时间复杂度:\(O(lenLA + lenLB)\)。因为我们只需要遍历 LA 和 LB 各一次,所以时间复杂度与两个线性表的长度之和成正比。
- 空间复杂度:\(O(1)\)。我们没有使用额外的数组或其他数据结构来存储元素,只是利用了 LA 预先分配的足够空间,通过指针操作来合并元素,所以空间复杂度是常数级别的。
这种从后往前合并的方法,最大限度地避免了元素的移动,相比从前往后合并需要大量移动元素的方法,效率要高很多。
6 顺序结构线性表是单链表吗?
顺序结构线性表不是单链表。
顺序结构线性表(比如数组)是用一段连续的存储单元依次存储线性表的数据元素,它可以随机访问元素,访问某个元素的时间复杂度是 (O(1)),但插入和删除元素时,需要移动大量元素,时间复杂度为(O(n))。
而单链表是一种链式存储结构,它通过指针将各个节点连接起来,节点在内存中可以是不连续的。单链表访问元素时,需要从表头开始依次遍历,时间复杂度为(O(n)),但插入和删除元素时,只需要修改指针,时间复杂度为(O(1))(如果找到插入或删除位置的时间不计的话)。
所以,顺序结构线性表和单链表是两种不同的线性表存储结构,有着不同的特点和适用场景。
7 将顺序存储的线性表逆置
要将顺序存储的线性表逆置,我们可以采用双指针的方法。设置两个指针,一个从线性表的起始位置(left
)开始,另一个从线性表的末尾位置(right
)开始。然后交换这两个指针所指向的元素,之后 left
指针向后移动,right
指针向前移动,直到 left
指针大于等于 right
指针,此时线性表就完成了逆置。这样做的时间复杂度是(O(n)),空间复杂度是(O(1)),因为只使用了常数个额外变量。
C 语言代码实现
cpp
#include <stdio.h>
// 逆置顺序存储的线性表
void reverseArray(int arr[], int n) {
// 定义左指针,初始指向数组第一个元素
int left = 0;
// 定义右指针,初始指向数组最后一个元素
int right = n - 1;
// 临时变量,用于交换元素
int temp;
// 当左指针小于右指针时,进行交换操作
while (left < right) {
// 交换 arr[left] 和 arr[right] 的值
temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
// 左指针向后移动一位
left++;
// 右指针向前移动一位
right--;
}
}
int main() {
// 示例数组
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原数组:");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 调用逆置函数
reverseArray(arr, n);
printf("逆置后数组:");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
代码详细解释
reverseArray
函数 :- 接收一个整型数组
arr
和数组长度n
作为参数。 - 定义
left
指针初始为0
(指向数组第一个元素),right
指针初始为n - 1
(指向数组最后一个元素),以及临时变量temp
用于交换元素。 - 在
while
循环中,当left < right
时,交换arr[left]
和arr[right]
的值,然后left
指针向后移动,right
指针向前移动,直到left >= right
,循环结束,数组逆置完成。
- 接收一个整型数组
main
函数 :- 定义一个示例数组
arr
并计算其长度n
。 - 先打印原数组。
- 调用
reverseArray
函数对数组进行逆置。 - 最后打印逆置后的数组。
- 定义一个示例数组
运行结果
原数组:1 2 3 4 5 6 7 8 9
逆置后数组:9 8 7 6 5 4 3 2 1
时间复杂度和空间复杂度
- 时间复杂度:(O(n)),其中 n 是线性表的长度。因为需要遍历数组的一半元素进行交换操作,循环执行的次数是 (n/2) 次,所以时间复杂度为 (O(n))。
- 空间复杂度 :(O(1)),只使用了常数个额外变量(
left
、right
、temp
),所以空间复杂度为 (O(1))。