java 二叉堆_【数据结构】二叉堆:Java实现最大堆及堆排序

堆在逻辑上一棵完全二叉树,所以可以通过数组进行数据存储,而其余的树大多采用链式结构进行数据存储

堆分类:

大顶堆:大顶堆就是无论在任何一棵(子)树中,父节点都是最大的

小顶堆:小顶堆就是无论在任何一棵(子)树中,父节点都是最小的

堆的两种操作:

上浮:一般用于向堆中添加新元素后的堆平衡

下沉:一般用于取出堆顶并将堆尾换至堆顶后的堆平衡

堆排序:利用大顶堆和小顶堆的特性,不断取出堆顶,取出的元素就是堆中元素的最值,然后再使堆平衡

下面的文章以大顶堆为例,拿Java实现堆的各种操作。

1.MaxHeap

大顶堆:对于任一个(子)堆,堆顶最大

// 这里Comparable保证所有结点可比,是成堆基础

public class MaxHeap >{

// 完全二叉树,排列整齐(相当于一层一层放入),可以用数组存储

private ArrayList data;

public MaxHeap(int capcity){

data = new ArrayList<>(capcity);

}

public MaxHeap(){

data = new ArrayList<>();

}

// 堆是否为空

public boolean isEmpty(){

return data.isEmpty();

}

// 堆中元素个数

public int size(){

return this.data.size();

}

//......

}

复制代码

2.操作一:获取父/子节点

parent()

// 返回idx位置元素的父节点

// 注:这里从0放起(parent = (i - 1)/2 ),若是从1放起(parent = i / 2)

private int parent(int idx){

if (idx == 0) {

throw new IllegalArgumentException("index-0 doesn't have parent");

}

return (idx - 1) / 2;

}

复制代码

leftChild() / rightChild()

// 返回idx位置元素的孩子节点

// 注:这里从0放起

private int leftChild(int idx){

// 若从1放起,leftChild = idx * 2

return idx * 2 + 1;

}

private int rightChild(int idx){

// 若从1放起,rightChild= idx * 2 + 1

return idx * 2 + 2;

}

复制代码

2.操作二:添加元素

add()

将元素放到堆尾,即数组最后一个元素

加入到最后了,上浮

public void add(E e){

data.add(e);

// 传入需要上浮的索引

siftUp(data.size() - 1);

}

复制代码

siftUp()

上浮:子节点与(小于自己的)父节点交换

Time:O(logn),获取parent是不断二分(/2) 的

private void siftUp(int k){

// 只要是父节点(data[parent]) 比 子节点(data[k])小,就进行交换

while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {

// 1.交换数组中位置

// 注:这里是

Collections.swap(k, parent(k));

// 2.更新子节点,进行下一轮上浮

// 注:也可以data.set进行三步走

k = parent(k);

}

}

复制代码

3.操作三:取出堆顶(最大元素)

extractMax()

取出堆顶

拿到堆顶元素

删除堆顶

将堆顶(0)与堆尾(size-1)交换,因为要产生新堆顶

删除堆尾

堆顶下沉

public E extractMax(){

// 1.获取堆顶元素

E ret = findMax() ;

// 2.1 将堆顶换到堆尾

Collections.swap(0, data.size() - 1);

// 2.2 删除堆尾

data.remove(data.size() - 1);

// 2.3 下沉堆顶

siftDown(0):

return ret;

}

复制代码

findMax()

获取堆中最大元素

public E findMax(){

if (data.size() == 0) {

throw new IllegalArgumentException("Can not findMax when heap is empty");

}

// 堆顶最大 = 数组第一个元素(0)

return data.get(0);

}

复制代码

siftDown()

堆顶下沉:与左右子节点较大值(且大于自己的)节点进行交换

Time:O(logn),获取Child是不断二分(/2) 的

