『数据结构』堆

本篇博客,我们介绍一下堆的相关概念,然后介绍一下C++中和Java中的优先级队列。

什么是堆?


堆是一种特殊的二叉树

  • 堆是一个完全二叉树
  • 堆采用顺序存储的方式;
  • 堆中任一结点都都满足比子节点的值大(大堆)或小(小堆)

我们来看一个大堆及其在内存中的存储方式
在这里插入图片描述
从上图我们可以看出,父结点和子节点的下标关系如下
已知父结点下标为x,则孩子结点下标为

  • 左孩子下标为: 2 ∗ x + 1 2*x+1 2x+1
  • 右孩子下标为: 2 ∗ x + 2 2*x + 2 2x+2

已知孩子下标为x,则父结点下标为

  • x为左孩子下标,则父结点下标为: ( x − 1 ) / 2 (x - 1) / 2 (x1)/2
  • x为右孩子下标,则父结点下标为: ( x − 1 ) / 2 (x - 1)/2 (x1)/2 ( x − 2 ) / 2 (x - 2)/2 (x2)/2

实现一个简单的堆


C语言版


heap.h

#pragma once

typedef int ElementType;

/*---堆---*/
typedef struct Heap {
	ElementType* heap;
	size_t size;
	size_t capacity;
} Heap;

/*---初始化---*/
void heapInit(Heap* h);

/*---释放---*/
void heapDestory(Heap* h);

/*---入堆---*/
void heapPush(Heap* h, ElementType x);

/*---出堆---*/
void heapPop(Heap* h);

/*---堆顶元素---*/
ElementType heapTop(const Heap* h);

/*---堆大小---*/
size_t heapSize(const Heap* h);

/*---空堆---*/
bool heapEmpty(const Heap* h);

/*---堆排序---*/
void heapSort(Heap* h);

/*---打印---*/
void heapDisplay(const Heap* h);

/*---堆测试---*/
void heapTest();

heap.c

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
#include "heap.h"

void heapInit(Heap* h){
	if (h == NULL){
		return;
	}

	h->capacity = 1;
	h->size = 0;
	h->heap = (ElementType*)malloc(sizeof(ElementType) * h->capacity);
	assert(h->heap != NULL);
}

void heapDestory(Heap* h){
	if (h == NULL){
		return;
	}

	h->size = 0;
	h->capacity = 0;
	free(h->heap);
	h->heap = NULL;
}

static void checkCapacity(Heap* h){
	if (h == NULL){
		return;
	}

	if (h->size >= h->capacity){
		h->capacity *= 2;
		h->heap = (ElementType*)realloc(h->heap,
			sizeof(ElementType) * h->capacity);
	}
}

static void swap(int* a, int* b){
	assert(a != NULL);
	assert(b != NULL);
	int temp = *a;
	*a = *b;
	*b = temp;
}

static void upwardAdjustment(Heap* h, int child){
	if (h == NULL){
		return;
	}
	
	if (child == 0){
		return;
	}

	int parent = (child - 1) / 2;
	if (parent < 0 || h->heap[child] <= h->heap[parent]){
		return;
	}

	swap(&h->heap[child], &h->heap[parent]);

	upwardAdjustment(h, parent);
}

void heapPush(Heap* h, ElementType x){
	if (h == NULL){
		return;
	}

	checkCapacity(h);
	++h->size;
	h->heap[h->size - 1] = x;

	upwardAdjustment(h, h->size - 1);
}

static void downwardAdjustment(ElementType heap[], size_t size, size_t root_index){
	if (heap == NULL){
		return;
	}

	size_t left_index = 2 * root_index + 1;
	if (2 * root_index + 1 >= size){
		return;
	}

	size_t max_index = left_index;
	size_t right_index = 2 * root_index + 2;
	if (right_index < size && heap[right_index] > heap[left_index]){
		max_index = right_index;
	}

	if (heap[root_index] < heap[max_index]){
		swap(&heap[root_index], &heap[max_index]);
	}
	else{
		return;
	}

	downwardAdjustment(heap, size, max_index);
}

void heapPop(Heap* h){
	if (h == NULL){
		return;
	}

	swap(&h->heap[0], &h->heap[h->size - 1]);

	--h->size;

	downwardAdjustment(h->heap, h->size, 0);
}

ElementType heapTop(const Heap* h){
	assert(h != NULL);

	return h->heap[0];
}

size_t heapSize(const Heap* h){
	if (h == NULL){
		return 0;
	}

	return h->size;
}

