【算法】从内部快排进阶外部快排
文章目录
1. Problem decription
实现外部快速排序算法,并完成如下实验要求:
- 用大小堆实现双端优先队列
- 用不同的数据进行测试
- IO计数
2. Algorithm idea
2.1 内部快速排序算法
我们之前已经学过内部快速排序的实现:
- 在待排序的数列中,我们首先要找一个数字作为基准数
- 接下来我们需要把这个待排序的数列中小于基准数的元素移动到待排序的数列的左边,把大于基准数的元素移动到待排序的数列的右边。这时,左右两个分区的元素就相对有序了
- 接着把两个分区的元素分别按照上面两种方法继续对每个分区找出基准数,然后移动,直到各个分区只有一个数时为止。
2.2 内部快速排序算法修改思路
而对于外部快排而言,有些许不同。首先,基准不再是一个数,而应该是一组有序数;其次,算法不再支持交换挪动操作,那样对IO极其不友好。而且,对于快排而言,为了使IO友好,每次读取数据皆以Buffer为单位。
因此,算法的基本实现步骤如下:
- 从外存输入数据到inputBuffer中,每当取空时重新读取
- 从inputBuffer中取出数据record放入Middle group中,Middle group由一个双端优先队列组成,可以得到最大值和最小值。
2.2 双端优先队列实现思路
为了简化难度,本题的双端优先队列我采取了用最大最小堆即两个优先队列组合实现。一个优先队列从小到大排序,一个优先队列从大到小排序。需要注意的是,在pop的时候要保证数据的同步性,要确保两个优先队列中的数据一致。
3. Function module design
3.1 Buffer类实现
为了存取方便input, small, large buffer类的实现都采取维护一个队列的策略。
同时,为了使排序算法实现更加清晰,Buffer类提供了代理函数——每当想向Buffer中取值却空时自动读取Disk;每当Buffer存满时自动写入Disk。当然,为了实现如下操作,需要用smallPtr和largePtr记录当前写入的两个指针位置,以提供下次书写。
public class Buffer{
private Queue<Integer> buffer;
private int size;
private int cur;
public int smallPtr;
public int largePtr;
private int readCount;
private int writeCount;
Buffer(int size){
readCount=0;
writeCount=0;
cur=0;
this.size=size;
buffer=new LinkedList<>();
}
boolean isEmpty(){
return (cur==0);
}
boolean isFilled(){
return (cur==size);
}
int pollOne(){
if(isEmpty()){
while(!isFilled()&&readPtr<toSortNumber){
addOneToSmall(disk[readPtr]);
readCount++;
readPtr++;
}
}
cur--;
return buffer.poll();
}
void addOneToLarge(int x){
if(isFilled()){
while(!isEmpty()){
temp[largePtr--]=pollOne();
writeCount++;
}
}
cur++;
buffer.add(x);
}
void addOneToSmall(int x){
if(isFilled()){
while(!isEmpty()){
temp[smallPtr++]=pollOne();
writeCount++;
}
}
cur++;
buffer.add(x);
}
}
3.2 Disk类实现
由于3个Buffer需要和Disk紧密数据交互,为了实现方便,我直接将Disk中的核心成员变量设置成了public static类型。
不难发现,为了降低难度,在本实验中我直接用一个array变量 int[ ] disk来模拟外存,这并不影响IO计数和实验结果分析。
public class Disk {
public static int[] disk;
public static int[] temp;
public static int toSortNumber;
public static int readPtr;
Disk(int toSortNumber){
this.toSortNumber=toSortNumber;
disk=new int[toSortNumber];
temp=new int[toSortNumber];
initialToSortArray();
System.out.println("-----------initial------------");
printDisk();
System.out.println("------------------------------");
}
void initialToSortArray(){
Random r=new Random();
for(int i=0;i< disk.length;i++){
disk[i]=r.nextInt(100);
}
}
public static void printDisk(){
for(int i=0;i<disk.length;i++){
System.out.print(disk[i]+" ");
if((i+1)%10==0){
System.out.println();
}
}
}
}
3.3 DoubleEndedPriorityQueue类实现
如 2.3 章节所写,在代码实现上,我直接利用了Java.util库中的数据结构PriorityQueue,用两个优先队列组合成一个双端优先队列,再写出 add( ), poll( ) 等函数对双端优先队列的操作进行简单封装保证两个优先队列的数据的一致性。
public class DoubleEndedPriorityQueue {
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer num1, Integer num2) {
return num1 >= num2 ? -1 : 1;
}
};
int size;
int cur;
private PriorityQueue<Integer> maxHeap;
private PriorityQueue<Integer> minHeap;
DoubleEndedPriorityQueue(int size){
maxHeap= new PriorityQueue<Integer>(comparator);
minHeap=new PriorityQueue<>();
this.size=size;
this.cur=0;
}
boolean isFilled(){
return (cur==size);
}
boolean isEmpty(){
return (cur==0);
}
int getMax(){
if(isEmpty()){
try {
throw new Exception("dq is empty!");
} catch (Exception e) {
e.printStackTrace();
}
}
return maxHeap.peek();
}
int getMin(){
if(isEmpty()){
try {
throw new Exception("dq is empty!");
} catch (Exception e) {
e.printStackTrace();
}
}
return minHeap.peek();
}
void add(int x){
if(isFilled()){
try {
throw new Exception("dq is filled!");
} catch (Exception e) {
e.printStackTrace();
}
}
maxHeap.add(x);
minHeap.add(x);
cur++;
}
int pollMax(){
if(isEmpty()){
try {
throw new Exception("dq is empty!");
} catch (Exception e) {
e.printStackTrace();
}
}
cur--;
int max=maxHeap.peek();
minHeap.remove(max);
return maxHeap.poll();
}
int pollMin(){
if(isEmpty()){
try {
throw new Exception("dq is empty!");
} catch (Exception e) {
e.printStackTrace();
}
}
cur--;
int min=minHeap.peek();
maxHeap.remove(min);
return minHeap.poll();
}
}
3.4 外部快速排序算法实现
void quickSort(int low,int high){
readPtr=low;
small.setSmallPtr(low);
large.setLargePtr(high-1);
if(high-low<depqSize){
for(int i=low;i<high;i++){
depq.add(disk[i]);
readPtr++;
}
for(int i=low;i<high;i++){
disk[i]=depq.pollMin();
}
}
else{
for(int i=low;i<depqSize+low;i++){
depq.add(disk[i]);
}
int ptr=low+depqSize;
while(ptr!=high){
//read data from disk
int i=input.pollOne();
int max=depq.getMax();
int min=depq.getMin();
if(i>=max){
large.addOneToLarge(i);
}
if(i<=min){
small.addOneToSmall(i);
}
if(i<max&&i>min){
large.addOneToLarge(depq.pollMax());
depq.add(i);
}
ptr++;
}
int s=small.getSmallPtr();
while(!depq.isEmpty()){
temp[s++]=depq.pollMin();
}
quickSort(low,small.getSmallPtr());
quickSort(large.getLargePtr(),high);
}
}
4. Test result and analysis
5. Project summary
本实验实现了新颖的外部快速排序算法,从前只知道外部归并算法,但是并不知道快速排序也可以通过变化应用到外部排序中去。此题与第一题的思路一脉相承,先从内部实现开始,当需要IO操作的时候,直接用代理函数自动更新读取,这将大大地降低思维难度。这其实也和程序实现原理类似,对于运行在操作系统上的程序来说,没有内存和外存,外存直接虚拟化为内存,对外存的数据直接由操作系统代理,而这也是我的思路的出发点。