private void siftDown(int k){

// 1.判断当前节点是否有子节点。下沉到叶节点,就没有儿子了,就不用下沉了

// 注:因为leftChild索引肯定比rightChild小,所以只要有leftChild就有子节点

while (leftChild(k) < data.size()) {

// 2.拿到leftChild与rightChild的大值

int j = leftChild(k);

if (j + 1 < data.size() && data.get(j + 1).compareTo(data.get(j)) > 0) {

j = rightChild(k);

}

// 3.判断子节较大值是否大于自己(父节点)

if (data.get(k).compareTo(data.get(j)) >= 0) {

break;

}

// 4.若大于,交换数组中两节点位置

Collections.swap(k, j);

// 更新父节点,进行下一轮下沉

k = j;

}

}

复制代码

4.操作四:数组构造堆

思路一:将已知数组一个一个加入到堆中 ====> Time = O(n*logn)

思路二:从第倒数一个非叶子节点开始下沉 ====> Time = O(n)

// 构造函数中传入要构造成堆的数组

public MaxHeap(E[] arr){

// 注:这里数组不能直接作为ArrayList参数,要先包装成List

data = new ArrayList<>(Arrays.asList(arr));

// 确定倒数第一个非叶子结点:最后一个叶子(length - 1)的父节点 ((i - 1)/ 2)

for (int i = parent(arr.length - 1); i >= 0; i--) {

// 逐个下沉

siftDown(i);

}

}

复制代码

=> 验证最大堆

在看堆排序前,我们先来验证我们写的代码是否正确的。思路是,向堆中添加一百万个随机数,然后依次取顶取出放入到一个数组中,最后验证看是否从大到小排列的:

public class Test{

public static void main(String[] args){

int n = 10000000;

MaxHeap heap = new MaxHeap<>();

Random random = new Random();

// 1.向堆中添加一百万个随机数

for (int i = 0; i < n; i++)

heap.add(random.nextInt(Integer.MAX_VALUE));

// 2.不断从最大堆中取出堆顶 --> 从大到小

int[] arr = new int[n];

for (int i = 0; i < n; i++) {

arr[i] = heap.extractMax();

}

// 3.验证取出的元素是否按照从大到小排列

for (int i = 0; i < n - 1; i++)

if (arr[i] < arr[i + 1])

throw new IllegalArgumentException("Error");

System.out.println("Test MaxHeap Success!");

}

}

复制代码

这里其实也可以直接创建出一个数组,然后构造成最大堆,再不断取顶。相信经过上面的示例,你已经看到了堆有进行排序的能力。

==> 堆排序

堆排序原理:简而言之,就是利用大顶堆和小顶堆的特性,不断取出堆顶(堆中元素的最值),然后再使堆平衡。

构建最大堆:借鉴上面构建堆的思路,从最后一个不是叶子节点的节点开始下沉

取出堆顶:不是真取出,而是通过交换堆顶(arr[0])到堆尾(arr[i])实现出堆顶 ==> 大顶堆的排序结果是从小到大

新堆再平衡:由于上一步已经将堆尾换到了堆首,所以直接再下沉就行

注意:这里采取的是数组从0开始放,即 idx 位置的 parent = idx / 2,leftChild = idx * 2,rightChild = idx * 2 + 1

public class HeapSort{

public void sort(int[] arr){

int length = arr.length;

// 1.构建最大堆:从最后一个不是叶子节点的节点开始下沉

// i=(length-1)/2,表示获取最后一个叶子节点的父亲,即最后一个不是叶子节点的节点

for (int i = (length - 1) / 2; i >= 0; i--) {

siftDown(arr,length, i);

}

for (int i = length - 1; i > 0; i--) {

// 2.取出堆顶:通过交换堆顶(arr[0])到堆尾(arr[i]),实现出堆顶

swap(arr, 0, i);

// 3.新堆再平衡:上一步已经将堆尾换到了堆首,所以直接再下沉就行

// 注:可以看到这里新堆的长度变成了i(比之前少了1)

siftDown(arr, i, 0);

}

}

private void siftDown(int[] arr, int length, int idx){

// 下沉到叶节点,就没有儿子了,就不用下沉了

// 这里用的leftChild(2*idx),因为如果左儿子都超过length了,右儿子也一定超过了

while (2 * idx < length) {

// 判断左儿子和右儿子哪个大

int j = 2 * idx;

if (j + 1 < length && arr[j+1] > arr[j])

j++; // 如果右儿子大,就j++

// 判断父亲是否比儿子大

if (arr[idx] >= arr[j])

break;

// 父亲小于儿子,那么就交换父子

swap(arr, idx, j);

// 更新父亲idx,继续去下一棵子树判断下沉

idx = j;

}

}

private void swap(int[] arr, int idx1, int idx2){

int t = arr[idx1];

arr[idx1] = arr[idx2];

arr[idx2] = t;

}

}

