当前位置: 首页 > news >正文

数据结构 01 线性表

1 基本操作

线性表(List)的抽象数据类型(ADT)定义

线性表(List)的抽象数据类型(ADT)定义,并非可直接运行的编程语言代码(如 Python、C、Java 等代码),而是对线性表 “数据结构 + 操作” 的抽象描述(类似 “设计蓝图”)。

若要将这些操作转化为具体代码,需先选定编程语言存储结构(比如用数组实现线性表,或用链表实现线性表),再手动编写函数。以下以 C 语言 + 顺序表(数组实现) 为例,给出这些操作的代码框架:

1. 定义线性表结构

#define MAXSIZE 100  // 线性表最大长度
typedef int ElemType;  // 假设元素类型为 int,可根据需求修改typedef struct {ElemType data[MAXSIZE];  // 数组存储元素int length;  // 线性表当前长度
} SqList;

2. 初始化线性表(InitList

void InitList(SqList *L) {L->length = 0;  // 初始长度为 0
}

3. 获取线性表长度(ListLength

int ListLength(SqList L) {return L.length;
}

4. 获取指定位置元素(GetElem

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-1return 1;  // 成功,返回 1
}

5. 获取前驱元素(PriorElem

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

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

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

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

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 语言的语法规定,两者完全等效,只是写法不同。

这背后的原因与指针的特性有关:

  1. L 是一个指向结构体的指针,存储的是结构体在内存中的地址
  2. *L 表示 "取指针所指向的结构体本身"(解引用操作)
  3. 当拿到结构体本身后,就需要用 . 来访问其成员,即 (*L).length

但这种写法 (*L).length 既麻烦又容易出错(比如忘记加括号),所以 C 语言专门设计了 -> 运算符作为简化写法,让指针访问结构体成员的语法更简洁。

可以理解为:

L->length  ≡  (*L).length  // 完全等价

这和 Java 有很大不同:

  • Java 中没有显式指针,也就不需要这种区分
  • Java 的引用变量虽然类似指针,但访问成员统一用 . 运算符
  • C 语言需要明确区分 "直接访问"(.)和 "通过指针访问"(->)

简单说,-> 就是 C 语言为了简化指针访问结构体成员而设计的语法糖,本质和 (*L).length 完全一样。

【length是一个内置的或者标准库的函数吗,需要自己额外写吗?】

在 C 语言中,length 既不是内置函数,也不是标准库中定义的函数 —— 它通常是程序员自己定义的结构体成员变量,用于表示某种 "长度"(比如数组长度、链表长度等)。

具体说明:

  1. length 是自定义的成员变量你看到的 L.length 或 L->length 中的 length,是程序员在结构体中自己定义的,例如:

    // 自定义一个链表结构体
    struct LinkedList {int data;               // 数据struct LinkedList* next;// 下一个节点int length;             // 自己定义的"长度"变量
    };
    

    这里的 length 只是一个普通的 int 变量,用于记录链表中节点的数量,完全由程序员自己维护(需要手动更新)。

  2. C 语言没有内置的 "长度" 函数

    • 对于数组,C 语言没有像 Java 中 array.length 这样的内置属性,需要自己计算(例如 sizeof(arr)/sizeof(arr[0]))。
    • 对于字符串(字符数组),标准库提供了 strlen() 函数计算长度,但这是针对字符串的特殊处理,并非通用的 length 函数。
  3. 是否需要自己写?

    • 如果你需要用 length 表示某种结构的长度,就必须自己在结构体中定义这个成员变量。
    • 同时需要自己编写逻辑来维护它(例如添加元素时 length++,删除时 length--)。

和 Java 的对比:

  • Java 中 length(数组)或 size()(集合)是语言或标准库内置的,自动维护,无需手动定义。
  • C 语言中完全需要手动实现,length 只是程序员约定俗成的变量名(也可以叫 lencount 等)。

所以,length 在 C 中是 "自定义变量",不是现成的函数或属性,需要自己定义和维护。

【我知道了,其实就是对于指针变量的结构体访问其成员】

简单总结一下核心区别:

  • 当 L 是结构体变量时,用 . 访问成员:L.length
  • 当 L 是结构体指针时,用 -> 访问成员:L->length(本质是 (*L).length 的简化写法)

这两种写法的目的都是访问结构体中的成员(比如你说的 length),只是根据 L 的类型(变量还是指针)选择不同的运算符。

C 语言中这种区分是因为指针直接存储的是内存地址,需要先 "解引用" 才能访问到实际的结构体内容,而 -> 运算符就是为了简化这个过程而设计的语法。这一点和 Java 确实不同,Java 会自动处理引用关系,所以统一用 . 即可。

【java里面length获取长度是一个内置函数还是什么】

在 Java 中,length 并不是函数(方法),而是数组的一个内置属性,用于表示数组的长度(即数组中元素的个数)。

它的特点是:

  1. 直接通过数组变量访问:格式为 数组名.length(注意没有括号,因为不是方法)

    int[] arr = new int[5];
    System.out.println(arr.length); // 输出 5(数组长度为5)
    
  2. 数组创建时自动确定length 的值在数组初始化时就已固定(由数组的容量决定),且无法修改(即使数组元素被修改,length 也不变)。

  3. 仅属于数组length 是数组特有的属性,其他数据结构(如集合类)不能用 length 获取长度,而是用 size() 方法(这是真正的方法,需要加括号):

    List<String> list = new ArrayList<>();
    System.out.println(list.size()); // 集合用 size() 方法(有括号)
    

总结:

  • Java 中数组的 length 是内置属性(不是函数),用于获取数组长度,格式为 数组名.length(无括号)。
  • 集合类(如 ListSet)则通过 size() 方法(有括号)获取元素数量,这是类中定义的方法。

2  线性表顺序结构的顺序存储结构

一、特点
  1. 存储单元连续:使用一组地址连续的存储单元,依次存放线性表中的元素。这意味着元素在内存中是紧密排列的,没有空隙。
  2. 逻辑关系通过物理位置体现:以元素在计算机内的 “物理位置相邻” 来表示数据之间的逻辑关系。若设线性表中第 i 个元素的存储位置为 LOC(a_i),每个元素占用 L 个存储单元,则有LOC(a_{i+1}) = LOC(a_i) + L,即后一个元素的存储位置是前一个元素存储位置加上单个元素占用的存储单元数。
  3. 随机存取:可以通过公式 LOC(a_i) = LOC(a_1) + (i - 1) * L 直接计算出线性表中第 i 个元素的存储位置,从而实现随机存取,能快速访问表中任意位置的元素。
  4. 插入 / 删除操作效率低:当进行插入或删除操作时,需要移动大量元素。例如,在第 i 个元素前插入一个新元素,需要将第 i 个及之后的所有元素向后移动一个位置;删除第 i 个元素时,需要将第 (i+1) 个及之后的所有元素向前移动一个位置。

3 线性表插入元素的时间复杂度

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

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

可以采用双指针从后往前遍历的方法来实现,这样能最大限度避免元素移动。具体步骤如下:

  1. 分别获取线性表 LA 和 LB 的长度,记为 lenLA 和 lenLB。
  2. 设定三个指针,i 指向 LA 中最后一个元素的位置(初始为 (lenLA - 1)),j 指向 LB 中最后一个元素的位置(初始为(lenLB - 1)),k 指向合并后 LA 中最后一个元素将要存放的位置(初始为 (lenLA + lenLB - 1))。
  3. 从后往前比较 (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。
  4. 重复步骤 3,直到 (j < 0)(此时 LB 中的元素已全部合并到 LA 中)。如果 i 还没到 -1,说明 LA 中剩下的元素已经在正确的位置上,不需要再处理。
#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 仍然保持非递减有序。

算法步骤详细分析

  1. 初始化指针
    • 我们设置三个指针:
      • 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\)。
  2. 从后往前比较并合并元素
    • 第一次比较:
      • \(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\))。
  3. 处理剩余元素
    • 此时 \(j = -1\),说明 LB 中的元素已经全部合并到 LA 中。而 \(i = 0\),LA 中还剩下元素 1,它已经在正确的位置(\(LA[0]\)),不需要再处理。
  4. 最终结果合并后的 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 语言代码实现

#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;
}

代码详细解释

  1. reverseArray 函数
    • 接收一个整型数组 arr 和数组长度 n 作为参数。
    • 定义 left 指针初始为 0(指向数组第一个元素),right 指针初始为 n - 1(指向数组最后一个元素),以及临时变量 temp 用于交换元素。
    • 在 while 循环中,当 left < right 时,交换 arr[left] 和 arr[right] 的值,然后 left 指针向后移动,right 指针向前移动,直到 left >= right,循环结束,数组逆置完成。
  2. 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)),只使用了常数个额外变量(leftrighttemp),所以空间复杂度为 (O(1))。
