一、数据结构与算法
1、概念
- 数据:描述客观事物的数值、字符以及能输入机器且能被处理的各种符号集合
- 数据元素:组成数据的基本单位,是数据集合的个体(可由若干数据项组成)
- 数据项:构成数据元素的不可分割的最小单位
- 一个数据元素可由若干个数据项组成,例如,一位学生的信息记录为一个数据元素,它是由学号、姓名、性别等数据项组成
- 数据对象:性质相同的数据元素的集合,是数据的一个子集。例如,整数数据对象是集合 N={ 0,士 1,士 2,… }
- 数据结构:相互之间存在一种或多种特定关系的数据元素的集合
- 数据结构包括三方面的内容:逻辑结构、存储结构和数据的运算
数据结构的形式定义为:数据结构是一个二元组
Data Structure=(D.S)
其中:D 是数据元素的有限集,S 是 D 上关系的有限集
2、逻辑结构
- 逻辑结构:指数据元素之间逻辑关系描述。数据的逻辑结构分为线性结构和非线性结构(逻辑结构独立于存储结构)
- 线性表是典型的线性结构;集合、树和图是典型的非线性结构
- 四种基本结构关系
3、存储结构(又称物理结构)
- 数据的存储结构主要有顺序存储,非顺序存储(包括链式存储、索引存储和散列存储)
- 顺序存储:逻辑上相邻的元素存储在物理位置上也相邻的存储单元中
- 链式存储:不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系
其优点是不会出现碎片现象,能充分利用所有存储单元;缺点是每个元素因存储指针而占用额外的存储空间,且只能实现顺序存取
- 索引存储:在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)
其优点是检索速度快;缺点是附加的索引表额外占用存储空间。另外,增加和删除数据时也要修改索引表,因而会花费较多的时间
- 散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储
其优点是检索、增加和删除结点的操作都很快;缺点是若散列函数不好,则可能出现元素存储单元的冲突,而解决冲突会增加时间和空间开销
4、算法
- 算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。此外,一个算法还具有下列几个重要特性
(一)有穷性。一个算法必须总是在执行有穷步后结束,且每一步都是在有穷时间内完成
(二)确定性。算法中每条指令必须有确切的含义,且相同的输入只能得到相同的输出
(三)可行性。算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现
(四)输入。一个算法有零个或多个输入
(五)输出。一个算法有一个或多个输出
- 通常设计一个“好“的算法应考虑达到以下目标
(一)正确性。算法应能够正确地求解问题
(二)可读性。算法能具有良好的可读性,以帮助人们理解
(三)健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果
(四)高效率与低存储量需求。效率是指算法执行的时间,存储量需求是指算法执行过程中所需的最大存储空间
- 算法效率:度量是通过时间复杂度和空间复杂度来描述的。一个语句的频度是指该语句在算法中被重复执行的次数 > 算法中所有语句的频度之和记为 T(n),它是该算法问题规模的函数,时间复杂度主要分析 T(n)的数量级
- 算法时间复杂度
5、细节补充
- size_t:无符号整型,只能赋正数,表示 C 中任何对象所能达到的最大长度
二、线性结构
1、线性表
- 线性表示具有 n 个数据元素的有限序列
- 线性表基本操作
- 顺序表:线性表的顺序存储称为顺序表,它是由一组地址连续的存储单元依次存储线性表中的数据元素,逻辑相邻,物理位置上也相邻
(一)存储密度高,每个节点只存储一个数据元素
(二)由于(逻辑相邻,物理位置上也相邻),插入删除需要移动大量元素
(三)顺序表插入或删除一个元素,需平均移动 (n-1)/2 个元素,具体移动元素数和插入删除位置有关
- 线性表的顺序存储-静态分配
//线性表元素类型为ElemType
#define MaxSize 50 //定义线性表最大长度
typedef struct {
ElemType data[MaxSize]; //数组 data 的大小在编译时就已经确定,且无法更改
int length; //顺序表的当前长度
}SqList;
//上述属于静态分配,由于数组大小和空间已经固定,一旦空间满了,加入新数据产生溢出,会使程序崩溃
- 线性表的顺序存储-动态分配
- 注意:malloc 的原型定义在 <stdlib.h> 头文件中,其基本形式为:void* malloc(size_t size);
意味着malloc 返回一个指向 void 类型的指针,(ElemType) 是一个类型转换,它将 malloc 返回的 void 指针转换为 ElemType* 类型的指针,ElemType 是一个通用类型名称或抽象名称
//动态内存分配,一旦空间满了,另外开辟一块更大的存储空间,替换原本
#define InitSize 50 //定义线性表最初长度
typedef struct {
ElemType* data; // 使用指针来动态分配数组
int length; // 顺序表的当前长度
int MaxSize; // 顺序表当前的最大容量
} SeqList;
//C语言初始动态分配语句
L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);
- 顺序表的插入(这里注意在位置 i 处放入新元素 e ,实际上是索引 i-1 处, i 的范围在 1 和 length 之间)
/* 在顺序表L的第i(1≤i≤L.length + 1)个位置插入新元素e。若i的输入不合法,则返回false,
表示插入失败;否则,将顺序表的第i个元素及其后的所有元素右移一个位置,腾出一个空位置插入
新元素e,顺序表长度增加1,插入成功返回true */
bool ListInsert(SqList& L, int i, ElemType e) {
if (i < 1 || i > L.length) { // 判断i范围是否有效
return false;
}
if (L.length == MaxSize) { // 检查顺序表是否已满
return false; // 如果已满,则无法插入新元素
}
for (int j = L.length; j >= i; j--) {
L.data[j] = L.data[j - 1]; // 将第i个元素及之后的元素后移
}
L.data[i - 1] = e; // 在位置i处放入新元素e
L.length++; // 线性表长度增加1
return true;
}
- 线性表的删除(必须要传地址,且要提前保存被删除元素)
(一)线性表中有 n 个数据元素,删除表中第 i 个元素需要移动 n-i 个元素
(二)顺序存储的线性表,访问结点是 O(1),增加删除结点是 O(n)
bool ListDelete(SqList& L, int i, ElemType& e) {
if (i < 1 || i > L.length) { // 判断i范围是否有效
return false;
}
e = L.data[i - 1]; // 保存被删除的元素
for (int j = i; j < L.length; j++) {
L.data[j - 1] = L.data[j]; // 将第i个元素之后的元素前移
}
L.length--; // 顺序表长度减1
return true;
}
- 线性表插入与删除异同
插入是 e = L.data[i - 1],删除是 L.data[i - 1] = e
插入是 j = L.length; j >= i; j–,删除是 j = i; j < L.length; j++
插入是(后移) L.data[j] = L.data[j - 1],删除是(前移) L.data[j - 1] = L.data[j]
- 按值查找:按值查找就是顺序查找,按位查找主要是判断,然后直接返回元素索引
(一)查找线性表 L 中元素值为 e 的元素,成功则返回元素位置,否则返回 0
int LocateElem(SqList L, ElemType e)
{
for (int i = 0; i < L.length; i++)
{
if (L.data[i] == e)
{
return i ; // 返回索引
}
}
return 0; // 查找失败
}
- 按位查找
int GetElem(SqList L, int i)
{
// 检查位置 i 是否在顺序表的合法范围内
if (i < 1 || i > L.length) {
// 如果位置不合法,返回 0(或其他表示错误的值)
// 注意:这里假设 0 不是 ElemType 的有效值,或者即使它是,调用者也知道如何区分这种情况
return 0;
}
else {
// 返回第 i 个元素的值(注意元素索引是 i - 1)
// 这里假设 L.data 是一个有效的指针,指向一个包含至少 L.length 个 ElemType 类型元素的数组
return L.data[i - 1];
}
}
2、链表
- 线性表的链式存储,又称单链表;每个链表结点(存放自身信息 + 存放后继指针)(数据域 + 指针域)
- 单链表中逻辑上相邻的元素,物理位置不一定相邻
- 单链表第一个结点之前附加一个结点(头结点),头结点指针域指向线性表的第一个元素结点(头结点是为了方便运算)
头结点和头指针的区别:不管带不带头结点,头指针始终指向链表的第一个结点;而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息
- 引入头结点
(一)第一个数据结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表的其他位置上的操作一致,无需进行特殊处理
(二)无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空)
- 按序号查找结点值(LNode* 是指向链表头节点(通常称为 LNode)的指针类型)
LNode* GetElem(LinkList L, int i) {
int j = 1;
LNode* p = L->next; // 从头节点的下一个节点开始遍历
if (L == NULL || i == 0) { // 如果链表为空或者i为0(通常头节点不计算在内),返回NULL
return NULL;
}
if (i < 0) { // 如果i小于0,也返回NULL
return NULL;
}
// 遍历链表直到找到第i个节点或链表结束
while (p != NULL && j < i) {
p = p->next;
j++;
}
// 如果j等于i,则找到了第i个节点,否则p为NULL(链表长度小于i)
return p;
}
- 插入结点
p = GetElem(L,i-1) //查找插入位置的前驱结点
s->next = p->next
p->next = s
- p,q,s 指向结点的指针,q 是 p 的直接前驱,在 q 与 p 之间插入 s,需执行 q->next=s ; s->next=p
- 双链表 > 每个结点含有两个指针域,一个指向前驱结点,另一个指向后继结点
// 定义双链表节点结构体
typedef struct DNode {
ElemType data; // 数据域
struct DNode *prior; // 前驱指针
struct DNode *next; // 后继指针
} DNode, *DLinkList;
- 循环链表
三、栈和队列
1、栈
- 栈(Sack):只允许在一端进行插入或删除操作的线性表 > 只能在某一端进行插入和删除操作,栈顶插入删除,先进后出
- 栈的基本操作
- 顺序栈:采用顺序存储,利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指向当前栈顶元素位置
#define MaxSize 50
typedef struct { //定义栈中元素的最大个数
ElemType data[MaxSize]; //存放栈中元素
int top; //栈顶指针
}SqStack;
- 顺序栈的初始化
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
- 判断栈空
bool StackEmpty(SqStack S)
{
if (S.top == -1) {
return true;
}
else{
return false;
}
}
- 进栈(先移动栈顶指针,再存入元素)
bool Push(SqStack& S, ElemType x) {
if (S.top == MaxSize - 1) {
return false; // 栈满
}
S.data[++S.top] = x; // 先增加栈顶指针,再向栈顶位置赋值
return true;
}
- 出栈
bool Pop(SqStack& S, ElemType& x) {
if (S.top == -1) { //既避免与0混淆,又表示了栈顶元素的前一个位置
return false; // 栈空
}
x = S.data[S.top--]; // 先出栈(通过引用赋值给 x),指针再减
return true;
}
- 在 Push 函数中,部需要通过引用传递 x(&),因为 x 是要复制到栈中的新元素。不需要修改 x 本身,而是将它的值复制到栈的顶部
- 初始化、进栈一半,出栈全部&;全前加后减
- 读栈顶元素
bool GetTop(SqStack S, ElemType &x)
{
// 检查栈是否为空
if (S.top == -1)
{
// 栈为空,无法获取栈顶元素
return false; // 返回 false 表示栈为空
}
// 栈不为空,获取栈顶元素
x = S.data[S.top];
return true; // 返回 true 表示成功获取栈顶元素
}
- 共享栈:两个栈的栈 J 顶指针都指向栈顶元素,top 0=-1 时 0 号栈为空,top 1=MaxSize时 1 号栈为空;仅当两个栈顶指针相邻(top 1-top 0=1)时,判断为栈满。当 0 号栈进栈时 top 0 先加 1 再赋值,1 号栈进栈时 top 1 先减 1 再赋值;出栈时则刚好相反
- 链栈:相比顺序栈,通常不会出现栈满的情况
2、队列
- 队列(Queue)简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除
- 队列操作的特性是先进先出(First In First Out,FIFO);队尾插入,队头删除
(一)向队列中插入元素称为入队或进队;删除元素称为出队或离队
(二)队头:允许删除的一端,又称队首
(三)队尾:允许插入的一端
- 队列的顺序存储:队头指针 front 指向队头位置;队尾指针指向队尾的下一个位置
#define MaxSize 50
typedef struct
{
ElemType data[MaxSize]; //存放队列元素
int front,rear; //队头指针和队尾指针
}SqQueue;
- 常用式子
初始时:Q->front = Q->rear=0
队首指针进 1:Q->front = (Q->front + 1) % MAXSIZE。
队尾指针进 1:Q->rear = (Q->rear + 1) % MAXSIZE。
队列长度:(Q->rear - Q->front + MAXSIZE) % MAXSIZE
队满条件: (Q->rear + 1)%Maxsize == Q->front
队空条件仍: Q->front == Q->rear
队列中元素的个数: (Q->rear - Q ->front + Maxsize)% Maxsize
- 进队操作:队不满时,先送值到队尾元素,再将队尾指针加 1
- 出队操作:队不空时,先取队头元素值,再将队头指针加 1
3、串
- 串是由 0 或多个字符组成的有限序列,一般记为,S = “a 1a 2…a n“
- 空串:n = 0 时串称为空串
- 空格串:由一个或多个空格组成的串称为空格串,其长度为串中空格字符的个数
- 两个串 S 1 和 S 2,求串 S 2 在 S 1中首次出现位置的运算称作串的模式匹配
4、矩阵
- 压缩存储:对多个值相同的元素只分配一个存储空间,对零元素不分配存储空间
- 特殊矩阵:具有许多相同矩阵元素或零元素,这些相同矩阵元素或零元素的分布有一定规律性(如对称矩阵、上(下)三角矩阵,稀疏矩阵)
四、树
1、概念
- 树是 n(n≥0)个结点的有限集。当 n=0 时,称为空树(最合适元素分支层次关系的数据)
(一)树的根结点没有前驱结点,除根结点外的所有结点有且只有一个前驱结点
(二)树中所有结点可以有零个或多个后继结点
(三)树中的结点数 = 所有结点的度数 + 1
(四)度为 m 的树中,第 i 层上至多有 mi-1 个结点
(五)高度为 h 的 m 叉树至多有(mh-1)/(m-1)个结点
(六)具有 n 个结点的 m 叉树的最小高度为[ logm(n(m-1)+1) ]
- 结点
(一)分支结点:度大于 0 的结点(非终端结点)
(二)叶子结点:度为 0(没有孩子结点)的结点(终端结点)
(三)兄弟结点:有相同双亲的结点
- 结点的层次、深度从根结点开始从上往下;结点的高度是从叶结点开始从下往上
- 有序树和无序树:有序树中结点的各子树从左到右是有次序的,否则称为无序树
- 路径和长度
(一)路径:由两个结点之间所经过的结点序列构成
(二)路径长度:路径上所经过的边的个数
2、二叉树
- 性质
(一)在二叉树的第 i 层最多有 2 i-1 个结点
(二)深度为 k 的二叉树最多有 2 k-1 个结点(高度为 h 的二叉树最多有 2 h-1 个结点)
(三)对任意一颗二叉树,如果其终端结点数为 n 0,度为 2 的结点数为 n 2,则 n 0= n 2+1
(四)具有 n 个结点的完全二叉树的深度为 log 2 n+1
(五)如果根节点标号为 1,则对任意结点 i,它的左孩子为 2 i,右孩子为 2 i+1
- 不存在度大于 2 结点的树,是 n(n>=0)个结点的有限集合
- 二叉树五种种类:空二叉树、只有一个节点、只有左子树、只有右子树,左右两子树都有
- 满二叉树:除叶子结点之外的每个结点度数均为 2;约定编号从根结点(根结点编号为 1)起,自上而下,自左向右
- 完全二叉树:高度为 h,有 n 个结点的二叉树
(一)叶子结点只能出现在最下两层
(二)最下层的叶子一定集中在左部连续位置
(三)倒数第二层,若有叶子结点,一定都在右部连续位置
(四)如果结点度为 1,则该结点只有左孩子
(五)同样结点数为 2 的二叉树,完全二叉树的深度最小
- 二叉排序树:左子树均小于根结点,右子树均大于根结点,左子树和右子树各是一颗二叉排序树
- 平衡二叉树:树上任一结点的左子树和右子树的深度之差绝对值不超过 1
- 二叉树的链式存储:用链表来表示一棵二叉树,链表中每个结点由三个域组成,数据域和左右指针域
- 二叉树遍历 ### (一)先序遍历(根左右)
void PreOrder(BiTree T)
{
if (T!= NULL)
{
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
(二)中序遍历(左根右)
void InOrder(BiTree T)
{
if (T!= NULL)
{
InOrder(T->Lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
(三)后序遍历(左右根)
void PostOrder(BiTree T)
{
if (T!= NULL)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
(四)层次遍历:从上往下一层一层遍历
void LevelOrder(BiTree T)
{
InitQueue(Q); //初始化辅助队列
BiTNode* p = T;
EnQueue(Q, p); //将根结点入队
while (!IsEmpty(Q)) { //队列不空则循环
DeQueue(Q, p); //队头元素出队
visit(p); //访问出队结点
if (p->lchild != NULL)EnQueue(Q, p->lchild); //左子树不空,左子树结点入队
if (p->rchild != NULL)EnQueue(Q, p->rchild); //右子树不空,右子树结点入队
}
}
- 已经三种遍历中一种求另一种遍历,必须要知道中序遍历
- 树转换成二叉树的画法
(一)在兄弟结点之间加一条线
(二)对每个结点,只保留它与第一个孩子的连线,抹去与其他孩子的连线
(三)以树根为轴心,顺时针旋转 45° > 树转换为二叉树,根结点的右子树总是空的
五、图
1、无向图
- 顶点 v 是依附于该顶点的边的条数。无向图的全部顶点的度的和等于边数的两倍
- n 个顶点的无向图,有 n-1 条边
2、有向图
- 入度是以顶点ⅴ为终点的有向边的数目。出度是以顶点ⅴ为起点的有向边的数目。顶点的度等于入度和出度之和。有向图的全部顶点的入度之和与出度之和相等,并且等于边数
3、边的权值和网
- 边上带有权值的图称为带权图,也称为网
4、图的两种存储结构
- 邻接矩阵法-无向图
(一)无向图的邻接矩阵一定是个对称矩阵
(二)对于无向图,邻接矩阵的第 i 行(或第 i 列)非零元素或非无穷元素)的 i 个数正好是第 i 个顶点的度
- 邻接矩阵法-有向图
(一)对于有向图,邻接矩阵的第 i 行(或第列)非零元素或非无穷元素的 i 个数正好是第ⅰ个顶点的出度(或入度)
- 邻接表-无向图
5、深度优先搜索和广度优先搜索
- 深度优先搜索(类似树的先根遍历):如图,采用深度优先搜索遍历顺序为 A->C->B->E->D->F->G
- 广度优先搜索(类似树的层次遍历),如图,采用广度优先遍历顺序为 A->B–>C->E->F->G->H->D
6、最小生成树
- 一个有 N 个点的图,边一定是大于等于 N-1 条的。图的最小生成树,就是在这些边中选择 N-1 条出来,连接所有的 N 个点。这 N-1 条边的边权之和是所有方案中最小的
- prim (普里姆算法):任取一顶点,去掉所有边,选择一个最近的顶点,并将该顶点与相应的边加入,不形成回路,再重复加点
- Kruskal 算法:去掉所有的边,选边(权值最小,不构成回路)重复,直到所有顶点并入
7、求最短路径
- Dijkstra 算法:一种标号法,给赋权图的每一个顶点记一个数,称为顶点的标号,临时标号 T 表示从始顶点到该标点的最短路长的上界、固定标号 P 表示从始顶点到该顶点的最短路长
- AOV网:若用DAG图表示一个工程,其顶点表示活动,用有向边<Vi,Vj>表示活动 Vi,必须先于活动 Vj进行的这样一种关系,则将这种有向图称为顶点表示活动的网络,记为 AOV 网
8、拓扑排序
- 从 AOV 网中选择一个没有前驱的顶点并输出
- 从网中删除该顶点和所有以它为起点的有向边
- 重复前面两步直到当前的 AOV 网为空或当前网中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环
9、关键路径
- 带权有向图,从开始顶点到结束顶点的所有路径中,具有最大路径长度的路径称为关键路径。而把关键路径上的活动称为关键活动
六、查找
1、顺序查找
- 主要用于线性表中进行查找,逐个检查关键字是否满足条件
- 顺序查找长度为 n 的顺序表,平均查找长度为 (n+1) /2
2、折半查找
- 仅适用于有序的顺序表 ## 3、哈希查找
- 解决冲突的方法:开放定址法、线性探测法、平方探测法、再散列法
七、排序
1、五大类
- 插入排序,交换排序,选择排序,归并排序,基数排序
2、插入排序
- 注意记忆
void InsertSort(ElemType A[], int n)
{
int i, j;
ElemType temp; // 引入临时变量用于存储当前要插入的元素
for (i = 1; i < n; i++) // 从第二个元素开始遍历
{
temp = A[i]; // 保存当前要插入的元素
// 将大于temp的元素向后移动一位
for (j = i - 1; (j >= 0) && (A[j] > temp); j--)
{
A[j + 1] = A[j];
}
// 将temp插入到正确位置
A[j + 1] = temp;
}
}
3、冒泡排序
- 使用最广泛,C 语言中运用: 1、C语言
- 注意冒泡排序顺序表,内层循环一轮只能让最后一个元素到第一个位置 ## 4、快速排序
- 注意记忆
#include <stdio.h>
// 快速排序的分区函数
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选择最右边的元素作为基准
int i = (low - 1); // 较小元素的索引
for (int j = low; j <= high - 1; j++) {
// 如果当前元素小于或等于基准
if (arr[j] <= pivot) {
i++; // 增加较小元素的索引
// 交换 arr[i] 和 arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 交换 arr[i+1] 和 arr[high](或基准元素)
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return (i + 1);
}
// 快速排序函数
void quickSort(int arr[], int low, int high) {
if (low < high) {
// pi 是分区后基准元素的正确位置
int pi = partition(arr, low, high);
// 递归地排序基准元素左边的子数组
quickSort(arr, low, pi - 1);
// 递归地排序基准元素右边的子数组
quickSort(arr, pi + 1, high);
}
}
int main() {
int arr[] = { 3, 5, 2, 1, 4 };
int len = sizeof(arr) / sizeof(arr[0]);
// 调用快速排序函数
quickSort(arr, 0, len - 1);
// 打印排序后的数组
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
5、堆排序
- 堆排序(Heapsort)是指利用堆积树(堆),通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆
6、基数排序
- 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
7、补充
- 初始基本有序,用插入排序;要求数据稳定性用冒泡排序
- 不稳定:快速,选择,希尔,堆
- 一趟结束不一定能选出一个元素放在最终位置:简单选择排序