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

堆的定义(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实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352