文章目录
2. 线性表
2.1 线性表的定义和操作
-
线性表的定义:线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n = 0时线性表是一个空表。
-
操作:
InitList(&L)
:初始化表。构造一个空的线性表L,分配内存空间。DestroyList(&L)
:销毁操作。销毁线性表,并释放线性表L所占用的内存空间。ListInsert(&L,i,e)
:插入操作。在表L中的第i个位置上插入指定元素e。ListDelete(&L,i,&e)
:删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。LocateElem(L,e)
:按值查找操作。在表L中查找具有给定关键字值的元素。GetElem(L, i, &e)
:按位查找操作。获取表L中第i个位置的元素的值。其他常用操作:
Length(L)
:求表长。返回线性表L的长度,即L中数据元素的个数。PrintList(L)
:输出操作。按前后顺序输出线性表L的所有元素值。Empty(L)
:判空操作。若L为空表,则返回true,否则返回false。
2.2 线性表的顺序表示
2.2.1 顺序表的定义
-
顺序表的定义:用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
-
顺序表的实现——静态分配
#include <stdio.h> #define MAXSIZE 10 // 定义最大长度 // 顺序表的静态实现 typedef struct { int data[MAXSIZE]; // 用静态的“数组”存放数据元素 int length; // 顺序表的当前长度 } SqList; // 基本操作,初始化一个顺序表 void InitList(SqList* list) { for (int i = 0; i < MAXSIZE; i++) { list->data[i] = 0; // 将所有元素设置为默认初始值 } list->length = 0; // 顺序表的原始长度为0 } int main() { SqList list; // 声明一个顺序表 InitList(&list); // 初始化一个顺序表 // 尝试违规打印数据表, 正常情况下是 i < list.length for (int i = 0; i < MAXSIZE; i++) { printf("data[%d] = %d\n", i, list.data[i]); } return 0; }
注意:顺序表静态分配实现的方式如果这个“数组”存满了,没有办法解决扩容问题,放弃治疗。
-
顺序表的实现——动态分配
#include <stdio.h> #include <stdlib.h> #define InitSize 5 // 默认的最大长度 // 顺序表的动态实现(动态数组) typedef struct { int* data; // 指示动态数组分配的指针 int MaxSize; // 顺序表的最大容量 int length; // 顺序表的当前长度 }SeqList; // 初始化一个顺序表 void InitList(SeqList* list) { // 用 malloc 函数在堆内存中申请一片连续的存储空间 list->data = (int*)malloc(sizeof(int) * InitSize); list->length = 0; list->MaxSize = InitSize; // 将所有元素设置为默认初始值 for (int i = 0; i < InitSize; i++) { list->data[i] = 0; } } /* * 扩容(增加动态数组的长度) * SeqList* list: 需要被扩容的线性表 * int len: 将线性表在原有的基础上加上len个长度 */ void IncreaseSize(SeqList* list, int len) { int *p = list->data; list->data = (int*)malloc(sizeof(int) * (list->MaxSize + len)); for (int i = 0; i < list->length; i++) { list->data[i] = p[i]; // 将数据复制到新的区域 } list->MaxSize = list->MaxSize + len; // 顺序表的最大长度增加了 len free(p); // 释放原来的内存空间 } int main() { SeqList list; InitList(&list); // 模拟往链表中插入三个数据 list.data[0] = 11; list.length = 1; list.data[1] = 22; list.length = 2; list.data[2] = 33; list.length = 3; for (int i = 0; i < list.length; i++) { printf("before--list.data[%d] = %d\n", i, list.data[i]); } // 将容量扩大5个单位 IncreaseSize(&list, 5); for (int i = 0; i < list.length; i++) { printf("after---list.data[%d] = %d\n", i, list.data[i]); } return 0; }
-
顺序表的特点:
① 随机访问,即可以在 O(1) 时间内找到第 i 个元素。
② 存储密度高,每个节点只存储数据元素
③ 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
④ 插入、删除操作不方便,需要移动大量元素
2.2.2_1 顺序表的插入删除
-
顺序表的基本操作——插入
注:代码建立在顺序表的“静态分配”实现方式之上
最差时间复杂度 O(n) 最好时间复杂度 O(1) 平均时间复杂度 O(n) #include <stdio.h> #define MAXSIZE 10 #define OK 1 // 表示操作成功 #define ERROR 0 // 表示操作失败 typedef int Status; // 状态码,其值有OK 和 ERROR typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; int length; } SqList; // 基本操作,初始化一个顺序表 void InitList(SqList* list) { for (int i = 0; i < MAXSIZE; i++) { list->data[i] = 0; } list->length = 0; } // 在表中的第i个位置(位序)插入指定元素e Status ListInsert(SqList *list, int i, ElemType e) { if (i < 0 || i > list->length + 1) // 判断 i 的范围是否有效 return ERROR; if (i >= MAXSIZE) // 当前存储空间已满,不能插入 return ERROR; // i 位置及 i 位置以后的所有元素往后移 for (int j = list->length - 1; j >= i - 1; j--) { list->data[j + 1] = list->data[j]; } list->data[i - 1] = e; // 在第i个位置插入元素 e list->length++; // 顺序表的长度加一 return OK; } int main() { SqList list; InitList(&list); // 初始化一个顺序表 ListInsert(&list, 1, 66) ? printf("insert OK\n") : printf("insert ERROR\n"); ListInsert(&list, 2, 77) ? printf("insert OK\n") : printf("insert ERROR\n"); ListInsert(&list, 1, 88) ? printf("insert OK\n") : printf("insert ERROR\n"); // 打印顺序表 for (int i = 0; i < list.length; i++) { printf("data[%d] = %d\n", i, list.data[i]); } return 0; }
-
顺序表的基本操作——删除
最差时间复杂度 O(n) 最好时间复杂度 O(1) 平均时间复杂度 O(n) #include <stdio.h> #define MAXSIZE 10 #define OK 1 // 表示操作成功 #define ERROR 0 // 表示操作失败 typedef int Status; // 状态码,其值有OK 和 ERROR typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; int length; } SqList; // 基本操作,初始化一个顺序表 void InitList(SqList* list) { for (int i = 0; i < MAXSIZE; i++) { list->data[i] = 0; } list->length = 0; } // 基本操作,删除第i(位序)个元素 Status ListDelete(SqList* list, int i, ElemType* e) { if (i < 0 || i > list->length) // 判断 i 的位置是否合法 return ERROR; *e = list->data[i - 1]; // 将被删除的元素赋值给e for (int j = i; j < list->length; j++) // 第i个元素及后面的元素向前移动一个位置 { list->data[j - 1] = list->data[j]; } list->length--; // 链表长度减一 return OK; } int main() { SqList list; InitList(&list); // 初始化一个顺序表 // 模拟往链表中插入三个数据 list.data[0] = 11; list.length = 1; list.data[1] = 22; list.length = 2; list.data[2] = 33; list.length = 3; // 打印顺序表 for (int i = 0; i < list.length; i++) { printf("data[%d] = %d\n", i, list.data[i]); } ElemType e; ListDelete(&list, 3, &e) ? printf("delete OK,e = %d\n", e) : printf("delete ERROR\n"); ListDelete(&list, 1, &e) ? printf("delete OK, e = %d\n", e) : printf("delete ERROR\n"); for (int i = 0; i < list.length; i++) { printf("data[%d] = %d\n", i, list.data[i]); } return 0; }
2.2.2_2 顺序表的查找
-
顺序表的基本操作——按位查找
时间复杂度 O(1) #include <stdio.h> #define MAXSIZE 10 #define OK 1 // 表示操作成功 #define ERROR 0 // 表示操作失败 typedef int Status; // 状态码,其值有OK 和 ERROR typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; int length; } SqList; // 基本操作,初始化一个顺序表 void InitList(SqList* list) { for (int i = 0; i < MAXSIZE; i++) { list->data[i] = 0; } list->length = 0; } // 基本操作,获取表L中第i个位置的元素的值。 Status GetElem(SqList list, int i, ElemType *e) { if (i < 0 || i > list.length) return ERROR; *e = list.data[i - 1]; // 将位序位 i 的元素赋值给e return OK; } int main() { SqList list; InitList(&list); // 初始化一个顺序表 // 模拟往链表中插入三个数据 list.data[0] = 11; list.length = 1; list.data[1] = 22; list.length = 2; list.data[2] = 33; list.length = 3; ElemType e; GetElem(list, 1, &e) ? printf("GetElem OK, e = %d\n", e) : printf("GetElem ERROR\n", e); return 0; }
-
顺序表的基本操作——按值查找
最差时间复杂度 O(n) 最好时间复杂度 O(1) 平均时间复杂度 O(n) #include <stdio.h> #define MAXSIZE 10 #define OK 1 // 表示操作成功 #define ERROR 0 // 表示操作失败 typedef int Status; // 状态码,其值有OK 和 ERROR typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; int length; } SqList; // 基本操作,初始化一个顺序表 void InitList(SqList* list) { for (int i = 0; i < MAXSIZE; i++) { list->data[i] = 0; } list->length = 0; } // 基本操作,获取表L中第i个位置的元素的值。 Status GetElem(SqList list, int i, ElemType *e) { if (i < 0 || i > list.length) return ERROR; *e = list.data[i - 1]; // 返回位序位 i 的元素 return OK; } /*基本操作,按值查找,*/ /*如果找到返回L中第1个与e满足关系的数据元素的位序。*/ /*如果未找到返回 -1 */ int LocateElem(SqList list, ElemType e) { for (int i = 0; i < list.length; i++) { if (e == list.data[i]) return i + 1; // 返回与e满足关系的数据元素的位序。 } return -1; // 表示未找到 } int main() { SqList list; InitList(&list); // 初始化一个顺序表 // 模拟往链表中插入三个数据 list.data[0] = 11; list.length = 1; list.data[1] = 22; list.length = 2; list.data[2] = 33; list.length = 3; ElemType e; GetElem(list, 1, &e) ? printf("GetElem OK, e = %d\n", e) : printf("GetElem ERROR\n", e); int i = LocateElem(list, 22); if (i != -1) printf("Your element in the list and rank is %d\n", i); else printf("Your element not in the list!\n"); return 0; }
2.3 线性表的链式表示
2.3.1 单链表的定义
-
单链表的定义:线性表的链式存储又称单链表,它是通过一组任意的存储单元来存储线性表中的数据元素。-优点:不要求大片连续空间,改变容量方便
缺点:不可随机存取,要耗费一定空间存放指针
-
不带头结点的单链表的定义及判空操作
#include <stdio.h> #define OK 1 #define ERROR 0 typedef int Status; typedef int ElemType; typedef struct LNode { ElemType data; struct LNode* next; } LNode, *LinkList; /*初始化一个不带头结点的空链表*/ // 注意:此时L必须是一个二级指针,因为我们要在被调函数中修改调用函数中指针变量的值 Status InitList(LinkList* L) { *L = NULL; // 空表,暂时还没有任何节点 return OK; } /*不带头结点的链表的判空操作*/ Status isEmpty(LinkList L) { return L == NULL; } int main() { LinkList L; // 声明一个指向单链表的指针 // 初始化一个单链表 InitList(&L) ? printf("init is OK\n") : printf("init is ERROR\n"); // 判断链表是否为空 isEmpty(L) ? printf("L is NULL\n") : printf("L is not NULL\n"); return 0; }
-
带头结点的单链表的定义及判空
#include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 typedef int Status; typedef int ElemType; typedef struct LNode { ElemType data; struct LNode* next; } LNode, * LinkList; /*初始化一个带头结点的空链表*/ // 注意:此时L必须是一个二级指针,因为我们要在被调函数中修改调用函数中指针变量的值 Status InitList(LinkList* L) { *L = (LNode*)malloc(sizeof(LNode)); if (*L == NULL) return ERROR; // 表示内存不足,malloc申请空间失败 (*L)->next = NULL; // 头节点之后暂时还没有节点 return OK; } /*带头结点的链表的判空操作*/ Status isEmpty(LinkList L) { return L->next == NULL; } int main() { LinkList L; // 声明一个指向单链表的指针 // 初始化一个单链表 InitList(&L) ? printf("init is OK\n") : printf("init is ERROR\n"); // 判断链表是否为空 isEmpty(L) ? printf("L is NULL\n") : printf("L is not NULL\n"); return 0; }
2.3.2_1 单链表的插入删除(带头节点)
-
Status ListInsert(LinkList* L, int i, ElemType e)
在表中第i个位置插入元素 e
时间复杂度 O(n) #include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 typedef int ElemType; typedef int Status; typedef struct LNode { ElemType data; struct LNode* next; }LNode, * LinkList; /*初始化一个带头结点的空链表*/ Status InitList(LinkList* L) { *L = (LNode*)malloc(sizeof(LNode)); if (*L == NULL) return ERROR; (*L)->next = NULL; return OK; } /*在表中的第i个位置插入指定元素 e */ Status ListInsert(LinkList* L, int i, ElemType e) { if (i < 1) // 判断插入位置是合法 return ERROR; LNode* p = *L; // 指针p指向头节点,第0个节点,不存储数据 int j = 0; while (j < i - 1 && p != NULL) // 循环找到第 i - 1个节点 { p = p->next; j++; } if (p == NULL) // i值不合法 return ERROR; LNode* s = (LNode*)malloc(sizeof(LNode)); // 申请要新插入的节点 s->data = e; s->next = p->next; p->next = s; // 将节点s连到p原来的后继 return OK; // 插入成功 } int main() { LinkList L; InitList(&L); ListInsert(&L, 1, 66); ListInsert(&L, 2, 77); ListInsert(&L, 1, 88); LNode* p = L->next; while (p) { printf("%d\n", p->data); p = p->next; } return 0; }
-
Status InsertNextNode(LNode* p, ElemType e)
后插操作:在p节点元素之后插入 e
注意:封装了该操作之后会简化
ListInsert()
函数的实现#include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 typedef int ElemType; typedef int Status; typedef struct LNode { ElemType data; struct LNode* next; }LNode, * LinkList; /*初始化一个带头结点的空链表*/ Status InitList(LinkList* L) { *L = (LNode*)malloc(sizeof(LNode)); if (*L == NULL) return ERROR; (*L)->next = NULL; return OK; } /*后插操作:在p节点元素之后插入 e */ Status InsertNextNode(LNode* p, ElemType e) { if (p == NULL) return ERROR; LNode* s = (LNode*)malloc(sizeof(LNode)); if (s == NULL) return ERROR; // 内存分配失败 s->data = e; // 用节点s保存数据e s->next = p->next; p->next = s; // 将节点s连到 p原来的后继 return OK; } /*在表中的第i个位置插入指定元素 e */ Status ListInsert(LinkList* L, int i, ElemType e) { if (i < 1) // 判断插入位置是合法 return ERROR; LNode* p = *L; // 指针p指向头节点,第0个节点,不存储数据 int j = 0; while (j < i - 1 && p != NULL) // 循环找到第 i - 1个节点 { p = p->next; j++; } return InsertNextNode(p, e); // 插入成功 } int main() { LinkList L; InitList(&L); ListInsert(&L, 1, 66); ListInsert(&L, 2, 77); ListInsert(&L, 1, 88); LNode* p = L->next; while (p) { printf("%d\n", p->data); p = p->next; } return 0; }
-
Status InsertPriorNode(LNode* p, ElemType e)
在p节点元素之前插入元素 e
两种解决方案:
(a)我们需要把头指针传入这个函数,循环遍历找到指定节点的前一个节点,然后在该位置插入新节 点。时间复杂度为O(n)
(b)我们直接在指定节点后方插入一个新节点,然后让这两个节点中的值互换,营造一种在指定节点前 插入新节点的假象。(代码实现我们采用的是这种)
#include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 typedef int ElemType; typedef int Status; typedef struct LNode { ElemType data; struct LNode* next; }LNode, * LinkList; /*初始化一个带头结点的空链表*/ Status InitList(LinkList* L) { *L = (LNode*)malloc(sizeof(LNode)); if (*L == NULL) return ERROR; (*L)->next = NULL; return OK; } /*在p节点元素之前插入元素 e */ Status InsertPriorNode(LNode* p, ElemType e) { if (p == NULL) return ERROR; LNode* s = (LNode *)malloc(sizeof(LNode)); if (s == NULL) return ERROR; s->next = p->next; p->next = s; // 新节点s连接到p的后面 s->data = p->data; // 将p中元素复制到s中 p->data = e; // p中元素覆盖为e return OK; } int main() { LinkList L; InitList(&L); // 模拟向链表中添加一个值为999的节点 LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = 999; s->next = NULL; L->next = s; // 测试 InsertPriorNode 函数是否可用 InsertPriorNode(s, 66); InsertPriorNode(s, 77); LNode* p = L->next; while (p) { printf("%d\n", p->data); p = p->next; } // 预计输出结果:66 77 999 // 但是实际输出结果是:77 66 999 // 为什么呢?因为链表中本身就有s(999)这个节点 我们向 999 前面 插入 66 // 其实本质上是向999 后面插入了 一个 66,然后让66 和 999 这两个值互换但是节点没换 // 此时s节点保存的值是66,我们向s节点之前插入77 其实是在66 后面插入77 然后互换 // 所以输出结果是 77 66 999 return 0; }
-
Status ListDelete(LinkList L, int i, ElemType* e)
按位序删除
最坏时间复杂度 O(n) 平均时间复杂度 O(n) 最好时间复杂度 O(1) #include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 typedef int ElemType; typedef int Status; typedef struct LNode { ElemType data; struct LNode* next; }LNode, * LinkList; /*初始化一个带头结点的空链表*/ Status InitList(LinkList* L) { *L = (LNode*)malloc(sizeof(LNode)); if (*L == NULL) return ERROR; (*L)->next = NULL; return OK; } /*按位序删除*/ Status ListDelete(LinkList L, int i, ElemType* e) { if (i < 0) return ERROR; LNode* p = L; // 让p节点指向链表的头节点 int j = 0; while (j < i - 1 && p != NULL) // 循环找到第 i - 1个节点 { j++; p = p->next; } if (p == NULL || p->next == NULL) // i值不合法 return ERROR; LNode* q = p->next; *e = q->data; // 令q指向被删除的节点 p->next = q->next; // 将 *q节点从链中断开 free(q); // 释放节点的存储空间 return OK; // 删除成功 } int main() { LinkList L; InitList(&L); // 模拟向链表中添加一个值为999的节点 LNode* s1 = (LNode*)malloc(sizeof(LNode)); s1->data = 999; s1->next = NULL; L->next = s1; // 模拟向链表中添加一个值为888的节点 LNode* s2 = (LNode*)malloc(sizeof(LNode)); s2->data = 888; s2->next = NULL; s1->next = s2; // 模拟向链表中添加一个值为777的节点 LNode* s3 = (LNode*)malloc(sizeof(LNode)); s3->data = 777; s3->next = NULL; s2->next = s3; LNode* p = L->next; while (p != NULL) { printf("%d\n", p->data); p = p->next; } ElemType e; // 按位序删除 ListDelete(L, 1, &e) ? printf("delete OK, and e = %d\n", e) : printf("delete ERROR!\n"); ListDelete(L, 3, &e) ? printf("delete OK, and e = %d\n", e) : printf("delete ERROR!\n"); ListDelete(L, 2, &e) ? printf("delete OK, and e = %d\n", e) : printf("delete ERROR!\n"); p = L->next; while (p != NULL) { printf("%d\n", p->data); p = p->next; } return 0; }
-
Status DeleteNode(LinkList L, LNode* p)
指定节点的删除
两种解决方案:
(a)我们使用类似于在指定节点前插的操作,只需传入需要删除的节点的指针,然后将指定节点后一个 节点保存的值赋值给指定删除的节点,然后将指定删除的节点的后继给删了,营造了一种删除指定节 点的假象。优点是时间复杂度低只需O(1),缺点是在删除最后一个元素时此种方法不具有通用性。
(b)直接将头指针和指定删除节点的指针传给此函数,此函数拿着头指针直接循环遍历找到指定删除节 点的前驱节点,然后让前驱节点直接指向指定删除的节点的后继,再释放被删除节点的内存即可。**优 点是该方法具有通用性,无论删除哪个节点都可以使用,缺点是时间发复杂度高:最坏和平均时间复 杂度都达到了O(n)级别。**我们代码实现采用的是这种。
#include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 typedef int ElemType; typedef int Status; typedef struct LNode { ElemType data; struct LNode* next; }LNode, * LinkList; /*初始化一个带头结点的空链表*/ Status InitList(LinkList* L) { *L = (LNode*)malloc(sizeof(LNode)); if (*L == NULL) return ERROR; (*L)->next = NULL; return OK; } // 比较两个节点是否相同 Status isEquals(LNode* p1, LNode* p2) { return p1->next == p2->next && p1->data == p2->data; } /*指定节点删除*/ Status DeleteNode(LinkList L, LNode* p) { if (p == NULL || L == NULL) return ERROR; LNode* s = L; // 让s节点指向头节点 while (!isEquals(s->next, p) && s != NULL) // 循环遍历找到指定节点的前一个节点 { s = s->next; } if (s == NULL || s->next == NULL) // 指定节点未找到 return ERROR; LNode* q = s->next; s->next = q->next; // 让s指向指定删除节点的后继节点 free(q); // 释放被删除的节点 return OK; } int main() { LinkList L; InitList(&L); // 模拟向链表中添加一个值为999的节点 LNode* s1 = (LNode*)malloc(sizeof(LNode)); s1->data = 999; s1->next = NULL; L->next = s1; // 模拟向链表中添加一个值为888的节点 LNode* s2 = (LNode*)malloc(sizeof(LNode)); s2->data = 888; s2->next = NULL; s1->next = s2; // 模拟向链表中添加一个值为777的节点 LNode* s3 = (LNode*)malloc(sizeof(LNode)); s3->data = 777; s3->next = NULL; s2->next = s3; LNode* p = L->next; while (p != NULL) { printf("%d\n", p->data); p = p->next; } DeleteNode(L, s3); // 删除 s3 节点 DeleteNode(L, s1); // 删除 s1 节点 printf("------------------\n"); p = L->next; while (p != NULL) { printf("%d\n", p->data); p = p->next; } return 0; }
2.3.2_2 单链表的查找
-
LNode* GetElem(LinkList L, int i)
按位查找——返回第i个元素
最坏时间复杂度 O(n) 平均时间复杂度 O(n) 最好时间复杂度 O(1) #include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 typedef int ElemType; typedef int Status; typedef struct LNode { ElemType data; struct LNode* next; }LNode, *LinkList; /*带头链表的初始化操作*/ Status InitList(LinkList *L) { LNode *s = (LNode*)malloc(sizeof(LNode)); if (s == NULL) return ERROR; s->next = NULL; (*L) = s; } /*按位查找——返回第i个元素*/ LNode* GetElem(LinkList L, int i) { if (i < 0) return NULL; int j = 0; LNode* p = L; // p指向被扫描的节点,现在指向L的头节点 while (j < i && p != NULL) // 循环遍历找到第 i 的节点 { p = p->next; j++; } return p; } int main() { LinkList L; InitList(&L); // 模拟向链表中添加一个值为999的节点 LNode* s1 = (LNode*)malloc(sizeof(LNode)); s1->data = 999; s1->next = NULL; L->next = s1; // 模拟向链表中添加一个值为888的节点 LNode* s2 = (LNode*)malloc(sizeof(LNode)); s2->data = 888; s2->next = NULL; s1->next = s2; // 模拟向链表中添加一个值为777的节点 LNode* s3 = (LNode*)malloc(sizeof(LNode)); s3->data = 777; s3->next = NULL; s2->next = s3; LNode* res1 = GetElem(L, 1); LNode* res2 = GetElem(L, 2); LNode* res3 = GetElem(L, 3); printf("res1->data = %d\n", res1->data); printf("res2->data = %d\n", res2->data); printf("res3->data = %d\n", res3->data); return 0; }
-
LNode* LocateElem(LinkList L, ElemType e)
按值查找,找到数据域等于 e 的节点
最坏时间复杂度 O(n) 平均时间复杂度 O(n) 最好时间复杂度 O(1) #include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 typedef int ElemType; typedef int Status; typedef struct LNode { ElemType data; struct LNode* next; }LNode, *LinkList; /*带头链表的初始化操作*/ Status InitList(LinkList *L) { LNode *s = (LNode*)malloc(sizeof(LNode)); if (s == NULL) return ERROR; s->next = NULL; (*L) = s; } /*按值查找,找到数据域等于 e 的节点*/ LNode* LocateElem(LinkList L, ElemType e) { LNode* p = L->next;// 从第一个节点开始查找数据为 e 的节点 while (p != NULL && p->data != e) p = p->next; return p; // 找到了则返回 p, 找不到则返回NULL } int main() { LinkList L; InitList(&L); // 模拟向链表中添加一个值为999的节点 LNode* s1 = (LNode*)malloc(sizeof(LNode)); s1->data = 999; s1->next = NULL; L->next = s1; // 模拟向链表中添加一个值为888的节点 LNode* s2 = (LNode*)malloc(sizeof(LNode)); s2->data = 888; s2->next = NULL; s1->next = s2; // 模拟向链表中添加一个值为777的节点 LNode* s3 = (LNode*)malloc(sizeof(LNode)); s3->data = 777; s3->next = NULL; s2->next = s3; LNode* res1 = LocateElem(L, 777); res1 != NULL ? printf("%d\n", res1->data) : printf("not found\n"); LNode* res2 = LocateElem(L, 222); res2 != NULL ? printf("%d\n", res2->data) : printf("not found\n"); return 0; }
-
int Length(LinkList L)
求表的长度
时间复杂度 O(n) #include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 typedef int ElemType; typedef int Status; typedef struct LNode { ElemType data; struct LNode* next; }LNode, *LinkList; /*带头链表的初始化操作*/ Status InitList(LinkList *L) { LNode *s = (LNode*)malloc(sizeof(LNode)); if (s == NULL) return ERROR; s->next = NULL; (*L) = s; } /*求表的长度*/ int Length(LinkList L) { int length = 0; LNode* p = L; while (p->next != NULL) { p = p->next; length++; } return length; } int main() { LinkList L; InitList(&L); // 模拟向链表中添加一个值为999的节点 LNode* s1 = (LNode*)malloc(sizeof(LNode)); s1->data = 999; s1->next = NULL; L->next = s1; int len = Length(L); printf("L len is %d\n", len); return 0; }
2.3.2_3 单链表的建立
-
尾插法建立单链表
两种实现思路:
(a)初始化单链表
设置变量 length 记录链表长度
While 循环 {
每次取一个数据元素 e;
ListInsert (L, length+1, e)
插到尾部; length++;
}
这种实现思路时间复杂度为O(n²)
(b)初始化单链表
设置一个rear尾指针,一直指向链表尾节点
While 循环 {
每次取一个数据元素 e;
Status InsertNextNode(r, e)
插到尾部 rear指向新的尾节点
}
这种实现思路的时间复杂度为O(n)
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> typedef int ElemType; typedef struct LNode { ElemType data; struct LNode* next; }LNode, * LinkList; /*尾插法建立单链表*/ LNode* List_TailInsert(LinkList* L) { int x; // 设 ElemType为整型 (*L) = (LNode*)malloc(sizeof(LNode)); // 建立头节点 (*L)->next = NULL; LNode *s, *r = *L; // r为表尾指针 scanf("%d", &x); while (x != 9999) // 输入9999表示结束 { s = (LNode*)malloc(sizeof(LNode)); s->data = x; s->next = NULL; r->next = s; // 将新节点插入表尾 r = s; // 保证表尾指针一直指向表尾节点 scanf("%d", &x); } return L; } int main() { LinkList L; List_TailInsert(&L); // 尾插法建立单链表 LNode* p = L->next; while (p != NULL) // 循环遍历输出单链表 { printf("%d\t", p->data); p = p->next; } return 0; }
-
头插法建立单链表
实现思路:
初始化单链表
While 循环 {
每次取一个数据元素 e;
InsertNextNode (L, e)
; }
时间复杂度 O(1) #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> typedef int ElemType; typedef struct LNode { ElemType data; struct LNode* next; }LNode, * LinkList; /*头插法建立单链表*/ LNode* List_HeadInsert(LinkList* L) { int x; // 设 ElemType为整型 (*L) = (LNode*)malloc(sizeof(LNode)); // 建立头节点 (*L)->next = NULL; LNode* s; scanf("%d", &x); while (x != 9999) // 输入9999表示结束 { s = (LNode*)malloc(sizeof(LNode)); s->data = x; s->next = NULL; s->next = (*L)->next; // 将 s 的后继设置为原来头节点的后继 (*L)->next = s; // 将新节点插入到头节点的后面,做头节点的后继 scanf("%d", &x); } return L; } int main() { LinkList L; List_HeadInsert(&L); // 头插法建立单链表 LNode* p = L->next; while (p != NULL) // 循环遍历输出单链表 { printf("%d\t", p->data); p = p->next; } return 0; }
-
头插法重要应用——链表逆置
实现思路:
循环遍历整个链表
每循环到一个节点,就把这个节点从链表中断开,用头插法的思想插入到链表中
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> typedef int ElemType; typedef struct LNode { ElemType data; struct LNode* next; }LNode, * LinkList; /*链表逆置*/ LinkList List_Reverse(LinkList L) { LNode* p, * r; // 两个都是用来遍历链表的 p = L->next; // p指向第一个节点 L->next = NULL; // 直接断开链表 while (p != NULL) // 循环遍历L链表 { r = p->next; // 存储p节点的下一个节点,因为下面要修改p->next p->next = L->next; // 让插入节点指向原来头节点的后继 L->next = p; // 让链表头节点指向要插入的节点 p = r; // 把 p的下一个节点赋值给p,来完成循环遍历整张链表的目的 } return L; } /*尾插法建立单链表*/ // 辅助建立单链表,用来测试List_Reverse() LNode* List_TailInsert(LinkList* L) { int x; // 设 ElemType为整型 (*L) = (LNode*)malloc(sizeof(LNode)); // 建立头节点 (*L)->next = NULL; LNode* s, * r = *L; // r为表尾指针 scanf("%d", &x); while (x != 9999) // 输入9999表示结束 { s = (LNode*)malloc(sizeof(LNode)); s->data = x; s->next = NULL; r->next = s; // 将新节点插入表尾 r = s; // 保证表尾指针一直指向表尾节点 scanf("%d", &x); } return L; } int main() { LinkList L; List_TailInsert(&L); // 尾插法建立单链表 LNode* p = L->next; while (p != NULL) // 循环遍历输出单链表 { printf("%d\t", p->data); p = p->next; } printf("\n"); List_Reverse(L); p = L->next; while (p != NULL) // 循环遍历输出单链表 { printf("%d\t", p->data); p = p->next; } return 0; }
2.4 双链表
2.4.1 双链表的初始化和判空
#include <stdio.h>
#include <stdlib.h>
#define true 1
#define false 0
typedef int bool;
typedef int ElemType;
typedef struct DNode { // 定义双链表节点类型
ElemType data; // 数据域
struct DNode *prior, *next; // 分别带票前驱指针和后继指针
}DNode, *DLinkList;
/*初始化一个双链表*/
bool InitDList(DLinkList* L)
{
(*L) = (DNode *)malloc(sizeof(DNode)); // 申请头结点,并让头指针指向头节点
if (*L == NULL) // 内存不足,分配失败
return false;
(*L)->next = NULL; // 让头节点的后继指针指向NULL
(*L)->prior = NULL; // 让头节点的前驱指针指向NULL
return true;
}
/*判断双链表是否为空*/
bool isEmpty(DLinkList L)
{
if (L->next == NULL)
return true; // 双链表为空,返回 true
return false; // 双链表不为空,返回 false
}
int main()
{
DLinkList L;
InitDList(&L) ? printf("init OK\n") : printf("init ERROR\n");
isEmpty(L) ? printf("L is empty\n") : printf("L not is empty\n");
return 0;
}
2.4.2 双链表的插入&删除&销毁
#include <stdio.h>
#include <stdlib.h>
#define true 1
#define false 0
typedef int bool;
typedef int ElemType;
typedef struct DNode { // 定义双链表节点类型
ElemType data; // 数据域
struct DNode* prior, * next; // 分别带票前驱指针和后继指针
}DNode, * DLinkList;
/*初始化一个双链表*/
bool InitDList(DLinkList* L)
{
(*L) = (DNode*)malloc(sizeof(DNode)); // 申请头结点,并让头指针指向头节点
if (*L == NULL) // 内存不足,分配失败
return false;
(*L)->next = NULL; // 让头节点的后继指针指向NULL
(*L)->prior = NULL; // 让头节点的前驱指针指向NULL
return true;
}
/*判断双链表是否为空*/
bool isEmpty(DLinkList L)
{
if (L->next == NULL)
return true; // 双链表为空,返回 true
return false; // 双链表不为空,返回 false
}
/*在p节点之后插入s节点*/
bool InsertNextDNode(DNode* p, DNode* s)
{
if (p == NULL || s == NULL) // 非法参数
return false;
s->next = p->next;
if(p->next != NULL) // 如果p节点有后继节点
p->next->prior = s;
p->next = s;
s->prior = p;
return true;
}
/*删除p节点的后继节点*/
bool DeleteNextNode(DNode* p)
{
if (p == NULL || p->next == NULL) // 参数不合法
return false;
DNode* q = p->next;
p->next = q->next;
if (q->next != NULL) // 如果被删除的节点有后继节点
q->next->prior = p;
free(q); // 释放被删除节点的空间
return true;
}
/*销毁链表*/
bool DestoryDList(DLinkList *L)
{
while ((*L)->next != NULL)
DeleteNextNode(*L);
free(*L);
*L = NULL;
}
int main()
{
DLinkList L;
InitDList(&L);
DNode* s1 = (DNode*)malloc(sizeof(DNode));
s1->data = 666;
InsertNextDNode(L, s1); // 插入节点 s1
DNode* s2 = (DNode*)malloc(sizeof(DNode));
s2->data = 777;
InsertNextDNode(s1, s2); // 向 s1 后面插入 s2
DeleteNextNode(s1); // 删除 s1 后面的节点
DestoryDList(&L); // 销毁 双向链表L
return 0;
}
注意:双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。时间复杂度 O(n)
2.5 循环链表
2.5.1 循环单链表
#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
typedef int Status;
typedef int ElemType;
typedef struct LNode {
ElemType data;
struct LNode* next;
}LNode, *LinkList;
/*循环单链表的初始化*/
Status InitList(LinkList* L)
{
*L = (LNode*)malloc(sizeof(LNode));
if (*L == NULL) // 内存不足,分配失败
return ERROR;
(*L)->next = *L; // 头节点的后继指针指向头节点自身
return OK;
}
/*判断单链表是否为空*/
Status isEmpty(LinkList L)
{
if (L->next == L)
return OK;
return ERROR;
}
/*判断p节点是否为链表的表尾节点*/
Status isTail(LinkList L,LNode* p)
{
if (p->next == L)
return OK;
return ERROR;
}
int main()
{
LinkList L;
InitList(&L) ? printf("init OK\n") : printf("init ERROR\n");
// 模拟像链表中添加一个节点s
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = 666;
L->next = s;
s->next = L;
// 判空
isEmpty(L) ? printf("The List is NULL\n") : printf("The List not is NULL\n");
// 判断是否是尾节点
isTail(L, s) ? printf("is Tail\n") : printf("not is Tail\n");
isTail(L, L) ? printf("is Tail\n") : printf("not is Tail\n");
return 0;
}
2.5.2 循环双链表
#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
typedef int Status;
typedef int ElemType;
typedef struct DNode {
ElemType data;
struct DNode *prior, *next;
}DNode, * DLinkList;
/*初始化空的循环双链表*/
Status InitDLinkList(DLinkList* L)
{
*L = (DNode*)malloc(sizeof(DNode));
if (*L == NULL) // 内存不足,分配失败
return ERROR;
(*L)->prior = *L; // 让头节点的前驱指针只想头节点自身
(*L)->next = *L; // 让头节点的后继指针只想头节点自身
return OK;
}
/*判断循环双链表是否为空*/
Status isEmpty(DLinkList L)
{
if (L->next == L)
return OK;
return ERROR;
}
/*判断节点p是否为循环双链表的尾节点*/
Status isTail(DLinkList L, DNode* p)
{
if (p->next == L)
return OK;
return ERROR;
}
/*在p节点之后插入s节点*/
Status InsertNextNode(DNode* p, DNode* s)
{
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
return OK;
}
/*删除p的后继节点*/
Status DeleteNextNode(DLinkList L, DNode* p)
{
DNode* q = p->next;
if (q == L) // 说明要删除的节点是头节点
return ERROR;
p->next = q->next;
q->next->prior = p;
free(q);
return OK;
}
int main()
{
DLinkList L;
InitDLinkList(&L);
isEmpty(L) ? printf("L is empty\n") : printf("L not is empty\n");
DNode* s = (DNode*)malloc(sizeof(DNode));
s->data = 666;
InsertNextNode(L, s);
isTail(s, L) ? printf("it is tail\n") : printf("it not is tail\n");
DeleteNextNode(L, L);
return 0;
}
2.6 静态链表
静态链表:用数组的方式实现的链表
优点:增、删 操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变
适用场景:
①不支持指针的低级语言;
②数据元素数量固定不变的场景
#include <stdio.h>
#include <stdlib.h>
#define true 1
#define false 0
#define MaxSize 10
typedef int bool;
typedef int ElemType;
typedef struct {
ElemType data; // 存储数据元素
int next; // 下一个元素的数组下标
}SLinkList[MaxSize]; // 可用 SLinkList 定义“一个长度为 MaxSize 的Node 型数组”
/*初始化静态链表*/
bool InitSLinkList(SLinkList L)
{
L[0].data = -1; // L[0]充当头节点,不存储数据
for (int i = 1; i < MaxSize; i++)
{
L[i].data = -2; // 让除了头节点以外的节点的指针域都存储-2,目的是为了区分节点是否可用
}
}
/*插入位序为i的节点*/
ListInsert(SLinkList L, int i, ElemType e)
{
// 1. 找到一个可用节点,把数据给存下来
// 2. 找到位序为 i - 1的节点
// 3. 修改新节点的 next
// 4. 修改 i-1 号节点的next
}
/*删除某个节点*/
void DeleteNode()
{
// 1. 从头结点出发找到前驱节点
// 2. 修改前驱节点的游标
// 3. 被删除的节点的 next 设为 -2
}
int main()
{
SLinkList L;
InitSLinkList(L);
return 0;
}
2.7 顺序表和链表的比较
-
逻辑结构:都属于线性表,都是线性结构
-
存储结构:
- 顺序表是顺序存储
- 优点:支持随机存取,存储密度高。
- 缺点:大片连续空间分配不方便,改变容量不方便
- 链表是链式存储
- 优点:离散的小空间分配方便,改变容量方便
- 缺点:不可随机读取,存储密度低。
- 顺序表是顺序存储
-
基本操作:
- 创
- 顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
- 采用静态分配时,容量不可改变。
- 采用动态分配时,容量可改变,但需要移动大量元素,时间代价高。
- 链表:只需分配一个头结点(也可以不要头结点,只声明一个头指针),之后方便拓展。
- 顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
- 销
- 增 / 删
- 顺序表:插入/删除元素要将后续元素都后移/前移,时间复杂度 O(n),时间开销主要来自移动元素
- 链表:插入/删除元素只需修改指针即可,时间复杂度 O(n),时间开销主要来自查找目标元素
- 查
- 顺序表:按位查找:O(1);按值查找:O(n)若表内元素有序,可在O(lo₂n) 时间内找到
- 链表:按位查找:O(n);按值查找:O(n)
- 创
-
综上可知:顺序表和链表各适用于什么样的场景
-
表长难以预估、经常要增加/删除元素 ——链表
-
表长可预估、查询(搜索)操作较多 ——顺序表
-
-
开放式问题的答题思路:
问题: 请描述顺序表和链表的 bla bla bla…实现线性表时,用顺序表还是链表好?
答:顺序表和链表的逻辑结构都是线性结构,都属于线性表。但是二者的存储结构不同,顺序表采用顺序存储…(特点,带来的优点缺点);链表采用链式存储…(特点、导致的优缺点)。`
由于采用不同的存储方式实现,因此基本操作的实现效率也不同。当初始化时…;当插入一个数据元
素时…;当删除一个数据元素时…;当查找一个数据元素时…