快速排序
一、问题描述
快速排序问题是数据结构与算法中经典的排序之一,本次试验主要是实现基本快速排序与随机快速排序实现数据的排序和性能分析。
二、算法原理
(1)快速排序(Quick Sort)是一种最基本的排序算法,它的基本思想是:在当前无序区R[1,n]中取一个记录作为比较的“基准”(一般取第一个、最后一个或中间位置的元素),用此基准将当前的无序区R[1,n]划分成左右两个无序的子区R[1,i-1]和R[i,n](1≤i≤n),且左边的无序子区中记录的所有关键字均小于等于基准的关键字,右边的无序子区中记录的所有关键字均大于等于基准的关键字;当R[1,i-1]和R[i,n]非空时,分别对它们重复上述的划分过程,直到所有的无序子区中的记录均排好序为止。
步骤如下:
1.从数列中挑出一个元素,称为 “基准”(pivot),
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
(2)随机快速排序
在快速排序中每次对数组的分界操作Partition都是以数组的其实点作为分界点(key),这样就带来了一个问题——当待排序数组是一个已排好序的数组时,快速排序的执行时间是O(n平方),在执行 时间上比较耗时,于是就有了随机快速排序的思想。
步骤如下:
先在a[p:r]中随机选出一个元素a[i]作为划分基准,然后调用函数Randomized_Partition将a[i]与第一个元素a[p]进行交换。再调用函数Partition,即以一个确定的基准元素a[p]对数组a[p:r]进行划分,这也是快速排序算法的关键。最后调用函数Randomized_QuickSort,采用分治策略将数组划分成左右半段分别进行排序。
三、实验数据
(1)输入数据:这里我们用50个随机数进行分析。
(2)输出数据:排序完成的数据和算法执行时间微秒级。
四、实验结果
普通快排:
随机快排:
五、性能分析
这里我们通过实验结果可以看出随机快排会快于普通快排,但理论上来说并不是绝对的。
原因如下:
快速排序算法的性能主要决定于输入数组的划分是否均衡,而这与基准元素的选择密切相关。在最坏的情况下,划分的结果是一边有n-1个元素,而另一边有0个元素(除去被选中的基准元素)。如果每次递归排序中的划分都产生这种极度的不平衡,那么整个算法的复杂度将是Θ(n2)。在最好的情况下,每次划分都使得输入数组平均分为两半,那么算法的复杂度为O(nlogn)。在一般的情况下该算法仍能保持O(nlogn)的复杂度,只不过其具有更高的常数因子。
在随机快速排序中,每次对数组的分界操作Partition都是在数组中随机寻找一个元素作为分界点(key),这样就避免了上诉问题,随机排序的平均在执行时间为O(n*lg(n))。比上面的O(n平方)有很大的提高。
六、实验源码
见附件。
背包问题
一、问题描述
0-1背包问题,部分背包问题。分别实现0-1背包的DP算法,部分背包的贪心算法和DP算法。
二、算法原理
(1)0-1背包的动态规划算法
0-1背包问题:有n件物品和一个容量为W的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。其中每种物品只有一件,可以选择放或者不放。
最优子结构性质:对于0-1问题,考虑重量至多W的最值钱的一包东西。如果去掉其中一个物品j,余下的必是除j以外的n-1件物品中,可以带走的重量至多为W-wj的最值钱的一包东西。
用子问题定义状态:令c[i,w]表示前i件物品恰放入一个容量为w的背包可以获得的最大价值。则其状态转移方程便是:
在将前i件物品放入容量为w的背包中这个子问题,若只考虑第i件物品的策略,如果选择不放第i件物品,那么问题就转化为“前i-1件物品放入容量为w的背包中”,价值为c[i-1,w];如果选择放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为w-wi的背包中”,此时能获得的最大价值就是c [i-1,w-wi]再加上通过放入第i件物品获得的价值vi的和。按照这种思路进行递归,最后的能获得的最大价值即为c[n, W]。
(2)部分背包的贪心算法
部分背包问题与0-1背包问题相似,不同点在于部分背包问题可以选择物品的一部分,而不是像0-1背包一样只能做二分选择。
部分背包问题同样具有最优子结构的性质。考虑如果从最优货物中去掉某物品j的重量w,则余下的货物必是可以从n-1件原有物品和物品j的wj-w中可带走的,重量至多为W-w的价值最大的一包东西。
对于部分背包问题,可以使用贪心策略解决。首先对计算每件物品的单位价值,即vi/wi,然后按照贪心策略,在每次进行选择时优先选择单位价值高的物品。也就是说先选择当前单位价值最高的物品,如果拿完了该物品,并且仍然可以选取一些其他物品时,就再选取当前单位价值次高的物品,一直进行下去,直到不能再取为止。
(3)部分背包的动态规划算法
部分背包问题也可以用DP算法解决。由于题设中已说明所有物品重量和价值均为整数,利用这一特点,可以巧妙的将部分背包问题转化为0-1背包问题,然后调用0-1背包问题的DP算法进行求解。
转化方法是:把第i种物品拆成重量依次为1,2,4...2^(k-1),wi-2^k+1的物品,对应的价值则依次是单位价值乘以拆分重量所得结果。在拆分序列中k是满足wi -2^k+1>0的最大整数。例如,如果wi为14,就将这种物品分成系数分别为1,2,4,7的四件物品。这是二进制的思想,这种划分总可以表示该物品可以选择的所有重量值。通过这样的划分得到一个新的重量序列和价值序列,然后将新的重量序列和价值序列作为输入调用0-1背包算法即可解决部分背包问题。(详细思想可以参考背包九讲中的内容)。
三、实验数据
(1)三个算法的实验数据输入均为:
a) 物品的个数n
b) 每个物品的价值v1,v2……vn
c) 每个物品的重量w1,w2……wn
d) 背包的最大重量W
(2)输出均为:
当前选择的方案所能获得的最大价值
在本实验对三个算法测试中,将取背包最大容量W为100,物品的个数n为5,所有物品组成的价值序列{v}为{60,100,120,80,90},重量序列{w}为{10,20,30,40,5}。将这些数据依次输入到各个算法中进行测试。
四、实验结果
贪心算法实现0-1背包算法:
动态规划实现部分背包:
五、实验源码
见附件。
一个任务调度问题
一、问题描述
在单处理器上具有期限和惩罚的单位时间任务调度问题。
二、算法原理
任务调度问题就是给定一个有穷单位时间任务的集合S,集合S中的每个任务都有一个截止期限di和超时惩罚wi,需要找出集合S的一个调度,使得因任务误期所导致的总惩罚最小,这个调度也称为S的一个最优调度。
实现任务的最优调度主要就是利用贪心算法中拟阵的思想。如果S是一个带期限的单位时间任务的集合,且I是所有独立的任务集构成的结合,则对应的系统M=(S,I)是一个拟阵。利用拟阵解决任务调度问题的算法原理主要就是将最小化迟任务的惩罚之和问题转化为最大化早任务的惩罚之和的问题,也就是说在任务调度的时候优先选择当前任务序列中惩罚最大的任务。这里,假设集合A存放任务的一个调度。如果存在关于A中任务的一个调度,使得没有一个任务是迟的,称任务集合A是独立的。实现该问题的贪心算法如下:
A <- Ø
Sort S[M] into monotonically decreasing order by w
for each x∈S[M]
do if AU{x} ∈ I[M]
then A <- AU{x}
初始时A为空集,然后对S中的所有任务按惩罚大小降序排序,然后进入循环。循环的过程中依次访问S的中所有任务,然后进行独立性检查,如果当前任务x加入集合A之后A依然是独立的,则将A置为AU{x},否则检查下一个任务直到扫描结束,最后所得到的集合A即是一个最优子集,然后进行相应的调整得到最后的输出。
三、实验数据
(1)任务调度问题的输入:
a) 任务的数量n,即表示当前任务集合S={a1,a2,a3……an};
b) n个任务的期限d1,d2,d3……dn,每个任务的di满足1≤ di ≤n,且任务要求ai在di之前完成;
c) n个任务的权(惩罚)w1,w2,w3……wn。表示任务ai如果没在时间di之前完成,则导致惩罚wi,如果任务在期限之前完成,则没有惩罚; 同时在本实验中,还会将每个wi值替换为max{w1,w2,w3……wn}-wi,并运行算法进行第二次实验,然后比较两次实验所得结果。
在本次实验中,n取值为7,每个任务的期限为4,2,4,3,1,4,6,对应的惩罚为70,60,50,40,30,20,10。
(2)任务调度问题的输出:
a) 贪心算法所选的任务以及放弃的任务,所选的任务即表示最终会在期限内完成的任务。放弃的任务表示最终会因误期而受到惩罚的任务。
b) 最终的最优调度序列
c) 最优调度所带来的总的惩罚数
四、实验截图
贪心算法和替换后的选择及调度结果:
五、实验源码
见附件。
常见平衡树的实现与比较
一、问题描述
实现3种树中的两种:红黑树,AVL树,Treap树
二、算法原理
(1)红黑树
红黑树是一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是red或black。红黑树满足以下五个性质:
1) 每个结点或是红色或是黑色
2) 根结点是黑色
3) 每个叶结点是黑的
4)如果一个结点是红的,则它的两个儿子均是黑色
5) 每个结点到其子孙结点的所有路径上包含相同数目的黑色结点
本实验主要实现红黑树的初始化,插入和删除操作。当对红黑树进行插入和
删除操作时,可能会破坏红黑树的五个性质。为了保证红黑树的性质,需要进行修改颜色和指针结构等。指针结构的修改是通过旋转操作完成。因此这里首先描述红黑树的旋转操作。
A.旋转操作
旋转操作主要分为左旋和右旋操作。当在某个节点x上进行左旋操作时,假设x的右孩子y不是nil[T],左旋以x到y之间的链为轴进行,使得y成为该子树新的根,x成为y的左孩子,而y的左孩子则成为x的右孩子。右旋类似。
B.插入操作
红黑树的插入操作主要有两个步骤。
a) 首先以二叉查找树的方式插入一个红色结点;
b) 然后对新的二叉树进行修复,即进行颜色调整或旋转操作,以此满足红
黑树的五个性质。
在对红黑树进行插入修复操作时,循环进入条件是当前结点的父结点为红色
即性质4破坏。在修复函数中主要考虑以下三种情况:
情况1:插入结点z的父结点为红色,而z的叔叔结点y是红色
此时z的父结点的父结点一定存在,而且必为黑色结点,否则插入前即不是红黑树。这种情况的主要思路就是修改z的父结点和叔叔结点y的颜色为黑色,z的祖父结点的颜色修改为红色,目的就是红色上移,然后当前结点指针指向祖父结点,循环进行修复操作
情况2:z的父结点为红色,而z的叔叔结点y是黑色,而且z是右孩子
此时将当前节点的z的父结点设为新的当前节点,并以新当前节点为支点左旋。将情况2转化为情况3。
情况3:z的父结点为红色,而z的叔叔结点y是黑色,而且z是左孩子
此时将z的父节点修改为黑色,祖父节点变为红色,并以祖父节点为支点进行右旋操作。此时一行中不再有两个连续的红色结点,红黑树所有性质调整完成,因此所有的处理到此完毕。
C.删除操作
类似插入,红黑树的删除操作主要有两个步骤。
c) 首先以二叉查找树的方式删除一个指定结点;
d) 然后对新的二叉树进行修复,即进行颜色调整或旋转操作,以此满足红
黑树的五个性质。
在对红黑树进行删除修复操作时,循环进入条件是当前结点x为非根结点并
且是双重黑结点。在修复函数中主要考虑以下四种情况:
情况1:x的兄弟结点w是红色的
此时需把父结点修改成红色,把兄弟结点w修改成黑色。然后,针对父结点进行左旋操作,此时红黑树性质5不变。将情况1转化为其他情况。
情况2:x的兄弟w是黑色的,而且w的两个孩子都是黑色的
此时需从当前结点x和兄弟节点w去掉一重黑色,从而x只有一重黑色而w是红色。同时为了补偿去掉的一重黑色,需在x的父结点上增加一重黑色,并把x的父结点作为新的当前节点,重新进入循环。
情况3:x的兄弟w是黑色的,w的左孩子是红色的,右孩子是黑色的
此时需把兄弟结点w修改成红色,兄弟w的左孩子修改成黑色,然后以兄弟结点w作为支点进行右旋操作,之后重新进入算法。将情况5转化为情况6。
情况4:x的兄弟w是黑色的,而且w的右孩子是红色的
此时需将兄弟结点w修改成x的父结点的颜色,然后将x的父结点修改成黑色,兄弟结点w的右孩子修改成黑色,然后以x的父结点为支点进行左旋操作。此时算法结束,红黑树所有性质调整完成。
D.计算根结点到叶结点所有路径对应的黑高度
对红黑树进行插入或删除结点时,为了能够保持红黑树的性质会进行对应的修复操作。为了能更直观的测试经过该修复操作后红黑树的性质是否正确的得到满足,在本次实验的结果中将依次输出根结点到叶结点的所有路径并输出该路径上所有黑色结点的个数。因此需要实现一个输出路径并计算对应黑高度的方法。
该方法的主要原理就是利用递归思想,在函数内部维护一个记录路径结点的数组path[]。每访问一个结点时,首先判断该结点是否为哨兵结点。若为哨兵,则表示该路径已扫描结束。此时需把路径输出,并输出对应黑高度。若该结点不是哨兵结点,此时需把该结点关键字值记录到path[]数组中。同时如果该结点为黑色,则将表示黑高度的变量值加1.做完这些操作之后,则一次递归调用该结点的左右子树。实现该操作的主要代码如下:
E.打印树的结构
在对红黑树进行插入或删除操作之后,为了能够更加直观的观察当前红黑树的特点,本实验将在结果中输出当前红黑树的结构。实现该操作的主要原理就是利用树的结构特点,优先打印出右子树,然后打印根结点值和该结点的颜色,最后打印出左子树,在函数中通过空格来控制最后的显示效果。主要代码如下:
(2)AVL树
AVL树是一种高度平衡的二叉查找树。它或者是一颗空树,或者是具有下列性质的二叉树:它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。在AVL树中的每个结点都有一个平衡因子bf,它表示这个结点的左、右子树的高度差,即左子树的高度减去右子树的高度。AVL树上所有结点的平衡因子只能是-1、0、1。
实现AVL树的关键在于维持树的平衡性。每当进行插入或删除操作时都有可能破坏了树的平衡性。因此首先检查是否破坏了树的平衡性,如果因插入结点而破坏了二叉查找树的平衡,则找出离插入点最近的不平衡结点,然后将该不平衡结点为根的子树进行旋转操作。旋转操作主要分为:LL型、RR型、LR型、RL型等。
A.旋转操作
a) LL型
由于在A的左子树根结点B的左子树上插入结点,使A的平衡因子由1增至2而失去平衡。故需进行一次向右顺时针旋转操作。 即将A的左孩子B向右上旋转代替A作为根结点,A向右下旋转成为B的右子树的根结点。而原来B的右子树则变成A的左子树。
b) RR型
由于在A的右子树根结点B的右子树上插入结点,使A的平衡因子由-1增至-2而失去平衡。故需进行一次向左顺时针旋转操作。
c) LR型
由于在A的左子树根结点B的右子树上插入结点,使A的平衡因子由1增至2而失去平衡。故需进行两次旋转操作,先左旋再右旋。
d) RL型
由于在A的右子树根结点B的左子树上插入结点,使A的平衡因子由-1增至-2而失去平衡。故需进行两次旋转操作,先左旋再右旋。
B.插入操作
在AVL树上插入一个新的数据元素e的递归算法可描述如下:
(1)若BBST为空树,则插入一个数据元素为e的新结点作为BBST的根结点,树的深度增1;
(2)若e的关键字和BBST的根结点的关键字相等,则不进行;
(3)若e的关键字小于BBST的根结点的关键字,而且在BBST的左子树中不存在和e有相同关键字的结点,则将e插入在BBST的左子树上,并且当插入之后的左子树深度增加(+1)时,分别就下列不同情况处理之:
a、BBST的根结点的平衡因子为-1(右子树的深度大于左子树的深度,则将根结点的平衡因子更改为0,BBST的深度不变;
b、BBST的根结点的平衡因子为0(左、右子树的深度相等):则将根结点的平衡因子更改为1,BBST的深度增1;
c、BBST的根结点的平衡因子为1(左子树的深度大于右子树的深度):若BBST的左子树根结点的平衡因子为1:则需进行单向右旋平衡处理,并且在右旋处理之后,将根结点和其右子树根结点的平衡因子更改为0,树的深度不变;
(4)若e的关键字大于BBST的根结点的关键字,而且在BBST的右子树中不存在和e有相同关键字的结点,则将e插入在BBST的右子树上,并且当插入之后的右子树深度增加(+1)时,分别就不同情况处理之。
C.删除操作
假设被删结点x的父结点为y, 如果删除的是y的左子树, 则y的平衡因子bf减1, 否则y的平衡因子bf加1. 根据y的平衡因子的值分为以下三种情况:
情况一:
如果结点y的新的平衡因子bf等于0,则表示删除前y的平衡因子等于1 或者-1, 此时高度减1, 需改变父节点和其他祖父节点的平衡因子
情况二:
如果结点y的新的平衡因子bf等于1或者-1,则表示删除前y的平衡因子等于0, 左右子树高度一样, 此时高度不变, 需改变父节点和其他某些祖父节点的平衡因子.
情况三:
如果结点y新的平衡因子bf等于-2或2, 则表示删除前y的平衡因子1等于或-1, 左右子树高度不相同, 此时树在结点y是不平衡的, 需要做平衡化处理
三、实验数据
(1)输入:
红黑树实验和AVL树实验采用的数据均是随机生成的20个关键字值。然后依次将这些关键字利用插入算法插入到相应树中。在进行删除操作时,删除的数据是随机选取的3个关键字值,然后依次删除对应结点。
(2)输出:
对于红黑树,在进行插入和删除操作后,将输出对应树的结构,并标明结点的颜色(red或black),然后计算根结点到叶结点所有路径的黑高度并依次输出显示。
四、实验截图
平衡树操作:
红黑树操作:
Treap Tree操作:
五、性能分析
红黑树并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。
红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构,能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。
当然,红黑树并不适应所有应用树的领域。如果数据基本上是静态的,那么让他们待在他们能够插入,并且不影响平衡的地方会具有更好的性能。如果数据完全是静态的,做一个哈希表,性能可能会更好一些。
红黑树是一个更高效的检索二叉树,因此常常用来实现关联数组。典型地,JDK 提供的集合类 TreeMap 本身就是一个红黑树的实现。
TreeMap 和 TreeSet 是 Java Collection Framework 的两个重要成员,其中 TreeMap 是 Map 接口的常用实现类,而 TreeSet 是 Set 接口的常用实现类。虽然 HashMap 和 HashSet 实现的接口规范不同,但 TreeSet 底层是通过 TreeMap 来实现的,因此二者的实现方式完全一样。而 TreeMap 的实现就是红黑树算法。
对于 TreeMap 而言,由于它底层采用一棵“红黑树”来保存集合中的 Entry,这意味这 TreeMap 添加元素、取出元素的性能都比 HashMap 低:当 TreeMap 添加元素时,需要通过循环找到新增 Entry 的插入位置,因此比较耗性能;当从 TreeMap 中取出元素时,需要通过循环才能找到合适的 Entry,也比较耗性能。
但 TreeMap、TreeSet 比 HashMap、HashSet 的优势在于:TreeMap 中的所有 Entry 总是按 key 根据指定排序规则保持有序状态,TreeSet 中所有元素总是根据指定排序规则保持有序状态。
为了保证平衡,AVL树中的每个结点都有一个平衡因子(balance factor,以下用BF表示),它表示这个结点的左、右子树的高度差,也就是左子树的高度减去右子树的高度的结果值。AVL树上所有结点的BF值只能是-1、0、1。反之,只要二叉树上一个结点的BF的绝对值大于1,则该二叉树就不是平衡二叉树。下图演示了平衡二叉树和非平衡二叉树。
从1这点来看红黑树是牺牲了严格的高度平衡的优越条件为代价红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高.
六、实验代码
见附件。