面向对象的数据结构 – 以 Heap 为例
之前学数据结构时都是使用 C/C++ 实现,习惯了面向过程的方式再使用面向对象还有一点点不知所措。
借着 操作系统短进程优先算法 的机会第一次尝试使用 Java 实现了一个简单的数据结构。
这里实现的 Heap 只包含了最基础的排序,没有添加任意删除等操作。
Motivation
在此之前,有个还没有公开发表的 flag,想在这学期结束前后写一个运行在 C51/STM32 上的简易的操作系统(尽管这学期才过了四分之一,但我已经可以预见这个 flag 要鸽了),因此最初的选择就是用 C 写。
可经过复习后发现想要实现操作系统级的调度算法有些许困难,如果只是实现算法思想,语言只是工具平台而已。用 C 当然可以,想着写习惯了面向过程还是应该试一下面向对象的方式。
Realization
Heap 的实现还是很简单的,这里整理了一下以前的笔记:
定义
堆是一颗完全二叉树,树中的每一个结点都大于(小于)等于其父亲节点的值,由此引出两种堆:
- 大根堆:父亲结点的值大于或等于左右节点
- 小根堆:父亲结点的值小于或等于左右结点
存储方式
由于堆是一个完全二叉树,可以使用数组进行存储,减少空间的浪费。(但也可以使用链式存储)
因为 Java 的链式存储没有写过,这里直接使用了 ArrayList
替代数组进行存储
维护
up 和 down 是两个基础的维护操作,指的是元素在树中的深度变化。两个操作的最坏时间复杂度均为$O(log h)$
在 up 和 down 操作里的 swap
和 compare
是两个自定义的方法,这里的 PCB 是一个自定义的对象,直接沿用的操作系统实验中的代码片段,具体定义可以参考这里:Operating Systems: Lab 1 SPF(Shortest-Process-First)
public void swap(int u, int v) { PCB Temp = heap.get(u); heap.set(u, heap.get(v)); heap.set(v, Temp); }
public int compare(int u, int v) { PCB pcb1 = this.heap.get(u); PCB pcb2 = this.heap.get(v); if (pcb1.getRemaining() == pcb2.getRemaining()) { if(pcb1.getQueueID() > pcb2.getQueueID()) { return 1; }else{ return -1; } } if (pcb1.getRemaining() > pcb2.getRemaining()) { return 1; } if (pcb1.getRemaining() < pcb2.getRemaining()) { return -1; } System.out.println("Error, please check the method!"); return 0; }
up操作
up操作实际上就是对比自己与父节点的大小,从而进行交换,其中使用while或递归进行书写,可以保证把当前结点调整到合适的位置。
public void up(int point) { while (point != 0 && (this.compare(point, point / 2) < 0)) { this.swap(point, point / 2); point /= 2; } }
down操作
down操作实际上就是用自己和两个子节点进行比较,选择较大的子节点然后和父亲结点交换,其中使用while或者递归的方式书写,可以保证结点被放到合适的位置。
public void down(int point) { int size=heap.size()-1; while(point * 2 <= size) { int temp = point; if(point * 2 <= size && (this.compare(point, point * 2) > 0)) { temp = 2 * point; } if(point * 2 + 1 <= size && (this.compare(temp,point * 2 + 1) > 0)) { temp = 2 * point + 1; } if(point != temp) { this.swap(point,temp); point = temp; }else break; } }
基本操作
Heap 的操作其实也不算少,这里只根据实际实现了三个:
建堆与插入
想要使用数组维护 Heap,需要让数组的下标从 1 开始,对于 ArrayList
来说需要预先插入一个空节点,这里在 Constructor 中进行了处理。
Heap() { heap = new ArrayList<PCB>(); PCB emptyPCB = new PCB(-1); heap.add(emptyPCB); order = 0; }
之后的建堆过程是通过不断的插入实现的。
public void push(PCB newNode) { newNode.setQueueID(order); heap.add(newNode); order++; this.up(heap.size() - 1); }
查询最值
由于堆的性质,查找最小(最大)值只需要$O(1)$的操作
public PCB top() { if(this.empty()) return null; else return heap.get(1); }
删除最值
删除时需要三个操作维护堆的合法性:
- 将最后一个元素赋值给堆顶元素
- 将最后一个元素删除
- 对堆顶元素进行
down()
操作
public void pop() { int size = heap.size(); heap.set(1,heap.get(size - 1)); heap.remove(size-1); down(1); }
Thinking
从抽象到更抽象,面向对象的思想确实是一个巨大的进步,受限于个人水平,在实现中还带有一些面向过程的风格,技术上还未涉及泛型、接口等内容,再次回忆 C++ 的 STL,也是从 具体 到 抽象 的过程,从学习的角度在一定阶段是要避免使用的,但是在实际的解决问题中 抽象/面向对象 的思的的确确为我们提供了便利。
Code
import java.util.*; public class Heap { ArrayList<PCB> heap; private static int order; /* * Initialize the heap. * In order make the index of first process is one, * we need use the constructor to push 'empyt process' to the heap. */ Heap() { heap = new ArrayList<PCB>(); PCB emptyPCB = new PCB(-1); heap.add(emptyPCB); order = 0; } /* * Swap the element in ArrayList. */ public void swap(int u, int v) { PCB Temp = heap.get(u); heap.set(u, heap.get(v)); heap.set(v, Temp); } /* * Compare the element in ArrayList. * The first keyword is remaining time, * The second keyword is queue id. * * Queue id is the order of the element push the heap. * * As we know that remaining time may be the same, but queueID is unique. * So according to the queueID to compare can always be true. */ public int compare(int u, int v) { PCB pcb1 = this.heap.get(u); PCB pcb2 = this.heap.get(v); if (pcb1.getRemaining() == pcb2.getRemaining()) { if(pcb1.getQueueID() > pcb2.getQueueID()) { return 1; }else{ return -1; } } if (pcb1.getRemaining() > pcb2.getRemaining()) { return 1; } if (pcb1.getRemaining() < pcb2.getRemaining()) { return -1; } System.out.println("Error, please check the method!"); return 0; } /* * We need two operations to make the heap is legal. * The purpose of up() is make the lesser element to upper layer. * The purpose of down() is make the bigger element to lower layer. */ public void up(int point) { while (point != 0 && (this.compare(point, point / 2) < 0)) { this.swap(point, point / 2); point /= 2; } } public void down(int point) { int size=heap.size()-1; while(point * 2 <= size) { int temp = point; if(point * 2 <= size && (this.compare(point, point * 2) > 0)) { temp = 2 * point; } if(point * 2 + 1 <= size && (this.compare(temp,point * 2 + 1) > 0)) { temp = 2 * point + 1; } if(point != temp) { this.swap(point,temp); point = temp; }else break; } } /* * There is some common method of data structure. */ public void push(PCB newNode) { newNode.setQueueID(order); heap.add(newNode); order++; this.up(heap.size() - 1); } public PCB top() { if(this.empty()) return null; else return heap.get(1); } public void pop() { int size = heap.size(); heap.set(1,heap.get(size - 1)); heap.remove(size-1); down(1); } public boolean empty() { if(heap.size() > 1) { return false; }else{ return true; } } public int size() { return heap.size(); } }
评论