一:数据结构基础
定义1(宏观):数据结构是为了高效访问数据而设计出的一种数据的组织和存储方式。
定义2(微观):数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
简言之,数据结构是带结构的数据元素的集合,“结构”就是指数据元素之间存在的关系。
数据结构,关注的是对这些数据元素的操作和数据元素之间的关系,不关心具体的数据项内容。目的是为了高效的访问数据。
数据结构是算法的基础,好的算法也离不开数据结构。
(1)数据结构和编程语言的关系
简言之,数据结构本身和编程语言是无关的。二者关系类似于数学定理和描述它的自然语言一样,你可以用中文表达勾股定理,也可以用英文,法文或者世界上任何一种语言表达它,但是世上只有一个勾股定理。但如果任何一种语言你都不会,那就无法学好数据结构。还类似于房屋设计图纸和实现它所需要的水泥或木材一样。
(2)内存的理解
一般而言,数据结构针对的是内存中的数据。所以在学习数据结构之前,需要先对内存有一个简单的了解。
内存由许多存储单元组成,每个存储单元可以存储一个固定大小的数据块,通常以字节(Byte)为单位。每个存储单元都有一个唯一的地址,操作系统正是根据这一地址去访问内存中的数据的。
我们讨论的数据结构中的数据元素就保存在这些一个个的内存单元中,这些数据元素或存储在连续的内存单元中,或存储在分散的内存单元中。
(3)数据结构的研究方向
数据的逻辑结构
数据元素之间逻辑关系的描述:D_S = (D,S)。这里有两个要素:数据元素、关系。其中,关系是指数据元素间的逻辑关系。
根据数据元素间关系的不同特性,可以分为:
分类角度1:
逻辑结构有四种基本类型:集合结构、线性结构、树形结构、图形结构(或网状结构)。
集合结构:数据结构中的元素之间除了“同属一个集合” 的相互关系外,别无其他关系。是数据之间最弱的一种关系。
线性结构:结构中的元素存在一对一的相互关系。所有数据元素按照顺序排列,强调数据元素的前后顺序。结构中必须存在唯一的首元素和唯一的尾元素。比如:排队。
树形结构:结构中的元素存在一对多的相互关系。所有数据元素按照层次结构进行组织,强调元素之间的父子关系。比如:家谱、文件系统、组织架构、思维导图。
图形结构(网状结构):数据结构中的元素存在多对多的相互关系。由顶点和边组成,顶点对应数据元素,边连接两个顶点,表示两个数据元素之间的关系。比如:全国铁路网、地铁图、社交网络。
分类角度2:
因为线性结构比较突出,所以,我们又习惯上将逻辑结构分为:线性结构和非线性结构。其中:
线性结构,包括线性表(顺序表、链表)、栈、队列、字符串、数组、广义表等。
非线性结构,各个数据元素不再保持在一个线性序列中。比如,集合结构、树结构,图结构等。
数据的逻辑结构指反映数据元素之间的逻辑关系,是独立于计算机的,与数据的存储无关(即与数据元素本身的形式、内容无关,与数据元素的相对位置无关,与所含结点个数无关)。
比如,栈属于线性结构,是一种逻辑结构,可以采用顺序存储,也可以采用链式存储。
数据的存储结构
数据的存储结构又称为物理结构,是数据的逻辑结构在计算机中的表示(又称映像)。它包括数据元素的表示和关系的表示(即逻辑关系)。
数据的存储结构是逻辑结构用计算机语言的实现,它依赖于计算机语言。人们为了合理利用计算机的存储空间,研究出了两种基本的存储方式:
顺序存储结构
顺序存储结构把逻辑上相邻的数据元素存储在物理位置上也相邻的存储单元中,即所有的元素依次存放在一片连续的存储空间中。
在C语言中,使用一维数组表示顺序存储结构。
优点:数据元素存放的地址是连续的,支持下标访问,随机存取表中元素。
链式存储结构
链式存储结构把逻辑上相邻的数据元素存储在物理上可以不相邻的存储空间中。其中,为每个数据元素构造一个结点(用结构体类型表示),结点中除了存放数据本身以外,还存放指向下一个结点的指针(pointer)。
C语言中用指针来表示数据元素之间的这种结构。
优点:克服顺序存储结构中预知元素个数的缺点;插入、删除灵活 (不必移动节点,只要改变节点中的指针)。
缺点:需要额外的空间来表达数据之间的逻辑关系(每个节点都由数据域和指针域组成,所以相同内存空间内全存满的话,顺序比链式存储更多数据)。
由两类基本的存储方式,又引申出两类存储结构:
索引存储结构
把索引想象成书的目录,有了目录,我们找书的相关内容就十分简单。索引存储在存储数据元素之外,同时建立数据元素的目录,便于快速检索。
MySQL数据库的索引的设计方案:
优点:用节点的索引号来确定结点存储地址,检索速度快。
缺点: 增加了附加的索引表,会占用较多的存储空间。在增加和删除数据时要修改索引表,因而会花费较多的时间。
散列存储结构
根据数据元素的关键字直接计算出该元素的关键码,由关键码决定其存储地址,又称为Hash存储。在初中,我们就学过一种能够将一个x值通过一个函数获得对应的一个y值的操作,叫做映射。散列表的实现原理正是映射的原理。在映射的过程中,事先设定的函数称作散列函数或者哈希函数。
优点:检索、增加或删除结点的操作都很快。
缺点:不支持排序,一般比用线性表存储需要更多的空间,并且记录的关键字不能重复。
数据的运算
施加在数据上的运算包括运算的定义和实现。运算的实现是针对存储结构的,指出运算的具体操作步骤。
- 分配资源,建立结构,释放资源
- 插入和删除
- 获取和遍历
- 修改和排序
二:线性结构之数组
C 语言中,数组是具有相同类型的数据元素的集合。在所有的数据结构中,数组算是最常见、最简单的一种数据结构。
C 语言中,数组的长度一旦确定就不可以再变了。
(1)数组优缺点
优 点
- 查找容易(通过下标),时间复杂度为O(1)。不需要额外申请或删除空间。
- 使用下标位置索引(index)十分高效的访问任意元素,修改快
缺 点
- 插入、删除元素难,效率低。(需要移动大量元素以使元素空间连续)。
- 插入操作平均需要移动n/2个元素。
- 删除操作平均需要移动(n-1)/2个元素。
- 扩展相对繁琐。一方面需要确保能提供更大区域的连续内存空间,另一方面需要将原有数据复制到新的顺序表中。
(2)可变长的动态数组
前文提到过数组这一数据结构的一个局限性是长度固定,本节我们来实现一个增强版的数组——可变长的动态数组,需要实现以下函数:
//初始化动态数组 |
void initDynamicArray(DynamicArray *array, size_t initialCapacity) |
//释放动态数组内存 |
void destroyDynamicArray(DynamicArray *array) |
//调整动态数组内存大小 |
void resizeDynamicArray(DynamicArray *array, size_t newCapacity) |
//获取动态数组长度(元素个数) |
size_t getLength(const DynamicArray *array) |
//在指定位置插入新元素 |
void insertAt(DynamicArray *array, size_t index, int element) |
//在末尾插入新元素 |
void insertEnd(DynamicArray *array, int element) |
//删除指定位置的元素并返回被删除的元素 |
int deleteAt(DynamicArray *array, size_t index) |
//删除末尾的元素并返回被删除的元素 |
int deleteEnd(DynamicArray *array) |
//遍历所有的元素 |
void print(DynamicArray *array) |
(3)实现原理
可变长的动态数组是一种数据结构,它允许在运行时根据需要动态地调整数组的大小,而不需要提前指定固定的大小。这种动态数组通常被称为动态数组、动态分配数组、动态增长数组或动态内存数组。int arr[10];
C语言中是通过使用指针和内存分配函数来实现动态数组,常见的内存分配函数是malloc、realloc和free。下面是一些相关的概念和操作:
- 分配内存(malloc): 在C语言中,可以使用malloc函数来分配一块指定大小的内存。例如,int *arr = (int *)malloc(n * sizeof(int)); 将分配能够存储n个整数的内存空间。
- 重新分配内存(realloc): 如果需要改变动态数组的大小,可以使用realloc函数来重新分配内存。这允许你在保留原有数据的情况下扩展或缩小数组的大小。
- 释放内存(free): 当不再需要动态数组时,应使用free函数释放之前分配的内存,以避免内存泄露。内存溢出。
(4)代码实现
结构体构造
typedef struct
{
// 数组元素数目
int size;
// 数组内存地址
int *date;
// 数组最大数目
int maxSize;
} DynamicArray;
初始化动态数组函数
// 初始化动态数组(结构体指针,开辟数目)
void initDynamicArray(DynamicArray *array, int maxSize)
{
// 开辟空间
array->date = (int *)malloc(sizeof(int) * maxSize);
// 设置初始元素数目为0
array->size = 0;
// 设置最大元素数目
array->maxSize = maxSize;
}
释放动态数组
// 释放动态数组内存
void destroyDynamicArray(DynamicArray *array)
{
// 释放内存
free(array->date);
// 清空数目记录
array->size = 0;
array->maxSize = 0;
}
调整数组内存大小
// 调整动态数组内存大小
void resizeDynamicArray(DynamicArray *array, int newCapacity)
{
//判断是否需要扩容
if (array->size >= array->maxSize)
{
array->date = (int *)realloc(array->date,sizeof(int) * newCapacity);
// 存储一下最新的最大元素个数
array ->maxSize = newCapacity;
}else{
printf("当前不需要扩容");
}
}
获取动态数组长度
//获取动态数组长度(元素个数)
int getLength(DynamicArray *array){
return array->size;
}
在指定位置插入元素
// 在指定位置插入新元素
void insertAt(DynamicArray *array, int index, int element)
{ // 判断插入是否合理
if (index < 0 || index > array->maxSize)
{
printf("插入失败,请检查");
return;
}
// 判断是否需要扩容
if (array->size >= array->maxSize)
{ // 扩容
resizeDynamicArray(array, array->maxSize * 2);
}
// 将插入位置的元素向后移动
for (int i = array->size; i > index; i--)
{
array->date[i] = array->date[i - 1];
}
// 插入新元素
array->date[index] = element;
array->size += 1;
}
在末尾插入元素
//在末尾插入新元素
void insertEnd(DynamicArray *array, int element){
//调用插入元素函数
insertAt(array,array->size,element);
}
删除指定位置元素
// 删除指定位置的元素并返回被删除的元素
int deleteAt(DynamicArray *array, int index)
{
// 先判断删除角标是否合理
if (index < 0 || index > array->size - 1)
{
// 删除角标不对直接返回-1
return -1;
}
// 保存删除调用数据
int deleteData = array->date[index];
// 删除数据
for (int i = index; i < array->size - 1; i++)
{
array->date[i] = array->date[i + 1];
}
array->size -=1;
return deleteData;
};
删除末尾元素
// 删除末尾的元素并返回被删除的元素
int deleteEnd(DynamicArray *array)
{
deleteAt(array, array->size - 1);
}
遍历所有元素
遍历所有的元素
void print(DynamicArray *array)
{
循环打印出动态数组内部的元素
for (int i = 0; i < array->size; i++)
{
printf("%d\n", array->date[i]);
}
}
完整程序
// 动态数组
#include <stdio.h>
#include <stdlib.h>
// 结构体
typedef struct
{
// 数组元素数目
int size;
// 数组内存地址
int *date;
// 数组最大数目
int maxSize;
} DynamicArray;
// 初始化动态数组(结构体指针,开辟数目)
void initDynamicArray(DynamicArray *array, int maxSize)
{
// 开辟空间
array->date = (int *)malloc(sizeof(int) * maxSize);
// 设置初始元素数目为0
array->size = 0;
// 设置最大元素数目
array->maxSize = maxSize;
}
// 释放动态数组内存
void destroyDynamicArray(DynamicArray *array)
{
// 释放内存
free(array->date);
// 清空数目记录
array->size = 0;
array->maxSize = 0;
}
// 调整动态数组内存大小
void resizeDynamicArray(DynamicArray *array, int newCapacity)
{
// 判断是否需要扩容
if (array->size >= array->maxSize)
{
array->date = (int *)realloc(array->date, sizeof(int) * newCapacity);
// 存储一下最新的最大元素个数
array->maxSize = newCapacity;
}
else
{
printf("当前不需要扩容");
}
}
// 获取动态数组长度(元素个数)
int getLength(DynamicArray *array)
{
return array->size;
}
// 在指定位置插入新元素
void insertAt(DynamicArray *array, int index, int element)
{ // 判断插入是否合理
if (index < 0 || index > array->maxSize)
{
printf("插入失败,请检查");
return;
}
// 判断是否需要扩容
if (array->size >= array->maxSize)
{ // 扩容
resizeDynamicArray(array, array->maxSize * 2);
}
// 将插入位置的元素向后移动
for (int i = array->size; i > index; i--)
{
array->date[i] = array->date[i - 1];
}
// 插入新元素
array->date[index] = element;
array->size += 1;
}
// 在末尾插入新元素
void insertEnd(DynamicArray *array, int element)
{
// 调用插入元素函数
insertAt(array, array->size, element);
}
// 删除指定位置的元素并返回被删除的元素
int deleteAt(DynamicArray *array, int index)
{
// 先判断删除角标是否合理
if (index < 0 || index > array->size - 1)
{
// 删除角标不对直接返回-1
return -1;
}
// 保存删除调用数据
int deleteData = array->date[index];
// 删除数据
for (int i = index; i < array->size - 1; i++)
{
array->date[i] = array->date[i + 1];
}
array->size -=1;
return deleteData;
};
// 删除末尾的元素并返回被删除的元素
int deleteEnd(DynamicArray *array)
{
deleteAt(array, array->size - 1);
}
遍历所有的元素
void print(DynamicArray *array)
{
循环打印出动态数组内部的元素
for (int i = 0; i < array->size; i++)
{
printf("%d\n", array->date[i]);
}
}
int main()
{
DynamicArray arry;
initDynamicArray(&arry, 5);
resizeDynamicArray(&arry, 20);
insertAt(&arry, 0, 10);
insertAt(&arry, 1, 20);
insertAt(&arry, 2, 30);
insertAt(&arry, 3, 30);
insertAt(&arry, 4, 40);
insertAt(&arry, 5, 50);
insertAt(&arry, 6, 60);
deleteAt(&arry,3);
deleteEnd(&arry);
print(&arry);
printf("%d",getLength(&arry));
destroyDynamicArray(&arry);
return 0;
}
三:线性结构之链表
(1)何为链表
逻辑结构:链表是一个线性结构,由一系列`结点(Node)`组成,每个结点包含一个数据元素和一个指向下一个结点的`指针(Pointer)`。所有结点通过指针相连,形成一个链式结构。通常我们将链表中的第一个结点称为`头结点`。
data :数据域,存放结点的值。
next :指针域,存放结点的直接后继的地址。
物理结构:与数组不同,链表中的结点需要自行组织,向系统申请很多分散在内存各处的结点,每个结点都保存了当前结点的数据和下一个结点的地址(指针),通过指针将结点串成一串。
链表的分类
链表又分为单链表、双链表、循环单链表、循环双链表和静态链表。
(2)链表优缺点
优点:
1.插入和删除操作效率高。
2.动态扩展性能更好,链表不需要像数组那样预先指定固定的大小,而是可以随时动态的增长或缩小。链表是真正的动态数据结构,不需要处理固定容量的问题。
缺点:
1. 查找慢。由于链表中的结点不是连续存储的,无法像数组一样根据索引直接计算出每个结点的地址。必须从头结点开始遍历链表,直到找到目标结点,这导致了链表的随机访问效率较低。
2. 额外的存储空间。链表的每个结点都需要存储指向下一个结点的指针,这会占用额外的存储空间。所以,相比于数组,链表需要更多的内存空间来存储相同数量的数据元素。
(3)实现链表
前文提到过链表的一些基础概念,本节我们通过实现一个链表,来进一步加深对链表的理解,需要实现以下函数:
初始化链表 |
void initLinkedList(LinkedList *list) |
返回链表的长度 |
size_t getLength(const LinkedList *list) |
在指定位置插入元素 |
void insertAt(LinkedList *list, size_t index, int element) |
在末尾插入元素 |
void insertEnd(LinkedList *list, int element) |
删除指定位置的元素并返回被删除的元素 |
int deleteAt(LinkedList *list, size_t index) |
删除末尾元素 |
int deleteEnd(LinkedList *list) |
获取指定位置的元素 |
int getElementAt(const LinkedList *list, size_t index) |
修改指定位置的元素 |
void modifyAt(LinkedList *list, size_t index, int newValue) |
释放链表内存 |
void destroyLinkedList(LinkedList *list) |
(4)代码实现
定义一个结点
// 定义一个结点
typedef struct Node
{
// 结点保存数据的地方
int data;
// 指针保存下一个结点的地址
struct Node *next;
} Node;
链表
// 链表
typedef struct
{
// 存储结点的个数
int size;
// 存储"头结点"
Node *head;
} LinkedList;
初始化链表
// 初始化链表
void initLinkedList(LinkedList *list)
{
// 链表的结点的个数
list->size = 0;
// 头结点指向NULL
list->head = NULL;
}
返回节点个数
// 返回结点的个数
int getLength(LinkedList *list)
{
return list->size;
}
在指定位置插入元素
// 在指定位置插入元素
// 第一个参数:链表 第二个参数:插入这个数据角标 第三个参数:节点保存的数据
void insertAt(LinkedList *list, int index, int element)
{
// 先判断插入的位置是不是合法的
if (index < 0 || index > list->size)
{
return;
}
// 创建新的结点:动态开辟内存的,结点保存数据、保存下一个节点的地址
Node *node = (Node *)malloc(sizeof(Node));
// 创建新节点需要保存的数据
node->data = element;
// 第一个位置插入
if (index == 0)
{
// 当前节点保存下一个节点的地址(暂时没有下一个)
node->next = list->head;
// 链表的头结点指向第一个节点的地址
list->head = node;
}
else
{
// 先找到插入位置的上一个节点
Node *prevNode = list->head;
for (int i = 1; i < index; i++)
{
prevNode = prevNode->next;
}
// 找到上一个节点的位置
node->next = prevNode->next;
prevNode->next = node;
}
// 节点的个数+1
list->size++;
}
在链表末尾插入元素
// 在链表的末尾插入元素
void insertEnd(LinkedList *list, int element)
{
insertAt(list, list->size, element);
}
封装一个函数,专门找链表的某一个位置的前一个节点
Node *findPrevNode(LinkedList *list, int index)
{
// 从第一个结点向后找
Node *prevNode = list->head;
for (int i = 1; i < index; i++)
{
prevNode = prevNode->next;
}
// 返回不就是某一个位置前面的结点
return prevNode;
}
删除指定的元素
// 删除指定的元素
// 返回结点保存data数据 第一个参数:链表 第二个参数:删除元素的角标
int deleteAt(LinkedList *list, int index)
{
// 先判断给的角标对不对
if (index < 0 || index > list->size - 1)
{
return -1;
}
// 准备一个节点-代表将来要删除节点
Node *deleteNode = NULL;
// 删除的第一个节点
if (index == 0)
{
// 删除的结点
deleteNode = list->head;
// 修改头结点的地址
list->head = deleteNode->next;
}
else
{
// 找到index这个位置的前面节点
Node *prevNode = findPrevNode(list, index);
// 确定删除的结点
deleteNode = prevNode->next;
prevNode->next = deleteNode->next;
}
// 执行到这里说明干掉的结点已经找到了
// 取出结点的数据
int deleteData = deleteNode->data;
// 链表长度-1
list->size--;
// 节点空间free
free(deleteNode);
return deleteData;
}
删除末尾的元素
// 删除末尾的元素
int deleteEnd(LinkedList *list)
{
return deleteAt(list, list->size - 1);
}
获取指定位置的元素
// 获取指定位置的元素
int getElementAt(LinkedList *list, int index)
{
// 先判断角标是否合法化
if (index < 0 || index > list->size - 1)
{
return -1;
}
// 第一个
if (index == 0)
{
return list->head->data;
}
else
{
// 获取上一个节点
Node *prevNode = findPrevNode(list, index);
return prevNode->next->data;
}
}
修改指定的元素
void modifyAt(LinkedList *list, int index, int newValue)
{
// 判断角标合法性
if (index < 0 || index > list->size - 1)
{
return;
}
// 判断第一个元素
if (index == 0)
{
list->head->data = newValue;
}
else
{
Node *prevNode = findPrevNode(list, index);
prevNode->next->data = newValue;
}
}
释放链表
// 释放链表
void destroyLinkedList(LinkedList *list)
{
// 释放全部节点
Node *node = list->head;
// 全部节点释放
while (node != NULL)
{
Node *tmp = node;
node = node->next;
free(tmp);
}
// 节点的长度归零
list->size = 0;
// 头结点指向NULL
list->head = NULL;
}
打印输出
// 打印输出
void print(LinkedList *list)
{
Node *node = list->head;
while (node != NULL)
{
printf("%d\n", node->data);
node = node->next;
}
}
完整程序
#include <stdio.h>
#include <stdlib.h>
// 定义一个结点
typedef struct Node
{
// 结点保存数据的地方
int data;
// 指针保存下一个结点的地址
struct Node *next;
} Node;
// 链表
typedef struct
{
// 存储结点的个数
int size;
// 存储"头结点"
Node *head;
} LinkedList;
// 初始化链表
void initLinkedList(LinkedList *list)
{
// 链表的结点的个数
list->size = 0;
// 头结点指向NULL
list->head = NULL;
}
// 返回结点的个数
int getLength(LinkedList *list)
{
return list->size;
}
// 在指定位置插入元素
// 第一个参数:链表 第二个参数:插入这个数据角标 第三个参数:节点保存的数据
void insertAt(LinkedList *list, int index, int element)
{
// 先判断插入的位置是不是合法的
if (index < 0 || index > list->size)
{
return;
}
// 创建新的结点:动态开辟内存的,结点保存数据、保存下一个节点的地址
Node *node = (Node *)malloc(sizeof(Node));
// 创建新节点需要保存的数据
node->data = element;
// 第一个位置插入
if (index == 0)
{
// 当前节点保存下一个节点的地址(暂时没有下一个)
node->next = list->head;
// 链表的头结点指向第一个节点的地址
list->head = node;
}
else
{
// 先找到插入位置的上一个节点
Node *prevNode = list->head;
for (int i = 1; i < index; i++)
{
prevNode = prevNode->next;
}
// 找到上一个节点的位置
node->next = prevNode->next;
prevNode->next = node;
}
// 节点的个数+1
list->size++;
}
// 在链表的末尾插入元素
void insertEnd(LinkedList *list, int element)
{
insertAt(list, list->size, element);
}
// 封装一个函数,专门找链表的某一个位置的前一个节点
Node *findPrevNode(LinkedList *list, int index)
{
// 从第一个结点向后找
Node *prevNode = list->head;
for (int i = 1; i < index; i++)
{
prevNode = prevNode->next;
}
// 返回不就是某一个位置前面的结点
return prevNode;
}
// 删除指定的元素
// 返回结点保存data数据 第一个参数:链表 第二个参数:删除元素的角标
int deleteAt(LinkedList *list, int index)
{
// 先判断给的角标对不对
if (index < 0 || index > list->size - 1)
{
return -1;
}
// 准备一个节点-代表将来要删除节点
Node *deleteNode = NULL;
// 删除的第一个节点
if (index == 0)
{
// 删除的结点
deleteNode = list->head;
// 修改头结点的地址
list->head = deleteNode->next;
}
else
{
// 找到index这个位置的前面节点
Node *prevNode = findPrevNode(list, index);
// 确定删除的结点
deleteNode = prevNode->next;
prevNode->next = deleteNode->next;
}
// 执行到这里说明干掉的结点已经找到了
// 取出结点的数据
int deleteData = deleteNode->data;
// 链表长度-1
list->size--;
// 节点空间free
free(deleteNode);
return deleteData;
}
// 删除末尾的元素
int deleteEnd(LinkedList *list)
{
return deleteAt(list, list->size - 1);
}
// 获取指定位置的元素
int getElementAt(LinkedList *list, int index)
{
// 先判断角标是否合法化
if (index < 0 || index > list->size - 1)
{
return -1;
}
// 第一个
if (index == 0)
{
return list->head->data;
}
else
{
// 获取上一个节点
Node *prevNode = findPrevNode(list, index);
return prevNode->next->data;
}
}
// 修改指定的元素
void modifyAt(LinkedList *list, int index, int newValue)
{
// 判断角标合法性
if (index < 0 || index > list->size - 1)
{
return;
}
// 判断第一个元素
if (index == 0)
{
list->head->data = newValue;
}
else
{
Node *prevNode = findPrevNode(list, index);
prevNode->next->data = newValue;
}
}
// 释放链表
void destroyLinkedList(LinkedList *list)
{
// 释放全部节点
Node *node = list->head;
// 全部节点释放
while (node != NULL)
{
Node *tmp = node;
node = node->next;
free(tmp);
}
// 节点的长度归零
list->size = 0;
// 头结点指向NULL
list->head = NULL;
}
// 打印输出
void print(LinkedList *list)
{
Node *node = list->head;
while (node != NULL)
{
printf("%d\n", node->data);
node = node->next;
}
}
int main()
{
// 进行初始化链表
LinkedList list;
initLinkedList(&list);
insertAt(&list, 0, 10);
insertAt(&list, 1, 20);
insertAt(&list, 2, 30);
insertAt(&list, 3, 40);
insertAt(&list, 4, 50);
insertAt(&list, 5, 150);
insertAt(&list, 6, 250);
// 尾巴插入一个350
insertEnd(&list, 350);
insertEnd(&list, 450);
// 删除元素
printf("干掉的====%d\n", deleteAt(&list, 4));
printf("干掉的====%d\n", deleteAt(&list, -2));
deleteEnd(&list);
deleteEnd(&list);
modifyAt(&list, 0, 12306);
modifyAt(&list, 5, 6666);
printf("0==%d\n", getElementAt(&list, 0));
print(&list);
destroyLinkedList(&list);
return 0;
}
四:线性结构之栈
(1)栈的定义
栈(stack),是限制在只能在表的一端进行插入和删除操作的线性表。应用范围非常广泛。生活中也有栈的场景,比如堆叠的盘子、报 ,电梯中的人们,邮局的邮筒等。
特点:`后进先出` (LIFO,Last In First Out)或`先进后出` (FILO,First In Last Out)的线性表。
(2)相关概念
栈顶(Top):允许进行插入、删除操作的一端,又称为`表尾`。栈顶由一个称为栈顶指针的位置指示器(其实就是一个变量)来指示,它是动态变化的。
栈底(Bottom):是固定不变的,不允许进行插入和删除的一端,又称为`表头`。
空栈:不含任何元素的空表。
设栈S=(a1,a2,…,an ),则a1称为栈底元素,an为栈顶元素,栈中元素按a1,a2,…,a_n的次序进栈(压栈、push),出栈(弹栈,pop)的第一个元素应为栈顶元素,出栈顺序为:an,...,a2,a1。
栈最常用的地方就是计算机的函数调用,不管何种语言,最先被调用的一定最后返回。举例:
void funcC() {
}
void funcB() {
funcC();
}
void funcA() {
funcB();
}
- 调用顺序是: funcA() -> funcB()-> funcC()。
- 返回顺序是: funcC() -> funcB()-> funcA()。
funcC()最后被调用,却最先被执行完,然后把结果返回给funcB(),funcB()再执行完毕返回,把结果返回给funcA()。
可用顺序表(数组)和链表来存储栈,栈可以依照存储结构分为两种:顺序栈和链式栈。
(3)代码实现
栈的底层实现既可以使用数组也可以使用链表,本节基于动态数组实现一个栈,需要实现如下函数方法:
初始化栈 |
void initStack(Stack *stack, size_t capacity) |
返回栈内元素个数 |
size_t getSize(const Stack *stack) |
添加新元素 |
void push(Stack *stack, int element) |
栈顶元素出栈并返回 |
int pop(Stack *stack) |
释放栈内存 |
void destroyStack(Stack *stack) |
定义一个栈
// 定义一个栈
typedef struct
{
// 栈需要内存空间
int *data;
// 统计元素的个数
int size;
// 限制元素的上线个数
int maxSize;
} Stack;
初始化栈
// 初始化栈
// 第一个参数:栈 结构体指针 第二个参数:首次创建的栈的最大容量
void initStack(Stack *stack, int maxSize)
{
// 初始化栈
stack->data = (int *)malloc(sizeof(int) * maxSize);
// 元素个数为零
stack->size = 0;
// 首次初始化栈元素上限个数
stack->maxSize = maxSize;
}
获取栈内元素
// 获取栈内元素的个数
int getSize(Stack *stack)
{
return stack->size;
}
添加元素,像栈内压入数据
// 添加元素,向栈内压数据
void push(Stack *stack, int element)
{
// 考虑是否拓容
if (stack->size >= stack->maxSize)
{
// 上限修改为元素2倍
stack->maxSize *= 2;
// 拓容
stack->data = (int *)realloc(stack->data, sizeof(int) * stack->maxSize);
}
// 栈压数据
stack->data[stack->size] = element;
stack->size++;
}
出栈:返回int,代表的是出栈的那个元素的数据
// 出栈
// 出栈:返回int,代表的是出栈的那个元素的数据
// 形参:栈
int pop(Stack *stack)
{
// 先判断是不是空栈
if (stack->size == 0)
{
return -1;
}
stack->size--;
return stack->data[stack->size];
}
释放内存
// 释放内存
void destroyStack(Stack *stack)
{
// 释放内存
free(stack->data);
// 一切数据从头开始
stack->data = NULL;
stack->size = 0;
stack->maxSize = 0;
}
完整程序
#include <stdio.h>
#include <stdlib.h>
// 定义一个栈
typedef struct
{
// 栈需要内存空间
int *data;
// 统计元素的个数
int size;
// 限制元素的上线个数
int maxSize;
} Stack;
// 初始化栈
// 第一个参数:栈 结构体指针 第二个参数:首次创建的栈的最大容量
void initStack(Stack *stack, int maxSize)
{
// 初始化栈
stack->data = (int *)malloc(sizeof(int) * maxSize);
// 元素个数为零
stack->size = 0;
// 首次初始化栈元素上限个数
stack->maxSize = maxSize;
}
// 获取栈内元素的个数
int getSize(Stack *stack)
{
return stack->size;
}
// 添加元素,向栈内压数据
void push(Stack *stack, int element)
{
// 考虑是否拓容
if (stack->size >= stack->maxSize)
{
// 上限修改为元素2倍
stack->maxSize *= 2;
// 拓容
stack->data = (int *)realloc(stack->data, sizeof(int) * stack->maxSize);
}
// 栈压数据
stack->data[stack->size] = element;
stack->size++;
}
// 出栈
// 出栈:返回int,代表的是出栈的那个元素的数据
// 形参:栈
int pop(Stack *stack)
{
// 先判断是不是空栈
if (stack->size == 0)
{
return -1;
}
stack->size--;
return stack->data[stack->size];
}
// 释放内存
void destroyStack(Stack *stack)
{
// 释放内存
free(stack->data);
// 一切数据从头开始
stack->data = NULL;
stack->size = 0;
stack->maxSize = 0;
}
int main()
{
// 创建栈
Stack stack;
initStack(&stack, 5);
push(&stack, 10);
push(&stack, 20);
push(&stack, 30);
push(&stack, 40);
push(&stack, 50);
push(&stack, 60);
printf("出栈了:%d\n", pop(&stack));
printf("出栈了:%d\n", pop(&stack));
printf("出栈了:%d\n", pop(&stack));
printf("出栈了:%d\n", pop(&stack));
printf("出栈了:%d\n", pop(&stack));
printf("出栈了:%d\n", pop(&stack));
printf("出栈了:%d\n", pop(&stack));
printf("获取栈内的元素个数:%d\n", getSize(&stack));
return 0;
}
五:线性结构之队列
(1)基本概念
队列(Queue):也是操作受限的线性表,限制为仅允许在表的一端进行插入(入队或进队),在表的另一端进行删除(出队或离队)操作。
队首(front) :允许进行删除的一端称为队首。
队尾(rear): 允许进行插入的一端称为队尾。
在空队列中依次加入元素a1,a2, …, an之后,a1是队首元素,an是队尾元素。显然退出队列的次序也只能是a1,a2, …, an。
队列,是一种先进先出(First In First Out ,简称FIFO)的线性结构。类似于生活中的排队行为。
队列中没有元素时,称为空队列。
可用顺序表(数组)和链表来存储队列,队列按存储结构可分为顺序队列和链式队列两种。
(2)队列的常见操作与问题
顺序队列中存在“假溢出”现象:尽管队列中实际元素个数可能远远小于数组大小,但可能由于尾指针rear已超出向量空间的上界而不能做入队操作。
为充分利用空间,克服上述“假溢出”现象,有两种方法
方法1:使用数组实现,入队列时添加到队列的最后,出队列时移除第一个元素同时将右侧的元素左移。
方法2:为队列分配的向量空间看成是一个首尾相接的圆环,这种队列称为循环队列。
可用顺序表(数组)和链表来存储队列,队列可以依照存储结构分为两种:顺序队列和链式队列。
(3)功能实现
队列的底层实现也既可以选择数组也可以选择链表,本节基于动态数组实现一个队列,该队列需要实现如下函数:
void initQueue(Queue *queue, size_t capacity) |
返回队列内元素个数 |
size_t getSize(const Queue *queue) |
添加新元素 |
void enqueue(Queue *queue, int element) |
元素出队列 |
int dequeue(Queue *queue) |
释放队列内存 |
void destroyQueue(Queue *queue) |
遍历队列 |
void printQueue(Queue *queue) |
(4)代码实现
创建结构体
// 结构体标识循环队列
typedef struct
{
// 设置队列的所占空间
int *data;
// 当前队列有多少个元素
int size;
// 队列存储数据上限制
int maxSize;
// 队首的标记
int front; // 数据出去的标记
// 对尾的标记
int rear; // 数据进来的标记
} Queue;
初始化队列
// 初始化队列
// 第一个队列空间 队列存储数据的上限
void initQueue(Queue *queue, int maxSize)
{
// 初始化
queue->data = (int *)malloc(sizeof(int) * maxSize);
// 对列的元素的个数
queue->size = 0;
// 上限值
queue->maxSize = maxSize;
// 队首
queue->front = 0;
// 队尾
queue->rear = 0;
}
返回队列元素个数
// 返回队列的元素的个数
int getSize(Queue *queue)
{
return queue->size;
}
添加元素->入队
// 添加元素->入队
void enqueue(Queue *queue, int element)
{
// 先判断能否入队
if (queue->size >= queue->maxSize)
{
printf("队列已满!你先等着\n");
return;
}
// 入队
queue->data[queue->rear] = element;
queue->rear = (queue->rear + 1) % queue->maxSize;
// 元素的个数+1
queue->size++;
}
删除元素->离队
// 删除元素->离队
// int返回数据类型:出队的元素的数据
int dequeue(Queue *queue)
{
// 合法,如果是空的队列
if (queue->size == 0)
{
return -1;
}
// 出队
int data = queue->data[queue->front];
queue->front = (queue->front + 1) % queue->maxSize;
queue->size--;
return data;
}
遍历队列
// 遍历队列
void printQueue(Queue *queue)
{
int index = queue->front;
for (int i = 0; i < queue->size; i++)
{
printf("%d\n", queue->data[index]);
index = (index + 1) % queue->maxSize;
}
}
释放内存
// 释放内存
void destroyQueue(Queue *queue)
{
free(queue->data);
queue->data = NULL;
queue->size = 0;
queue->maxSize = 0;
queue->front = 0;
queue->rear = 0;
}
全部程序
#include <stdio.h>
#include <stdlib.h>
// 结构体标识循环队列
typedef struct
{
// 设置队列的所占空间
int *data;
// 当前队列有多少个元素
int size;
// 队列存储数据上限制
int maxSize;
// 队首的标记
int front; // 数据出去的标记
// 对尾的标记
int rear; // 数据进来的标记
} Queue;
// 初始化队列
// 第一个队列空间 队列存储数据的上限
void initQueue(Queue *queue, int maxSize)
{
// 初始化
queue->data = (int *)malloc(sizeof(int) * maxSize);
// 对列的元素的个数
queue->size = 0;
// 上限值
queue->maxSize = maxSize;
// 队首
queue->front = 0;
// 队尾
queue->rear = 0;
}
// 返回队列的元素的个数
int getSize(Queue *queue)
{
return queue->size;
}
// 添加元素->入队
void enqueue(Queue *queue, int element)
{
// 先判断能否入队
if (queue->size >= queue->maxSize)
{
printf("队列已满!你先等着\n");
return;
}
// 入队
queue->data[queue->rear] = element;
queue->rear = (queue->rear + 1) % queue->maxSize;
// 元素的个数+1
queue->size++;
}
// 删除元素->离队
// int返回数据类型:出队的元素的数据
int dequeue(Queue *queue)
{
// 合法,如果是空的队列
if (queue->size == 0)
{
return -1;
}
// 出队
int data = queue->data[queue->front];
queue->front = (queue->front + 1) % queue->maxSize;
queue->size--;
return data;
}
// 遍历队列
void printQueue(Queue *queue)
{
int index = queue->front;
for (int i = 0; i < queue->size; i++)
{
printf("%d\n", queue->data[index]);
index = (index + 1) % queue->maxSize;
}
}
// 释放内存
void destroyQueue(Queue *queue)
{
free(queue->data);
queue->data = NULL;
queue->size = 0;
queue->maxSize = 0;
queue->front = 0;
queue->rear = 0;
}
int main()
{
// 初始化队列
Queue queue;
initQueue(&queue, 6);
enqueue(&queue, 10);
enqueue(&queue, 20);
enqueue(&queue, 30);
enqueue(&queue, 40);
enqueue(&queue, 50);
enqueue(&queue, 60);
enqueue(&queue, 9990); // 没有入队
// 出队列
printf("我%d数出去离开队伍\n", dequeue(&queue));
printf("我%d数出去离开队伍\n", dequeue(&queue));
enqueue(&queue, 70);
enqueue(&queue, 80);
printf("我%d数出去离开队伍\n", dequeue(&queue));
// 获取队列的元素的个数
printf("队列当前元素的个数:%d\n", getSize(&queue));
printf("*********");
printQueue(&queue);
return 0;
}
本章内容到此结束,期待你的关注。