如果你想让自己的编程能力有质的飞跃,不再停留于调用现成的API,而是追求更完美的实现,那么这门课程就是你的必修课,因为程序设计=数据结构+算法。
通过对基础数据结构和算法的学习,能更深层次的理解程序,提升编写代码的能力,让程序的代码更优雅,性能更高。
官方解释:数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及他们之间的关系和操作等相关问题的学科。
大白话:数据结构就是把数据元素按照一定的关系组织起来的集合,用来组织和储存数据。
传统上,我们可以把数据结构分为逻辑结构和物理结构两大类。
逻辑结构是从具体问题中抽象出的模型,是抽象意义上的结构,按照对象中数据元素之间的关系分类。
逻辑结构在计算机中真正的表示方式(映像),称为物理结构,也可以叫做存储结构。常见的存储结构有顺序储存结构、链式存储结构。
官方解释:算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。
大白话:根据一定的条件,对一些数据进行计算,得到需要的结果。
在程序中我们可以用不同的算法解决相同的问题,而不同的算法成本也不同。总体上,一个优秀的算法追求以下两个目标:
例1:计算1~100的和:
解法1:累加
解法2:使用等差数列公式
例2:计算n的阶乘:
解法1:递归调用
解法2:从1×到n
事后分析估算方法:不建议,通常需要花费大量的时间和精力,测试完了如果发现是非常糟糕的算法,那么之前所做的事情就白费了。
事前分析估算方法:主要取决于以下几个要素:
抛开计算机硬件,软件的因素,一个程序运行时间依赖于算法的好坏和问题的输入规模。
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随着n的变化情况并确定T(n)的量级。算法的时间复杂度,就是算法的时间量度,记作:T(n)=O(f(n))。它表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度,其中f(n)是问题规模n的某个函数。
推导大O阶的表示法有以下几个规则:
线性阶 一个for
平方阶 二个for
立方阶 三个for
对数阶 :随着输入规模n的增大,不管底数为多少,他们的增长趋势是一样的,所以我们会忽略底数。
int i=1,n=100;
while(i<n){
i=i*2;
}//2^x=n;则x=log(2)n
最坏情况是一种保证,在应用中,这是一种最基本的保障,即使在最坏情况下,也能够正常提供服务,所以,除非特别指定,我们提到的运行时间都指的是最坏情况下的运行时间。
了解了java的内存最基本的机制,就能够有效地帮助我们估计大量程序的内存使用情况。
例如:对指定的数组进行反转
解法1:使用temp变量
解法2:使用temp[]数组
//定义比较规则
@Override
public int compareTo(Student o){
return this.getAge()-o.getAge();
}
排序原理:
public static void sort(int[] a){
for(int i=a.length-1;i>0;i++){
for(int j=0;j<i;j++){
if(a[j]>a[j+1]){
int temp=0;
temp=a[j+1];
a[j+1]=a[j];
a[j]temp;
}
}
}
}
排序原理:
public static void sort(int[] a){
for(int i=0;i<=a.length-1;i++){
int minIndex=i;
for(int j=i+1;j<a.length;j++){
if(a[minIndex]>a[j]){
minIndex=j;
}
}
int temp=a[i];
a[i]=a[minIndex];
a[minIndex]=a[i];
}
}
排序原理:
public static void sort(int[] a){
for(int i=1;i<a.length;i++){
for(int j=i;j>0;j--){
if(a[j-1]>a[j]){
int temp=a[j-1];
a[j-1]=a[j];
a[j]=temp;
}else{
break;
}
}
}
}
排序原理:
增量h的确定:
int h=1;
while(h<5){
h=2h+1;//3,7
}
//循环结束后我们就可以确定h的最大值
//h的减小规则为:
h=h/2;
public static void sort(int[] a){
int N=a.length;
int h=1;
while(h<N/2){
h=h*2+1;
}
while(h>=1){
for(int i=h;i<N;i++){
for(int j=i;j>=h;j-=h){
if(a[j-h]>a[j]){
int temp=a[j-h];
a[j-h]=a[j];
a[j]=a[j-h];
}else{
break;
}
}
}
h/=2;
}
}
排序原理:
public static void sort(int[] a,int left,int right){
if(left>=right){
return;
}
//中间索引
int center=(left+right)/2;
sort(a,left,center);
sort(a,center+1,right);
merge(a,left,center,right);
}
public static void merge(int[] a,int left,int center,int right){
int[] tepArr=new int[data.length];
int mid=center+1;
int third=left;
int tmp=left;
while(left<=center&&mid<=right){
if(a[left]<=a[mid]){
tmpArr[third++]=a[left++];
}else{
tmpArr[third++]=a[mid++];
}
}
while(mid<=right){
tmpArr[third++]=a[mid++];
}
while(left<=center){
tmpArr[third++]=a[left++];
}
//将临时数组中的内容拷贝回原数组
while(tmp<=right){
a[tmp]=tmpArr[tmp++];
}
}
排序原理:
public static void quickSort(int[] arr,int low,int high){
int i,j,temp,t;
if(low>high){
return;
}
i=low;
j=high;
while(i<j){
while(temp<=arr[j]&&i<j){
j--;
}
while(temp>=arr[i]&&i<j){
i++;
}
if(i<j){
t=arr[j];
arr[j]=arr[i];
arr[i]=t;
}
}
//将基准为与i和j相等位置的数字交换
temp=arr[low];
arr[low]=arr[i];
arr[i]=temp;
//递归调用左半数组
quickSort(arr,low,j-1);
//递归调用右半数组
quickSort(arr,j+1,high);
}
稳定性的定义:数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,如果使用某种排序算法排序后,能够保证A元素依然在B元素的前面,可以说这个该算法是稳定的。
稳定性的意义:如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。例如要排序的内容是一组商品对象,第一次排序按照价格由低到高排序,第二次排序按照销量由高到低排序,如果第二次排序使用稳定性算法,就可以使得相同销量的对象依旧保持着价格高低的顺序展现,只有销量不同的对象才需要重新排序。这样既可以保持第一次排序的原有意义,而且可以减少系统开销。
常见排序算法的稳定性:
稳定:冒泡排序,插入排序,归并排序
不稳定:选择排序,希尔排序,快速排序
线性表的特征:数据元素之间具有一种“一对一”的逻辑关系。
线性表的分类:
线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序表和链表。
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中的各个元素、使得线性表中再逻辑结构上响铃的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。
例如:java中的ArrayList实现
java中ArrayList集合的底层也是一种顺序表,使用数组实现,同样提供了增删改查以及扩容等功能。
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。
例如:java中的LinkedList实现
java中LinkedList集合也是使用双向链表实现,并提供了增删改查等相关方法
相关问题:链表反转;中间值问题;判断链表是否有环;有环链表入口问题;约瑟夫问题
栈是一种基于先进后出(FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
我们称数据进入到栈的动作为压栈,数据从栈中出去的动作为弹栈。
相关问题:括号匹配问题;逆波兰表达式求值问题
队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先读被读出来。
符号表最主要的目的就是将一个键和一个值联系起来,符号表能够将存储的数据元素是一个键和一个值共同组成的键值对数据,我们可以根据键来查找对应的值。
符号表的增删查操作,随着元素个数N的增多,其耗时也是线性增多的,时间复杂度都是O(n),为了提高运算效率,接下来我们学习树这种数据结构。
树是由n(n>=1)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
树具有以下特点:
结点的度:一个结点含有紫薯的个数;
叶结点:度为0的结点称为叶节点,也可以叫做终端结点
分支结点:度不为0的结点,也可以叫非终端结点
结点的层次:从根结点开始,根结点对的层次为1,根的直接后继层次为2,以此类推
结点的层次编号:将树中的结点,按照从上层到下层,从左到右的层序排成一个线性序列,把他们编成连续的自然数。
树的度:树中所有结点度的最大值
树的高度(深度):树中结点的最大层次
森林:m(m>=0)个互不相交的树的集合,将一颗非空树的根结点点删去,树就变成了一个森林;给森林增加一个统一的根结点,森林就变成一棵树。
孩子结点:一个结点的直接后继
双亲结点(父结点):一个结点的直接前驱
兄弟结点:同一双亲结点的孩子结点间互称兄弟结点
二叉树就是度不超过2的树(每个结点最多有两个子结点)
满二叉树:一个二叉树,每一个层的结点都达到最大值
完全二叉树:叶节点只能出现在下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树
相关问题:层序遍历;前序遍历;后序遍历;中序遍历;二叉树的最大深度问题;折纸问题
堆是数据结构中一类特殊数据结构的统称,堆通常可以被看做是一颗完全二叉树的数组对象。
堆的特性:
实现步骤:
优先队列按照其作用不同,可以分为以下两种:
可以获取并删除队列中最大的值;基于大根堆实现
可以获取并删除队列中最小的值;基于小根堆来实现
在之前实现的最大优先队列和最小优先队列,他们可以分别快速访问到队列中最大元素和最小元素,但是他们有一个缺点,就是没有办法通过索引访问已存在于优先队列中的对象,并更新它们。为了实现这个目的,在优先队列的基础上,学习一种新的数据结构,索引优先队列。接下来我们以最小索引优先队列举列。
之前我们学习过二叉查找树,发现它的查询效率比单纯的链表和数组的查询效率要高很多,大部分情况下,确实是这样的,但不幸的是,在最坏情况下,二叉查找树的性能还是很糟糕。
生如果成的树都像完全二叉树那样,那么即使在最坏情况下,查找的效率依旧会很好。
为了保证查找树的平衡性,我们需要一些灵活性,因此在这里我们允许树中的一个结点保存多个键。确切的说,我们将一棵标准的二叉查找树中的结点称为2-结点(含有一个键和两条链),而现在我们引入3-结点,它含有两个键和三条链。2-结点和3-结点中的每条链都对应着其中保存的键所分割产生的一个区间。
一棵2-3查找树要么为空,要么满足满足下面两个要求:
2-结点:含有一个键(及其对应的值)和两条链,左链接指向2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。
3-结点:含有两个键(及其对应的值)和三条链,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。
我们前面介绍了2-3树,可以看到2-3树能保证在插入元素之后,树依然保持平衡状态,它的最坏情况下所有子结点都是2-结点,树的高度为lgN,相比于我们普通的二叉查找树,最坏情况下树的高度为N,确实保证了最坏情况下的时间复杂度,但是2-3树实现起来过于复杂,所以我们介绍一种2-3树思想的简单实现:红黑树。
红黑树主要是对2-3树进行编码,红黑树背后的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。我们将树中的链接分为两种类型:
红链接:将两个2-结点连接起来构成一个3-结点;黑链接:则是2-3树中的普通链接。
确切的说,我们将3-结点表示为由由一条左斜的红色链接(两个2-结点其中之一是另一个的左子结点)相连的两个2-结点。这种表示法的一个优点是,我们无需修改就可以直接使用标准的二叉查找树的get方法。
红黑树是含有红黑链接并满足下列条件的二叉查找树:
在对红黑树进行一些增删改查的操作后,很有可能会出现红色的右链接或者两条连续红色的链接,而这些都不满足红黑树的定义,所以我们需要对这些情况通过旋转进行修复,让红黑树保持平衡。
当某个结点的左子结点为黑色,右子结点为红色,此时需要左旋。
前提:当前结点为h,它的右子结点为x;
左旋过程:
当某个结点的左子结点是红色,且左子结点的左子结点也是红色,需要右旋
前提:当前结点为h,它的左子结点为x;
右旋过程:
当一个结点的左子结点和右子结点的color都为RED时,也就是出现了临时的4-结点,此时只需要把左子结点和右子结点的颜色变为BLACK,同时让当前结点的颜色变为RED即可。
由于根结点不存在父节点,所以每次插入操作后,我们都需要把根结点的颜色设置为黑色。
前面我们已经学习了二叉查找树、2-3树以及它的实现红黑树。2-3树中,一个结点做多能有两个key,它的实现红黑树中使用对链接染色的方式去表达这两个key。接下来我们学习另外一种树型结构B树,这种数据结构中,一个结点允许多于两个key的存在。
B树是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(logn)的时间复杂度进行查找、顺序读取、插入和删除等操作。
B树中允许一个结点中包含多个key,可以是3个、4个、5个甚至更多,并不确定,需要看具体的实现。现在我们选择一个参数M,来构造一个B树,我们可以把它称作是M阶的B树,那么该树会具有如下特点:
在实际应用中B树的阶数一般都比较大(通常大于100),所以,即使存储大量的数据,B树的高度仍然比较小,这样在某些应用场景下,就可以体现出它的优势。
在我们的程序中,不可避免的需要通过IO操作文件,而我们的文件是存储在磁盘上的。计算机操作磁盘上的文件是通过文件系统进行操作的,在文件系统中就使用到了B树这种数据结构。
文件系统的设计者利用了磁盘预读原理,将一个结点的大小设为等于一个页(1024个字节或其整数倍),这样每个结点只需要一次I/O就可以完全载入。那么3层的B树可以容纳102410241024差不多10亿个数据,如果换成二叉查找树,则需要30层!假定操作系统一次读取一个节点,并且根节点保留在内存中,那么B树在10亿个数据中查找目标值,只需要小于3次硬盘读取就可以找到目标值,但红黑树需要小于30次,因此B树大大提高了IO的操作效率。
B+树是对B树的一种变形树,它与B树的差异在于:
B+树的优点在于:
B树的优点在于:
由于B树的每一个节点都包含key和value,因此我们根据key查找value时,只需要找到key所在的位置,就能找到value,但B+树只有叶子结点存储数据,索引每一次查找,都必须一次一次,一直找到树的最大深度处,也就是叶子结点的深度,才能找到value。
在数据库的操作中,查询操作可以说是最频繁的一种操作,因此在设计数据库时,必须要考虑到查询的效率问题,在很多数据库中,都是用到了B+树来提高查询的效率;
在操作数据库时,我们为了提高查询效率,可以基于某张表的某个字段建立索引,就可以提高查询效率,那其实这个索引就是B+树这种数据结构实现的。
执行select * from user where id=18,需要从第一条数据开始向下查询
根据主键索引在二叉树上查询
执行select * from user where id>=12 and id<=18,如果有了索引,由于B+树的叶子结点形成了一个有序链表,所以我们只需要找到id为12的叶子结点,按照遍历链表的方式顺序往后查即可,效率非常高。
并查集是一种树型的数据结构,并查集可以高效地进行如下操作:查询元素p和元素q是否属于同一组合并元素p和元素q所在的组。
并查集也是一种树型结构,但这棵树跟我们之前讲的二叉树、红黑树、B树等都不一样,这种树的要求比较简单:
在现实生活中,有许多应用场景会包含很多点以及点与点之间的连接,而这些应用场景我们都可以用图这种数据结构去解决。
定义:图是由一组顶点和一组能够将两个顶点相连的边组成的
特殊的图:
图的分类:按照连接两个顶点的边的不同,可以 把图分为以下两种:
相邻顶点:当两个顶点通过一条边相连时,我们称这两个顶点是相邻的,并且称这条边依附于这两个顶点。
度:某个顶点的度就是依附于该顶点的边的个数。
子图:是一幅图的所有边的子集(包含这些边依附的顶点)组成的图。
路径:是由边顺序连接的一系列的顶点组成
环:是一条至少含有一条边且终点和起点相同的路径
连通图:如果图中任意一个顶点都存在一条路径到达另外一个顶点,那么这幅图就称之为连通图
连通子图:一个非连通图由若干连通的部分组成,每一个连通的部分都可以称为该图的连通子图
要表示一幅图,只需要表示清楚以下两部分内容即可:
常见的图的存储结构有两种:邻接矩阵和邻接表
很明显,邻接矩阵这种存储方式的空间复杂度是V^2的,如果我们处理的问题规模比较大的话,内存空间极有可能不够用。
很明显,邻接表的空间并不是是线性级别的,所以后面我们一直采用邻接表这种存储形式来表示图。
在很多情况下,我们需要遍历图,得到图的一些性质,例如,找出图中与指定的顶点相连的所有顶点,或者判定某个顶点与指定顶点是否相通,是非常常见的需求。
有关图的搜索,最经典的算法有深度优先搜索和广度优先搜索,接下来我们分别讲解这两种搜索算法。
所谓的深度优先搜索,指的是在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找子结点,然后找兄弟结点。
所谓的深度优先搜索,指的是在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找兄弟结点,然后找子结点。
在实际生活中,很多应用相关的图都是有方向性的,最直观的就是网络,可以从A页面通过链接跳转到B页面,那么a和b连接的方向是a->b,但不能说是b->a,此时我们就需要使用有向图来解决这一类问题,它和我们之前学习的无向图,最大的区别就在于连接是具有方向的,在代码的处理上也会有很大的不同。
定义:有向图是一副具有方向性的图,是由一组顶点和一组有方向的边组成的,每条方向的边都连着一对有序的顶点。
出度:由某个顶点指出的边的个数称为该顶点的出度。
入度:指向某个顶点的边的个数称为该顶点的入度。
有向路径:由一系列顶点组成,对于其中的每个顶点都存在一条有向边,从它指向序列中的下一个顶点。
有向环:一条至少含有一条边,且起点和终点相同的有向路径。
一副有向图中两个顶点v和w可能存在以下四种关系:
给定一副有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素,此时就可以明确的表示出每个顶点的优先级。
如果我们要使用拓扑排序解决优先级问题,首先得保证图中没有环的存在。
加权无向图是一种为每条边关联一个权重值或是成本的图模型。这种图能够自然地表示许多应用。在一副航空图中,边表示航线,权值则可以表示距离或是费用。在一副电路图中,边表示导线,权值则可能表示导线的长度即成本,或是信号通过这条先所需的时间。此时我们很容易就能想到,最小成本的问题,例如,从西安飞纽约,怎样飞才能使时间成本最低或者是金钱成本最低?
加权无向图中的边我们就不能简单的使用v-w两个顶点表示了,而必须要给边关联一个权重值,因此我们可以使用对象来描述一条边。
之前学习的加权图,我们发现它的边关联了一个权重,那么我们就可以根据这个权重解决最小成本问题,但如何才能找到最小成本对应的顶点和边呢?最小生成树相关算法可以解决。
图的生成树是它的一棵含有其所有顶点的无环连通子图,一副加权无向图的最小生成树它的一棵权值(树中所有边的权重之和)最小的生成树。
约定:只考虑连通图。最小生成树的定义说明它只能存在于连通图中,如果图是不联通的,那么分别计算每个连通图子图的最小生成树,合并到一起称为最小生成森林。
要从一副连通图中找出该图的最小生成树,需要通过切分定理完成。
切分:将图的所有顶点按照某些规则分为两个非空且没有交集的集合。
横切边:连接两个属于不同集合的顶点的边称之为横切边。
切分定理:在一幅加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图中的最小生成树。
注意:一次切分产生的多个横切边中,权重最小的边不一定是所有横切边中唯一属于图的最小生成树的边。
贪心算法是计算图的最小生成树的基础算法,它的基本原理就是切分定理,使用切分定理找到最小生成树的一条边,不断的重复直到找到最小生成树的所有边。如果图有V个顶点,那么需要找到V-1条边,就可以表示该图的最小生成树。
计算图的最小生成树的算法有很多种,但这些算法都可以看做是贪心算法的一种特殊情况,这些算法的不同之处在于保存切分和判定权重最小的横切边的方式。
我们学习第一种计算最小生成树的方法叫Prim算法,它的每一步都会为一棵生成中的树添加一条边。一开始这棵树只有一个顶点,然后会向它添加V-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入到树中。
Prim算法的切分规则:把最小生成树中的顶点看做是一个集合,把不在最小生成树中的顶点看做是另外一个集合。
Prim算法始终将图中的顶点切分成两个集合,最小生成树顶点和非最小生成树顶点,通过不断的重复做某些操作,可以逐渐将非最小生成树中的顶点加入到最小生成树中,直到所有的顶点都加入到最小生成树中。
kruskal算法是计算一副加权无向图的最小生成树的另外一种算法,它的主要思想是按照边的权重(从小到大)处理它们,将边加入最小生成树中,加入的边不会与已经加入最小生成树的边构成环,直到树中含有V-1条边为止。
kruskal算法和prim算法的区别:Prim算法是一条边一条边的构造最小生成树,每一步都为一棵树添加一条边。kruskal算法构造最小生成树的时候也是一条边一条边地构造,但它的切分规则是不一样的。它每一次寻找的边会连接一片森林中的两棵树。如果一副加权无向图由V个顶点组成,初始化情况下每个顶点都构成一棵独立的树,则V个顶点对应V棵树,组成一片森林,kruskal算法每一次处理都会将两棵树合并为一棵树,直到整个森林中只剩一棵树为止。
之前学习的加权无向图中,边是没有方向的,并且同一条边会同时出现在该边的两个顶点的邻接表中,为了能够处理含有方向性的图的问题,我们需要加权有向图。
定义:在一副加权有向图中,从顶点s到顶点t的最短路径是所有从顶点s到顶点t的路径中总权重最小的那条路径。
性质:
最短路径树:给定一副加权有向图和一个顶点s,以s为起点的一棵最短路径树是图的一副子图,它包含顶点s以及从s可达的所有顶点。这棵有向树的根结点为s,树的每条路径都是有向图中的一条最短路径。
松弛这个词来源于生活:一条橡皮筋沿着两个顶点的某条路径紧紧展开,如果这两个顶点之间的路径不止一条,还有存在更短的路径,那么把皮筋转移到更短的路径上,皮筋就可以放松了。
边的松弛:放松边v->w意味着检查从s到w的最短路径是否先从s到v。如果不是则放弃这条边。
顶点的松弛:只需要把某个顶点指出的所有边松弛,那么该顶点就松弛完毕。例如要松弛顶点v,只需要遍历v的邻接表,把每一条边都松弛,那么顶点v就松弛了。
Disjstra算法的实现和Prim算法很类似,构造最短路径树的每一步都是向这棵树中添加一条新的边,而这条新的边是有效横切边队列中的权重最小的边。
两者之间的区别。
在图论中,Prim算法解决的问题是连通无向有权图中最小生成树问题,而Dijkstra算法解决的问题是源点到目标点的最短路径问题。
虽然这两个算法在添加新结点时,都是选择“距离最短”的结点加入集合,但是Prim算法中,“距离最短”是指未访问的结点到已经访问的所有结点距离最小,即将已经访问的结点视为一个整体,将距离最小的结点加入到已访问的集合中;而在Dijkstra算法中,“距离最短”是指所有未访问结点(通过已访问的结点)到源点距离最小。