数据结构学习笔记
一、绪论
1.1 数据结构的基本概念
01、基本概念
- 数据
- 数据元素、数据项
- 数据对象、数据结构
- 数据类型、抽象数据类型(ADT)
02、三要素
- 逻辑结构
- 线性结构
- 非线性结构
- 集合
- 树形结构
- 图结构(网状结构)
- 物理结构(存储结构)
- 顺序存储
- 非顺序存储
- 链式存储
- 索引存储
- 散列存储
- 数据的运算
- 根据逻辑结构来定义,根据存储结构来实现
1.2 算法的基本概念
01、什么是算法
- 程序=数据结构+算法
- 数据结构要处理的信息
- 算法是处理信息的步骤
02、算法的五个特性
- 有穷性
- 有穷时间内能执行完
- 算法是有穷的
- 程序可以是无穷的
- 有穷时间内能执行完
- 确定性
- 输入相同输入只会产生相同输出
- 可行性
- 可以用已有的基本操作实现算法
- 输入
- 丢给算法处理的数据
- 输出
- 算法处理的结果
03、”好“算法的特质
- 正确性
- 能正确解决问题
- 可读性
- 对算法的描述要让其他人也看得懂
- 健壮性
- 算法能处理一些异常状况
- 高效率与低存储量需求
- 即算法执行省时、省内存
- 时间复杂度低、空间复杂度低
1.3 时间复杂度
01、如何计算
- 找到一个基本操作(最深层循环)
- 分析该基本操作的执行次数x与问题规模n的关系x=f(n)
- x的数量级O(x)就是算法时间复杂度T(n)
02、常用技巧
- 加法规则:O(f(n)) + O(g(n)) = O(max(f(n), g(n)))
- 乘法规则:O(f(n)) + O(g(n)) = O(f(n) x g(n))
- “常对幂指阶”:O(1) < O(log2n) < O(n) < O(nlog2n) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
03、三种复杂度
- 最坏时间复杂度:考虑输入数据”最坏“的情况
- 平均时间复杂度:考虑所有输入数据都等概率出现的情况
- 最好时间复杂度:考虑输入数据”最好“的情况
1.4 空间复杂度
01、如何计算
-
普通程序
- 找到所占空间大小与问题规模相关的变量
- 分析所占空间x与问题规模n的关系 x=f(n)
- x的数量级O(x)就是算法空间复杂度S(n)
-
递归程序
-
找到递归调用的深度x与问题规模的关系 x=f(n)
-
x的数量级O(x)就是算法空间复杂度S(n)
注:有的算法各层函数所需存储空间不同,分析方法略有区别
-
02、常用技巧
- 加法规则:O(f(n)) + O(g(n)) = O(max(f(n), g(n)))
- 乘法规则:O(f(n)) + O(g(n)) = O(f(n) x g(n))
- “常对幂指阶”:O(1) < O(log2n) < O(n) < O(nlog2n) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
二、线性表
2.1 线性表的定义和基本操作
01、定义(逻辑结构)
- 值得注意的特性:数据元素同类型、有限、有序
- 重要术语
- 表长、空表
- 表头、表尾
- 前驱、后继
- 数据元素的位序(从1开始)
02、基本操作(运算)
- 创销、增删改查(所有数据结构适用的记忆思路)
- 判空、判长、打印输出(还可以会根据实际需求增加其他基本操作)
- 其他值得主要的点:
- 理解什么时候要传入参数的引用“&”
- 函数命名要有可读性
2.2 顺序表的定义(线性表的顺序表示)
01、存储结构
- 逻辑上相邻的数据元素物理上也相邻
02、实现方式
-
静态分配
- 使用“静态数组”实现
- 大小一旦确定就无法改变
//顺序表的实现方式--静态分配 #include<stdio.h> #include<stdbool.h> //C语言中没有布尔类型 #define MaxSize 10 //定义最大长度 typedef struct { int data[MaxSize]; //用静态的“数组”存放数据元素 int length; //顺序表的当前长度 }SqList; //顺序表的类型定义 //初始化顺序表 void InitList(SqList *L) { for (int i = 0; i < MaxSize; i++) L->data[i] = 0; //将所有元素设置为默认初始值 L->length = 0; //顺序表的初始长度0 }
//顺序表的插入 bool ListInsert(SqList* L, int i, int e) { //在顺序表L的位序i插入元素e if (i < 1 || i > L->length+1)//判断i的范围是否有效 return false; if (L->length >= MaxSize) //存储空间已达到最大长度,不能继续插入 return false; for (int j = L->length; j > i - 1; j--)//将第i个元素及以后的元素后移 L->data[j] = L->data[j - 1]; L->data[i - 1] = e; //在位序为i的位置插入元素e L->length++; //长度+1 return true; } //顺序表的删除 bool ListDelete(SqList* L, int i) {//在顺序表L的位序i删除元素 if (i < 1 || i > L->length)//判断i的范围是否有效 return false; for (int j = i - 1; j < L->length; j++)//将第i个元素及以后的元素前移 L->data[j] = L->data[j + 1]; L->length--; //长度-1 return true; }
//按位查找 int GetElem(SqList L, int i) {//获取第i位序的元素 if (i < 1 || i > L.length) {//判断i的范围是否有效 printf("查找位序不合法!"); return -1; //返回-1查找失败 } return L.data[i - 1];//获取第i位序的元素 } //按元素值查找 int LocatieElem(SqList L, int e) {//获取顺序表L中第一个等于e的元素 for (int i = 0; i < L.length; i++)//遍历顺序表L if (L.data[i] == e) //判断元素值是否等于e return i + 1; //获取第i位序的元素 return 0; //返回0为没有找到 }
//调用 int main() { SqList L; //声明一个顺序表 InitList(&L); //初始化顺序表 ListInsert(&L, 1, 1); //在第1个位置上插入元素1 ListInsert(&L, 2, 2); //在第1个位置上插入元素1 ListInsert(&L, 3, 3); //在第1个位置上插入元素1 ListInsert(&L, 4, 2); //在第1个位置上插入元素1 ListDelete(&L, 5); //删除第5个位置上的元素 for (int i = 0; i < L.length; i++) printf("%d\n", L.data[i]);//查看当前数据表中的数据 printf("数据表长度:%d\n", L.length); //查看当前顺序表长度 printf("第2位的元素为:%d\n", GetElem(L, 2));//查看第2位的元素的值 printf("第一个元素值为2的位序为:%d", LocatieElem(L, 2));//查看第一个元素值为2的位序为 return 0; }
-
动态分配
- 使用“动态数组”实现
- L.data=(ElemType *)malloc(sizeof(ElemType) * size);
- 顺序表存满时,可再用malloc动态拓展顺序表的最大容量
- 需要将数据元素复制到新的存储区域,并用free函数释放原区域
//顺序表的实现方式--动态分配 #include<stdio.h> #include<stdbool.h> //C语言中没有布尔类型 #include<stdlib.h> //malloc、free函数的头文件 #define InitSize 10 //默认的表长度的初始定义 typedef struct { int* data; //指示动态分配数组的指针 int MaxSize; //顺序表的最大容量 int length; //顺序表的当前长度 }SeqList; //初始化顺序表 void InitList(SeqList* L) { L->data = (int*)malloc(InitSize * sizeof(int)); //用malloc函数申请一片连续的存储空间 L->length = 0; //初始化长度为0 L->MaxSize = InitSize; //初始化最大长度为默认表长度 }
//增加动态数组的长度 void IncreaseSize(SeqList* L, int len) { int* p = L->data; //定义一个p指针指向顺序表L中的存储空间 L->data = (int*)malloc((L->MaxSize + len) * sizeof(int)); //用malloc函数申请长度为MaxSize+len连续的存储空间 for (int i = 0; i < L->length; i++) { L->data[i] = p[i]; //将数据复制到新区域 } L->MaxSize = L->MaxSize + len; //顺序表最大长度增加 len free(p); //释放原来的内存空间 }
//顺序表的销毁 bool DestroyList(SeqList* L) { if (L->data) { free(L->data); //释放L->data指向的内存空间 L->data = NULL; //释放L->data本身 L->length = 0; //将顺序表长度置为0 L->MaxSize = 0; //最大长度也置为0 } else return false; return true; }
//顺序表的插入 bool ListInsert(SeqList* L, int i, int e) { //在顺序表L的位序i插入元素e if (i < 1 || i > L->length+1)//判断i的范围是否有效 return false; if (L->length >= L->MaxSize) //存储空间已达到最大长度,不能继续插入 return false; for (int j = L->length; j > i - 1; j--)//将第i个元素及以后的元素后移 L->data[j] = L->data[j - 1]; L->data[i - 1] = e; //在位序为i的位置插入元素e L->length++; //长度+1 return true; } //顺序表的删除 bool ListDelete(SeqList* L, int i) {//在顺序表L的位序i删除元素 if (i < 1 || i > L->length)//判断i的范围是否有效 return false; for (int j = i - 1; j < L->length; j++)//将第i个元素及以后的元素前移 L->data[j] = L->data[j + 1]; L->length--; //长度-1 return true; }
//按位查找 int GetElem(SeqList L, int i) {//获取第i位序的元素 if (i < 1 || i > L.length) {//判断i的范围是否有效 printf("查找位序不合法!"); return -1; //返回-1查找失败 } return L.data[i - 1];//获取第i位序的元素 } //按元素值查找 int LocatieElem(SeqList L, int e) {//获取顺序表L中第一个等于e的元素 for (int i = 0; i < L.length; i++)//遍历顺序表L if (L.data[i] == e) //判断元素值是否等于e return i + 1; //获取第i位序的元素 return 0; //返回0为没有找到 }
//调用 int main() { SeqList L; //声明一个顺序表 InitList(&L); //初始化顺序表 ListInsert(&L, 1, 1); //在第1个位置上插入元素1 ListInsert(&L, 2, 2); //在第1个位置上插入元素2 ListInsert(&L, 3, 3); //在第1个位置上插入元素3 ListInsert(&L, 4, 2); //在第1个位置上插入元素2 for (int i = 0; i < L.length; i++) printf("%d\n", L.data[i]); //查看当前数据表中的数据 ListDelete(&L, 3); //删除第5个位置上的元素 printf("数据表长度:%d\n", L.length); //查看当前顺序表长度 printf("第2位的元素为:%d\n", GetElem(L, 2)); //查看第2位的元素的值 printf("第一个元素值为2的位序为:%d\n", LocatieElem(L, 2));//查看第一个元素值为2的位序为 printf("增加之前长度:%d\n", L.MaxSize); IncreaseSize(&L, 5); //顺序表L中最大的数组长度+5 printf("增加之后长度:%d\n", L.MaxSize); DestroyList(&L); //销毁表 return 0; }
03、特点
- 随机访问:能在O(1)时间内找到第i个元素
- 存储密度高
- 拓展容量不方便
- 插入、删除数据元素不方便
2.3 单链表的定义
-
单链表
- 用“链式存储”(存储结构)实现了“线性结构”(逻辑结构)
- 一个节点存储一个数据元素
- 各结点间的先后关系用一个指针表示
-
用代码定义一个单链表
//单链表的定义 typedef struct Node { int data; //定义结点int类型的数据域 struct Node* next; //指针指向下一个结点 }Node, * LinkList;
-
两种实现:
-
不带头结点
//单链表的实现方式——不带头结点 //单链表的实现方式——带头结点 #include<stdio.h> #include<stdbool.h> //C语言中没有布尔类型 #include<stdlib.h> //malloc、free函数的头文件 //初始化一个单链表(不带头结点) bool InitList(LinkList *L) { (*L) = NULL; //空表暂时没有任何结点 printf("初始化成功!\n"); return true; }
- 空表判断:L==NULL。写代码不方便
-
带头结点(头结点不存数据,只是为了操作方便)
//单链表的实现方式——带头结点 #include<stdio.h> #include<stdbool.h> //C语言中没有布尔类型 #include<stdlib.h> //malloc、free函数的头文件 //初始化一个单链表(带头结点) bool InitList(LinkList *L) { (*L) = (Node*)malloc(sizeof(Node));//分配一个头结点申请内存空间 if ((*L) == NULL) //内存不足分配失败 return false; (*L)->next = NULL; //头结点之后暂时没有结点 printf("初始化成功!\n"); return true; }
- 空表判断:L->next == NULL。写代码更方便
-
-
其他值得注意的点:
- typedef关键字的用法
- “LinkList”等价于“LNode *”前者强调这是链表,后者强调这是结点,合适的地方使用合适的名字,代码可读性更高
定义
//单链表的实现方式——带头结点 #include<stdio.h> #include<stdbool.h> //C语言中没有布尔类型 #include<stdlib.h> //malloc、free函数的头文件 //单链表的定义 typedef struct Node { int data; //定义结点int类型的数据域 struct Node* next; //指针指向下一个结点 }Node,*LinkList;
初始化
//初始化一个单链表(带头结点) bool InitList(LinkList *L) { (*L) = (Node*)malloc(sizeof(Node));//分配一个头结点申请内存空间 if ((*L) == NULL) //内存不足分配失败 return false; (*L)->next = NULL; //头结点之后暂时没有结点 printf("初始化成功!\n"); return true; } //判断单链表是否为空 bool Empty(LinkList L) { if (L->next == NULL) //带头结点的下一个结点是否为空 return true; else return false; }
插入操作
//单链表按位序插入(带头结点) bool ListInsert(LinkList *L, int i, int e) { if (i < 1) //判断插入位序是否合法 return false; Node* p; //指针p指向当前扫描到的结点 int j = 0; //当前p指向的是第几个结点 p = (*L); //L指向头结点,头结点是第0个(不存数据) while (p != NULL && j < i - 1) {//循环找到第i-1个结点 p = p->next; j++; } if (p == NULL) //指向最后一个结点为空,插入不合法 return false; Node* s = (Node*)malloc(sizeof(Node));//给待插入的结点申请内存空间 s->data = e; //将新结点数据域赋值为e s->next = p->next; //新结点的指针指向当前p结点的下一个结点 p->next = s; //将结点s连接到p之后 return true; } //后插操作 bool InsertNextNode(Node* p, int e) { //在p结点之后插入元素e if (p == NULL) //指向最后一个结点为空,插入不合法 return false; Node* s = (Node*)malloc(sizeof(Node));//给待插入的结点申请内存空间 if (s == NULL) //内存分配失败 return false; s->data = e; //将新结点数据域赋值为e s->next = p->next; //新结点的指针指向当前p结点的下一个结点 p->next = s; //将结点s连接到p之后 return true; } //前插操作 bool InsertPriorNode(Node* p, int e) { //在p结点之前插入元素e if (p == NULL) //指向最后一个结点为空,插入不合法 return false; Node* s = (Node*)malloc(sizeof(Node));//给待插入的结点申请内存空间 if (s == NULL) //内存分配失败 return false; s->next = p->next; //新结点的指针指向当前p结点的下一个结点 p->next = s; //将结点s连接到p之后 s->data = p->data; s->data = e; //将新结点数据域赋值为e return true; }
删除操作
//按位序删除(带头结点) bool ListDelete(LinkList *L,int i, int e) { //删除表L中第i个位置的元素,并用e返回删除元素的值 if (i < 1) //判断删除位序是否合法 return false; Node* p; //指针p指向当前扫描到的结点 int j = 0; //当前p指向的是第几个结点 p = (*L); //L指向头结点,头结点是第0个(不存数据) while (p != NULL && j < i - 1) {//循环找到第i-1个结点 p = p->next; j++; } if (p == NULL) //指向最后一个结点为空,删除不合法 return false; Node* q = p->next; //令q指向被删除结点 e = q->data; //用e返回元素的值 p->next = q->next; //将*q结点从链中“断开” free(q); //释放结点的存储空间 printf("删除结点的值为:%d\n", e); return true; } //删除指定结点(删除的为最后一个结点时出现BUG) bool DeleteNode(Node* p) { if (p == NULL) //指向最后一个结点为空,删除不合法 return false; Node* q = p->next; //令q指向*p的后继结点 p->data = p->next->data; //和后继结点交换数据与 p->next = q->next; //将*q结点从链中“断开” free(q); //释放结点的存储空间 return true; }
查找操作
//按位查找(带头结点) Node* GetElem(LinkList L, int i) { //返回第i个元素 if (i < 1) //判断插入位序是否合法 return NULL; Node* p; //指针p指向当前扫描到的结点 int j = 0; //当前p指向的是第几个结点 p = L; //L指向头结点,头结点是第0个(不存数据) while (p != NULL && j < i - 1) {//循环找到第i-1个结点 p = p->next; j++; } return p; } //按值查找 Node* LocateElem(LinkList L, int e) {//找到数据域==e的结点 Node* p = L->next; //p指针指向链表L的结点 while (p != NULL && p->data != e)//从第1个结点开始查找数据域e的结点 p = p->next; return p; }
//求表的长度 int Length(LinkList L) { int len = 0;//统计表长 Node* p = L;//p指针指向链表L的头结点 while (p->next != NULL) {//遍历链表统计长度 p = p->next; len++; } return len; }
单链表建立
//建立单链表——尾插法(带头结点):得到为插入的正序 LinkList List_TailInsert(LinkList *L) {//正向建立单链表 int x; //结点数据域数据类型为整型 (*L) = (LinkList)malloc(sizeof(Node));//建立头结点 Node* s, * r = (*L); //声明s,r指针指向头结点,r为表尾指针 scanf("%d", &x); //输入结点的值 while (x != 408) { //输入408表示结束 s = (Node*)malloc(sizeof(Node));//将s的指针指向新结点 s->data = x; //将数据存入新结点中 r->next = s; //并将存入数据的新结点插入到r结点之后 r = s; //r指向新的表尾结点 scanf("%d", &x); //继续输入结点的值 } r->next = NULL; //尾结点指针置空 return (*L); } //建立单链表——头插法(带头结点):得到为插入的逆序 LinkList List_HeadInsert(LinkList *L) {//逆向建立单链表 Node* s; //声明一个s指针 int x; //结点数据域数据类型为整型 (*L) = (LinkList)malloc(sizeof(Node));//建立头结点 (*L)->next = NULL; //将头结点指向NULL(防止脏数据) scanf("%d", &x); //输入结点的值 while (x != 408) { //输入408表示结束 s = (Node*)malloc(sizeof(Node));//将s的指针指向新结点 s->data = x; //将数据存入新结点中 s->next = (*L)->next; //将新结点的下一个结点指向头结点的下一个结点(NULL) (*L)->next = s; //并将头结点的下一个结点指向存入新数据的s结点中 scanf("%d", &x); //继续输入结点的值 } return (*L); }
主函数调用
//单链表的打印(带头结点) void printlist(LinkList L) { printf("该链表的内容为:"); while (L->next != NULL) { //遍历链表L printf("%d ", L->next->data);//打印头结点的下一个结点中的数据 L = L->next; //将当前结点指向下一个结点 } printf("\n"); } //调用 void main() { int e = 0; //定义一个为0的数据值 LinkList L //声明一个指向单链表的指针并动态分配存储空间 InitList(&L); //初始化一个空表 printf("头插法1 2 3:\n"); L = List_HeadInsert(&L); //头插法建立链表1 2 3 (408结束) printlist(L); //打印链表 printf("当前链表长度:%d\n", Length(L));//打印当前链表长度 printf("尾插法1 2 3:\n"); L = List_TailInsert(&L); //尾插法建立链表1 2 3 (408结束) printlist(L); //打印链表 printf("当前链表长度:%d\n", Length(L));//打印当前链表长度 printf("在链表第2位处插入4:\n"); ListInsert(&L, 2, 4); //在链表第2位处插入4 printlist(L); //打印链表 printf("当前链表长度:%d\n", Length(L));//打印当前链表长度 printf("删除链表第1位处的数据值:\n"); ListDelete(&L, 1, e); //删除链表第1位处的数据值并将删除值赋值给e printlist(L); //打印链表 printf("当前链表长度:%d\n", Length(L));//打印当前链表长度 }
2.4 双链表
01、初始化
- 头结点的prior、next都指向NULL
//初始化双链表
bool InitDLinkList(DLinkList* L) {
(*L) = (DNode*)malloc(sizeof(DNode)); //设置头结点并分配内存空间
if ((*L) == NULL) //结点不能为NULL
return false;
(*L)->prior = NULL; //头结点的前指针设永远指向NULL
(*L)->next = NULL; //后指针暂时设置为NULL
return true;
}
02、插入(后插)
- 注意新插入结点、前驱结点、后续结点的指针修改
- 边界情况:新插入结点在最后一个位置,需特殊处理
//双链表的插入(后插)
bool InsertDLinkList(DNode* p, DNode* s) { //将结点s插入到p结点之后
if (p == NULL || s == NULL) //结点不能为空
return false;
s->next = p->next; //将结点s插入到p结点之后
if (p->next != NULL) //如果p结点有后继结点
p->next->prior = s; //p结点的下一个结点的前指针指向s结点
s->prior = p; //s结点的前指针指向p结点
p->next = s; //p结点的后指针指向s
return true;
}
03、删除(后删)
- 注意删除结点的前驱结点、后续结点的指针修改
- 边界情况:如果被删除结点是最后一个数据结点,需特殊处理
//双链表删除后面结点
bool DeleteDLinkList(DNode* p) { //删除p结点的后继结点
if (p == NULL) //p结点不能为NULL
return false;
DNode* q = p->next; //q结点为p结点的下一个结点
if (q == NULL) //q结点不能为NULL(p->next不能为空)
return false;
p->next = q->next; //p结点下一个结点指向q结点的下一个结点
if (q->next != NULL) //如果p结点有后继结点
q->next->prior = p; //q结点的下一个结点的前指针指向p结点
free(q); //释放q结点
return true;
}
04、遍历
- 从一个给定结点开始,后向遍历、前向遍历的实现(循环的终止条件)
- 链表不具备随机存取特性,查找操作只能通过顺序遍历实现
//向后遍历
void Lastloop(DLinkList L) { //正序
DNode* p = L->next; //定义一个p结点为头节点的下一个结点(第一个存放链表元素的结点)
while (p != NULL) { //遍历p结点直至为NULL
printf("%d ", p->data); //打印当前结点中的数据
p = p->next; //将p移动到下一个结点
}
printf("\n");
}
//向前遍历
void Preloop(DLinkList L) { //逆序
DNode* p = L->next; //定义一个p结点为头节点的下一个结点(第一个存放链表元素的结点)
while (p->next != NULL) { //遍历找到链表的最后一个结点
p = p->next;
}
while (p->prior != NULL) { //遍历链表直至前结点为空
printf("%d ", p->data); //打印当前结点中的数据
p = p->prior; //将p结点移动到前一个结点
}
printf("\n");
}
05、全部
//双链表的实现方式——带头结点
#include<stdio.h>
#include<stdbool.h> //C语言中没有布尔类型
#include<stdlib.h> //malloc、free函数的头文件
//定义双链表
typedef struct DNode {
int data; //链表结点数据域
struct DNode* prior, * next; //为链表结点设置前指针prior,和尾指针next
}DNode, *DLinkList;
//初始化双链表
bool InitDLinkList(DLinkList* L) {
(*L) = (DNode*)malloc(sizeof(DNode)); //设置头结点并分配内存空间
if ((*L) == NULL) //结点不能为NULL
return false;
(*L)->prior = NULL; //头结点的前指针设永远指向NULL
(*L)->next = NULL; //后指针暂时设置为NULL
return true;
}
//双链表长度
int len(DLinkList L) {
int len = 0;
while (L->next != NULL) { //正向遍历
L = L->next;
len++;
}
return len;
}
//双链表的插入(后插)
bool InsertDLinkList(DNode* p, DNode* s) { //将结点s插入到p结点之后
if (p == NULL || s == NULL) //结点不能为空
return false;
s->next = p->next; //将结点s插入到p结点之后
if (p->next != NULL) //如果p结点有后继结点
p->next->prior = s; //p结点的下一个结点的前指针指向s结点
s->prior = p; //s结点的前指针指向p结点
p->next = s; //p结点的后指针指向s
return true;
}
//双链表删除后面结点
bool DeleteDLinkList(DNode* p) { //删除p结点的后继结点
if (p == NULL) //p结点不能为NULL
return false;
DNode* q = p->next; //q结点为p结点的下一个结点
if (q == NULL) //q结点不能为NULL(p->next不能为空)
return false;
p->next = q->next; //p结点下一个结点指向q结点的下一个结点
if (q->next != NULL) //如果p结点有后继结点
q->next->prior = p; //q结点的下一个结点的前指针指向p结点
free(q); //释放q结点
return true;
}
//销毁双链表
void DestoryDList(DLinkList* L) {
while ((*L)->next != NULL) //从头结点开始遍历L链表的结点
DeleteDLinkList((*L)); //每次删除链表中的一个结点
free((*L)); //释放L链表
(*L) = NULL; //将链表置为空
}
//向后遍历
void Lastloop(DLinkList L) { //正序
DNode* p = L->next; //定义一个p结点为头节点的下一个结点(第一个存放链表元素的结点)
while (p != NULL) { //遍历p结点直至为NULL
printf("%d ", p->data); //打印当前结点中的数据
p = p->next; //将p移动到下一个结点
}
printf("\n");
}
//向前遍历
void Preloop(DLinkList L) { //逆序
DNode* p = L->next; //定义一个p结点为头节点的下一个结点(第一个存放链表元素的结点)
while (p->next != NULL) { //遍历找到链表的最后一个结点
p = p->next;
}
while (p->prior != NULL) { //遍历链表直至前结点为空
printf("%d ", p->data); //打印当前结点中的数据
p = p->prior; //将p结点移动到前一个结点
}
printf("\n");
}
//主函数
void main() {
DLinkList L; //定义一个双链表L
InitDLinkList(&L); //初始化双链表L
DNode* s = (DNode*)malloc(sizeof(DNode)); //定义一个s结点
DNode* r = (DNode*)malloc(sizeof(DNode)); //定义一个r结点
DNode* w = (DNode*)malloc(sizeof(DNode)); //定义一个w结点
s->data = 2; //s结点中的数据设为2
r->data = 5; //r结点中的数据设为5
w->data = 3; //w结点中的数据设为3
InsertDLinkList(L, s); //在初始换的L链表后插入s结点
InsertDLinkList(s, r); //在初s结点后插入r结点
InsertDLinkList(r, w); //在初r结点后插入s结点
printf("正向遍历:");
Lastloop(L); //正向遍历链表
printf("逆向遍历:");
Preloop(L); //逆向遍历链表
printf("长度:%d\n",len(L));
DeleteDLinkList(r); //删除r结点后面的结点
printf("删除r结点后面的结点:");
Lastloop(L); //正向遍历删除结点后的链表
printf("长度:%d\n", len(L));
DestoryDList(&L); //销毁链表
if (L == NULL) { //检查销毁是否成功
printf("链表为空");
}
}
2.5 循环链表
主要举例双链表
01、循环单链表
核心:单手抱自己
与单链表区别:
单链表是从一个结点出发只能找到后续的各个结点
循环单链表是从一个结点出发可以找到其他任何一个结点
- 定义初始化和判空
//循环链表——循环单链表
#include<stdio.h>
#include<stdbool.h> //C语言中没有布尔类型
#include<stdlib.h> //malloc、free函数的头文件
//单链表的定义
typedef struct Node {
int data; //定义结点int类型的数据域
struct Node* next; //指针指向下一个结点
}Node, * LinkList;
//判断循环单链表是否为空——头结点的下一个结点是否还是头结点
bool Empty(LinkList L) {
if (L->next == L) //判断头结点是否为自己
return true;
else
return false;
}
//初始化循环单链表
bool InitLinkList(LinkList* L) {
(*L) = (Node*)malloc(sizeof(Node));
if (Empty((*L))) { //头结点不能为空
return false;
}
(*L)->next = (*L); //头结点指向自己
return true;
}
02、循环双链表
核心:双手抱自己
与双链表区别:
双链表的表头结点的prior指向NULL;表尾结点的next指向NULL
循环双链表的表头结点的prior指向表尾结点;表尾结点的next指向头结点
- 定义初始化和判空
//循环链表——循环双链表
#include<stdio.h>
#include<stdbool.h> //C语言中没有布尔类型
#include<stdlib.h> //malloc、free函数的头文件
//定义双链表
typedef struct DNode {
int data; //链表结点数据域
struct DNode* prior, * next; //为链表结点设置前指针prior,和尾指针next
}DNode, * DLinkList;
//判断循环双链表是否为空——头结点的下一个结点是否还是头结点
bool Empty(DLinkList L) {
if (L->next == L) //判断头结点是否是自己
return true;
else
return false;
}
//初始化双链表--保证链表的前后结点不为NULL
bool InitDLinkList(DLinkList* L) {
(*L) = (DNode*)malloc(sizeof(DNode)); //设置头结点并分配内存空间
if (Empty((*L))) //结点不能为空
return false;
(*L)->prior = (*L); //头结点的前指针指向头结点
(*L)->next = (*L); //后指针指向头结点
return true;
}
- 判断结点p是否为循环双链表的表尾结点
//判断结点p是否为循环双链表的表尾结点
void isTail(DLinkList L, DNode* p) {
if (p->next == L) //该结点的下一个结点为头结点
printf("该结点为尾结点!");
else
printf("该结点不为尾结点!");
}
- 正向遍历循环双链表
//正向遍历循环双链表
void Lastloop(DLinkList L) {
DNode* p = L; //定义一个p结点为头节点
while (p->next != L) { //遍历p结点直至为头节点
p = p->next; //将p移动到下一个结点
printf("%d ", p->data); //打印当前结点中的数据
}
printf("\n");
}
- 循环双链表的插入和删除
//循环双链表的插入
bool InsertDLinkList(DNode* p, DNode* s) { //将结点s插入到p结点之后
s->next = p->next; //将结点s插入到p结点之后
p->next->prior = s; //p结点的下一个结点的前指针指向s结点
s->prior = p; //s结点的前指针指向p结点
p->next = s; //p结点的后指针指向s
return true;
}
//循环双链表删除后面结点
bool DeleteDLinkList(DNode* p) { //删除p结点的后继结点
if (Empty(p)) //p结点不能为空
return false;
DNode* q = p->next; //q结点为p结点的下一个结点
p->next = q->next; //p结点下一个结点指向q结点的下一个结点
q->next->prior = p; //q结点的下一个结点的前指针指向p结点
free(q); //释放q结点
return true;
}
- 调用
//调用
void main() {
DLinkList L; //定义一个双链表L
InitDLinkList(&L); //初始化为循环双链表L
if (Empty(L)) {
printf("双链表为空\n");
}
DNode* s = (DNode*)malloc(sizeof(DNode)); //定义一个s结点
DNode* r = (DNode*)malloc(sizeof(DNode)); //定义一个r结点
DNode* w = (DNode*)malloc(sizeof(DNode)); //定义一个w结点
s->data = 2; //s结点中的数据设为2
r->data = 5; //r结点中的数据设为5
w->data = 3; //w结点中的数据设为3
InsertDLinkList(L, s); //在初始换的L链表后插入s结点
InsertDLinkList(s, r); //在初s结点后插入r结点
InsertDLinkList(r, w); //在初r结点后插入s结点
printf("正向遍历:");
Lastloop(L); //正向遍历链表
DeleteDLinkList(r); //删除r结点后面的结点
printf("删除r结点后面的结点:");
Lastloop(L); //正向遍历删除结点后的链表
isTail(L, r); //判断r结点是否是尾结点
}
2.6 静态链表
核心:用数组的方式实现的链表
优点:增、删操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不变
- 定义初始化
//静态链表的实现--带头节点
#include<stdio.h>
#include<stdbool.h> //C语言中没有布尔类型
#include<stdlib.h> //malloc、free函数的头文件
//定义静态链表
#define MaxSize 10 //静态链表的最大长度
typedef struct { //静态链表结构类型的定义
int data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
//初始化静态链表
bool InitSLinkList(SLinkList L) { //下标为0的为头结点,下标为-1为存有数据的静态链表结束,下标为-2为”脏数据“的下标
L[0].next = -1; //将下标为0的头结点的下一个元素下标-1
for (int i = 1; i < MaxSize; i++) {
L[i].next = -2; //将头结点除外的其他结点的下一个元素的下标置为-2
}
r
- 判空
//判断静态链表结点是否为空--该链表头结点的下一个元素的下标为-1,且其他元素的下标为-2
bool Empyt(SLinkList L) {
int l = 0; //用于统计下标为-2的结点个数
if (L[0].next == -1) //头结点的下一个元素的下标是否为 - 1
for (int i = 1; i < MaxSize; i++) //其他元素的下标是否为-2
if (L[i].next == -2)
l++;
if (l != MaxSize - 1) //下标个数不等于链表最大长度-1为空
return false;
return true;
}
- 插入
//插入
bool LinkInsert(SLinkList L, int i, int e) {//在静态链表L下标为i处插入e
if (i<1 || i>MaxSize-1) //判断i的范围是否有效
return false;
int n = 0; //用于获取当前数据的下标
int f = L[0].next; //f为头结点指向一下元素的下标
while (f != -1) { //遍历下标直至为-1
n = f; //将f赋值给n,便于找到一个元素下标为-1的结点的下标
f = L[f].next; //将f等于自身的下一个元素的下标
}
L[n].next = i; //将下标为n的结点的下一个元素下标置为i
L[i].data = e; //将下标为i的结点的数据设置为e
L[i].next = -1; //将下标为i的结点的下一个元素下标置为-1
return true;
}
- 删除
//删除
bool LinkDelete(SLinkList L, int i) { //删除静态链表下标为i的数据
if (i<1 || i>MaxSize - 1) //判断i的范围是否有效
return false;
int n = 0; //用于获取当前数据的下标
int f = L[0].next; //f为头结点指向一下元素的下标
while (f != i) { //遍历下标直至为i
n = f; //将f赋值给n,便于找到一个元素下标为i的结点的下标
f = L[f].next; //将f等于自身的下一个元素的下标
}
L[n].next = L[i].next; //将下标为n的结点的下一个元素下标置为需要删除的结点的下一个元素下标
L[i].next = -2; //将被删除结点的下标置为-2,表示结点删除
return true;
}
- 遍历
//遍历
void Loop(SLinkList L) {
if (Empyt(L)) { //判断链表是否为我们定义的空链表
printf("循环链表为空\n");
return;
}
int f = L[0].next; //f为头结点指向一下元素的下标
while (f != -1) { //遍历下标直至为-1
printf("%d ", L[f].data); //打印链表下标的数据
f = L[f].next; //将f等于自身的下一个元素的下标
}
printf("\n");
}
- 调用
//调用
void main() {
SLinkList a; //定义一个静态链表(实质为结构体数组)
InitSLinkList(a); //初始化静态链表
printf("链表内容为:");
Loop(a); //链表为空,只是链表中没有存放对应数据
LinkInsert(a, 2, 3); //在a静态链表下标为2处插入数据3
LinkInsert(a, 1, 4); //在a静态链表下标为1处插入数据4
LinkInsert(a, 3, 5); //在a静态链表下标为3处插入数据5
printf("链表内容为:");
Loop(a); //遍历链表
LinkDelete(a, 1); //删除a静态链表下标为1的结点
printf("删除a静态链表下标为1的结点:\n");
printf("链表内容为:");
Loop(a)
}
2.7 顺序表与链表的比较
-
顺序表
表厂可估计、查询(搜索)操作较多
-
链表
表长难以预估、经常要增加/删除元素
参考:王道和网上各大博主