复制代码

另外,如果对其他排序感兴趣的同学,可以参考这篇文章 图解十大排序算法及Java实现(详细)...

5.操作五:取出堆顶,再加入一个元素

思路一:取出堆顶(extractMax),然后再加入一元素(add) ====> 2 * O(logn)

思路二:将堆顶直接修改,然后下沉 ====> O(logn)

replace()

public E replace(E e){

// 1.获取堆顶元素

E ret = findMax();

// 2.修改堆顶,即数组0位置

data.set(0, e);

// 3.下沉

siftDown(0);

return ret;

}

复制代码

完整代码

import java.util.ArrayList;

import java.util.Arrays;

import java.util.Collections;

public class MaxHeap >{

private ArrayList data;

public MaxHeap(int capcity){

data = new ArrayList<>(capcity);

}

public MaxHeap(){

data = new ArrayList<>();

}

public MaxHeap(E[] arr){

data = new ArrayList<>(Arrays.asList(arr));

for (int i = parent(arr.length - 1); i >= 0; i--) {

siftUp(i);

}

}

// 堆是否为空

public boolean isEmpty(){

return data.isEmpty();

}

// 堆中元素个数

public int size(){

return this.data.size();

}

// 返回idx位置元素的父节点

// 注:这里从0放起(parent = (i - 1)/2 ),若是从1放起(parent = i / 2)

private int parent(int idx){

if (idx == 0) {

throw new IllegalArgumentException("index-0 doesn't have parent");

}

return (idx - 1) / 2;

}

// 返回idx位置元素的孩子节点

// 注:这里从0放起

private int leftChild(int idx){

// 若从1放起,leftChild = idx * 2

return idx * 2 + 1;

}

private int rightChild(int idx){

// 若从1放起,leftChild = idx * 2

return idx * 2 + 2;

}

// 添加元素

public void add(E e){

data.add(e);

siftUp(data.size() - 1);

}

private void siftUp(int k){

while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {

Collections.swap(data, k, parent(k));

k = parent(k);

}

}

// 获取堆中最大元素

public E findMax(){

if (data.size() == 0) {

throw new IllegalArgumentException("Can not findMax when heap is empty");

}

// 堆顶最大,

return data.get(0);

}

public E extractMax(){

E ret = findMax() ;

Collections.swap(data, 0, data.size() - 1);

data.remove(data.size() - 1);

siftDown(0);

return ret;

}

private void siftDown(int k){

while (leftChild(k) < data.size()) {

int j = leftChild(k);

if (j + 1 < data.size() && data.get(j + 1).compareTo(data.get(j)) > 0) {

j = rightChild(k);

}

if (data.get(k).compareTo(data.get(j)) >= 0) {

break;

}

Collections.swap(data, k, j);

k = j;

}

}

public E replace(E e){

E ret = findMax();

data.set(0, e);

siftDown(0);

return ret;

}

}

复制代码

最后,对堆在Java中的应用感兴趣的同学看看 PriorityQueue 的源码,它就是通过小顶堆实现的,这里放个传送门 【Java集合源码】PriorityQueue源码分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/506580.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java int stack_java Stack的使用

