一·概念
1.数据:数据是信息的载体,是描述客观事物属性的数、字符以及所有能够输入到计算机中并被识别处理的符号集合。
2.数据元素:数据元素是数据的基本单位,一个数据元素可由若干个数据项组成,数据项是构成数据元素的
不可分割的最小单位。
3.数据类型:数据类型是一个值的集合和定义在此集合上的一组操作的总称。
- 原子类型:其值不可在分,如int,long,double,float,char
- 结构类型:其值可以再分解为若干成员,如结构体struct
- 抽象数据类型:抽象数据组织和与之相关的操作。 抽象数据类型(ADT)是指一个数学模型以及定义在该模型上的一组操作。抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示无关。通常用数据对象、数据关系、基本操作集这样的三元来表示抽象数据类型。
4.数据结构:在任何问题中,数据元素都不是孤立存在的,而是在他们之间存在着某种关系,这种数据元素相互之间的关系成为结构(Structure)。数据结构是相互之间存在一种或多种特性关系的数据元素的集合。
数据结构包括三方面内容:逻辑结构、存储结构和数据的运算。数据的逻辑结构和存储结构是密不可分的两个方面。一个算法的设计取决于所选定的逻辑结构,而算法的实现依赖于所采用的存储结构。
5.逻辑结构:是指数据元素之间的逻辑关系,即从逻辑关系上描述数据,与数据的存储无关,逻辑结构分为线性结构,非线性结构。
6.存储结构:是指数据结构在计算机中的表示(映像),也称物理结构。他包括数据元素的表示和关系的表示。
物理结构和逻辑结构的关系:肉体—灵魂。主要存储结构有:顺序存储、链式存储、索引存储和散列存储。
- 顺序存储:存储的物理位置相邻,是一种’随机存取’的存储结构,目标地址=首地址+单元长度*单元序号,如数组。
- 链式存储:存储的物理位置未必相邻,通过记录相邻元素的物理位置来找到相邻元素,是一种’顺序存取’的存储结构,如链表、栈、队列。
- 索引存储:类似于目录。
- 散列存储:通过某种方式计算出元素的物理位置。
二.算法
一.概念
1.算法:是对问题求解步骤的描述,通过有限序列的指令来实现。
2.五大特征:
- 有穷性:有限步之后结束,不会出现无限循环。
- 确定性:不存在二义性,算法的每个步骤被精确定义。
- 可行性:在计算机的计算能力可行下,算法是可行的。
- 输入:能被计算机处理的各种类型数据。
- 输出:一至多个程序输出结果。
3.时间复杂度:
- 它用来衡量算法随着问题规模增大,算法执行时间增长的快慢。
- 时间复杂度是问题规模的函数:记作T(n),时间复杂度主要分析T(n)的数量级。
- T(n)=O(f(n)),f(n)是一个函数,一般考虑最坏情况下的时间复杂度。函数f(n)的常数c、常数乘积c省略,底数用10
- c < log2N < n < nLog2N < n^2 < n^3 < 2^n < 3^n < n!
4.空间复杂度:
- 空间复杂度S(n)=O(f(n))指算法运行过程中所使用的辅助空间的大小。
- 辅助空间:除了存储算法本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。
二.时间复杂度计算
1.加法法则:T(n)=max(T1(n)+T2(n))
2.乘法法则:T(n)=T1(n)*T2(n)
void algor1() {
int sum = 0;
int n = 100;
for (int i = 0; i < n; i++) //外层循环次数为n
{
sum = sum + i; //循环体时间复杂度为O(1)
}
//f(n)*f(1),所以这个循环体时间复杂度为O(n)
}
void algor2() {
int sum = 0;
int n = 100;
//设执行k轮结束,则i*(2^k)>n结束,省略常数等,则k<=logn
for (int i = 1; i <= n; i=2*i) //外层循环时间复杂度O(logN)
{
sum = sum + i; //循环体时间复杂度为O(1)
}
//f(logN)*f(1),所以这个循环体时间复杂度为O(logN)
}
void algor3() {
int sum = 0;
int n = 100;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
sum++;
}
}
//乘法法则:T(n)=f(n) * f(n))=O(n^2)
}
void algor4() {
int sum = 0;
int n = 100;
for (int i = 0; i < n; i++)
{
sum = sum + i;
}
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
sum++;
}
}
//加法法则:T(n)=max(f(n),f(n^2))=O(n^2)
}
三.线性结构
一.顺序存储
1.线性表的顺序存储是用一组地址(存储器每个存储单元的编号)连续的存储单位,如数组,依次存储线性表中的数据元素。
2.是一个等差数列:an=am+(n-m)d ----> ai=a1+(i-1)d,d是元素占据存储单元数,所以时间复杂度为O(1),即随机存取。
#include<stdio.h>
#include <stdlib.h>
#define MaxSize 50
typedef int Elemtype;
//1.静态建表大小固定,假定表中元素类型为int
typedef struct {
Elemtype data[MaxSize];
int lenth;
}List;
//2.动态建表
typedef struct {
Elemtype* data;
int length;
}SeqList;
SeqList sList;
int main() {
//静态建表
List list;
list.data[0] = 1;
list.lenth = 50;
printf("%d\n", sizeof(list));
//动态建表
sList.data = (Elemtype*)malloc(sizeof(Elemtype) * 5);
printf("%d\n", sizeof(sList.data));
return 0;
}
二.链式存储
1.双链表:节点有数据域data和指针域头,前结点指针pre和尾结点指针next,指针域存储的是物理地址
2.静态链表:借助数组描述线性表的链式存储结构,节点有数据域data和指针域,不过指针域是数组下标
#include<stdio.h>
#define MaxSize 50
typedef int Elemtype;
//定义双链表
typedef struct DNode {
Elemtype data;
//前驱和后继指针
struct DNode* prior, * next;
};
//定义静态链表
typedef struct {
Elemtype data;
//前驱和后继指针
int next;
}SLinkList[MaxSize];
三.栈
一端操作,后进先出
1.顺序栈:就是一个数组,以下标0为栈底,空栈top==-1,满栈top==MaxSize-1,元素个数top+1
2.共享栈:每个栈各自开辟空间,有时利用率不如一个共享空间的栈,共享栈就是将一个数组分成两部分,数组的两端分别作为两个栈的栈底,栈满top1+1==top2
3.链式栈:以头指针作为栈顶指针,空栈top==NULL;很明显,不存在满栈
#include<stdio.h>
#define MaxSize 50
typedef int Elemtype;
//1.定义顺序栈
typedef struct {
Elemtype data[MaxSize];
int top;
}SqStack;
//2.定义共享栈
typedef struct {
Elemtype data[MaxSize];
int top1;
int top2;
}SqDoubleStack;
//3.定义链式栈
typedef struct SNode {
Elemtype data;
//栈顶指针
struct SNode* next;
}sNode;
typedef struct LinkStack {
//栈顶指针
sNode top;
//节点数
int count;
}LinkStack;
四.队列
只允许一端插入,另一端删除的线性表,先进先出
1.顺序队列:用数组实现队列,可用两个指针记录队首和队尾,但是由于队首指针和队尾指针有固定的先后之分,如果队首指针front前面有空余空间,而队尾指针rear又无法指向该位置,就会造成假溢出,那就可以采用循环队列。
队空:front==rear
2.循环队列:将数组的首位相连,如果队尾和队首指针指到了数组末尾,那么还能继续指向下标0
入队:rear=(rear+1)%MaxSize
出队:front=(front+1)%MaxSize
队空:front == rear
队满:牺牲rear的一个插入机会,保留一个数组空间,表示队满(rear+1)%MaxSize==front
元素个数:(rear-front+MaxSize)%MaxSize
2.2 解决循环队列牺牲空间:使用一个tag标志,区分队满和队空。每次入队操作,tag设为1,每次出队操作,tag设为0
队空:front == rear,tag=0
队满:front == rear,tag=1
3.链式队列:只能尾插和头删的单链表,分别设置头指针指向头结点,尾指针指向尾节点
队空:front==rear
4.双端队列:双端队列是允许两端都可以进行入队和出队操作的队列,
‘输入受限的双端队列’:限制某一端只读,另一端可读可写
‘输出受限的双端队列’:限制某一端只写,另一端可读可写
#include<stdio.h>
#define MaxSize 50
typedef int Elemtype;
//1.顺序队列
typedef struct {
Elemtype data[MaxSize];
int front, rear;
}SqQueue;
//2.链式队列
typedef struct {
Elemtype data;
struct Node* next;
}Node;
typedef struct {
Node* front, * rear;
}LinkQueue;
四.非线性结构
一.树
1.树是递归定义的一对多结构
2.树是N(N>=0)个结点的有限集合,N=0时,称为空树,这是一种特殊情况。
3.任意一个非空树中应满足:
- 有且仅有一个特定的称为根的结点
- 当N>1时,其余结点可分为m(m>0)个互不相交的有限集合T1,T2,···,Tm,其中每个集合本身又是一课子树
4.树相关概念:
- 层次:根为第一层,它的孩子为第二层,以此类推
- 树的高度(深度):树中结点的最大层数
- 树的度:树中所有结点的度数的最大值
5.树的性质:
- 树中所有结点数等于所有结点的度数加1
- n个结点的树有n-1条边
- 度为M的树中第i层上至多有M^(i-1)个结点(i>=1),数学归纳法
- 高度为h的M叉树至多有(M^h - 1)/(M-1)个结点,等比数列
- 具有n个结点的m叉树的最小高度为logm(n(m-1)+1)
- 非空树中结点总数为N=∑Ni,且N=分支数为+1,
设树中度为i(i=0,1,2,3,4)的结点数分别为Ni,
且分支数=树中全部结点度的和∑i*Ni
二.结点
1.结点的相关概念:
-
根结点:树只有一个根结点
-
结点的深度:从根结点到该结点的最长简单路径
-
结点的高度:从该结点到叶子结点的最长简单路径
-
结点的度:结点拥有的子树的数量
- 度为0:叶子结点或者终端结点
- 度不为0:分支结点或者非终端结点,分支结点中除去根结点也称为内部结点
4.结点关系:
- 祖先结点:根结点到该结点的唯一路径上的任意结点
- 子孙结点:该结点的所有后代
- 双亲结点:根结点到该结点的唯一路径上最接近该结点的结点
- 孩子结点:最接近该结点的后代节点
- 兄弟结点:有相同双亲结点的结点
#include<stdio.h>
/*
一.顺序存储结构:
1.双亲表示法:用一组连续的存储空间存储树的结点,同时在每个结点中,
用一个变量存储该结点的双亲结点在数组中的位置,根据parent值找到该结点的双亲结点,时间复杂O(1),
但不能找到某个结点的孩子结点
二.链式存储结构:
1.孩子表示法:把每个结点的孩子结点排列起来存储成一个单链表。所以n个结点就有n个链表;
如果是叶子结点,那这个结点的孩子单链表就是空的;
然后n个单链表的头指针又存储在一个顺序表(数组)中。
2.孩子兄弟表示法(二叉树):设置两个指针,分别指向该结点的孩子结点和该结点的兄弟结点
*/
#define MaxSize 100
typedef char Elemtype;
//一.定义顺序存储结构树,双亲表示法
//1.定义结点
typedef struct TNode {
Elemtype data;
int parent; //该结点双亲在数组中的下标
}TNode;
//2.定义树的双亲表示结构
typedef struct {
TNode nodes[MaxSize]; //结点数组
int n; //结点数量
}Tree;
//二.1.定义链式存储结构树,孩子表示法,需要两种结构类型
//1.定义孩子链表结点
typedef struct CNode {
int child; //该孩子在表头数组的下标
struct CNode* next; //指向该结点的下一个孩子结点
}CNode,*Child; //孩子结点数据类型
//2.定义孩子链表的表头结点,存储在数组(散列表)中
typedef struct {
Elemtype data;
Child firstChild; //指向该结点的第一个孩子结点
}TNode;
//二.2.定义链式存储结构树,孩子兄弟表示法(二叉树)
typedef struct CSNode {
Elemtype data;
struct CSNode* firstChild, * rightSib; //指向该结点的第一个孩子结点和该结点的右兄弟结点
}CSNode;
三.二叉树
是n(n>=0)个结点的有限集合:
- 由一个根结点和两个互不相交的被称为根的左子树和右子树组成,左子树和右子树又分别是一颗二叉树
- 空二叉树,即n=0
二叉树性质:
- 非空二叉树上叶子结点数等于度为2的结点数加1
- 非空二叉树上第k层上至多有2^(k-1)个结点(k>=1)
- 高度为H的二叉树至多有2^H - 1个结点(H>=1)
四.特殊的二叉树
一.斜树
每层只有一个结点,结点数==树的深度,只有左子结点是左斜树,只有右子结点的树是右斜树
二.满二叉树
概念:
- 叶子都在同一层
- 分支结点都存在左子树和右子树
性质:
- 非叶子结点的度一定是2,相同深度二叉树中满二叉树的结点个数最多,也子树最多
- 高度为h(h>0)的满二叉树对应的森林所含的树的个数一定为h
三.完全二叉树
设一个高度为h,有n个结点的二叉树,但且仅当其每一个结点都与高度为h的满二叉树中编号1~n的结点一一对应时,称为完全二叉树,其实就是由满二叉树删除最右部分结点形成。
性质:
- 叶子结点只可能在层次最下两层上出现,且最下层的叶子结点一定集中在左部连续的位置
- 如果有度为1的结点,只有可能一个,且该结点只有左孩子而无右孩子
- 同样结点的二叉树,完全二叉树的深度是最小的
- 具有N个(N>0)结点的’完全二叉树’的高度为log2(N+1)或log2(N) + 1
- 对完全二叉树按从上到下、从左往右的顺序依次编号1,2,…,则有以下关系:
- 1.当i>1时,结点i的双亲结点编号为i/2,即当i为偶数时,其双亲结点的编号为i/2,它是双亲结点的左孩子;当i为奇数时,七双亲结点的编号为(i/2)/2,它是双亲结点的右孩子。
- 2.当2i<=N时,结点i的左孩子编号为2i,否则无左孩子。
- 3.当2i+1<=N时,结点i的右孩子编号为2i+1,否则无右孩子。
四.二叉排序树
左子树所有结点都要小于根,右子树所有结点都大于根结点,左<根<右
五.平衡二叉树
概念:
- 非空树中,任何一个结点的左子树与右子树都是平衡二叉树,高度之差的绝对值不超过 1。
- 平衡因子:结点的左子树与右子树的高度差
性质:对高度为N,左右子树的高度分别为N-1和N-2,所有非叶结点的平衡因子均为1的平衡二叉树,总结点数的公式为:Cn=C(n-1)+C(n-2)+1
1.左旋
树one中,结点c的平衡因子为-2,需要进行左旋调整为新的平衡树了:将节点的右支往左拉,右子节点变成父节点,并把晋升之后的左子节点让给降级节点的右子节点
2.右旋
树one中,结点c的平衡因子为2,需要进行右旋调整为新的平衡树了:将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点。
五.二叉树的遍历
1.先序遍历
父结点向下先左后右,‘根左右’:
- 首先访问根结点然后遍历左子树,最后遍历右子树。在遍历左、右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树,如果二叉树为空则返回。
- 其实就是从树的最根结点自上往下,根左右
2.中序遍历
左子节点开始先根再右,‘左根右’:
- 首先遍历左子树,然后访问根结点,最后遍历右子树,在遍历左、右子树时,仍然先遍历左子树,然后访问根结点,最后遍历右子树。
- 其实就是从树的最左结点自下往上,左根右
3.后序遍历
左子节点开始先右再根,‘左右根’。
- 首先遍历左子树,然后遍历右子树,最后访问根结点,在遍历左、右子树时,仍然先遍历左子树,然后遍历右子树,最后访问根结点。
- 其实就是从树的最左结点自下往上,左右根
4.层序遍历
从树的第一层开始访问,从上而下逐层遍历,在同一层中,从左往右逐个访问。
访问完一个结点,将它的孩子入队,先访问的结点,它的孩子也先出队。
#include<stdio.h>
typedef char Elemtype;
typedef struct BiTNode {
Elemtype data;
struct BiTNode* parent, * left, * right;
}BiTNode,*BiTree;
//1.1先序遍历,递归实现
void PreOrder(const BiTNode* root)
{
if (root!=NULL)
{
printf("%d\n", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
}
//1.2先序遍历,非递归
void PreOrderTraverse(BiTree b) {
InitStack(S); //初始化栈
BiTree p = b; //工作指针
while (p || !isEmpty(S))
{
while (p)
{
printf("%c", p->data); //先遍历节点
push(S, p); //入栈
p = p->left; //不断遍历左结点
}
if (!isEmpty(S))
{
p = pop(S);
p = p->right;
}
}
}
//2.1中序遍历,递归实现
void MidOrder(const BiTNode* root)
{
if (root != NULL)
{
PreOrder(root->left);
printf("%d\n", root->data);
PreOrder(root->right);
}
}
//2.2中序遍历,非递归
void MidOrderTraverse(BiTree b) {
InitStack(S); //初始化栈
BiTree p = b; //工作指针
while (p || !isEmpty(S))
{
while (p)
{
push(S, p); //入栈
p = p->left; //不断遍历左结点
}
p = pop(S);
printf("%c", p->data);
p = p->right; //遍历右结点
}
}
//3.1后序遍历,递归实现
void PostOrder(const BiTNode* root)
{
if (root != NULL)
{
PreOrder(root->left);
PreOrder(root->right);
printf("%d\n", root->data);
}
}
//3.2后序遍历,非递归
void PostOrderTraverse(BiTree b)
{
InitStack(S); //初始化栈
BiTree p = b,r=NULL; //工作指针p,辅助指针r
while (p || !isEmpty(S)) {
//1.从根结点到最左下角的左子树都入栈
if (p)
{
push(S, p);
p = p->left;
}
//2.返回栈顶的两种情况
else
{
getTop(S, p); //取栈顶但不出栈
//情况一:右子树还未访问,且右子树不空,第一次栈顶
if (p->right && p->right != r) {
p = p->right;
}
//情况二:右子树已经访问或为空,接下来出栈访问结点
else
{
pop(S, p);
printf("%c", p->data);
r = p; //指向访问过的右子树根结点
p = NULL; //使p为空,从而继续访问栈顶
}
}
}
}
//4.层序遍历
void LevelOrder(BiTree b) {
InitQueue(Q); //初始化队列
BiTree p;
EnQueue(Q,b); //根结点入队
while (!isEmpty(Q)) //队列不空循环
{
DeQueue(Q,p); //队头元素出队
printf("%c", p->data);
if (p->left!=NULL)EnQueue(Q, p->left);
if (p->right != NULL)EnQueue(Q, p->right);
}
}
六.树转二叉树
1.孩子兄弟法
(1)将树的根节点直接作为二叉树的根节点。
(2)将树的根节点的第一个子节点作为二叉树根节点的左指针,若该子节点存在兄弟节点,则将该子节点的第一个兄弟节点(方向从左往右)作为该子节点的右指针。
(3)重复上一步(左孩子,右兄弟),依序添加到二叉树中。直到树中所有的节点都在二叉树中。
2.兄弟连线法
(1)在所有兄弟结点之间加一连线
(2)对每个结点,除了保留与其长子的连线外,去掉该结点与其它孩子的连线
(3)结点的长子变成了它的左孩子,兄弟变成了右孩子
七.森林转二叉树
1.首先将森林中所有的普通树各自转化为二叉树;
2.因为转换所得的二叉树的根结点的右子树均为空,故可将各二叉树的根结点视为兄弟从左至右连在一起,就形成了一棵二叉树
特性:森林F转二叉树F,F中的叶结点个数为:T中左空指针的结点个数
八.图
一.概念
1.图G由顶点集V和边集E组成,记为G=(V,E) :
- V(G)表示图G中顶点的有限非空集,用|V|表示图G中顶点的个数,也称为图G的阶
- E(G)表示图G中顶点之间的关系(边)集合,用|E|表示图G中边的条数
2.度:以该顶点为一个端点的边数目
- 无向图中顶点v的度是指依法于该顶点的边的条数,记为TD(v)=边数*2
- 有向图中顶点v的度分为出度和入度,TD(v)=ID(v)+OD(v)=边数*2,ID(v)=OD(v)=边数
- 入度(ID)是以顶点v为终点的有向边的数目
- 出度(OD)是以顶点v为起点的有向边的数目
二.有向图
有向边(弧)的有限集合:
- 弧是顶点的有序对
- <v,w>(注意是尖括号),如A——>B的弧A是弧尾,B是弧头
- v是弧尾,w是弧头
- v邻接到w或w邻接自v
顶点集 V(G)={A,B,C,D,E,F}
边集 E(G)={<A,B>,<A,C>,<A,D>,<B,C>,<B,F>,<D,E>,<F,B>,<F,C>}
三.无向图
无向图,无向边(边)的有限集合:
- 边是顶点的无序对
- (v,w)(注意是圆括号)
- (v,w)=(w,v)
- w,v互为邻接点
- 无向图边数*2等于各顶点度数之和
V(G)={A,B,C,D,E,F}
E(G)={(A,B),(A,C),(A,D),(B,C),(B,F),(C,F),(D,E),(E,F)}
四.简单图
概念:
- 不存在顶点到自身的边
- 同一条边不重复出现
- 有向图和无向图都是简单图
五.多重图
概念:若图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联
六.完全图
概念:
- 无向完全图:如果任意两个顶点之间都存在边,n个顶点就有n*(n-1)/2条边
- 有向完全图:如果任意两个顶点之间都存在方向相反的两条弧,n个顶点就有n*(n-1)条边
七.子图
设有两个图G=(V,E)和G’=(V’,E’),若V’是V的子集,且E’是E的子集,则称G’是G的子图,若满足V(G’)=V(G)的子图G’,则为G的生成子图
八.连通图
概念:
- 无向图中,如果任意两个顶点之间都能够连通,则称此无向图为连通图
- 连通分量:若无向图不是连通图,但图中存储某个最大的子图符合连通图的性质,则称该子图为连通分量(无向图中的极大连通子图)
- 极大:
- 顶点足够多
- 极大连通子图包含这些依附这些顶点的所有边
结论:如果一个无向图有n个顶点,并且有小于n-1条边,则此图必是非连通图
九.强连通图(DAG)
有向图中任一对顶点都是强连通的
- 强连通:顶点V到顶点W和顶点W到顶点V都有’路径’
- 强连通分量:若有向图本身不是强连通图,但其包含的最大连通子图具有强连通图的性质,则称该子图为强连通分量(有向图中的极大连通分量)
- 极大:
- 顶点足够多
- 极大连通子图包含依附这些顶点的所有边
十.有向无环图
如果一个有向图无法从某个顶点出发经过若干条边回到该点,则这个图是一个有向无环图,可以运用到区块链中
十一.带权图(网)
图中每条边可以赋予一定意义的数值,这个数值叫做这条边的权,有权值的图称为带权图,也叫网。
十二.邻接表
使用邻接矩阵存储一个图时,在不考虑压缩存储的情况下,所占用的空间只与图中顶点数n有关,与边数无关,空间复杂度O(n^2)