数据结构
数据结构
概述
数据结构
数据结构在学什么
- 如何用程序代码把现实世界的问题信息化
- 如何用计算机高效地处理这些信息从而创造价值
数据:数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。数据是计算机程序加工的原料
现代计算机处理的数据:
现代计算机——经常处理非数值型问题
对于非数值型的问题:
- 我们关心每个个体的具体信息
- 我们还关心个体之间的关系
数据元素:
数据元素是数据的基本单位,通常作为一个整体进行考虑和处理
数据项:
一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位
数据对象:
数据对象是具有相同性质的数据元素的集合,是数据的一个子集
数据结构:
- 数据结构是相互之间存在一种或多种特定关系的数据元素的集合
- 数据结构这门课着重关注的是数据元素之间的关系,和对这些数据元素的操作,而不关心具体的数据项内容
三要素
逻辑结构
- 集合结构,各个元素同属一个集合,别无其他关系
- 线性结构,一对一,顺序关系
- 树状结构,一对多
- 图状结构,多对多
数据的运算,针对某种逻辑结构,结合实际需求,定义基本运算(增删改查)
物理结构(储存结构),如何用计算机实现这种数据结构
- 顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的储存单元中,元素之间的关系由储存单元的邻接关系来体现
- 链式存储:把逻辑上相邻的元素存储在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系
- 索引存储:在储存元素信息的同时,还简历附加的索引表。索引表中的每项成为索引项,索引项的一般形式是(关键字,地址)
- 散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希存储
数据类型:
数据类型是一个值的集合和定义在此集合上的一组操作的总称
- 原子类型:其值不可再分的数据类型(bool、int等)
- 结构类型:其值可以再分解为若干分量的数据类型(struct等)
抽象数据类型(ADT):
是抽象数据组织及其相关的操作,定义了一个ADT就是在定义一种数据结构
算法
算法:
- 程序=数据结构+算法
- 算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作
算法的特征:
- 算法必须拥有以下特性,否则不能被称为算法:
- 有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
- 确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
- 可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
- 输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
- 输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。
好算法的特质:
- 正确性:算法应能够正确地解决求解问题。
- 可读性:算法应具有良好的可读性,以帮助人们理解。
- 健壮性:输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
- 高效率:
- 花的时间少:时间复杂度低
- 低存储量需求:费内存,空间复杂度低
时间复杂度:事前预估算法时间开销T(n)与问题规模n的关系(T表示“time”)
- 加法规则:T(n) = T1(n) + T2(n) = O(f(n)) + O(g(n)) = O(max(f(n), g(n))) (多项相加,只保留最高阶的项,且系数变为1)
- 乘法规则:T(n) = T1(n)×T2(n) = O(f(n))×O(g(n)) = O(f(n)×g(n))(多项相乘,都保留 )
- 算法好坏:O(1) < O(log2n) < O(n) < O(nlog2n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)(口诀:常对幂指阶)
- 数量级仅需考虑循环内,最深层嵌套的部分
- 最坏时间复杂度:最坏情况下算法的时间复杂度
- 平均时间复杂度:所有输入示例等概率出现的情况下,算法的期望运行时间
- 最好时间复杂度:最好情况下算法的时间复杂度
- 一般只考虑最坏和平均复杂度
空间复杂度:空间开销(内存开销,S(n))与问题规模n之间的关系
- 算法原地工作: 算法所需内存空间为常量
- 只需关注存储空间大小与问题规模相关的变量
- 加法规则、乘法规则、算法好坏判定与时间复杂度一样
- 递归调用的大多数情况:空间复杂度=递归调用的深度
线性表
定义
- 线性表(Linear List)是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n = 0时线性表是一个空表
- 若用L命名线性表,则其一般表示为L = (a1, a2, … , ai, ai+1, … , an)
- a1是表头元素
- an是表尾元素
- 除第一个元素外,每个元素有且仅有一个直接前驱
- 除最后一个元素外,每个元素有且仅有一个直接后继
基本操作
- 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):按位查找操作。获取表L中第i个位置的元素的值。
- Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
- PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
- Empty(L):判空操作。若L为空表,则返回true,否则返回false。
- 什么时候要传入参数的引用“&”: 对参数的修改结果需要“带回来”,是引用类型而不是值类型
顺序表
用顺序存储的方式实现线性表顺序存储,把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现
实现
静态分配
// 头文件
#define MaxSize 10 // 定义最大长度
typedef struct{
int data[MaxSize]; // 静态数组存放元素
int length; // 顺序表长度
}SqList; // 顺序表定义
// 初始化顺序表
void InitList(SqList* L);
// 源文件
#include "sqList.h"
void InitList(SqList *L) {
// 不初始化默认都是0,但是内存中可能有脏数据
for (int i = 0; i < MaxSize; i++) {
L->data[i] = 0;
}
L->length = 0;
}
// 测试
#include <stdio.h>
#include "sqList.h"
int main() {
SqList L;
InitList(&L);
// 真正访问的时候用L.length
for (int i = 0; i < MaxSize; i++) {
printf("%d\n", L.data[i]);
}
return 0;
}
如果“数组”存满了怎么办:
可以放弃治疗,顺序表的表长刚开始确定后就无法更改(存储空间是静态的),同时如果提前初始化太多的空间而不用,又会造成资源的浪费,因此动态分配应运而生。
动态分配
// 头文件
#define InitSize 10
typedef struct {
int *data; // 动态分配数组指针
int MaxSize; // 最大容量
int length; // 顺序表长度
} SqList; // 顺序表定义
// 初始化顺序表
void InitList(SqList *L);
// 扩容 --- 将顺序表 L 的容量增加 length
void IncreaseSize(SqList *L, int length);
// 源文件
#include <malloc.h>
#include "sqList.h"
void InitList(SqList *L) {
// 不初始化默认都是0,但是内存中可能有脏数据
L->MaxSize = InitSize;
L->data = (int *) malloc(L->MaxSize * sizeof(int));
L->length = 0;
for (int i = 0; i < L->MaxSize; i++) {
L->data[i] = 0;
}
}
void IncreaseSize(SqList *L, int length) {
L->MaxSize += length;
int *p = L->data;
L->data = (int *) malloc((L->MaxSize) * sizeof(int));
// 初始化
for (int i = 0; i < L->MaxSize; i++) {
L->data[i] = 0;
}
// 复制
for (int i = 0; i < L->length; ++i) {
L->data[i] = p[i];
}
free(p);
p = NULL;
}
// 测试
int main() {
SqList L;
InitList(&L);
// 添加元素
for (int i = 0; i < L.MaxSize; i++) {
L.data[i] = i;
L.length++;
}
printf("============ %d\n",L.length);
// 扩容
IncreaseSize(&L, 5);
for (int i = 0; i < L.MaxSize; i++) {
printf("%d\n", L.data[i]);
}
return 0;
}
插入
// 插入 --- 在顺序表 L 中的 i 位置插入 e
void ListInsert(SqList *L, int i, int e) {
// 移动i后面的元素 --- 放置覆盖,从最后一个开始移动
for (int j = L->length; j >= i; --j) {
L->data[j] = L->data[j - 1];
}
L->length++;
L->data[i - 1] = e;
}
增加i的合法性判断
int ListInsert(SqList *L, int i, int e) { if (i < 1 || i > L->length + 1 || i >= L->MaxSize) return false; // 移动i后面的元素 --- 放置覆盖,从最后一个开始移动 for (int j = L->length; j >= i; --j) { L->data[j] = L->data[j - 1]; } L->length++; L->data[i - 1] = e; return true; }
删除
// 删除 --- 将顺序表 L 中的 i 位置删除,并将删除的元素通过 e 携带出来
int ListDelete(SqList *L, int i, int *e) {
if (i < 1 || i > L->length + 1 || i >= L->MaxSize)
return false;
*e = L->data[i - 1];
// 移动
for (int j = i; j < L->length; ++j) {
L->data[j - 1] = L->data[j];
}
L->length--;
// 尾元素初始化
L->data[L->length] = 0;
return true;
}
查找
// 按位查找
int GetElem(SqList *L, int i) {
return L->data[i - 1];
}
// 按值查找
int LocateElem(SqList *L, int e) {
for (int i = 0; i < L->length; ++i) {
if (L->data[i] == e) {
return i + 1;
}
}
return 0;
}
总结
#define InitSize 10
typedef struct {
int *data; // 动态分配数组指针
int MaxSize; // 最大容量
int length; // 顺序表长度
} SqList; // 顺序表定义
// 初始化顺序表
void InitList(SqList *L);
// 扩容 --- 将顺序表 L 的容量增加 length
void IncreaseSize(SqList *L, int length);
// 插入 --- 在顺序表 L 中的 i 位置插入 e
int ListInsert(SqList *L, int i, int e);
// 删除 --- 将顺序表 L 中的 i 位置删除,并将删除的元素通过 e 携带出来
int ListDelete(SqList *L, int i, int *e);
// 按位查找
int GetElem(SqList *L, int i);
// 按值查找
int LocateElem(SqList *L, int e);
//
// Created by ext.liyuanhao3 on 2023/1/28.
//
#include <malloc.h>
#include <stdbool.h>
#include "SqList.h"
void InitList(SqList *L) {
// 不初始化默认都是0,但是内存中可能有脏数据
L->MaxSize = InitSize;
L->data = (int *) malloc(L->MaxSize * sizeof(int));
L->length = 0;
for (int i = 0; i < L->MaxSize; i++) {
L->data[i] = 0;
}
}
void IncreaseSize(SqList *L, int length) {
L->MaxSize += length;
int *p = L->data;
L->data = (int *) malloc((L->MaxSize) * sizeof(int));
// 初始化
for (int i = 0; i < L->MaxSize; i++) {
L->data[i] = 0;
}
// 复制
for (int i = 0; i < L->length; ++i) {
L->data[i] = p[i];
}
free(p);
p = NULL;
}
int ListInsert(SqList *L, int i, int e) {
if (i < 1 || i > L->length + 1 || i >= L->MaxSize)
return false;
// 移动i后面的元素 --- 放置覆盖,从最后一个开始移动
for (int j = L->length; j >= i; --j) {
L->data[j] = L->data[j - 1];
}
L->length++;
L->data[i - 1] = e;
return true;
}
int ListDelete(SqList *L, int i, int *e) {
if (i < 1 || i > L->length + 1 || i >= L->MaxSize)
return false;
*e = L->data[i - 1];
// 移动
for (int j = i; j < L->length; ++j) {
L->data[j - 1] = L->data[j];
}
L->length--;
L->data[L->length] = 0;
return true;
}
int GetElem(SqList *L, int i) {
return L->data[i - 1];
}
int LocateElem(SqList *L, int e) {
for (int i = 0; i < L->length; ++i) {
if (L->data[i] == e) {
return i + 1;
}
}
return 0;
}
单链表
实现
typedef struct LNode {
int data;
struct LNode *next;
} LNode, *LinkList;
// 初始化单链表
LinkList InitLinkList();
#include <stdio.h>
#include <malloc.h>
#include "LinkList.h"
LinkList InitLinkList() {
LinkList L;
// L = NULL; // 空表,防止脏数据,没有头结点
// 分配一个头结点
L = (LNode *) malloc(sizeof(LNode));
if (L == NULL) {
return NULL;
}
L->next = NULL; // 头结点,尾部指向null
return L;
}
不带头结点,写代码更麻烦,我们一般使用的都是带头结点的单链表
- 对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑
- 对空表和非空表的处理需要用不同的代码逻辑
插入
// 按位插入
int LinkListInsert(LinkList L, int i, int e) {
if (i < 1) {
return 0; // i无效
}
LNode *p = GetLinkListElem(L, i); // 用于存放找到的结点
if (!LinkListInsertNextNode(p, e)) {
return 0;
}
return 1;
}
// 节点后插
int LinkListInsertNextNode(LNode *p, int e) {
if (p == NULL) { // i无效
return 0;
}
// 分配内存
LNode *s = (LNode *) malloc(sizeof(LNode));
// 内存不足等原因导致的失败
if (s == NULL) {
return 0;
}
s->data = e; // 添加元素
s->next = p->next; // 链接
p->next = s;
return 1;
}
// 按位获取节点
LNode *GetLinkListElem(LinkList L, int i) {
LNode *p = L;
// 找到第i位
for (int j = 0; p != NULL && j < i - 1; j++) {
p = p->next;
}
return p;
}
前插
方法一:通过头结点
int LinkListInsertPriorNode(LinkList L, LNode *p, int e) {
LNode *s = L->next; // 存放循环中的当前节点
LNode *q = L; // 存放前驱节点
while (s != p) {
q = s;
s = s->next;
}
LNode *m = (LNode *) malloc(sizeof(LNode));
m->data = e;
m->next = s;
q->next = m;
return 1;
}
方法二:改变data的引用
int LinkListInsertPriorNode(LNode *p, int e) {
if (p == NULL) { // i无效
return 0;
}
// 分配内存
LNode *s = (LNode *) malloc(sizeof(LNode));
// 内存不足等原因导致的失败
if (s == NULL) {
return 0;
}
s->next = p->next; // 依然后插链接
p->next = s;
s->data = p->data; // 改变data的引用
p->data = e;
return 1;
}
删除
按位删除
int LinkListDelete(LinkList L, int i, int *e) {
LNode *p = GetLinkListPriorNode(L, i); // 找到前驱
if (p->next == NULL) { // 要删的是尾元素NULL,不用删
return 0;
}
LNode *s = p->next; // 指向要删除的节点,用于释放内存
*e = s->data;
p->next = (s->next);
free(s);
return 1;
}
节点删除
int LinkListDeleteNode(LNode *p, int *e) {
// 方法1:通过头结点找
// 方法2:改变data的指向 --- 删除尾结点只能用方法1
if (p == NULL)
return 0;
LNode *s = p->next;
*e = p->data;
if (s != NULL) {
p->data = p->next->data;
p->next = p->next->next;
free(s);
s = NULL;
} else { // 尾结点无法删除
return 0;
}
return 1;
}
方法1
int LinkListDeleteNode(LinkList L, LNode *p, int *e) { // 方法1:通过头结点找 LNode *s = L->next; // 存放循环中的当前节点 LNode *q = L; // 存放前驱节点 while (s != p) { q = s; s = s->next; } *e = p->data; s = p->next; if (s == NULL) { q->next = NULL; free(p); p == NULL; } else { p->data = p->next->data; p->next = p->next->next; free(s); s = NULL; } return 1; }
查找
按位查找
LNode *GetLinkListElem(LinkList L, int i){
if(i < 0)
return NULL;
LNode *p = L;
// 找到第i位
for (int j = 0; p != NULL && j < i; j++) {
p = p->next;
}
return p;
}
按值查找
LNode *GetLinkListLocateElem(LinkList L, int e) {
LNode *p = L->next;
while (p != NULL && p->data != e) {
p = p->next;
}
return p;
}
长度
int LinkListLength(LinkList L) {
int len = 0;
LNode *p = L->next;
while (p != NULL) {
p = p->next;
len++;
}
return len;
}
尾插法
LinkList LinkListInsertInTail(LinkList L, int e) {
if (L == NULL) {
return NULL; // 未初始化 或 没有头结点
}
LNode *s = L;
while (s->next != NULL) { // 找尾结点
s = s->next;
}
LNode *t = (LNode *) malloc(sizeof(LNode)); // 新尾
t->data = e;
s->next = t;
s = t;
s->next = NULL;
return L;
}
头插法
LinkList LinkListInsertInHead(LinkList L, int e) {
if (L == NULL) {
return NULL; // 未初始化 或 没有头结点
}
LNode *h = (LNode *) malloc(sizeof(LNode)); // 新头
h->data = e;
h->next = L->next;
L->next = h;
return L;
}
总结
#ifndef DATA_STRUCTURE_LINKLIST_H
#define DATA_STRUCTURE_LINKLIST_H
typedef struct LNode {
int data;
struct LNode *next;
} LNode, *LinkList;
// 初始化单链表
LinkList InitLinkList();
// 按位插入
int LinkListInsert(LinkList L, int i, int e);
// 节点后插
int LinkListInsertNextNode(LNode *p, int e);
// 节点前插
int LinkListInsertPriorNode(LNode *p, int e);
// 按位查找前驱节点
LNode *GetLinkListPriorNode(LinkList L, int i);
// 按位查找
LNode *GetLinkListElem(LinkList L, int i);
// 按值查找
LNode *GetLinkListLocateElem(LinkList L, int e);
// 按位删除
int LinkListDelete(LinkList L, int i, int *e);
// 节点删除
int LinkListDeleteNode(LinkList L, LNode *p, int *e);
// 长度
int LinkListLength(LinkList L);
// 头插初始化
LinkList LinkListInsertInHead(LinkList L, int e);
// 尾插初始化
LinkList LinkListInsertInTail(LinkList L, int e);
#endif //DATA_STRUCTURE_LINKLIST_H
#include <stdio.h>
#include <malloc.h>
#include "LinkList.h"
LinkList InitLinkList() {
LinkList L;
// L = NULL; // 空表,防止脏数据,没有头结点
// 分配一个头结点
L = (LNode *) malloc(sizeof(LNode));
if (L == NULL) {
return NULL;
}
L->next = NULL; // 头结点,尾部指向null
return L;
}
int LinkListInsert(LinkList L, int i, int e) {
if (i < 1) {
return 0; // i无效
}
LNode *p = GetLinkListPriorNode(L, i); // 用于存放找到的结点
if (!LinkListInsertNextNode(p, e)) {
return 0;
}
return 1;
}
int LinkListInsertNextNode(LNode *p, int e) {
if (p == NULL) { // i无效
return 0;
}
// 分配内存
LNode *s = (LNode *) malloc(sizeof(LNode));
// 内存不足等原因导致的失败
if (s == NULL) {
return 0;
}
s->data = e; // 添加元素
s->next = p->next; // 链接
p->next = s;
return 1;
}
LNode *GetLinkListPriorNode(LinkList L, int i) {
LNode *p = L;
// 找到第i位
for (int j = 0; p != NULL && j < i - 1; j++) {
p = p->next;
}
return p;
}
int LinkListInsertPriorNode(LNode *p, int e) {
if (p == NULL) { // i无效
return 0;
}
// 分配内存
LNode *s = (LNode *) malloc(sizeof(LNode));
// 内存不足等原因导致的失败
if (s == NULL) {
return 0;
}
s->next = p->next; // 依然后插链接
p->next = s;
s->data = p->data; // 改变data的引用
p->data = e;
return 1;
}
int LinkListDelete(LinkList L, int i, int *e) {
LNode *p = GetLinkListPriorNode(L, i); // 找到前驱
if (p->next == NULL) { // 要删的是尾元素NULL,不用删
return 0;
}
LNode *s = p->next; // 指向要删除的节点,用于释放内存
*e = s->data;
p->next = (s->next);
free(s);
return 1;
}
int LinkListDeleteNode(LinkList L, LNode *p, int *e) {
// 方法1:通过头结点找
LNode *s = L->next; // 存放循环中的当前节点
LNode *q = L; // 存放前驱节点
while (s != p) {
q = s;
s = s->next;
}
*e = p->data;
s = p->next;
if (s == NULL) {
q->next = NULL;
free(p);
p == NULL;
} else {
p->data = p->next->data;
p->next = p->next->next;
free(s);
s = NULL;
}
return 1;
}
LNode *GetLinkListElem(LinkList L, int i) {
if (i < 0)
return NULL;
LNode *p = L;
// 找到第i位
for (int j = 0; p != NULL && j < i; j++) {
p = p->next;
}
return p;
}
LNode *GetLinkListLocateElem(LinkList L, int e) {
LNode *p = L->next;
while (p != NULL && p->data != e) {
p = p->next;
}
return p;
}
int LinkListLength(LinkList L) {
int len = 0;
LNode *p = L->next;
while (p != NULL) {
p = p->next;
len++;
}
return len;
}
LinkList LinkListInsertInHead(LinkList L, int e) {
if (L == NULL) {
return NULL; // 未初始化 或 没有头结点
}
LNode *h = (LNode *) malloc(sizeof(LNode)); // 新头
h->data = e;
h->next = L->next;
L->next = h;
return L;
}
LinkList LinkListInsertInTail(LinkList L, int e) {
if (L == NULL) {
return NULL; // 未初始化 或 没有头结点
}
LNode *s = L;
while (s->next != NULL) { // 找尾结点
s = s->next;
}
LNode *t = (LNode *) malloc(sizeof(LNode)); // 新尾
t->data = e;
s->next = t;
s = t;
s->next = NULL;
return L;
}
双链表
实现
typedef struct DNode{
int data;
struct DNode *prior, *next;
} DNode, *DLinkList;
DLinkList InitDLinkList() {
DLinkList L;
L = (DNode *) malloc(sizeof(DNode));
if (L == NULL)
return NULL;
L->next = NULL;
L->prior = NULL;
return L;
}
插入
int InsertNextDNode(DNode *p, int e) {
if (p == NULL)
return 0;
DNode *m = (DNode *) malloc(sizeof(DNode));
m->data = e;
m->next = p->next;
// 不是尾结点,链接后继节点的前驱
if (p->next != NULL)
p->next->prior = m;
p->next = m;
m->prior = p;
return 1;
}
删除
// 删除后继
int DeleteNextDNode(DNode *p) {
if (p == NULL)
return 0;
DNode *m = p->next;
if (m == NULL)
return 0;
if (m->next != NULL)
m->next->prior = p;
p->next = m->next;
free(m);
return 1;
}
销毁
int DestroyDLinkList(DLinkList L) {
while (L->next != NULL) {
DeleteNextDNode(L); // 释放各个节点
}
free(L); // 释放头结点
L = NULL;
return 1;
}
循环链表
循环单链表是在单链表基础上,初始化时,将尾结点指向头结点
循环双链表是在双链表基础上,初始化时,将尾结点指向头结点,头结点指向尾结点
初始化
循环单链表
// 结构体同单链表
LinkList InitCirculateLinkList() {
LinkList L;
// 分配一个头结点
L = (LNode *) malloc(sizeof(LNode));
if (L == NULL) {
return NULL;
}
L->next = L; // 头结点,尾部指向头部
return L;
}
循环双链表
// 结构体同双链表
DLinkList InitCirculateDLinkList(){
DLinkList L;
L = (DNode *) malloc(sizeof(DNode));
if (L == NULL)
return NULL;
L->next = L;
L->prior = L;
return L;
}
判空
if(L->next == L) return 1;
判尾
if(p->next == L) return 1;
插入
- 不用判空
删除
- 不用判空
静态链表
分配一整片连续的内存空间,各个结点集中安置
每个结点由两部分组成:data(数据元素)和next(游标)
0号结点充当“头结点”,不具体存放数据
游标为-1表示已经到达表尾
游标充当“指针”,表示下个结点的存放位置,下面举一个例子:
每个数据元素4B,每个游标4B(每个结点共8B),设起始地址为addr,e1的存放地址为addr + 8*2(游标值)
