数据结构学习笔记
数据结构
结构体
//Book结构体名
struct Books{
char name[50];
int book_id;
} book1 = {"C 语言", 123456}; //不同元素用引号圈起,逗号隔开,book1结构体变量名,定义时候一次性创建一个
struct Books book2;//定义一个结构体
struct Books *book3;//定义结构体指针
//book1.name
//book3->name
typedef struct {
char name[50];
int book_id;
}BOOK;//宏定义改名
typedef struct Books{
char name[50];
int book_id;
}BOOK;//宏定义改名
链表
单链表
链表在增添和删除上比较有优势,所以如果数据增删比较频繁的地方就适合用,如果需要频繁查找或者数据长度已知的情况,还是顺序存储结构比较好
struct Books{
char name[50];
int book_id;
struct Books* next;
}LinkList;//定义一个链表
创建一个空链表
LinkList* CreateLinkList(void){
LinkList* head=(LinkList*)malloc(sizeof(LinkList))
head->next=NULL;
return head;
}
删除一个节点的话,前往记得释放这个节点的内存,用free()
循环链表
循环链表就是首尾相接,最后一个指向头指针
其中,为了能快速访问头指针和尾指针,有用尾指针来表示链表
双向链表
双向链表就是一个节点既有前指向,又有后指向
还可以有循环双向链表
栈
栈的特点是,元素的进入和弹出都在栈顶,先进后出
顺序栈
使用顺序存储线性表
typedef struct{
int data[10];
int top;
}STACK;
为了提高栈的利用率,在比如两个栈数据是此消彼长的情况下,可以两个栈共享空间,一个从左往右增长,一个从右往左增长,如果top1+1==top2,则说明栈满了,如果top1=-1,则栈1空,top2=size,则栈2空。
链栈
使用链式存储线性表,在数据长度动态变化的情况下使用比较合适
typedef struct linknode{
int data;
struct linknode* next;
}LINKNODE;
typedef struct{
LINKNODE top;
int count;
}LINKSTACK;
栈的一个应用案例,在做四则运算时候:
(1)将中缀表达式转换为后缀表达式:
用一个栈来进出符号,遇到数字就排列到表达式,如果是第一个就进栈,遇到符号、左括号、优先级比栈顶元素高的,也进栈,如果遇到右括号,则到这对括号的位置出站,排列进表达式,括号不参与排列,如果遇到优先级低于栈顶的,则栈内元素全部出栈排列进表达式,该元素进栈,直到结束
(2)将后缀表达式进行运算:
用一个栈来进出元素,遇到数字就进栈,遇到符号就将栈顶的两个拿来做运算,结果再进栈,直到结束
C++的栈
#include <stack>
stack<int> s;
s.empty() //如果栈为空返回true,否则返回false
s.size() //返回栈中元素的个数
s.pop() //删除栈顶元素但不返回其值
s.top() //返回栈顶的元素,但不删除该元素
s.push() //在栈顶压入新元素
c++中栈的底层默认容器时deque(deque要和vector类比,他俩有相似)
队列
队列的特点是,只允许再队头进行删除,在队尾进行增加,先进先出
顺序队列由于对储存空间利用存在不足,因此主要是循环队列
循环队列
队列空时候,头指针和尾指针都指向同一个,如果队尾不为空,则指向最后一个元素的下一个(就是队列中其实都会有一个空着)
队列满的判断条件:(rear+1)%queuesize==front
队列长度=(rear-front+queuesize)%queuesize
尾指针后移一位:rear=(rear+1)/queuesize
定义一个队列:
typedef struct{
int data[10];
int front;
int rear;
}QUEUE;
链队列
定义一个链队列:
typedef struct linknode{
int data;
struct linknode* next;
}LINKNODE;
typedef struct{
LINKNODE front,rear;
}LINKQUEUE;
C++队列
- 单端队列
#include <queue>
queue<int> q;
q.empty(); //如果队列为空返回true, 否则返回false
q.size(); //返回队列中元素的个数
q.front(); //返回队首元素但不删除该元素
q.pop(); //弹出队首元素但不返回其值
q.push(); //将元素压入队列
q.back(); //返回队尾元素的值但不删除该元素
- 双端队列
deque 则是一种双向开口的连续线性空间。所谓的双向开口,意思是可以在头尾两端分别做元素的插入和删除操作,要和vectro类比,栈的默认底层容器就是deque
#include <deque>
deque<int> a;
deq.push_front(const T& x); //头部添加元素:
deq.push_back(const T& x); //末尾添加元素:
deq.insert(iterator it, const T& x); //任意位置插入一个元素:
deq.insert(iterator it, int n, const T& x); //任意位置插入 n 个相同元素:
deq.insert(iterator it, iterator first, iterator last); //插入另一个向量的 [forst,last] 间的数据:
deq.pop_front(); //头部删除元素:
deq.pop_back(); //末尾删除元素:
deq.erase(iterator it); //任意位置删除一个元素:
deq.erase(iterator first, iterator last); //删除 [first,last] 之间的元素:
deq.clear(); //清空所有元素:
原文链接
3. 优先级队列
#include<queue>
priority_queue <int,vector<int>,greater<int> > q; //升序队列,小的在头,小顶堆
priority_queue <int,vector<int>,less<int> > q; //降序队列,大的在头,大顶堆
priority_queue<int> a; //对于基础类型 默认是大顶堆,数据容器默认vector,等效于:priority_queue<int, vector<int>, less<int> > a;
//优先级队列的操作:
top //访问队头元素
empty //队列是否为空
size //返回队列内元素个数
push //插入元素到队尾 (并排序)
emplace //原地构造一个元素并插入队列
pop //弹出队头元素
swap //交换内容
他还可以自定义优先级队列的性质
原文链接
树
树的一些共同特点:
(1)树的起点叫做根节点,不能在延伸的是叶子节点,中间的叫中间节点
(2)一个节点拥有的子树个数成为节点的度,树的度是各个节点度的最大值
(3)树按照层划分,树的最大层数成为树的深度
二叉树
(1)二叉树说白了就是度最大为2的树,但是它的子树是要区分左子树和右子树的
(2)只有左子树或者右子树的成为斜树
(3)左右叶子节点都在最后一层的树就是满二叉树
(4)只有右下角有缺口的就是完全二叉树
(5)二叉树上第i层最多有2^(i-1)个节点
(6)深度为k的二叉树最多有(2^k)-1个节点
(7)任何一棵二叉树,度为2的节点都比叶子节点少一个
(8)具有n个节点的完全二叉树的深度为[log2n]+1 (其中[x]表示,不大于x的最大整数)
(10)完全二叉树适合使用顺序存储结构:左孩子为 2i+1, 右孩子为 2i-1,父节点为(i-1)/2
定义二叉链表
typedef struct node{
int data;
struct node* lchild,*rchild;
}BITREE;
普通树用二叉链表存储,如果左指针指向左孩子,右指针指向第一个右兄弟,这样就可以把树转成二叉树的新式存储,就可用二叉树的处理方法进行处理了
前序遍历
从根节点开始,中左右:
void PreOrder(BITREE* T){
if(T==NULL) return;
else{
cout<<T->data;//访问或者操作节点数据
PreOrder(T->lchild);//递归,先遍历左子树
PreOrder(T->rchild);//递归,后遍历右子树
}
}
遍历结果根节点在第一个
中序遍历
从叶子节点开始,左中右
void PreOrder(BITREE* T){
if(T==NULL) return;
else{
PreOrder(T->lchild);//递归,先走左子树
cout<<T->data;//访问或者操作节点数据
PreOrder(T->rchild);//递归,后走右子树
}
}
遍历结果根节点在中间,左边是左子树,右边是右子树
后序遍历
从叶子节点开始,左右中,最后根节点
void PreOrder(BITREE* T){
if(T==NULL) return;
else{
PreOrder(T->lchild);//递归,先走左子树
PreOrder(T->rchild);//递归,后走右子树
cout<<T->data;//访问或者操作节点数据
}
}
遍历结果根节点在最后
建立一个二叉树
这里与遍历的区别只是在数据操作那里,遍历是用数据,这里是创建控件和数据,这里创建也可以又先序、中序、后序方法
void CreateBitree(BITREE* T){
if(flag=="no") T=NULL;//这句的意思是判断一下这个节点到底是否创建
else{
T=(BITREE*)malloc(sizeof(BITREE));//为这个节点创建一个空间
T->data="data";//为这个节点写入数据
PreOrder(T->lchild);//递归,先走左子树
PreOrder(T->rchild);//递归,后走右子树
}
}
线索二叉树
规则是,一个节点再加两个标志位,用来指示,这个节点到底有没左右子树,如果有那就为0,左右子树指针指向对应的子树,如果没有,则左子树指针指向遍历的前驱节点,右子树指针指向遍历的后继节点
定义线索二叉树的节点
typedef struct node{
int data;
struct node* lchild,*rchild;
int Ltag,Ttag;
}BITREE;
创建线索二叉树
方法就是在二叉树创建完成之后,遍历一遍,把里面的标志位都写一遍,以及那些空指针都重新指一遍,这个过程叫做二叉树线索化,线索化在创建完之后,线索化的递归方式决定了线索的构成
void CreateLineBitree(BITREE* T){
if(T) return;
else{
if(!T->lchild){
T->Ltag=1;
T->lchild=pre;
};
if(pre->rchild){
pre->Rtag=1;
pre->rchild=T;
};
pre=p;//更新一下pre,使得递归继续
CreateLineBitree(T->lchild);//递归,左子树线索化
CreateLineBitree(T->rchild);//递归,右子树线索化
}
}
经过线索化之后,二叉链表变成了一个双向链表,在需要频繁进行前后节点访问的情况下,用线索二叉树就比较合适
普通树转换为二叉树
方法就是,把所有兄弟节点之间都连上线,看一个节点是否存在左孩子,如果存在,就把这个节点的右孩子连线都删掉,之后,按照目前的连线,节点的第一个孩子作为左孩子,该孩子和他的兄弟们组成右斜树
二叉树转换为普通树
把节点的孩子的右斜树节点都连成该节点的右孩子,再去掉兄弟间的连线
森林转为二叉树
森林就是多棵树,先把每棵树都转成二叉树,第一棵树作为头节点,剩下树的根节点和他一起组成右斜树,这样整体连起来就是一棵二叉树了
二叉树转为森林
头节点拿出,以他的右斜树的节点分离出来就是多棵二叉树,再把这几颗二叉树分别转成树
赫夫曼树
- 树中一个节点到根节点走过的分支数成为两个节点之间的路径长度
- 树的路径长度,就是各个节点到根节点的路径长度之和
- 每个叶子节点会带一个权值,叶子节点的带权路径长度就是该节点的路径长度与权值的乘积
- 树的带权路径长度就是各个叶子节点带权路径长度之和
- 有n个带权值的叶子节点构成的二叉树,如果他的带权路径长度最短,则这棵树成为赫夫曼树
构造赫夫曼树
- 把每个带权叶子节点作为一颗颗子树,放到一个排序集合中
- 从集合中选出最小的两个子树构造一棵新树,权值小的作为左子树,权值大的作为右子树,新构造的子树根节点权重为左右子树之和
- 在排序集合中删除那两个子树,同时把新构造的子树加入排序集合中
- 重复2、3步骤,知道只剩下一个子树
赫夫曼编码
案例:把一个个字符用0、1编码,尽可能高效
- 统计每个字符出现的频率,作为该字符的权重
- 用上面的字符和权重构造一棵赫夫曼树
- 编码时候:从根节点出发,左路径为0,右路径为1,走到叶子节点,以此为每个处于叶子节点上的字符进行编码
解码时候也是照着这个赫夫曼树,遇0走左边,遇1走右边,走到叶子节点就解出对应码了
二叉搜索树
- 就是规定好一个节点的左孩子小于它,右孩子大于他,二叉搜索树通过中序遍历,就得到升序得排列
- 用二叉搜索树来存有序数组得时候,有的特殊,就会变成一条链,就是不平衡,这样查找效率也不高,这个时候需要使他平衡,这样来提高效率,但是通常使二叉树树平衡需要经过旋转操作,这样也效率不高,因此引入红黑树
平衡二叉树(AVL树)
- 树种每个节点的左右子树得高度差得绝对值不超过1,
红黑树
- 红黑树本质是二叉查找树,但是是可以自平衡的,每个节点增加一位表示颜色,非黑即红,
- 根节点是黑色
- 红节点得孩子得是黑色,所以到任何一个叶子节点的任何一条路径上都不会出现连续两个红色节点
- 最后得null才是叶子节点,叶子节点都是黑色
- 从任何一个节点到叶子节点所经过得黑色节点数量相同
- 到叶子节点的任意一条路径都不会比其他路径长两倍,他是非严格平衡的二叉搜索树,这样在插入删除和搜索之间找到一个平衡,比严格平衡的二叉树效率高 ,它可以使二叉搜索树的时间复杂度不低于logN,性能稳定。
B树和B+树
-
B树的定义
一颗m阶的B树满足如下条件:
每个节点最多只有m个子节点。
除根节点外,每个非叶子节点具有至少有 m/2(向下取整)个子节点。
非叶子节点的根节点至少有两个子节点。
有k颗子树的非叶节点有k-1个键,键按照递增顺序排列。
叶节点都在同一层中。