bool heapEmpty(const Heap* h){
	if (h == NULL){
		return true;
	}

	return h->size == 0 ? 1 : 0;
}

void heapSort(Heap* h){
	if (h == NULL){
		return;
	}

	size_t i = 0;
	for (i = 0; i < h->size; ++i){
		swap(&h->heap[0], &h->heap[h->size - 1 - i]);
		downwardAdjustment(h->heap, h->size - 1 - i, 0);
	}

}

void heapDisplay(const Heap* h){
	if (h == NULL){
		return;
	}

	size_t i = 0;
	printf("The Heap is below: \n");
	for (i = 0; i < h->size; ++i){
		printf("%d ", h->heap[i]);
	}
	printf("\n");
}

void heapTest(){
	Heap h;

	heapInit(&h);

	heapPush(&h, 1);
	heapPush(&h, 2);
	heapPush(&h, 3);
	heapPush(&h, 5);
	heapPush(&h, 0);
	heapPush(&h, 8);

	heapPop(&h);

	ElementType top = heapTop(&h);
	printf("Heap Top: %lu\n", top);

	size_t size = heapSize(&h);
	printf("Heap Size: %lu\n", size);

	if (heapEmpty(&h)){
		printf("Heap is Empty!\n");
	}
	else{
		printf("Heap isn't Empty!\n");
	}

	heapDisplay(&h);

	heapSort(&h);

	heapDisplay(&h);
}

Java版


public class MyPriorityQueue {
    private int[] arr = new int[100];
    private int size = 0;

    public void offer(int val) {
        this.arr[this.size] = val;
        ++this.size;
        shiftUp(this.size - 1);
    }

    public Integer poll() {
        if (this.size == 0) {
            return null;
        }

        int ret = this.arr[0];
        swap(0, this.size - 1);
        --this.size;

        shiftDown(0);

        return ret;
    }

    public Integer peek() {
        if (this.size == 0) {
            return null;
        }

        return this.arr[0];
    }

    private void shiftDown(int index) {
        int parent = index;
        int child = 2 * parent + 1;

        while (child < this.size) {
            if (child + 1 < this.size && this.arr[child + 1] > this.arr[child]) {
                ++child;
            }

            if (this.arr[parent] < this.arr[child]) {
                swap(child, parent);
            } else {
                break;
            }

            parent = child;
            child = 2 * parent + 1;
        }
    }

    private void shiftUp(int index) {
        int child = index;
        int parent = (child - 1) / 2;

        while (child > 0) {
            if (this.arr[child] > this.arr[parent]) {
                swap(child, parent);
            } else {
                break;
            }

            child = parent;
            parent = (child - 1) / 2;
        }
    }

    private void swap(int i, int j) {
        int temp = this.arr[i];
        this.arr[i] = this.arr[j];
        this.arr[j] = temp;
    }
}

优先级队列


实际中,当我们需要使用堆的时候,我们并不需要去手动实现一个堆,一般的编程语言中都会提供堆这个数据结构

C++中的优先级队列


首先,我们来看一下官方文档
在这里插入图片描述
看不懂英文不要紧,翻译在下面(O(∩_∩)O哈哈~):

  1. 优先级队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中的最大者
  2. 此上下文类似于,在堆中可以随时插入元素,并且只能检索最大堆元素(优先级队列中位于顶部的元素)。
  3. 优先级队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供了一组特定的成员函数来访问其元素。元素从特定容器的尾部弹出,其称为优先级队列的顶部。
  4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作
    empty():检查容器是否为空
    size():返回容器中有效元素的数量
    front():返回容器中第一个元素的引用
    push_back():在容器尾部插入元素
    pop_back():删除容器尾部元素
  5. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
  6. 需要支持随机访问迭代器以便始终在内部保持堆结构容器适配器通过在需要时自动调用算法函数make_heap、push_heap、pop_heap来自动完成此操作

接口介绍


优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:priority_queue默认情况下是大堆

函数声明接口说明
priority_queue(const Compare& x = Compare(), const Container& y = Container());构造一个空的优先级队列
priority_queue(InputIterator first, InputIterator last, const Compare& comp = Compare(), const Container& ctnr = Container());用[first, last)区间中的元素构造优先级队列
bool empty() const;检测优先级队列是否为空,是空返回true,否则返回false
const value_type& top() const;返回优先级队列中最大(最小)元素,即堆顶元素
void push(const T& x);在优先级队列中插入元素x
void pop();删除优先级队列中最大(最小)元素,即堆顶元素