http://www.dtcms.com/a/420137.html

相关文章:

  • 为什么有的网站打不开WordPress京东自动转链插件
  • MySQL——数据库基础与库的操作
  • 网站建站上市公司国外论文类网站有哪些方面
  • 网站建设有哪些分工分建筑网站、
  • asp网站改php网站方法wordpress禁用修正版
  • 堆 动态内存 超级玛丽demo7
  • 空壳网站查询WordPress下拉菜单栏
  • 《高并发架构实战课》学习笔记
  • 网站备案 人工审核平面设计需要用到的软件
  • 网站301跳转怎么做的安阳市网站建设
  • 参考资料:Linux系统U盘拔出识别慢问题
  • 银川公司网站建设广州万安建设监理有限公司网站
  • 专业做鞋子网站苏州网站建设2万起
  • 19软件测试用例设计编写测试点-连接数据库服务器
  • 嵌入式科普(41)通过对比深刻理解CAN总线协议特性
  • 外综服网站开发专业设计网站公司
  • excel数据处理
  • 淄博网站建设 招聘对外贸易网站有哪些
  • 数据结构_ 二叉树线索化:从原理到手撕实现
  • 分享一个知识工程师单体智能体的简单提示词
  • 南宁伯才网络建站如何WordPress一键采集插件
  • 免费三网合一网站系统网站建设介绍书
  • 网站开发公司起名10分钟快速建网站
  • flink批处理-时间和窗口
  • 无锡有什么网站怎样免费注册个人网网址
  • SLAM | SLAM中松耦合与紧耦合技术对比分析
  • xtuoj 方程组
  • 重庆网站设计建设东莞微联建站
  • 北京医疗网站建设公司排名网站建设开票项目是什么
  • C#——方法的定义、调用与调试