堆栈是一种 “后进先出” (LIFO) 的数据结构&#xff0c; 只能在一端进行插入(称为 “压栈” ) 或删除 (称为“出栈”)数据的操作。所以很适合逆序操作&#xff0c;JAVA 中&#xff0c;使用 java.util.Stack 类的构造方法创建对象。Stack的基本方法1. public push (item )把项…

java 数据结构 迷宫_JAVA数据结构与算法之递归(一)~ 迷宫问题

递归递归需要遵守的重要规则1) 执行一个方法时&#xff0c;就创建一个新的受保护的独立空间(栈空间)2) 方法的局部变量是独立的&#xff0c;不会相互影响, 比如 n 变量3) 如果方法中使用的是引用类型变量(比如数组)&#xff0c;就会共享该引用类型的数据.4) 递归 必须向退出递归…

java web 自定义异常_Java web, service 层应该通过异常(自定义Exception)来中断业务吗?...

同意&#xff01;但是不是最佳实践还有待商榷&#xff0c;我这里给出自己一直使用的用异常控制流程的方案&#xff0c;分享讨论一下吧。由于JAVA只能有一个返回值&#xff0c;但有时候一个service方法除了返回结果外还真的需要有一些附加信息&#xff0c;比如用户非法操作时要中…

java 数据字典 spring_springboot+redis+切面实现数据字典功能

自定义注解&#xff1a;DataDict&#xff0c;用于bo对象类&#xff0c;需要翻译的属性package com.zddts.common.annotation.dict;import java.lang.annotation.*;/*** 说明&#xff1a;数据字典处理类* Created by luojie on 2019/05/29.*///DataDict( dict"patType"…

java支持多线程吗_Java多线程之一

进程与线程进程进程是进程实体的运行过程&#xff0c;是系统进行资源分配和调度的一个独立单位&#xff0c;比如我们windows电脑上运行的一个程序就是一个进程。在传统进程中进程是资源分配和调度的一个基本单位&#xff0c;在后来引入线程概念后&#xff0c;进程就变成了资源分…

java继承与多态性_Java继承与多态浅析

一、继承1、通过extends继承的父类可以是不加abstract关键字的普通类&#xff0c;也可以是加了abstract关键字的抽象类。继承普通类时可以覆写父类的方法&#xff0c;或者创建自己独有的方法&#xff0c;或者这两 者都不使用。继承抽象类时&#xff0c;必须覆写抽象类中的…

java队列加锁_java并发-----浅析ReentrantLock加锁,解锁过程,公平锁非公平锁,AQS入门,CLH同步队列...

前言为什么需要去了解AQS&#xff0c;AQS&#xff0c;AbstractQueuedSynchronizer&#xff0c;即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)&#xff0c;JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步…

java resttemplate_java-通过resttemplate通过Spring Rest服务发送文...

标题可能看起来很普通,但是没有一个适合我的问题.我有一个REST服务,它接受多部分形式的常规参数和文件.我想使用resttemplate将数据和文件发送到上述rest服务.直到我发送正常的字符串数据为止,没有任何问题.一旦我添加了发送字节的代码,那么我开始收到400错误的请求错误.如果我…

java中string的方法_java中String的常用方法