-
B+树的定义
一颗m阶的B+树满足如下条件:
每个节点最多只有m个子节点。
除根节点外,每个非叶子节点具有至少有 m/2(向下取整)个子节点。
非叶子节点的根节点至少有两个子节点。
有k颗子树的非叶节点有k个键,键按照递增顺序排列。
叶节点都在同一层中


图
又穷个非空顶点集合和顶点之间的边构成的集合称为图
- 图结构中,不允许没有顶点,顶点之间的关系用边来表示,边集可以为空
- 顶点A和顶点B之间的边没有方向,称为无向边,用(A,B)表示,如果图中各个顶点之间都是无向边,则称为无向图,如果每个顶点都与其他顶点有关系,则这个图称为完全无向图
- 顶点A和顶点B之间的边有方向,称为有向边(或者叫弧),用<A,B>表示,如果图中各个顶点之间都是无向边,则称为有向图,如果每个顶点都与其他顶点有关系,则这个图称为完全有向图
- 与图的边或者弧相关的数称为权,这样带权的图称为网
- 对于无向图,每个顶点有关的边的数量称为顶点的度(ID),图中边的数量等于所有顶点度之和的一半
- 对于有向图,以B为头<A,B>的弧的数量称为B的入度(ID),以B为尾<B,A>的弧的数量称为B的出度(OD),顶点B的度=ID(B)+OD(B)
- 图中从一个顶点到另外一个顶点需要走过的边称为路径,路径的长度就是所经过边或者弧的数量,路径中没有经过重复顶点的路径称为简单路径
- 第一个顶点和最后一个顶点相同的路径称为回路或者环,环中除了第一个节点重复的外,其他顶点都不重复,则这种环称为简单环
- 如果一个图中任意两个顶点之间都有路径,则称图是连通图
图的存储
- 邻接矩阵(数组+矩阵)图用一般的顺序存储和链式存储不合适,采用矩阵的方式,一个一维数组存放顶点数据,一个二维数组存放边的关系,矩阵的i行j列元素为0或者1,0反映顶点i到顶点j没有边,1反映顶点i到顶点j有边,矩阵元素也可以是权和∞,权反映顶点i到顶点j的权值,∞反映顶点i到顶点j没有边点j有边或者弧
定义一个图:
typedef struct{
int point[4];
int line[4][4];
int point_num,line_num;
}MGRAPH;
这样的缺点是,如果那种定点多,边长少的图,空间浪费
- 邻接表
用一个数组,存下各个顶点的数据和第一个邻接点的指针,他的各个邻接点组成一个单链表,链表节点中还可以存权值,对于有向图,可以弄两个,一个邻接表,一个逆邻接表
图的遍历
-
深度优先
沿着图的一种边往下走,直到不能往下,在往回返,再沿另外一种边往下走,如此反复递归 -
广度优先
先依次访问完邻接点,再访问邻接点的邻接点,如此循环,常用队列解决
最小生成树
- kruskal算法
思想总结就是:选最短的边,看这条边的两个顶点如果连接起来会不会形成回路,如果不会,则选中这条路径,然后继续重复处理剩下的边,直到全部边都处理完 - prim算法
思想总结就是:随便先选第一个,然后选中与他连接的最短的边,确定下一个顶点,选出来的作为一个整体,再选中与整体连接最短的边,确定下一个顶点,以此类推直到所有点走连成整体
代码实现
最短路径
-
迪杰斯特拉
这个算法可以算出图中从一个点到其他所有点之间的最短路径长度 -
弗洛伊德
这个算法可以算出图中任意两个顶点之间的最短路径长度
总结为:先弄出初始的邻接矩阵,假设其中有n个顶点,然后依次设定,可以通过ni进行中转,更新一次邻接表,直到每个点都做过一次中转点结束,最后得到的邻接矩阵就能反映图中任意两个节点之间的路径长度了
拓扑排序
总结为:依次删掉有向图(这个有向图中没有回路,这样的有向图也称为AOV网)中入度为0的顶点,删掉的顶点依次放入一个集合,到最后图删完之后,得到的集合就是拓扑序列
关键路径
本质就是找起点到终点最长的路径,关键路径上的活动就是关键活动,只有缩短关键路径上活动的时间才能缩短工期
查找
顺序表查找
###_ 顺序查找的优化
int Search(int* a ,int key){
int i =sizeof(a)/sizeof(a[0]);//i指向尾巴
a[0]=key;
while(a[i]!=key){
i--;
}
return i;//返回0说明没有找到
}
二分查找
int search(int* a,int key){
int low=0;
int hight=sizeof(a)/sizeof(a[0]);
int mid=(hight+low)/2;
while(low<=hight){
if(a[mid]==key) return mid;
else if(key<a[mid]) hight=mid-1;
else if(key>a[mid]) low=mid+1;
mid=(hight+low)/2;
}
}
表的数据需要顺序排列
差值查找
int search(int* a,int key){
int low=0;
int hight=sizeof(a)/sizeof(a[0]);
int mid=low+(hight-low)*((key-a[low])/(a[height]-a[low]));根据目标值在查找区间中分布的百分比来确定mid的值
while(low<hight){
if(a[mid]==key) return mid;
else if(key<a[mid]) hight=mid-1;
else if(key>a[mid]) low=mid+1;
mid=(hight+low)/2;
}
}
这个是对二分查找的衍生版,适合于表长比较大,且数据分布均匀一点的那种
斐波那契查找
视频讲解
总结就是,相对于二分查找是对他的mid的计算方式进行了改进,原来是mid为一半,现在mid更新要用斐波那契数列中的值,平均性能优于二分查找
二叉排序树
二叉树的节点按照左子树小,右子树大的模式来排列,对这样的二叉树进行中序遍历时候,得到的就是从小到大排列的序列,这样的二叉树就叫二叉排序树
- 插入:就往下找,肯定会找到一个合适的叶子节点的位置,接在那里就好
- 删除:第一种是删除位置只有左子树或者右子树,删掉后子承父业即可;第二种是叶子节点,那就之际删掉就好; 第三种,该节点有左右子树,这样的话需要删掉后,他的位置用他的前驱节点或者后继节点(中序遍历)替换
平衡二叉树
首先他得是一棵二叉排序树,并且他的任何一个节点,左子树和右子树的深度之差最大不能超过1,这样的二叉排序树才叫平衡二叉树
不平衡的二叉排序树查找效率很低,所以需要在构造二叉排序树的时候就注意做到平衡
997

被折叠的 条评论
为什么被折叠?



