堆数据结构和堆排序(Java)

2018-03-14  本文已影响0人  dreamsfuture

堆的定义(Heap)

public class Heap {    
    
    private int[] data;  
    private int count; //当前节点数  
    private int capacity; //容量  
}   

所以,堆中保存数据的就是一个数组

最大堆:根结点的键值是所有堆结点键值中最大者(不仅大于其子节点,同时大于堆中的所有节点值)。每个节点的值都>=其左右孩子(如果有的话)值的完全二叉树。
最小堆:根结点的键值是所有堆结点键值中最小者(同理)。每个节点的值都<=其左右孩子值的完全二叉树。

最大堆的特点:

已知一个节点i,求其左右节点索引和父节点索引

// 左节点下标  
    public int left(int i) { return i * 2 + 1;}  
  
    // 右节点下标  
    public int right(int i) {return i * 2 + 2;}  
  
    // 父节点下标  
    public int parent(int i) {return (i - 1) / 2;}  

输入一个数组Arr实现一个最大堆(完全二叉树)

实现最大堆的算法:

1.把输入的数组赋值给heap对象中的data数组,获取输入数组的长度;
2.建立的最大堆是一个完全二叉树,从完全二叉树最低层最右边的叶子节点的父节点开始,依次向左向上循环检索节点直到根节点;
3.在第2步获取的每一个节点都调用一次shiftDown函数,实现从此节点以下的节点都是最大堆的特性

所以,for循环是自下而上的算法,shiftDown最大堆排序是自上而下的算法。

特点:最大堆的一个父节点的左右节点大小没有顺序,只是两个子节点都比父节点小。

//实现最大堆(用数组来存放完全二叉树中的节点,从上到下,从左到右排序,按序存在数组,子节点的值小于等于父节点的值)  
public class Heap {  
    private int[] data;  
    private int count; //当前节点数  
    private int capacity; //容量  
      
    public Heap(int capacity) {  
        this.data=new int[capacity+1];  //因为索引0不存节点,所以长度加一  
        this.capacity=capacity;  
        this.count=0;  
    }  
    //将一个无序数组构造成一个最大堆:相当于堆排序  
    public MaxHeap(int[] arr,int n){  
        data=new int[n+1];  
        capacity=n;  
        for(int i=0;i<n;i++){  
            data[i+1]=arr[i];  
        }  
        count=n;  
        //通过for循环实现所有节点都遍历一遍,而且是从低层的最右边的叶节点的父节点一直从右到左、从下往上索引到根节点。所以,这个算是一个逆向的层次遍历
        for(int parent = count/2; parent >= 1; parent--){  
            shiftDown(parent);  
        }  
    }  
     //调用shiftDown只会从当前节点一直往深度走,往树叶子走而不会同层走,所以这是个深度优先遍历
    private void shiftDown(int parent){  
        int left = 2*parent;  // 节点 left 表示 parent父节点对应的左孩子节点
        int right = 2*parent+1;
        maxValueIndex = left;
        while(left <= count){     //有左子节点
             if( right <= count && data[right] > data[left]){ // 比较左右子节点那个值更大
                 maxValueIndex = right;
             }  
             if(data[parent] >= data[maxValueIndex])  //拿父节点和左右子节点更大的进行比较
                 break;  
             //如果子节点大于父节点,则交换数据
             swap(data,parent,maxValueIndex);  
             parent = maxValueIndex;       //k被赋为当前位置,为下次循环做初始化  
        }  
    }  

    public static void swap(int[] arr,int a,int b){  
        int tmp=arr[a];  
        arr[a]=arr[b];  
        arr[b]=tmp;  
    } 

最大堆建立的过程如下图[1]:


最大堆建立过程

利用最大堆实现堆排序

算法思路:

