数据结构与算法图
Ⅰ 数据结构中图的建立及算法实现
#include<stdio.h>
#include<stdlib.h>
#define MaxSize 20
struct ArcNode
{
int adjvex;
struct ArcNode *nextarc;
};
struct Vnode
{
int data;
struct ArcNode *firstarc;
};
struct Vnode AdjList[MaxSize];
int m,n,v,cord;
void main()
{
void creatgraph(struct Vnode A[MaxSize]);
void dfs(struct Vnode A[MaxSize]);
do
{
printf("\n 主菜单");
printf("\n 1 建立无向图的邻接表");
printf("\n 2 按深度遍历图");
printf("\n 3 结束程序运行");
printf("\n-----------------------------------");
printf("\n 请输入您的选择 1, 2, 3 -->");
scanf("%d",&cord);
switch(cord)
{
case 1:
creatgraph(AdjList);
break;
case 2:
dfs(AdjList);
break;
case 3:
exit(0);
}
}while(cord<=3);
}//main end
void creatgraph(struct Vnode A[MaxSize])
{
int i,j,k;
struct ArcNode *p;
printf("input arces and vexes:");
scanf("%d %d",&m,&n);
for(k=0;k<n;k++)
{
printf("\ninput arc:");
scanf("%d%d",&i,&j);
p=(struct ArcNode*)malloc(sizeof(struct ArcNode));
p->adjvex=j;
p->nextarc=A[i-1].firstarc;
A[i-1].firstarc=p;
p=(struct ArcNode*)malloc(sizeof(struct ArcNode));
p->adjvex=i;
p->nextarc=A[j-1].firstarc;
A[j-1].firstarc=p;
}
printf("\n");
for(k=0;k<n;k++)
{
printf("%d",A[k].data);
p=A[k].firstarc;
while(p)
{
printf("%d",p->adjvex);
p=p->nextarc;
}
printf("\n");
}
}///creatgraph end
void dfs(struct Vnode A[MaxSize])
{
struct ArcNode *p,*ar[MaxSize];
int x,i,y,top=-1;
int visited[MaxSize];
for(i=0;i<n;i++)
visited[i]=0;
printf("\ninput x:");
scanf("%d",&x);
printf("%d",x);
visited[x-1]=1;
p=A[x-1].firstarc;
while((p)||(top>=0))
{
if(!p)
{
p=ar[top];
top--;
}
y=p->adjvex;
if(visited[y-1]==0)
{
visited[y-1]=1;
printf("->%d",y);
p=p->nextarc;
if(p)
{
top++;
ar[top]=p;
}
p=A[y-1].firstarc;
}
else p=p->nextarc;
}
}
Ⅱ 一文带你认识30个重要的数据结构和算法
数组是最简单也是最常见的数据结构。它们的特点是可以通过索引(位置)轻松访问元素。
它们是做什么用的?
想象一下有一排剧院椅。每把椅子都分配了一个位置(从左到右),因此每个观众都会从他将要坐的椅子上分配一个号码。这是一个数组。将问题扩展到整个剧院(椅子的行和列),您将拥有一个二维数组(矩阵)。
特性
链表是线性数据结构,就像数组一样。链表和数组的主要区别在于链表的元素不存储在连续的内存位置。它由节点组成——实体存储当前元素的值和下一个元素的地址引用。这样,元素通过指针链接。
它们是做什么用的?
链表的一个相关应用是浏览器的上一页和下一页的实现。双链表是存储用户搜索键哗显示的页面的完美数据结构。
特性
堆栈是一种抽象数据类型,它形式化了受限访问集合的概念。该限制遵循 LIFO(后进先出)规则。因此,添加到堆栈中的最后一个元素是您从中删除的第一个元素。
堆栈可以使用数组或链表来实现。
它们是做什么用的?
现实生活中最常见的例子是在食堂中将盘子叠放在宽枝一起。位于顶部的板首先被移除。放置在最底部的盘子是在堆栈中保留时间最长的盘子。
堆栈最有用的一种情况是您需要获取给定元素的相反顺序。只需将它们全部推入堆栈,然后弹出它们。
另一个有趣的应用是有效括号问题。给定一串括号,您可以使用堆栈检查它们是否匹配。
特性
队列是受限访问集合中的另一种数据类型,就像前面讨论的堆栈一样。主要区别在于队列是按照FIFO(先进先出)模型组织的:队列中第一个插入的元素是第一个被移除的元素。队列可以使用固定长度的数组、循环数组或链表来实现。
它们是做什么用的?
这种抽象数据类型 (ADT) 的最佳用途当然是模拟现实生活中的队列。例如,在呼叫中心应用程序中,队列用于保存等待从顾问那里获得帮助的客户——这些客户应该按照他们呼叫的顺序获得帮助。
一种特殊且非常重要的队列类型是优先级队列。元素根据与它们关联的“优先级”被引入队列:具有最高优先级的元素首先被引入队列。这个 ADT 在许多图算法(Dijkstra 算法、BFS、Prim 算法、霍夫曼编码 )中是必不可少的。它是使用堆实现的。
另一种特殊类型的队列是deque 队列(双关语它的发音是“deck”)。可以从队列的两端插入/删除元素。
特性
Maps (dictionaries)是包含键集合和值集合的抽象数据类型。每个键都有一个与之关联的值。
哈希表是一种特殊类型的映射。它使用散列函数生成一个散列码,放入一个桶或槽数组:键被散列,结果散列指示值的存储位置。
最常见的散列函数(在众多散列函数中)是模常数函数。例如,如果常量是 6,则键 x 的值是x%6。
理想情况下,散列函数会将每个键分配给一个唯一的桶,但他们的大多数设计都采用了不完善的函数,这可能会导致具有相同生成值的键之间发生冲突。这种碰撞总是以某种方式适应的。
它们是做什么用的?
Maps 最着名的应用是语言词典。语言中的每个词都为其指定了定义。它是使用有序映射实现的(其键按字母顺序排列)。
通讯录也是一张Map。每个名字都有一个分配给它的电话号码。
另一个有用的应用是值的标准化。假设我们要为一天中的每一分钟(24 小时 = 1440 分钟)分配一个从 0 到 1439 的索引。哈希函数将为h(x) = x.小时*60+x.分钟。
特性
术语:
因为maps 是使用自平衡红黑树实现的(文章后面会解释),所以所有操作都在 O(log n) 内完成;所有哈希表操作都是常量。
图是表示一对两个集合的非线性数据结构:G={V, E},其中 V 是顶点(节点)的集合,而 E 是边(箭头)的集合。节点是由边互连的值 - 描述两个节点之间的依赖关系(有时与成本/距离相关联)的线。
图有两种主要类型:有稿巧行向图和无向图。在无向图中,边(x, y)在两个方向上都可用:(x, y)和(y, x)。在有向图中,边(x, y)称为箭头,方向由其名称中顶点的顺序给出:箭头(x, y)与箭头(y, x) 不同。
它们是做什么用的?
特性
图论是一个广阔的领域,但我们将重点介绍一些最知名的概念:
一棵树是一个无向图,在连通性方面最小(如果我们消除一条边,图将不再连接)和在无环方面最大(如果我们添加一条边,图将不再是无环的)。所以任何无环连通无向图都是一棵树,但为了简单起见,我们将有根树称为树。
根是一个固定节点,它确定树中边的方向,所以这就是一切“开始”的地方。叶子是树的终端节点——这就是一切“结束”的地方。
一个顶点的孩子是它下面的事件顶点。一个顶点可以有多个子节点。一个顶点的父节点是它上面的事件顶点——它是唯一的。
它们是做什么用的?
我们在任何需要描绘层次结构的时候都使用树。我们自己的家谱树就是一个完美的例子。你最古老的祖先是树的根。最年轻的一代代表叶子的集合。
树也可以代表你工作的公司中的上下级关系。这样您就可以找出谁是您的上级以及您应该管理谁。
特性
二叉树是一种特殊类型的树:每个顶点最多可以有两个子节点。在严格二叉树中,除了叶子之外,每个节点都有两个孩子。具有 n 层的完整二叉树具有所有2ⁿ-1 个可能的节点。
二叉搜索树是一棵二叉树,其中节点的值属于一个完全有序的集合——任何任意选择的节点的值都大于左子树中的所有值,而小于右子树中的所有值。
它们是做什么用的?
BT 的一项重要应用是逻辑表达式的表示和评估。每个表达式都可以分解为变量/常量和运算符。这种表达式书写方法称为逆波兰表示法 (RPN)。这样,它们就可以形成一个二叉树,其中内部节点是运算符,叶子是变量/常量——它被称为抽象语法树(AST)。
BST 经常使用,因为它们可以快速搜索键属性。AVL 树、红黑树、有序集和映射是使用 BST 实现的。
特性
BST 有三种类型的 DFS 遍历:
所有这些类型的树都是自平衡二叉搜索树。不同之处在于它们以对数时间平衡高度的方式。
AVL 树在每次插入/删除后都是自平衡的,因为节点的左子树和右子树的高度之间的模块差异最大为 1。 AVL 以其发明者的名字命名:Adelson-Velsky 和 Landis。
在红黑树中,每个节点存储一个额外的代表颜色的位,用于确保每次插入/删除操作后的平衡。
在 Splay 树中,最近访问的节点可以快速再次访问,因此任何操作的摊销时间复杂度仍然是 O(log n)。
它们是做什么用的?
AVL 似乎是数据库理论中最好的数据结构。
RBT(红黑树) 用于组织可比较的数据片段,例如文本片段或数字。在 Java 8 版本中,HashMap 是使用 RBT 实现的。计算几何和函数式编程中的数据结构也是用 RBT 构建的。
在 Windows NT 中(在虚拟内存、网络和文件系统代码中),Splay 树用于缓存、内存分配器、垃圾收集器、数据压缩、绳索(替换用于长文本字符串的字符串)。
特性
最小堆是一棵二叉树,其中每个节点的值都大于或等于其父节点的值:val[par[x]]
Ⅲ 面试准备之【数据结构】1——图
共有:邻接表,邻接矩阵
有向图独有:十字链表,边集数组
无向图独有:邻接多重表
一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
设图G有n个顶点,则邻接矩阵是一个nxn的方阵,定义为:Arc[i][j]=1,若<vi,vj>∈E或<vi,vj>∈E,反之等于0。
可以看出,无向图的邻接矩阵是对称矩阵,要想知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和。
在有向图的邻接矩阵中,某个顶点的出(入)度是这个顶点vi在邻接矩阵中第i 行(列)的元素之和;
我们发现,当图中的边数相对于顶点较少时,邻接矩阵是对存储空间的极大浪费。我们可以考虑对边或弧使用链式存储的方式来避免空间浪费的问题。回忆树结构的孩子表示法,将结点存入数组,并对结点的孩子进行链式存储,不管有多少孩子,也不会存在空间浪费问题。
邻接表的创建过程如下:
1) 图中顶点用一个一维数组存储,当然也可以用单链表来存储,不过用数组可以较容易的读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。
2) 图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为以vi为弧尾的出边表。
从图中我们知道,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息。
firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。
边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指
向边表中下一个结点的指针,比如v1顶点与v0、v2互为邻接点,则在v1的边表中,adjvex分别为v0的0和v2的2.
如果想知道某个顶点的度,就去查找这个顶点的边表中结点的各数。
若要判断顶点vi和vj是否存在边,只需要测试顶点vi的边表adjvex中是否存在结点vj的下标就行了。
若求顶点的所有邻接点,其实就是对此顶点的边表进行遍历,得到的adjvex域对应的顶点就是邻接点。
有向图的邻接表中顶点vi的边表是指以vi 为弧尾 的弧来存储的,这样很容易就可以得到每个顶点的出度。
有时为了便于确定顶点的入度或以顶点为弧头的弧,可以建立一个有向图的逆邻接表,即对每个顶点vi都建立
一个链接为vi为弧头的表。如下图所示:
此时我们很容易就可以算出某个顶点的入度或出度是多少,判断两顶点是否存在弧也很容易实现。
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可
对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道。反之,逆邻接表解决了入度
却不了解出度的情况。有没有可能把邻接表和逆邻接表结合起来呢?
答案是肯定的,就是把它们整合在一起。这种存储有向图的方法是:十字链表(Orthogonal List).
我们重新定义顶点表结点结构为:
| data | firstin | firstout |
其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout表示出边表头指针,指向该顶点的出边表中的第一个结点。
重新定义的 边表 结点结构如下表:
| tailvex | headvex | headlink | taillink |
其中tailvex是指弧起点在顶点表的下标,headvex是指弧终点在顶点表中的下标,headlink是指入边表指针域,指向终点(弧头)相同的
下一条边,taillink是指出边表指针域,指向起点(弧尾)相同的下一条边。如果是带权值的网,还可以再增加一个weight域来存储权值。
如下图表示的十字链表:
顶点表依然是存入一个一维数组{v0,v1,v2,v3},以顶点v0来说,firstout指向的是出边表中的第一个结点v3。所以v0边表结点的headvex=3,
而tailvex其实就是当前顶点v0的下标0,由于v0只有一个出边顶点,所以headlink和taillink都是空。
这里虚线箭头的含义,其实就是逆邻接表的表示。对于v0来说,它有两条入边,分别来自顶点v1和v2。因此v0的firstin指向顶点v1的边表
结点中headvex为0的结点,虚线(1),接着由入边结点的headlink指向下一个入边顶点v2,虚线(2)。
对于顶点v1,它有一个入边顶点v2,2个出边顶点v0和v2,所以它的firstin指向顶点v2的边表结点中headvex为1的结点,虚线(3).
十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以vi为尾的弧,也容易找到以vi为头的弧,因而容易求得
顶点的出度和入度。除了结构复杂一点外,其实创建图算法的时间复杂度和邻接表是相同的,因此很好的应用在有向图中。
十字链表主要是针对有向图的存储结构进行了优化,那么对于无向图的邻接表,有没有问题呢?如果我们在无向图的应用中,关注的重点是顶点,那么邻接表是不错的选择,但如果我们更关注边的操作,比如对已访问过的边做标记,删除某一条边等操作,那就意味着需要找到这条边的两个边表结点进行操作。如下图,若要删除(v0,v2)这条边,需要对邻接表结构中右边表的两个结点进行删除,显然这是比较繁琐的。
因此,我们也仿照十字链表的方式,对边表结点的结构进行一些改造,重新定义的边表结点结构如下表:
| ivex | ilink | jvex | jlink |
其中ivex和jvex是指某条边依附的两个顶点在顶点表中的下标。ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。
这就是邻接多重表结构。如上图有4个顶点和5条边,先将边表结点画出来。由于是无向图,所以ivex,jvex正反过来都可以,为了绘图
方便,都将ivex值设置的与一旁的顶点下标相同。
下面开始连线,首先连线的(1)(2)(3)(4)是将顶点的firstedge指向一条边,顶点下标要与ivex的值相同。接着,由于顶点v0的(v0,v1)边的
邻边有(v0,v3)和(v0,v2)。因此(5)(6)的连线就是满足指向下一条依附于顶点v0的边的目标,注意ilink指向的结点的jvex(ivex)一定要与它本身
的jvex(ivex)的值相同。同理,连线(7)就是指(v1,v0)这条边,它是相当于顶点v1指向(v1,v2)边后的下一条。v2有三条边依附,所以(3)之后就有
了(8)(9)。连线(10)就是顶点v3在连线(4)之后的下一条边。左图一共有5条边,所以右图有10条连线,完全符合预期。
邻接多重表与邻接表的差别, 仅仅是在于同一条边在邻接表中用两个边表结点表示,而在邻接多重表中只有一个结点 。这样对边的操作就方便
多了,若要删除左图的(v0,v2)这条边,只需要将右图的(6)(9)的链接指向改为^即可。
---- 边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
如上图所示,边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次
进行处理的操作,而不适合对顶点相关的操作
路径长度:路径上各活动持续时间的总和(即路径上所有权之和)。
完成工程的最短时间:从工程开始点(源点)到完成点(汇点)的最长路径称为完成工程的最短时间。
关键路径:路径长度最长的路径称为关键路径。
二分图是一类特殊的图,又称为双分图、二部图、偶图。二分图的顶点可以分成两个互斥的独立集 U 和 V 的图,使得所有边都是连结一个 U 中的点和一个 V 中的点。顶点集 U、V 被称为是图的两个部分。等价的,二分图可以被定义成图中所有的环都有偶数个顶点。可以将 U 和 V 当做一个着色:U 中所有顶点为蓝色,V 中所有顶点着绿色,每条边的两个端点的颜色不同,符合图着色问题的要求。相反的,非二分图无法被二着色
完全二分图 是一种特殊的二分图,可以把图中的顶点分成两个集合,使得第一个集合中的所有顶点都与第二个集合中的所有顶点相连。
欧拉图是指通过图(无向图或有向图)中所有边且每边仅通过一次通路,相应的回路称为欧拉回路。具有欧拉回路的图称为欧拉图(Euler Graph),具有欧拉通路而无欧拉回路的图称为半欧拉图。欧拉证明了如下定理: 一个非空连通图是欧拉图当且仅当它的每个顶点的度数都是偶数。 由此可得如下结论:一个连通图有欧拉迹当它至多有两个度数是奇数的顶点。
AOE网Activity On Edge Network:在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,一个工程常被分为多个小的子工程,这些子工程被称为活动(Activity),在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间,这样的图简称为AOE网。
图的存储结构-邻接助阵和邻接表 https://blog.csdn.net/dongyanxia1000/article/details/53582186
图的存储结构-十字链表和邻接多重表 https://blog.csdn.net/dongyanxia1000/article/details/53584496
Ⅳ 数据结构与算法-队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
队列这个概念非常好理解。你可以把它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。先进者先出,这就是典型的“队列”。
我们知道,CPU 资源是有限的,任务的处理速度与线程个数并不是线性正相关。相反,过多的线程反而会导致 CPU 频繁切换,处理性能下降。所以,线程池的大小一般都是综合考虑要处理任务的特点和硬件环境,来事先设置的。当我们向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?
看完下面队列C语言实现,相信你会多少有些了解
队列只支持两个基本操作:入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。
队列跟栈一样,也是一种操作受限的线性表数据结构。
队列跟栈一样,也是一种抽象的数据结构。它具有先进先出的特性,支持在队尾插入元素,在队头删除元素。
跟栈一样,队列可以用数组来实现,也可以用链表来实现。用数组实现的栈叫作顺序栈,用链表实现的栈叫作链式栈。同样,用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。
随着不停地进行入队、出队操作, front 和 rear 都会持续往后移动。当 rear 移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。同时也不好判断队满条件,可以使用循环队列来实现
循环队列,顾名思义,它长得像一个环。原本数组是有头有尾的,是一条直线。现在我们把首尾相连,扳成了一个环。
经过推算,可以发现:
队空 Q.rear==Q.front。
队满 (Q.rear+1)%MAXSIZE==Q.front。
注意,当队列满时,图中的 Q.rear 指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。
1.初始化空队列
2.清空队列
4.判断队满
5.入队
6.出队
7.获取队列当前元素个数
8.若队列不空,则用e返回Q的队头元素,并返回OK,否则返回ERROR
9.从队头到队尾依次对队列的每个元素数组
10.主函数中验证
输出结果
1.初始化
2.销毁
3.置空
4.判断队列是否为空
5.获取元素个数
6.入队
7.出队
8.获取队头元素
9.遍历队列
验证
输出
Ⅳ 数据结构与算法-二叉树(Binary Tree)
名词解释
节点: 每个元素
父子关系: 用来连线相邻节点之间的关系
父节点: A节点就是B节点的父节点
子节点: B节点就是A节点的子节点
兄弟节点: B、C、D这三个节点的父节点是同一个节点
根结点: 没有父节点的节点
叶子结点: 没有子节点的节点
节点的高度: 节点到叶子结点到最长路径(边数) (计数起点为0, 从下往上)
节点的深度: 根节点到这个节点经历过的边个数 (计数起点为0, 从上往下)
节点的层数: 节点到深度 + 1 (计数起点为1)
树的高度: 根节点的高度
特点
最常用的树数据结构
每个节点最多有两个子节点(左子节点、右子节点)
满二叉树: 叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点
完全二叉树: 叶子节点都在最底下两层,最后一层的叶子节点都 靠左排列 ,并且除了最后一层,其他层的节点个数都要达到最大
二叉树存储方式
数组顺序存储法
通过数组下标来顺序存储数据 (i表示当前节点深度,从0开始)
根节点: i = 1,左节点:2 * i,右节点: 2 * i + 1,父节点: i / 2
完全二叉树采用此方式节省内存空间
链式存储法
每个节点需要存储三分数据:当前节点数据、左节点指针、右节点指针,比较占用空间
遍历
常用方式
前序遍历: 树任意节点,先打印当前节点,再打印它的左子树,最后打印它的右子树
中序遍历: 树任意节点,先打印它的左子树,再打印当前节点,最后打印它的右子树
后序遍历: 树任意节点,先打印它的左子树,再打印它的右子树,最后打印当前节点
二叉树的前、中、后序遍历就是一个递归的过程
时间复杂度是O(n)
每个节点最多被访问两次,遍历操作的时间复杂度跟节点的个数n成正比
特点
二叉查找树为实现快速查找而生,支持快速查找一个数据、快速插入、快速删除一个数据
特殊结构: 其左子树每个节点的值 < 树的任意一个节点的值 < 其右子树每个节点的值
先取根节点,如果它等于要查找的数据,那就返回。
如果要查找的数据比根节点的值小,那就在左子树中递归查找;
如果要查找的数据比根节点的值大,那就在右子树中递归查找
一般插入的数据在叶子节点上,从根节点开始依次比较要插入的数据和节点的大小关系
如果插入数据比节点的数值大,并且节点的右子树为空,将新数据插到右子节点位置;
如果不为空,就再递归遍历右子树,查找插入位置。
如果插入数据比节点的数值小,并且节点的左子树为空,将新数据插到左子节点位置;
如果不为空,就再递归遍历左子树,查找插入位置。
针对要删除节点的子节点个数的不同,需分三种情况来处理
1.如果要删除的节点没有子节点,步骤如下: (如图中的删除节点55)
只需将父节点中指向要删除节点的指针置为null
2.如果要删除的节点只有一个子节点,步骤如下: (如图中删除节点13)
只需将父节点中指向要删除节点的指针,让它指向要删除节点的子节点即可
3.如果要删除的节点有两个子节点,步骤如下: (如图中的删除节点18)
首先,需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上;
然后,再删除掉这个最小节点,因为最小节点肯定没有左子节点
删除操作,有个优化方案: 就是单纯将要删除的节点标记为“已删除”,
这种方案删除操作就变简单很多,但是比较浪费内存空间
支持快速地查找最大节点和最小节点、前驱节点和后继节点
另外一种重要特性:
中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度为O(n)
因此,二叉查找树也叫作二叉排序树
以上几种操作都默认树中节点存储的都是数字,而且都是不存在键值相同的情况
实际应用场景中采用对象的某个字段作为键值来构建二叉查找树,其他字段称为卫星数据
如果存储的两个对象键值相同,两种解决方案
1.把值相同的数据都存储在同一个节点(采用链表或支持动态扩容的数组等数据结构)
2.每个节点只存储一个数据,把这个新插入的数据当作大于这个节点的值来处理,如下图:
查找操作
当查找数据时遇到值相同的节点,继续在右子树中查找,直到遇到叶子节点才停止。
这样就把键值等于要查找值的所有节点都查找出来
删除操作
先查找到每个要删除的节点,然后再按前面讲的删除操作的方法,依次删除
对于同一组数据可构造不同二叉查找树。查找、插入、删除操作的执行效率都不一样
图最左边树,根节点的左右子树极度不平衡,退化成链表,查找时间复杂度为O(n)
最理想的情况,二叉查找树是一棵完全二叉树(或满二叉树)
时间复杂度都跟树的高度成正比,也就是O(height)
树的高度就等于最大层数减一,为了方便计算,我们转换成层来表示
满二叉树: 下一层节点个数是上一层的2倍,第K层包含节点个数就是2^(K-1)
完全二叉树: 假设最大层数是L,总的节点个数n,它包含的节点个数在1个到2^(L-1)个之间
L的范围是[ , +1],完全二叉树的高度小于等于
极度不平衡的二叉查找树,它的查找性能肯定不能满足我们的需求
平衡二叉查找树: 树的高度接近logn,时间复杂度较稳定为O(logn)
1.排序对比
散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序
二叉查找树只需要中序遍历,就可以在O(n)的时间复杂度内,输出有序的数据序列
2.性能稳定性对比
散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定
最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在O(logn)
3.时间复杂度对比
散列表查找等操作时间复杂度是常量级,因存在哈希冲突,这个常量不一定比logn小
另外加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高
4.结构设计对比
散列表构造比较复杂,需要考虑:散列函数设计、冲突解决办法、扩容、缩容等
平衡二叉查找树只需要考虑平衡性,而且目前这个的解决方案较成熟、固定
5.空间复杂度
散列表: 避免过多散列冲突,装载因子不能太大,特别基于开放寻址法,否则浪费太多空间
Ⅵ 图解:数据结构与算法之字典树
字典树(Trie树)这一数据结构是不太常见但是十分好用<typo id="typo-32" data-origin="而" ignoretag="true">而</typo>一种数据结构,博主也就是最近一段时间做了几道字节的题目才了解到字典树这一数据结构。并将自己的学习内容跟大家分享。
首先,何为字典树(Trie树)?顾名思义,就是在查询目标时,像字典一样按照一定排列顺序标准和步骤访问树的节点,举一个简单例子,英文字典查单词"He",那么第一步你肯定要按照a-z的顺序先找到h这个首字母,然后再按照相同顺序找到e。博主所要介绍的字典树就是类似字典这样的结构。
上述查找单词的过程就是不断查找与所查单词相同前缀的字符串,直至查找到所查单词的最后一个字母。因此,字典树又称为前缀树(prefix Tree)。
以hell、hi、new、nop为例建立一个字典树,构造如下
根据上文所述可以得到字典树的结构性质
根据以上三点来构造字典树。
字典树的构造其实较为简单,和一般树的构造没有太大区别。接下来将对字典树的插入、删除、查询操作进行分析与讲解。
在没有字典树的时候,我们需要先构建出字典树。
以插入hell为例:
再插入单词hit,过程如下,检查=>存在则访问/不存在则建立新节点再访问=>直到要插入的单词到达最后一个字符。
字典树的插入操作比较简单,不需要考虑太多排序问题。
正如上文所说,按照一定的标准进行查询目标字符串,每个节点都储存一个字符,根节点到达子节点路径组成的字符串即为该节点所对应的字符串,那么查询目标字符串时按照从根节点一步一步访问相应字符所在的节点,其实也就是匹配字符串前缀的过程。
如下图,在字典树中,查询"hell",
[图片上传失败...(image-f028c4-1611057619223)]
如果在该字典中查询no
删除操作相对于插入与查询复杂一点,但是也很简单,删除的前提是单词已经存在于字典树。
删除字典树节点的操作需要考虑目标字符串最后一个字符是否是树中的叶子节点。
因为一个单词可能是另一个单词的前缀部分,如果不是叶子节点,我们只需要把该单词的单词标志位清空即可,无需删除整个“树枝”。
比如,想要删除"no"这个单词
比如,想要删除"hell"这个单词,与第一种删除相同,只不过是从最后一个节点,'l'节点是叶子节点,开始往上进行节点删除操作。
比如,想要删除"hi",那么与前两种其实一致,访问到叶子节点'i',删除叶子节点,并向上访问,访问到'h',由于删除'i'以后,'h'依然不是叶子节点,因此不再继续删除节点。
比如,想要删除"nop",与前几种类似,先访问到叶子节点'p'删除,然后上移发现'o'是叶子节点,然而'o'有单词标记位,所以,这里不再继续删除。
有上面几种删除操作,我们得到了删除的标准:
了解了这么多字典树的各种操作,相信你对字典树的用途有个大概了解了,字典树最大作用是用于==字符串的各种匹配==,前缀匹配(模糊搜索),字符串查找(字典)等等。
博主只打出了“涓涓清泉”四个关键字,其搜索列表返回了诸多以涓涓清泉为首的选项
顾名思义,就是一个单纯的字典而已,不多举例。
字典树的构建,通过利用空间换时间的思想以及字符串的公共前缀减少无效的字符串比较操作从而使得插入和查找字符串变得高效.其插入或者查找的时间复杂度为O(n),n为字符串长度。
当然,字典树有着它的弊端,当所插入的单词没有很多公共前缀时,字典树的构建变得十分复杂和低效。
字典树的难度不是很大,但却是一种十分有用的数据结构,掌握之后,对于解决一些有关字符串匹配、公共前缀的问题十分有帮助。
当然我们也说了,字典树有着自己的弊端,由于用空间换时间,如果遇到了一堆公共前缀很少的单词进行字典树构造时,空间需求就显得十分大了。
Ⅶ 数据结构与算法-队列
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。
线性表有顺序存储和链式存储,栈是线性表,所以有这两种存储方式。同样,队列作为一种特殊的线性表,也同样存在这两种存储方式。
我们假设一个队列有n个元素,则顺序存储的队列需建立一个大于n的数组,并把队列的所有元素存储在数组的前n个单元,数组下标为0的一端即是队头。所谓的入队列操作,其实就是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度为 。
与栈不同的是,队列元素的出列是在队头,即下标为0的位置,那也就意味着,队列中的所有元素都得向前移动,以保证队列的队头,也就是下标为0的位置不为空,此时时间复杂度为 。
可有时想想,为什么出队列时一定要全部移动呢,如果不去限制队列的元素必须存储在数组的前n个单元这一条件,出队的性能就会大大增加。也就是说,队头不需要一定在下标为0的位置,
为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时,此队列不是还剩一个元素,而是空队列。
假设是长度为5的数组,初始状态,空队列列如图所示,front与rear指针均指向下标为0的位置。然后入队a1、a2、a3、a4,front指针依然指向下标为0位置,而rear指针指向下标为4的位置。
出队a1、a2,则front指针指向下标为2的位置,rear不变,如图4-12-5的左图所示,再入队a5,此时front指针不变,rear指针移动到数组之外。嗯?数组之外,那将是哪里?如下图的右图所示。
假设这个队列的总个数不超过5个,但目前如果接着入队的话,因数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上,我们的队列在下标为0和1的地方还是空闲的。我们把这种现象叫做“假溢出”。
所以解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。
如果将rear的指针指向下标为0的位置,那么就不会出现指针指向不明的问题了,如下图所示。
接着入队a6,将它放置于下标为0处,rear指针指向下标为1处,如下图的左图所示。若再入队a7,则rear指针就与front指针重合,同时指向下标为2的位置,如下图的右图所示。
由于rear可能比front大,也可能比front小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相差整整一圈。
所以若队列的最大尺寸为QueueSize,那么队列满的条件是(rear+1)%QueueSize==front(取模“%”的目的就是为了整合rear与front大小为一圈问题)。比如上面这个例子,QueueSize=5,上图的左图中front=0,而rear=4,(4+1)%5=0,所以此时队列满。
再比如图下图中的,front=2而rear=1。(1+1)%5=2,所以此时队列也是满的。
而对于下图,front=2而rear=0,(0+1)%5=1,1≠2,所以此时队列并没有满。
另外,当rear>front时,此时队列的长度为rear-front。
但当rear<front时,,队列长度分为两段,一段是QueueSize-front,另一段是0+rear,加在一起,队列长度为rear-front+QueueSize。因此通用的计算队列满队公式为:
单是顺序存储,若不是循环队列,算法的时间性能是不高的,但循环队列又面临着数组可能会溢出的问题,所以我们还需要研究一下不需要担心队列长度的链式存储结构。
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点。
空队列时,front和rear都指向头结点。
链队列的结构为:
初始化一个空队列
入队操作时,其实就是在链表尾部插入结点,如图所示。
出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头结点,如图所示。
对于循环队列与链队列的比较,可以从两方面来考虑,从时间上,其实它们的基本操作都是常数时间,即都为O(1)的,不过循环队列是事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。对于空间上来说,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更加灵活。
总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。
栈和队列也都可以通过链式存储结构来实现,实现原则上与线性表基本相同如图所示。
Ⅷ 数据结构——图graph(基础概念)
【各种东拼西凑来的】
图(Graph)是由顶点和连接顶点的边构成的离散结构。在计算机科学中,图是最灵活的数据结构之一,很多问题都可以使用图模型进行建模求解。例如:生态环境中不同物种的相互竞争、人与人之间的社交与关系网络、化学上用图区分结构不同但分子式相同的同分异构体、分析计算机网络的拓扑结构确定两台计算机是否可以通信、找到两个城市之间的最短路径等等。
图的结构很简单,就是由顶点$V$集和边$E$集构成,因此图可以表示成$G=(V, E)$。
注意: 顶点有时也称为节点或者交点,边有时也称为链接。
无向图
我们可以说这张图中,有点集$V=\{1, 2, 3, 4, 5, 6\}$,边集$E=\{(1, 2), (1, 5), (2, 3), (2, 5), (3, 4), (4, 5), (4, 6)\}$。在无向图中,边$(u, v)$和边$(v, u)$是一样的,因此只要记录一个就行了。简而言之,对称。
有向图
也很好理解,就是加上了方向性,顶点$(u, v)$之间的关系和顶点$(v,u)$之间的关系不同,后者或许不存在。例如,地图应用中必须存储单行道的信息,避免给出错误的方向。
加权图 :
权:与图的边或弧相关的数叫做权。
与加权图对应的就是无权图,或叫等权图。如果一张图不含权重信息,我们就认为边与边之间没有差别。不过,具体建模的时候,很多时候都需要有权重,比如对中国重要城市间道路联吵隐笑系的建模,总不能认为从北京去上海和从北京去广州一样远(等权)。
还有很多细化的概念,比如:无向图中,任意两个顶点间都有边,称为 无向完全图 ;加权图起一个新名字,叫 网(network) ……然而,如无必要,毋增实体。
邻接(adjacency) :邻接是 两个顶点之间 的一种关系。如果图包含$(u,v)$,则称顶点$v$与顶点$u$邻接。当然,在无向图中,这也意味着顶点$u$与顶点$v$邻接。
关联(incidence) :关联是 边和顶点之间 的关系。在有向图中,边$(u,v)$从顶点$u$开始关联到$v$,或者相反,从$v$关联到$u$。注意,有向图中,边不一定是对称的,有去无回是完全有可能的。细化这个概念,就有了顶点的 入度(in-degree) 和 出度(out-degree) 。无向图中,顶点的度就是与顶点相关联的边的数目,没有入度和出度。在有向图中,我们以图1-2为例,顶点10有2个入度,$3\rightarrow10$,$11\rightarrow10$,但是升含没有从10指向其它顶点的边,因此顶点10的出度为0。
路径(path) :依次遍历顶点序列之间的边所形成的轨迹。注意,依次就意味着有序,先1后2和先2后1不一样。
简单路径 : 没有重复顶点的路径称为简单路径。说白了,这一趟路里没有出现绕了一圈回到同一点的情况,也就是没有 环 。
环/回路 :包含相同的顶点两次或者两次以上。图1-3中的顶点序列$<1,2,4,3,1>$,1出现了两次,当然还有其它的环,比如$<1,4,3,1>$。
简单回路/简单环: 除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路
无环图 :没有环的图,携毕其中, 有向无环图 有特殊的名称,叫做 DAG(Directed Acyline Graph) (最好记住,DAG具有一些很好性质,比如很多动态规划的问题都可以转化成DAG中的最长路径、最短路径或者路径计数的问题)。
两个连通分支:
连通的 :无向图中每一对不同的顶点之间都有路径。如果这个条件在有向图里也成立,那么就是 强连通 的。
连通分量 :无向图中的极大连通子图。
两点强连通:在有向图G中,如果两点互相可达
强连通图: 如果有向图G的每两个顶点都强连通(任意两点互相可达),称G是一个 强连通图 。
强连通分量: 非强连通有向图的极大强连通子图,称为强连通 分量 (strongly connected components)。
关节点(割点) :某些特定的顶点对于保持图或连通分支的连通性有特殊的重要意义。如果 移除某个顶点 将使图或者分支 失去连通性 ,则称该顶点为 关节点 。(在某图中,若删除顶点V以及V相关的边后,图的一个连通分量分割为两个或两个以上的连通分量,则称顶点V为该图的一个关节点)。
桥(割边) :和关节点类似,删除一条边,就产生比原图更多的连通分支的子图,这条边就称为 割边 或者 桥 。
双连通图 :在无向连通图中,如果删除该图的任何一个结点都不能改变该图的连通性,则该图为双连通的无向图。个人理解就是一个双连通图没有割点,没有桥的图。
1.2 一些有趣的图概念
这一部分属于图论的内容,基础图算法不会用到,但是我觉得挺有意思的,小记如下。【这部分我没看,照搬过来了】
同构 4 :图看起来结构不一样,但它是一样的。假定有$G_1$和$G_2$,那么你只要确认对于$G_1$中的所有的两个 相邻点 $a$和$b$,可以通过某种方式$f$映射到$G_2$,映射后的两个点$f(a)$、$f(b)$也是相邻的。换句话说,当两个简单图同构时,两个图的顶点之间保持相邻关系的一一对应。
图1-7就展示了图的同构,这里顶点个数很少判断图的同构很简单。我们可以把v1看成u1,自然我们会把u3看出v3。用数学的语言就是$f(u_1)=v_1$,$f(u_3)=v_3$。u1的另外一个连接是到u2,v1的另外一个连接是到v4,不难从相邻顶点的关系验证$f(u_2)=v_4$,$f(u_4)=v_2$。
欧拉回路(Euler Circuit) :小学数学课本上的哥尼斯堡七桥问题,能不能从镇里的某个位置出发 不重复的经过所有桥(边)并且返回出发点 。这也就小学的一笔画问题,欧拉大神解决里这个问题,开创了图论。结论很简单:至少2个顶点的连通多重图存在欧拉回路的充要条件是 每个顶点的度都是偶数 。证明也很容易,大家有兴趣可以阅读相关资料。结论也很好理解,从某个起点出发,最后要回起点,中间无论路过多少次起点,都会再次离开,进、出的数目必然相等,故一定是偶数。
哈密顿回路(Hamilton Circuit) :哈密顿回路条件就比欧拉回路严格一点, 不能重复经过点 。你可能会感到意外,对于欧拉回路,我们可以轻而易举地回答,但是 我们却很难解决哈密顿回路问题,实际上它是一个NP完全问题 。这个术语源自1857年爱尔兰数学家威廉·罗万·哈密顿爵士发明的智力题。哈密顿的智力题用到了木质十二面体(如图1-8(a)所示,十二面体有12个正五边形表面)、十二面体每个顶点上的钉子、以及细线。十二面体的20个顶点用世界上的不同城市标记。智力题要求从一个城市开始,沿十二面体的边旅行,访问其他19个城市,每个恰好一次,最终回到第一个城市。
因为作者不可能向每位读者提供带钉子和细线的木质十二面体,所以考虑了一个 等价的问题 :对图1-8(b)的图是否具有恰好经过每个顶点一次的回路?它就是对原题的解,因为这个平面图 同构 于十二面体顶点和边。
着名的 旅行商问题(TSP) 要求旅行商访问一组城市所应当选取的最短路线。这个问题可以归结为求完全图的哈密顿回路,使这个回路的边的权重和尽可能的小。同样,因为这是个NP完全问题,最直截了当的方法就检查所有可能的哈密顿回路,然后选择权重和最小的。当然这样效率几乎难以忍受,时间复杂度高达$O(n!)$。在实际应用中,我们使用的启发式搜索等 近似算法 ,可以完全求解城市数量上万的实例,并且甚至能在误差1%范围内估计上百万个城市的问题。
关于旅行商问题目前的研究进展,可以到 http://www.math.uwaterloo.ca/... 。
1.3 小结
以为可以一带而过,结果写了那么多。也没什么好总结的了,当然这些也至是图论概念的一小部分,还有一些图可能我们以后也会见到,比如顺着图到网络流,就会涉及二分图,不过都很好理解,毕竟有图。
1、数组(邻接矩阵)
2、邻接表
3、十字链表
4、邻接多种表