【数据结构】Java实现堆:堆的各种操作&堆排序

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

  • 堆分类:
    • 大顶堆:大顶堆就是无论在任何一棵(子)树中,父节点都是最大的
    • 小顶堆:小顶堆就是无论在任何一棵(子)树中,父节点都是最小的
  • 堆的两种操作:
    • 上浮:一般用于向堆中添加新元素后的堆平衡
    • 下沉:一般用于取出堆顶并将堆尾换至堆顶后的堆平衡
  • 堆排序:利用大顶堆和小顶堆的特性,不断取出堆顶,取出的元素就是堆中元素的最值,然后再使堆平衡

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

1.MaxHeap

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

// 这里Comparable保证所有结点可比,是成堆基础
public class MaxHeap <E extends Comparable<E>> {
    
    
	// 完全二叉树,排列整齐(相当于一层一层放入),可以用数组存储
    private ArrayList<E> 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放起,leftChild = idx * 2
    return idx * 2 + 2;
}

2.操作二:添加元素

add()

  1. 将元素放到堆尾,即数组最后一个元素
  2. 加入到最后了,上浮
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()

取出堆顶

  1. 拿到堆顶元素
  2. 删除堆顶
    1. 将堆顶(0)与堆尾(size-1)交换,因为要产生新堆顶
    2. 删除堆尾
    3. 堆顶下沉
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;
    }
}

=> 堆排序

堆排序原理:简而言之,就是利用大顶堆和小顶堆的特性,不断取出堆顶,取出的元素就是堆中元素的最值,然后再使堆平衡。下面放一个示例代码:

public class Test {
    
    

    public static void main(String[] args) {
    
    
        int n = 10000000;

        MaxHeap<Integer> heap = new MaxHeap<>();
        Random random = new Random();
        // 一百万个随机数
        // 注:这里用的random类!!!
        for (int i = 0; i < n; i++)
            heap.add(random.nextInt(Integer.MAX_VALUE));

        // 不断从最大堆中取出堆顶  --> 从大到小
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
    
    
            arr[i] = heap.extractMax();
        }
		
        // 验证取出的元素是否按照从大到小排列
        for (int i = 0; i < n - 1; i++)
            if (arr[i] < arr[i + 1])
                throw  new IllegalArgumentException("Error");

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

    }
}

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

  • 思路一:取出堆顶(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;
}

5.操作五:数组堆化

heapify()

  • 思路一:将已知数组一个一个加入到堆中 ====> 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);
    }
}

完整代码

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;

public class MaxHeap <E extends Comparable<E>> {
    
    

    private ArrayList<E> 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源码分析

猜你喜欢

转载自blog.csdn.net/weixin_43935927/article/details/108859896
今日推荐