目录
1 时间复杂度
2 树
3 散列
4 优先级队列(堆)
5 排序
6 图参考资料
· 《数据结构与算法分析》
1 时间复杂度
(1)当出现对数时间复杂度时,可以优先考虑折半查找,如二分法、归并排序等。
(2)关于尾递归
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
原理:当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
参考:递归与尾递归总结 - huan欢 - 博客园
针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。
2 树
2.1 相关概念
(1)树的定义,一种最自然的方式是递归的方式。
(2)深度:从根到一个节点的唯一路径的长
(3)叶节点(树叶):没有儿子的节点。在Java中,叶节点的左右节点为null。
(4)高:一个节点到叶节点的最长路径的长
(5)常见的遍历方式
· DFS:前序、中序、后序
· BFS:层序
2.2 二叉树
(1)定义:每个节点都不能有多于两个的儿子
(2)实现
public class TreeNode {
Object element;
TreeNode left;
TreeNode right;}
(3)其他概念
· 完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
· 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
2.3 二叉查找树
(1)定义:对于树中的每个节点X,它的左子树中所有项的值小于X中的项,而它的右子树中所有项的值大于X中的项。注意:二叉树中元素不能相等。
(2)查找元素contains
根据目标元素和左右子树元素的大小关系,递归查找。
public boolean contains( int x, TreeNode t){
if(t == null){
return false;
}
int res = x.compareTo(t.val);
if(res < 0){
return contains(x, t.left);
}else if(res > 0){
return contains(x, t.right);
}else {
return true;
}
}
(3)findMin和findMax
往树的最左节点和最右节点找。
(4)insert
根据大小关系,遍历并比较,遍历到叶节点时插入
(5)remove
用其右子树的最小的数据代替该节点的数据,并递归的删除那个节点。
2.4 AVL树
2.4.1 概念
(1)背景:对于二叉搜索树,当向一棵树输入预先排好序的数据,将会退化成一个链表。
(2)特性:带有平衡条件的二叉查找树,它必须保证树的深度须是O(log N)。
(3)定义:一颗AVL树是其每个节点的左子树和右子树的高度最多差1的二叉查找树(空树的高度为-1)
2.4.2 旋转
当进行插入操作时,可能破坏AVL树的特性。通过旋转操作来恢复平衡特性。
2.4.2.1 分类
2.4.2.2 代码
(1)插入
(2)右旋
(3)左旋
2.5 伸展树
2.5.1 概念
(1)伸展树(Splay Tree)主要特点是不会保证树一直是平衡的,但各种操作的平摊时间复杂度是O(log n)
(2)伸展树的出发点
考虑到局部性原理(刚被访问的内容下次可能仍会被访问,查找次数多的内容可能下一次会被访问),为了使整个查找时间更小,被查频率高的那些节点应当经常处于靠近树根的位置。这样,很容易得想到以下这个方案:每次查找节点之后对树进行重构,把被查找的节点搬移到树根,这种自调整形式的二叉查找树就是伸展树。每次对伸展树进行操作后,它均会通过旋转的方法把被访问节点旋转到树根的位置。
为了将当前被访问节点旋转到树根,我们通常将节点自底向上旋转,直至该节点成为树根为止。“旋转”的巧妙之处就是在不打乱数列中数据大小关系(指中序遍历结果是全序的)情况下,所有基本操作的平摊复杂度仍为O(log n)。
2.5.2 旋转方式
伸展树主要有三种旋转操作,分别为单旋转,一字形旋转和之字形旋转。为了便于解释,我们假设当前被访问节点为X,X的父亲节点为Y(如果X的父亲节点存在),X的祖父节点为Z(如果X的祖父节点存在)。
(1)单旋转
节点X的父节点Y是根节点。这时,如果X是Y的左孩子,我们进行一次右旋操作;如果X 是Y 的右孩子,则我们进行一次左旋操作。经过旋转,X成为二叉查找树T的根节点,调整结束。
(2)一字型旋转
节点X 的父节点Y不是根节点,Y 的父节点为Z,且X与Y同时是各自父节点的左孩子或者同时是各自父节点的右孩子。这时,我们进行一次左左旋转操作或者右右旋转操作。
(3)之字形旋转
节点X的父节点Y不是根节点,Y的父节点为Z,X与Y中一个是其父节点的左孩子而另一个是其父节点的右孩子。这时,我们进行一次左右旋转操作或者右左旋转操作。
(4)伸展树区间操作
在实际应用中,伸展树的中序遍历即为我们维护的数列,这就引出一个问题,怎么在伸展树中表示某个区间?比如我们要提取区间[a,b],那么我们将a前面一个数对应的结点转到树根,将b 后面一个结点对应的结点转到树根的右边,那么根右边的左子树就对应了区间[a,b]。原因很简单,将a 前面一个数对应的结点转到树根后, a 及a 后面的数就在根的右子树上,然后又将b后面一个结点对应的结点转到树根的右边,那么[a,b]这个区间就是下图中B所示的子树。
利用区间操作我们可以实现线段树的一些功能,比如回答对区间的询问(最大值,最小值等)。具体可以这样实现,在每个结点记录关于以这个结点为根的子树的信息,然后询问时先提取区间,再直接读取子树的相关信息。还可以对区间进行整体修改,这也要用到与线段树类似的延迟标记技术,即对于每个结点,额外记录一个或多个标记,表示以这个结点为根的子树是否被进行了某种操作,并且这种操作影响其子结点的信息值,当进行旋转和其他一些操作时相应地将标记向下传递。
2.6 B树(B-树)、B+树、B*树
参考连接:B树(B-树)、B+树、B*树 - 、B树与B+树简明扼要的区别_Hannah-CSDN博客_b树和b+树区别
2.7 红黑树
2.7.1 定义
是AVL树的另一种变种,带有下列附加条件的自平衡二叉查找树:
(1)节点是红色或黑色。
(2)根节点是黑色。
(3)每个叶子节点都是黑色的空节点(NIL节点)。
(4)每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
(5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
2.7.2 调整
插入一个叶子节点后,由于插入破坏的红黑树性质,要进行一定的调整。有变色和旋转两种方式。
2.7.2.1 变色
为了重新符合红黑树的规则,尝试把红色节点变为黑色,或者把黑色节点变为红色。
2.7.2.2 旋转
2.7.3 插入
新插入的节点是红色的(之所以将新插入的结点的颜色赋为红色,是因为:如果设为黑色,就会导致根到叶子的路径上有一条路上,多一个额外的黑结点,这个是很难调整的。但是设为红色结点后,可能会导致出现两个连续红色结点的冲突,那么可以通过颜色调换和树旋转来调整,这样简单多了),插入修复操作如果遇到父节点的颜色为黑则修复操作结束。也就是说,只有在父节点为红色节点的时候是需要插入修复操作的。
插入修复操作分为以下的三种情况,而且新插入的节点和父节点都是红色的:
· 叔叔节点也为红色。
· 叔叔节点为空,且祖父节点、父节点和新节点处于一条斜线上。
· 叔叔节点为空,且祖父节点、父节点和新节点不处于一条斜线上。
(1)插入操作-case 1
case 1的操作是将父节点和叔叔节点与祖父节点的颜色互换,这样就符合了RBTRee的定义。即维持了高度的平衡,修复后颜色也符合RBTree定义的第三条和第四条。下图中,操作完成后A节点变成了新的节点。如果A节点的父节点不是黑色的话,则继续做修复操作。
(2)插入操作-case 2
case 2的操作是将B节点进行右旋操作,并且和父节点A互换颜色。通过该修复操作RBTRee的高度和颜色都符合红黑树的定义。如果B和C节点都是右节点的话,只要将操作变成左旋就可以了。
(3)插入操作-case 3
case 3的操作是将C节点进行左旋,这样就从case 3转换成case 2了,然后针对case 2进行操作处理就行了。case 2操作做了一个右旋操作和颜色互换来达到目的。如果树的结构是下图的镜像结构,则只需要将对应的左旋变成右旋,右旋变成左旋即可。
插入操作的总结:插入后的修复操作是一个向root节点回溯的操作,一旦牵涉的节点都符合了红黑树的定义,修复操作结束。
2.7.4 对比AVL树
·从查找或者读取性质来说的话,AVL更好,因为AVL是更平衡的
·红黑树提供了更快的插入和删除操作,因为AVL的旋转次数会更多,红黑树只是一个近似平衡的
·AVL树需要存储平衡因子或是高度信息,但红黑树只需要1个bit存储红或者黑即可。
·总结上述:
当读操作很多,写操作很少的时候,用AVL树
当读写操作相当时,优先使用红黑树,比较简洁也比较好实现。红黑树尝尝用在高级语言库里面,比如TreeMap或者TreeSet。
另外参考:关于AVL树和红黑树的一点看法 - 知乎
3 散列
3.1 概念
散列是一种用于以常数平均时间执行插入、删除和查找的技术。
3.2 散列函数
(1)每个关键字被映射到从0到TableSize-1这个范围中的某个数,并被放到适当的单元中,这个映射即为散列函数。
(2)有写关键字不是整型,需要通过hashcode()先转换为int类型,再进行hash & (TableSize - 1)
3.3 哈希冲突
3.3.1 概念
当两个关键字散列到同一个值的时候,即为冲突
3.3.2 解决方式
3.3.2.1 分离链接法
(1)概念
将散列到同一个值的所有元素保留到一个链表中
(2)新的元素插入在链表的表头,不仅仅因为方便,还因为新近插入的元素最有可能不久又被访问。
3.3.2.2 开放定址法
(1)f(i)为冲突解决方法,h为散列表对应位置
·
· f的常见设计方法:
· 线性探测法f(i) = i
· 平方探测法f(i) = i^2
· 双散列f(i) = i * hash'(x)
(2)对于不适用分离链接的散列表来说,填装因子(已有元素个数 / TableSize)应该不低于0.5.这样的表叫做探测散列表。
3.4 再散列
在哈希表填装到一定程度时(由负载因子决定),将表先扩大到原来的两倍,再将元素重新散列到新表上。
4 优先级队列(堆)
4.1 模型
堆至少需要下列两种操作的数据结构:
(1)Insert
(2)deleteMin(删除最小者,也可以是最大者)
4.2 二叉堆
4.2.1 性质
恰似AVL树,对堆的一次操作可能破坏这两个性质中的一个。
4.2.1.1 结构性
(1)堆是一棵被完全填满的二叉树,有可能例外是在底层,底层元素从左到右填入,即完全二叉树。
(2)如是数组实现二叉堆,则对于当前位置i,左儿子在2i,右儿子在2i + 1,父节点在i / 2位置。
4.2.1.2 堆序性
对于小根堆,每一个节点X,X的父亲中的关键字小于或等于X中的关键字。所以可以常数时间,获得极值。
4.2.2 基本堆操作
两个操作时间复杂度都为O(log N)
(1)insert插入(上滤)
· 将新元素插入到下一个可用位置(保证结构性,即完全二叉树)
· 比较新元素和其父节点元素的大小,若比父元素小,就交换两个位置,继续向上比较
(2)deleteMin删除堆顶(下滤)
· 将堆最后一个元素放入堆顶,即删除堆顶元素
· 将该元素与两个儿子比较,与其中的较小者交换,并继续向下比较。
4.3 d-堆
d-堆是二叉堆的简单推广,就像一个二叉堆,只是所有的节点需要有d个儿子。
4-4 左式堆
4-5 斜堆
4-6 二项队列
5 排序
参考:developer1024 - 知乎
5.1 基于比较的排序
5.1.1 插入排序
(1)算法:由N-1趟排序组成,对于p = 1到N-1趟,插入排序保证从位置0到位置p上的元素为已排序状态。
(2)时间复杂度 O(N^2)
(3)说明图
(4)代码
public static void insertSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j > -1; j--) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
}
5.1.2 冒泡排序
5.1.3 选择排序
5.1.4 希尔排序
(1)算法
先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。
因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比冒泡和插入有较大提高。
步骤:
① 先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。
② 所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序。
③ 取第二个增量d2小于d1重复上述的分组和排序,直至所取的增量dt=1(dt小于dt-l小于…小于d2小于d1),即所有记录放在同一组中进行直接插入排序为止。
(2)时间复杂度
· 平均时间复杂度:O(Nlog2N)
· 最佳时间复杂度:
· 最差时间复杂度:O(N^2)
· 空间复杂度:O(1)
· 稳定性:不稳定
· 复杂性:较复杂
(3)说明图
(4)代码
5.1.5 堆排序
(1)算法:
① 将待排序的序列构造成一个最大堆,此时序列的最大值为根节点
② 依次将根节点与待排序序列的最后一个元素交换
③ 再维护从根节点到该元素的前一个节点为最大堆,如此往复,最终得到一个递增序列
(2)复杂度分析
· 平均时间复杂度:O(nlogn)
· 最佳时间复杂度:O(nlogn)
· 最差时间复杂度:O(nlogn)
· 稳定性:不稳定
5.1.6 归并排序
(1)算法
归并排序是用分治思想,分治模式在每一层递归上有三个步骤:
· 分解(Divide):将n个元素分成个含n/2个元素的子序列。
· 解决(Conquer):用合并排序法对两个子序列递归的排序。
· 合并(Combine):合并两个已排序的子序列已得到排序结果。
(2)复杂度分析
· 平均时间复杂度:O(nlogn)
· 最佳时间复杂度:O(n)
· 最差时间复杂度:O(nlogn)
· 空间复杂度:O(n)
· 排序方式:In-place
· 稳定性:稳定
(3)说明图
5.1.7 快速排序
(1)算法:
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
① 从数列中挑出一个元素,称为 “基准”(pivot),
② 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
③ 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
(2)复杂度分析
· 平均时间复杂度:O(NlogN)
· 最佳时间复杂度:O(NlogN)
· 最差时间复杂度:O(N^2)
· 空间复杂度:根据实现方式的不同而不同
5.2 基于计数的排序
5.2.1 计数排序
(1)算法
使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),然后进行分配、收集处理:
① 分配。扫描一遍原始数组,以当前值-minValue作为下标,将该下标的计数器增1。
② 收集。扫描一遍计数器数组,按顺序把值收集起来。
具体步骤:
① 找出待排序的数组中最大和最小的元素
② 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
③ 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
④ 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
(2)复杂度分析
· 平均时间复杂度:O(n + k)
· 最佳时间复杂度:O(n + k)
· 最差时间复杂度:O(n + k)
· 空间复杂度:O(n + k)
(3)代码
public class CountingSort{
public static void main(String[]argv){
int[] A = CountingSort.countingSort(new int[]{16,4,10,14,7,9,3,2,8,1});
Utils.print(A);
}
public static int[] countingSort(int[]A){
int[] B = new int[A.length];// 假设A中的数据a'有,0<=a' && a' < k并且k=100
int k=100;
countingSort(A,B,k);
returnB;
}
private static void countingSort(int[]A,int[]B,intk){
int[]C =new int[k];// 计数
for(int j=0; jint a = A[j];
C[a]+=1;
}
Utils.print(C);// 求计数和
for( int i=1;iC[i]=C[i]+C[i-1];
}
Utils.print(C);// 整理
for(int j= A.length-1;j>=0;j--){
int a=A[j];
B[C[a]-1] = a;
C[a] -=1;
}
}
}
5.2.2 桶排序
(1)算法
桶排序的思想近乎彻底的分治思想。桶排序假设待排序的一组数均匀独立的分布在一个范围中,并将这一范围划分成几个子范围(桶)。然后基于某种映射函数f ,将待排序列的关键字 k 映射到第i个桶中 (即桶数组B 的下标i) ,那么该关键字k 就作为 B[i]中的元素 (每个桶B[i]都是一组大小为N/M 的序列 )。接着将各个桶中的数据有序的合并起来 : 对每个桶B[i] 中的所有元素进行比较排序 (可以使用快排)。然后依次枚举输出 B[0]….B[M] 中的全部内容即是一个有序序列。
(2)复杂度分析
· 平均时间复杂度:O(n + k)
· 最佳时间复杂度:O(n + k)
· 最差时间复杂度:O(n ^ 2)
· 空间复杂度:O(n * k)
· 稳定性:稳定
(3)说明图
5.2.3 基数排序
(1)算法
原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
· MSD:先从高位开始进行排序,在每个关键字上,可采用计数排序
· LSD:先从低位开始进行排序,在每个关键字上,可采用桶排序
具体步骤:
① 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
② 从最低位开始,依次进行一次排序。
③ 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
(2)复杂度分析
· 时间复杂度:O(k*N)
· 空间复杂度:O(k + N)
· 稳定性:稳定
(3)说明图
5.3 外部排序
5.3.1 概念
外部排序算法由两个阶段构成:
(1)按照内存大小,将大文件分成若干长度为 l 的子文件(l 应小于内存的可使用容量),然后将各个子文件依次读入内存,使用适当的内部排序算法对其进行排序(排好序的子文件统称为“归并段”或者“顺段”),将排好序的归并段重新写入外存,为下一个子文件排序腾出内存空间;
(2)对得到的顺段进行合并,直至得到整个有序的文件为止。
5.3.2 示例
例如,有一个含有 10000 个记录的文件,但是内存的可使用容量仅为 1000 个记录,毫无疑问需要使用外部排序算法,具体分为两步:
· 将整个文件其等分为 10 个临时文件(每个文件中含有 1000 个记录),然后将这 10 个文件依次进入内存,采取适当的内存排序算法对其中的记录进行排序,将得到的有序文件(初始归并段)移至外存。
· 对得到的 10 个初始归并段进行如图的两两归并,直至得到一个完整的有序文件。
这里只用了2路进行归并,还有更多路平衡归并。
6 图
6.1 一些概念
(1)图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为G =(V,E);
图的顶点个数不能为0,但边数可以为0,一般没有空图的说法,图论里的零图是表示只由孤立节点组成的图。
(2)权:边有值
(3)路径:一个顶点序列;路径的长:路径上的边数
(4)环:含有一条从一个顶点到它自身的边
(5)简单路径:该路径上的所有顶点都是互异的,但第一个顶点和最后一个顶点可能相同
(6)圈:有向图中,满足w1=wn且长至少为1的一条路径
(7)连通:无向图中,每个顶点到其他顶点都有一条路径。具有这样性质的有向图称为强连通的。若有向图去掉方向后的基础图是连通的,则称该有向图为弱连通的。
(8)完全图:每一对顶点间都存在一条边的图。
含有n个顶点的无向完全图有n*(n-1)/2条边,有向完全图有n*(n-1)条边。
(9)入度:有向图中,顶点v的入度是边(u, v)的条数
(10)
入度等于出度等于边数,图的度之和等于边数的二倍。
简单图,即没有环也没有重边的图。
边数少的图就是稀疏图,边数多的图就是稠密图。
顶点的度是依附于该顶点的边数
简单通路就是顶点没有重复的路径。
简单回路:除了第一个点和最后一个点外,没有重复访问的点。
无权图的路径长度为路径边的条数。
连通分量:非联通图的极大连通子图称为连通分量,(连同依附于所有点的边)
生成树:n个节点的连通图G的生成树是包含G中全部顶点的一个极小连通子图。含有n-1条边,多一条边构成回路,少一条边不连通。生成森林就是分别得到一棵生成树。
6.2 图的表示
这里只考虑有向图。
(1)邻接矩阵(稠密图适用)
对于每条边(u, v)置A[u][v]等于true,否则数组的元素就是false。如果边有一个权,那么可以置A[u][v]等于该权,使用一个很大或者很小的全标记不存在的边。
(2)邻接表(稀疏图适用)
对每个顶点,使用一个表存放所有邻接的顶点。
6.3 拓扑排序
对有向无圈图的顶点的一种排序,使得如果存在一条从u到v的路径,那么在排序中v就在u的后面。
拓扑排序不是唯一的。
6.4 最短路径算法
单源最短路径问题:给定一个赋权图G=(V,E)和一个特定顶点s作为输入,找出从s到G中每一个其他顶点的最短赋权路径。
6.4.1 无权最短路径
先搜索距离s为1的点,再搜索距离为2的点,依次往下,这种方法称为广度优先搜索(BFS)。这类似树中的层序遍历。同样适用队列进行算法运算。
6.4.2 Dijkstra算法
(1)概念
是解决单源最短路径问题(赋权图中)的一般解法。
是贪婪(心)算法最好的例子,每个阶段把它当作最好的去处理。
只要边的值没有负值,该算法总能顺利工作。
(2)算法
参考:(七)通俗易懂理解——dijkstra算法求最短路径 - 知乎
6.4.3 具有负边值的图
6.4.4 无圈图
以拓扑顺序选择顶点。当一个顶点v被选取后,按照拓扑顺序的法则它没有从unknown顶点发出的进入边,因此它的距离d可不再被降低。
6.5 最小生成树
6.5.1 概念
(1)最小生成树
由图的那些连接图的所有顶点的边构成的树,且其总价值最低。
(2)最小生成树存在当且仅当图是连通的。
(3)最小生成树的边数为顶点总数V - 1
(4)贪婪算法是成立的,即在建立生成树时所添加的边在所有避免成圈的边中其值最小。
6.5.2 Prim算法
算法在每一阶段都可以通过选择边(u, v)使得(u,v)的值是所有u在树上但v不在树上的边的值中的最小者而找出一个新的顶点并把它添加到这棵树中。
6.5.3 Kruskal算法
算法连续地按照最小的权选择边,并且当所选的边不产生圈时就把它作为所选定的边。
Kruskal算法也是贪婪算法。
算法在实施的任意时刻,两个顶点属于同一个集合当且仅当他们在当前的生成森林中连通。因此,每个顶点最初是在它自己的集合中,如果u和v在同一个集合中,那么连接他们的边就要放弃,由于他们已经连通了,因此再添加边(u, v)就会形成一个圈;如果这两个顶点不在同一个集合中,则将该边加入。
6.6 深度优先搜索
深度优先搜索(DFS)是从某个顶点出发,然后递归地遍历所有与其邻接的顶点。
为了避免圈的出现,当访问一个顶点v的时候,由于我们当时已经到该点处,因此可以标记该点是访问过的,并且对于尚未被标记的所有邻接顶点递归调用深度优先搜索。
6.6.1 无向图
利用上述步骤,可以在无向图中构成深度优先生成树。
6.6.2 双连通性
(1)双连通性
一个连通的无向图如果不存在被删除之后使得剩下的图不在连通的顶点,那么这样的无向连通图就称为是双连通的。
(2)如果一个图不是双连通的,那么,将其删除使图不再连通的那些顶点叫作割点。
(3)利用深度优先搜索,可以找出连通图中的所有割点的线性时间算法。
6.6.3 欧拉回路
在图中找出一条圈路径,使得该路径访问图的每条边恰好一次,该问题即为欧拉回路问题。
可以得出,欧拉回路只有当图是连通的且每个顶点的度(即,边的条数)是偶数时才有可能存在。
7 算法
7.1 贪婪算法
7.1.1 算法
贪婪算法分阶段的工作,在每一个阶段,可以认为所做决定是好的,而不考虑将来的后果。当算法终止时,希望局部最优等于全局最优。
7.1.2 哈夫曼编码
(1)数据结构
· 每个字符通过从根节点开始用0指示左分支用1指示右分支而以记录路径的方法表示出来
· 所有的字符都放在树叶上,每个字符编码后,前缀不同,这种编码方式称为前缀码。
(2)哈夫曼算法
算法对由树组成的一个森林进行。一棵树的权等于它的树叶的频率的和。任意选取最小权的两棵树T1和T2,并任意形成以T1和T2为子树的新树,直到形成一个森林算法结束。
(3)特点:两个频率最小的字符必然是两个最深的节点。
7.2 分治算法
7.2.1 算法
分治算法主要分为两部分组成:
· 分:递归解决较小的问题(基本情况除外)
· 治:从子问题的解构建原问题的解
一般坚持子问题不相交的原则。
7.2.2 定理
7.3 动态规划
编译器尝尝不能正确对待递归算法,结果导致低效的程序。
将递归算法重新写成非递归算法,把子问题的答案系统地记录在一个表内,即为动态规划。
7.4 随机化算法
(1)算法
在算法期间,随机数至少有一次用于决策。该算法的运行时间不只依赖于特定的输入,而且依赖于所出现的随机数。
(2)示例
将快排改进,枢纽元在整个数组中随机选取。
7.5 回溯算法
相当于穷举搜索的巧妙实现。
在一步内删除一大组可能性的做法叫做裁剪(剪枝)。