引言
在上一篇博文中,尝试实现了二叉堆的结构。在本篇博文中,将建立在堆的基础之上,讨论如何用堆实现排序。二叉堆的代码直接引用昨天的实现源码,在代码的基础上做一些修改使其变成堆排序。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290
二叉堆源码
如果对二叉堆不是很了解,可以查阅相关资料,或者阅读一下我写的这篇文章《java实现-堆》,地址是:http://blog.csdn.net/u012403290/article/details/71126768
下面是上篇博文二叉堆的实现源码:
package com.brickworkers;
public class Heap<T extends Comparable<? super T>> {
private static final int DEFAULT_CAPACITY = 10; //默认容量
private T[] table; //用数组存储二叉堆
private int size; //表示当前二叉堆中有多少数据
public Heap(int capactiy){
this.size = 0;//初始化二叉堆数据量
table = (T[]) new Comparable[capactiy + 1];//+1是因为我们要空出下标为0的元素不存储
}
public Heap() {//显得专业些,你就要定义好构造器
this(DEFAULT_CAPACITY);
}
//插入
public void insert(T t){
//先判断是否需要扩容
if(size == table.length - 1){
resize();
}
//开始插入
//定义一个预插入位置下标
int target = ++size;
//循环比较父节点进行位置交换
for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){
table[target] = table[target/2];//如果满足条件,那么两者交换,知道找到合适位置(上滤)
}
//插入数据
table[target] = t;
print();
}
//删除最小
//删除过程中,需要重新调整二叉堆(下滤)
public void deleteMin(){
if(size == 0){
throw new IllegalAccessError("二叉堆为空");
}
//删除元素
table[1] = table[size--];
int target = 1;//从顶部开始重新调整二叉堆
int child;//要处理的节点下标
T tmp = table[ target ];
for( ; target * 2 <= size; target = child )
{
child = target * 2;
if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
child++;
}
if( table[ child ].compareTo( tmp ) < 0 ){
table[ target ] = table[ child ];
table[child] = null;
}
else{
break;
}
}
table[ target ] = tmp;
print();
}
//如果插入数据导致达到数组上限,那么就需要扩容
private void resize(){
T [] old = table;
table = (T []) new Comparable[old.length*2 + 1];//把原来的数组扩大两倍
for( int i = 0; i < old.length; i++ )
table[ i ] = old[ i ]; //数组进行拷贝
}
//打印数组
private void print(){
System.out.println();
for (int i = 1; i <= size; i++) {
System.out.print(table[i] + " ");
}
System.out.println("二叉堆大小:"+size);
}
public static void main(String[] args) {
Heap<Integer> heap = new Heap<>();
//循环插入0~9的数据
for (int i = 0; i < 10; i++) {
heap.insert(i);
}
//循环删除3次,理论上是删除0,1,2
for (int i = 0; i < 3; i++) {
heap.deleteMin();
}
}
}
思考
因为二叉堆中在堆顶,要么是最大值,要么是最小值。我们每次进行一次deleteMin(deleteMax)就可以把这个数据存储起来,一直到二叉堆删除结束。那么,我们考虑两个方面:①如何把数据放入二叉堆;②如何把排好序的数据提取出来。
对于第一个问题,如何把数据放入二叉堆,最简单的就是可以调用二叉堆的insert方法,把要排序的数据一个个放入其中,或者优雅一些,我们在源码中新增一个构造函数,直接入参一个数组,直接转化成二叉树。那或许有的小伙伴又要问了,这个优雅不优雅有必要吗?构造函数里面进行insert操作?不!我们不在构造函数中调用insert操作(但是在源码中,我们还是要进行比较),我们可以直接把所有数据全部插入,然后在把它整个调整成二叉堆。在后面的代码中,我们将比较这两者的优劣。
对于第二个问题,其实也有两种解决办法,第一种是我们创立一个新的数组,这个数组核心功能就是接收二叉堆删除的数据,这样一来,直到二叉堆删除结束,那么这个数组就是排序好了的数据,实现了数据存储和提取。第二种,就显得要高明一点点,我们试想,在二叉堆的实现过程中,我们本身就是用数组来实现的。而且,在上篇博文中,我们讨论它的删除的时候,最后一个元素的位置肯定会变动,所以,这种情况下,我们完全可以在二叉堆删除堆顶元素的时候,把删除的数据存储在最后一个元素的位置。这么一来在二叉堆删除结束的时候,那么这个原本表示二叉堆的数组已经是有序的了。
新增一个接收数组的构造函数
以下的所有代码,都是建立在上述的二叉堆源码上进行修改的。
1、接收数组的第一种方式:
//实现方式一,直接用insert来实现数据放入二叉堆中
public Heap(T[] array){
super();
for (T t : array) {
insert(t);//直接调用二叉堆的insert方法进行插入
}
}
第二种方式,我们先把数据全部随意插入到数组中,然后把数组调整成二叉堆。如何调整呢?在上一篇博文中,我们在删除的情况下,我们需要通过下滤的方式从新调整二叉堆,其实思想是一样的,只是删除的时候调整从最上层开始,而在这里我们从最下层(标准来说是倒数第二层)开始。下图表达了操作过程:
在源码中,我把删除节点后面的重新调整二叉堆抽离了出来:
private void dealHeap(int target) {
int child;//要处理的节点下标
T tmp = table[ target ];
for( ; target * 2 <= size; target = child )
{
child = target * 2;
if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
child++;
}
if( table[ child ].compareTo( tmp ) < 0 ){
table[ target ] = table[ child ];
table[child] = null;
}
else{
break;
}
}
table[ target ] = tmp;
}
2、下面就是第二种实现放置,把数据全部放进去然后调整二叉堆:
public Heap( T [ ] array )
{
size = array.length;
array = (T[]) new Comparable[ size + 2 ];//存储的数组要略微长
for (int i = 0; i < array.length; i++) {
table[ i++ ] = array[i];
}
//到这里为止,数据已经全部都放入了table中
//接下来要做的就是要把这些数据转变成二叉堆
for(int i = size/2; i > 0; i--){//从倒数第一个有儿子的节点开始处理
dealHeap(i);
}
}
新增有序数据存储
1、第一种实现,我们在二叉堆中新增一个resultTable,接收每次删除的数据:
//成员变量
private T[] resultTable;//用于存储有序数据(每次删除的数据就放入其中)
//修改构造函数,确认好存储数组的长度
resultTable = (T[]) new Comparable[ size ];//指定存储数组大小
//在resultTable末端添加数据
//把数据插入到resultTable末端
private void insertResult(T t){
resultTable[resultTable.length - size] = t;
}
//在delateMin方法中,把insertResult方法加入其中,使得每次删除的时候,就把数据移除出来
2、不新增数组直接把要删除的数据放到二叉堆数组的最末端,为什么可以如此做,在思考模块已经表述过了。
//删除最小
//删除过程中,需要重新调整二叉堆(下滤)
public void deleteMin(){
if(size == 0){
throw new IllegalAccessError("二叉堆为空");
}
// //把删除的元素放入resultTable中
// insertResult(table[1]);
//把要删除的数据先存储起来
T temp = table[1];
//删除元素
table[1] = table[size--];
//把要删除的数据放到末尾
table[size -- ] = temp;
int target = 1;//从顶部开始重新调整二叉堆
dealHeap(target);
print();
}
这样一来,最后打印的时候,就可以直接打印二叉堆数组,不过这个时候的二叉堆不存在了。
效率比较
在上面所有的描述中,存在许多情况,接下来我们对他们进行比较,看看哪种效率会更高:
1、比较是把目标排序数组一个个insert块还是直接放入数组在调整二叉堆快
在测试过程中,我们保证存储数据一致,都采用额外的数组存储。
下面是我比较的源码和结果:
package com.brickworkers;
public class Heap<T extends Comparable<? super T>> {
private static final int DEFAULT_CAPACITY = 10; //默认容量
private T[] table; //用数组存储二叉堆
private T[] resultTable;//用于存储有序数据(每次删除的数据就放入其中)
private int size; //表示当前二叉堆中有多少数据
public Heap(int capactiy){
this.size = 0;//初始化二叉堆数据量
table = (T[]) new Comparable[capactiy + 1];//+1是因为我们要空出下标为0的元素不存储
}
public Heap() {//显得专业些,你就要定义好构造器
this(DEFAULT_CAPACITY);
}
//实现方式二,直接先插入,然后对数据进行整理成二叉堆
public Heap( T [ ] array , boolean methodType){
if(methodType){
size = 0;
table = (T[]) new Comparable[DEFAULT_CAPACITY + 1];
//实现方式一,直接用insert来实现数据放入二叉堆中
long insertStratTime = System.currentTimeMillis();
for (T t : array) {
insert(t);
}
System.out.println("直接插入的方式耗时:"+(System.currentTimeMillis() - insertStratTime));
}else{
size = array.length;
table = (T[]) new Comparable[ size + 2 ];//存储的数组要略微长
long dealHeapStratTime = System.currentTimeMillis();
for (int i = 0; i < array.length; i++) {
table[ i+1] = array[i];//arr[0]位置不存放数据
}
//到这里为止,数据已经全部都放入了table中
//接下来要做的就是要把这些数据转变成二叉堆
for(int i = size/2; i > 0; i--){//从倒数第二层开始处理
dealHeap(i);
}
System.out.println("随机插入重新调整的方式耗时:"+(System.currentTimeMillis() - dealHeapStratTime));
}
resultTable = (T[]) new Comparable[ size ];//指定存储数组大小
}
//插入
public void insert(T t){
//先判断是否需要扩容
if(size == table.length - 1){
resize();
}
//开始插入
//定义一个预插入位置下标
int target = ++size;
//循环比较父节点进行位置交换
for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){
table[target] = table[target/2];//如果满足条件,那么两者交换,知道找到合适位置(上滤)
}
//插入数据
table[target] = t;
print();
}
//删除最小
//删除过程中,需要重新调整二叉堆(下滤)
public void deleteMin(){
if(size == 0){
throw new IllegalAccessError("二叉堆为空");
}
// //把删除的元素放入resultTable中
insertResult(table[1]);
//把删除的元素放到末尾
// T temp = table[1];
//删除元素
table[1] = table[size--];
// table[size -- ] = temp;
int target = 1;//从顶部开始重新调整二叉堆
dealHeap(target);
print();
}
private void dealHeap(int target) {
int child;//要处理的节点下标
T tmp = table[ target ];
for( ; target * 2 <= size; target = child )
{
child = target * 2;
if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
child++;
}
if( table[ child ].compareTo( tmp ) < 0 ){
table[ target ] = table[ child ];
table[child] = null;
}
else{
break;
}
}
table[ target ] = tmp;
}
//如果插入数据导致达到数组上限,那么就需要扩容
private void resize(){
T [] old = table;
table = (T []) new Comparable[old.length*2 + 1];//把原来的数组扩大两倍
for( int i = 0; i < old.length; i++ )
table[ i ] = old[ i ]; //数组进行拷贝
}
//打印数组
private void print(){
/* System.out.println();
for (int i = 1; i <= size; i++) {
System.out.print(table[i] + " ");
}
System.out.println("二叉堆大小:"+size);*/
}
//把数据插入到resultTable末端
private void insertResult(T t){
resultTable[resultTable.length - size] = t;
}
public static void main(String[] args) {
Integer[] target = new Integer[100000];
for (int i = 0; i < 100000; i++) {
target[i] = i;
}
new Heap<Integer>(target, true);
new Heap<Integer>(target, false);
}
}
//运行结果:
//直接插入的方式耗时:5
//随机插入重新调整的方式耗时:3
//
从试验的结果来看,先随机插入,后调整二叉堆的效率会高很多。当然了,不排除扩容机制占时的影响,至于为什么会如此,我只能告诉你第一种方式的时间复杂度为O(N),而第二种方式的时间复杂度为O(NlogN),所以第二种是优先的。
2、我们比较是新数组存储排序数据还是直接用二叉堆数组存储排序数据。
package com.brickworkers;
public class Heap<T extends Comparable<? super T>> {
private static final int DEFAULT_CAPACITY = 10; //默认容量
private T[] table; //用数组存储二叉堆
private T[] resultTable;//用于存储有序数据(每次删除的数据就放入其中)
private int size; //表示当前二叉堆中有多少数据
public Heap(int capactiy){
this.size = 0;//初始化二叉堆数据量
table = (T[]) new Comparable[capactiy + 1];//+1是因为我们要空出下标为0的元素不存储
}
public Heap() {//显得专业些,你就要定义好构造器
this(DEFAULT_CAPACITY);
}
//实现方式二,直接先插入,然后对数据进行整理成二叉堆
public Heap( T [ ] array , boolean methodType){
if(methodType){
size = 0;
table = (T[]) new Comparable[DEFAULT_CAPACITY + 1];
//实现方式一,直接用insert来实现数据放入二叉堆中
for (T t : array) {
insert(t);
}
}else{
size = array.length;
table = (T[]) new Comparable[ size + 2 ];//存储的数组要略微长
for (int i = 0; i < array.length; i++) {
table[ i+1] = array[i];//arr[0]位置不存放数据
}
//到这里为止,数据已经全部都放入了table中
//接下来要做的就是要把这些数据转变成二叉堆
for(int i = size/2; i > 0; i--){//从倒数第二层开始处理
dealHeap(i);
}
}
resultTable = (T[]) new Comparable[ size ];//指定存储数组大小
}
//插入
public void insert(T t){
//先判断是否需要扩容
if(size == table.length - 1){
resize();
}
//开始插入
//定义一个预插入位置下标
int target = ++size;
//循环比较父节点进行位置交换
for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){
table[target] = table[target/2];//如果满足条件,那么两者交换,知道找到合适位置(上滤)
}
//插入数据
table[target] = t;
print();
}
//删除最小
//删除过程中,需要重新调整二叉堆(下滤)
public void deleteMin(boolean store){
if(size == 0){
throw new IllegalAccessError("二叉堆为空");
}
if(store){
// //把删除的元素放入resultTable中
insertResult(table[1]);
//删除元素
table[1] = table[size--];
}else{
//把删除的元素放到末尾
T temp = table[1];
//删除元素
table[1] = table[size--];
table[size + 1] = temp;
}
int target = 1;//从顶部开始重新调整二叉堆
dealHeap(target);
print();
}
private void dealHeap(int target) {
int child;//要处理的节点下标
T tmp = table[ target ];
for( ; target * 2 <= size; target = child )
{
child = target * 2;
if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
child++;
}
if( table[ child ].compareTo( tmp ) < 0 ){
table[ target ] = table[ child ];
table[child] = null;
}
else{
break;
}
}
table[ target ] = tmp;
}
//如果插入数据导致达到数组上限,那么就需要扩容
private void resize(){
T [] old = table;
table = (T []) new Comparable[old.length*2 + 1];//把原来的数组扩大两倍
for( int i = 0; i < old.length; i++ )
table[ i ] = old[ i ]; //数组进行拷贝
}
//打印数组
private void print(){
/*System.out.println();
for (int i = 1; i < table.length; i++) {
System.out.print(table[i] + " ");
}
System.out.println("二叉堆大小:"+size);*/
}
//把数据插入到resultTable末端
private void insertResult(T t){
resultTable[resultTable.length - size] = t;
}
public static void main(String[] args) {
Integer[] target = new Integer[100000];
for (int i = 0; i < 100000; i++) {
target[i] = i;
}
Heap<Integer> heap1 = new Heap<>(target, true);
long copyArrStartTime = System.currentTimeMillis();
//循环删除
for (int i = 0; i < 100000; i++) {
heap1.deleteMin(true);
}
System.out.println("拷贝数组耗时:"+(System.currentTimeMillis() - copyArrStartTime));
Heap<Integer> heap2 = new Heap<>(target, true);
long ArrStartTime = System.currentTimeMillis();
//循环删除
for (int i = 0; i < 100000; i++) {
heap2.deleteMin(false);
}
System.out.println("直接存储耗时:"+(System.currentTimeMillis() - ArrStartTime));
}
}
//输出结果:
//拷贝数组耗时:23
//直接存储耗时:14
//
其实这个效率问题是显而易见的,少一个数组之间的拷贝,当然会显得高效很多。
希望对你有所帮助。