  1. 先用Heap函数把输入的数组A构造成最大堆。
  2. 把下标heapSize - 1的元素和下标为0的元素对换,通过减小heapSize,让下标为heapSize - 1的元素从堆中剔除,
  3. 再调用MaxHeap(heap, 0)即可保证最大堆的性质。
  4. 重复2和3两个过程,直到堆中只剩下一个元素。
/ * 堆排序
 *
 * 1. 先将初始序列K[1..n]建成一个大顶堆, 那么此时第一个元素K1最大, 此堆为初始的无序区.
 * 2. 再将关键字最大的记录K1 (即堆顶, 第一个元素)和无序区的最后一个记录 Kn 交换, 由此得到新的无序区K[1..n−1]和有序区K[n], 且满足K[1..n−1].keys⩽K[n].key
 * 3. 交换K1 和 Kn 后, 堆顶可能违反堆性质, 因此需将K[1..n−1]调整为堆. 然后重复步骤②, 直到无序区只有一个元素时停止.
 */
public static void heapSort(int[] arr){
    for(int i = arr.length; i > 0; i--){
        // 把数组中(0,i)之间的索引的数据送入到最大堆函数中实现最大堆
        max_heapify(arr, i);  //由于i在减小,所以输入的可以操作的数组也在变小,实现了后面排序好的数据的不可操作性
        swap(arr, i-1, 0);  //堆顶元素(第一个元素)与Kn交换
    }
}

private static void max_heapify(int[] arr, int limit){
    if(arr.length <= 0 || arr.length < limit) return;
    int parentIdx = limit / 2;
    //横向层序遍历,从右向左,从下往上,实现把最大的数值往上走,从而实现最大堆
    for(; parentIdx >= 0; parentIdx--){
        if(parentIdx * 2 >= limit){
            continue;
        }
        int left = parentIdx * 2;       //左子节点位置
        int right = (left + 1) >= limit ? left : (left + 1);    //右子节点位置,如果没有右节点,默认为左节点位置

        int maxChildId = arr[left] >= arr[right] ? left : right;
        if(arr[maxChildId] > arr[parentIdx]){   //交换父节点与左右子节点中的最大值
            swap(arr, maxChildld, parentldx);
        }
        // 实现了堆中每一个节点值都大于其所有节点值
        /*int left = parentldx * 2;
         *int right = left + 1
         *while(left <= limit){
         *  right >= limit ? left : (left + 1);
         *  int maxChildld = arr[left] >= arr[right] ? left : right;
         *  if(arr[maxChildId] > arr[parentIdx]) swap(arr, maxChildld, parentldx);
         *  left = parentldx *2;
         *  right = left + 1;
         *}
         */
        
    }
    System.out.println("Max_Heapify: " + Arrays.toString(arr));
}
public static void swap(int[] arr,int a,int b){  
    int tmp=arr[a];  
    arr[a]=arr[b];  
    arr[b]=tmp;  
} 
//对排序的实现不一定要让所有节点都大于其节点的节点,我们只要找到一个堆的最大值,然后与数组当前阶段最后面一个值交换便可

//当然,最大堆还是要实现每一个节点都要实现大于其所有子节点

实现的过程图如下:

堆排序实现过程

时间和空间复杂度

时间复杂度

堆排序基本思想是创建最大堆,需要lgn的时间复杂度,同时n中的每一个数都需要操作一遍,所以时间复杂度为nlgn

公式

T(n) = 2T(n/2) + f(n) = 2T(n/2) +Θ(n)

推算过程[5]
首先要理解怎么计算这个堆化过程所消耗的时间,可以直接画图去理解;
假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;
那么总的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要比较的次数,如果在最差的条件下,就是比较次数后还要交换;因为这个是常数,所以提出来后可以忽略;
S = 2^(k-2) * 1 + 2(k-3)*2.....+2*(k-2)+2(0)*(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
这个等式求解,我想高中已经会了:等式左右乘上2,然后和原来的等式相减,就变成了:
S = 2^(k - 1) + 2^(k - 2) + 2^(k - 3) ..... + 2 - (k-1)
除最后一项外,就是一个等比数列了,直接用求和公式:S = { a1[ 1- (q^n) ] } / (1-q);
S = 2^k -k -1;又因为k为完全二叉树的深度,所以 (2^k) <= n < (2^k -1 ),总之可以认为:k = logn (实际计算得到应该是 log(n+1) < k <= logn );
综上所述得到:S = n - longn -1,所以时间复杂度为:O(n)
更改堆元素后重建堆时间:O(nlogn)
推算过程
1、循环 n -1 次,每次都是从根节点往下循环查找,所以每一次时间是 logn,总时间:logn(n-1) = nlogn - logn ;
综上所述,堆排序的时间复杂度为:O(nlogn)

空间复杂度

从代码来看,输入的数组的引用直接赋值给了最大堆创建的数组,所以不需要额外n的辅助数组,而每次迭代循环中除了定义的左右孩子节点left, right 和交换数据的tmp,没有其他的变量,此外因为是while循环,所以不存在stack占用过多的递归空间,使用完了就直接出来,所以空间复杂度为O(1).

参考文献:

[1] 堆排序Heap Sort——浅显易懂+Java实现
[2] java实现最大堆及堆排序
[3] java最大最小堆
[4] 八大排序算法总结与java实现

上一篇下一篇

猜你喜欢

热点阅读