package com.string;public class string1 {public static void main(String args[]){//将char[]数组转换成Stringchar[] ch{h,我,是,中,国,人};String strnew String(ch);System.out.println(str); //结果为str"我是中国人"//将字符串转换为char数组,方法一使用getC…

Java是否为回文_java语言判断一个数字是否为回文数字

判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。示例 1:输入: 121输出: true示例 2:输入: -121输出: false解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。示例 3:输入: 10输出: false解释: 从右向左读, 为…

java floyd_百度百科里面的floyd算法java的代码,总是无法运行。请问是代码有问题吗,如何编译啊?...

展开全部不能编译运行的说法是错误&#xff0c;但是结果是否正确&#xff0c;我就32313133353236313431303231363533e59b9ee7ad9431333335303539不知道了&#xff0c;我不懂这个算法public class FLOYD {int[][] length null;// 任意两点之间路径长度int[][][] path null;// …

cache数据库和mysql_并发环境下,先操作数据库还是先操作缓存?

原标题&#xff1a;并发环境下&#xff0c;先操作数据库还是先操作缓存&#xff1f;来源&#xff1a;捡田螺的小男孩前言在分布式系统中&#xff0c;缓存和数据库同时存在时&#xff0c;如果有写操作&#xff0c;先操作数据库还是先操作缓存呢&#xff1f;本文将分5种方案 展开…

contab 手动可以 java_crontab 定时执行脚本出错,但手动执行脚本正常

原因&#xff1a; crontab 没有去读环境变量&#xff0c;需要再脚本中手动引入环境变量&#xff0c;可以用source 也可以用export 写死环境变量。为了定时监控Linux系统CPU、内存、负载的使用情况&#xff0c;写了个Shell脚本&#xff0c;当达到一定值得时候&#xff0c;发送邮…

java的mybatis批量更新_mybatis批量更新的问题

一、问题描述场景描述&#xff1a;有这样一个service方法&#xff0c;调用了两个dao中的方法。第一个方法按照传入的id批量更新用户名。第二个dao方法无数据库操作&#xff0c;仅仅抛出一个RuntimeException.这个service方法通过xml配置由spring事务管理的。两个DAO类中分别有S…

java 查看垃圾收集器_JVM系列:查看JVM使用的什么垃圾收集器

一、方法一打印虚拟机所有参数[rootlocalhost ~]# java -XX:PrintFlagsFinal -version | grep :uintx InitialHeapSize : 258689024 {product}uintx MaxHeapSize : 4139778048 {product}bool PrintFlagsFinal : true {product}bool UseCompressedOops : true {lp64_product}boo…

java在W n8安装_在windows中安装JDK8并配置环境变量-java环境变量设置

学习JAVA&#xff0c;必须得安装一下JDK(Java development kit java开发工具包)&#xff0c;配置一下环境就可以学习JAVA了&#xff0c;下面是下载和安装JDK的教程&#xff1a;一、去oracle官网上下载jdk8的下载地址&#xff1a;https://www.oracle.com/technetwork/java/javas…

lisp java_从Java调用的LISP代码

长篇小说:我正在为我的函数编程类做一个项目,我想到在Lisp中为Mario AI competition.我正在研究从Java调用LISP代码的框架/库/方式,甚至更好的LISP Java互通信。我看过Jacol但它是旧的,对我来说也不是很好。到目前为止,我的最佳选择是:Jatha.它真的很整洁,虽然一些Lisp构造还没…

java将图片上传数据库_〔技巧实例〕轻松实现将上传图片到数据库

很久就想自己写一写程序了&#xff0c;不过由于赖就不想写我&#xff0c;今天刚好有空&#xff0c;所以写了这个小小的程序很容易一看就知道的&#xff0c;不多说了就此开始&#xff1a;我们做一个上传的。数据据库的字段就id自动编号 big 字段类型是 OLE 呵呵就简单的那个字段…

mysql带参数的sql_MySql存储过程是带参数的存储过程(动态执行SQL语句)

下文介绍的MySql存储过程是带参数的存储过程(动态执行SQL语句)&#xff0c;该MySql存储过程是根据用户输入的条件和排序方式查询用户的信息&#xff0c;排序条件可以没有调用方式&#xff1a;call GetUsersDynamic(age<30,);/********动态查询用户的信息********/CREATE PRO…

java 注释 depredated_depredated是什么意思_depredated怎么读_depredated翻译_用法_发音_词组_同反义词-新东方在线英语词典...

双语例句1.Theentireareahasbeendepredatedinthewar.整个地区在战争中都遭到破坏。2.WehopethatHaitiwhich washeavilydepredatedby the killerquakewillbereconstructedintheforseeablefuture.我们希望在大地震中受重创的海地在不久的将来可以重建。3.TwoIssuesonAddresseesDe…