优先级队列的简单使用


默认情况下,priority_queue是大堆

#include <iostream>
#include <vector>
#include <queue>
#include <functional>
using namespace std;

int main(){
	vector<int> v = {
		3, 8, 1, 0, 5, 4, 7, 2, 9, 6
	};

	// 默认创建为大堆
	priority_queue<int> q1;
	for (const auto& e : v){
		q1.push(e);
	}
	// 打印堆顶元素
	cout << q1.top() << endl;

	// 创建小堆
	priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
	// 打印堆顶元素
	cout << q2.top() << endl;

	return 0;
}

运行结果如下:
在这里插入图片描述
如果在priority_queue中放自定义类型的数据用户需要在自定义类型中提供>或者<的重载

#include <iostream>
#include <vector>
#include <queue>
#include <functional>
using namespace std;


class Date{
public:
	Date(int year = 2000, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d) const{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date d) const{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

	friend ostream& operator<<(ostream& _cout, const Date& d){
		_cout << d._year << "-" << d._month 
			<< "-" << d._day << endl;

		return _cout;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main(){
	// 大堆,需要重载<
	priority_queue<Date> q1;
	q1.push(Date(2019, 8, 31));
	q1.push(Date(2019, 9, 1));
	q1.push(Date(2019, 9, 2));
	cout << q1.top();

	// 小堆,需要重载>
	priority_queue<Date, vector<Date>, greater<Date>> q2;
	q2.push(Date(2019, 8, 31));
	q2.push(Date(2019, 9, 1));
	q2.push(Date(2019, 9, 2));
	cout << q2.top();

	return 0;
}

运行结果如下:
在这里插入图片描述
有些情况下,用户可能需要提供比较器规则

#include <iostream>
#include <vector>
#include <queue>
#include <functional>
using namespace std;


class Date{
public:
	Date(int year = 2000, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d) const{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date d) const{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

	friend ostream& operator<<(ostream& _cout, const Date& d){
		_cout << d._year << "-" << d._month 
			<< "-" << d._day << endl;

		return _cout;
	}

private:
	int _year;
	int _month;
	int _day;
};

class Less{
public:
	bool operator()(const Date* d1, const Date* d2){
		return *d1 < *d2;
	}
};

int main(){
	priority_queue<Date*, vector<Date*>, Less> q;
	q.push(&Date(2019, 9, 1));
	q.push(&Date(2019, 8, 31));
	q.push(&Date(2019, 9, 2));

	// 打印堆顶元素
	cout << *q.top();

	return 0;
}

运行结果如下:
在这里插入图片描述

优先级队列的实际应用


数组中第K大的元素

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        // 将数组中的元素放入优先级队列
        priority_queue<int> pq(nums.begin(), nums.end());
        
        // 将优先级队列中前K-1个元素删掉
        for(int i = 0; i < k - 1; ++i){
            pq.pop();
        }
        
        return pq.top();
    }
};

在这里插入图片描述

Java中的优先级队列


在这里插入图片描述

接口介绍


错误处理抛异常返回特殊值
入队列add(e)offer(e)
出队列remove()poll()
取队首元素element()peek()

实际应用


我们来看一个问题来理解一下PriorityQueue的使用
查找和最小的k对数

class Solution {
    static class Pair {
        public int num1;
        public int num2;
        public int sum;
        
        public Pair(int num1, int num2) {
            this.num1 = num1;
            this.num2 = num2;
            this.sum = num1 + num2;
        }
    }
    
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        List<List<Integer>> result = new ArrayList<>();
        if (k < 1) {
            return result;
        }
        
        PriorityQueue<Pair> queue = new PriorityQueue<>(k, new Comparator<Pair>(){
            @Override
            public int compare(Pair o1, Pair o2) {
                return o2.sum - o1.sum;
            }
        });
        for (int i = 0; i < nums1.length && i < k; ++i) {
            for (int j = 0; j < nums2.length && j < k; ++j) {
                queue.offer(new Pair(nums1[i], nums2[j]));
                if (queue.size() > k) {
                    queue.poll();
                }
            }
        }
        
        while (!queue.isEmpty()) {
            Pair pair = queue.poll();
            
            List<Integer> curPair = new ArrayList<>();
            curPair.add(pair.num1);
            curPair.add(pair.num2);
            
            result.add(0, curPair);
        }
        
        return result;
    }
}

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值