【堆】二分堆的实现以及STL中的堆

本文介绍了堆的概念,包括最小堆和最大堆,详细解析了堆的节点关系和创建过程。接着讨论了堆的调整方法,包括插入和删除操作。最后,探讨了STL中的堆操作函数及堆在优先级队列和堆排序等应用场景。
堆的概念

binary_heap就是一种完全二叉树,也就是说整棵二叉树除了最底层的叶节点之外,是填满的,而最底层的叶节点由左至右又不得有空隙。

底层实现就是一个数组 vector。
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2*i+1 且 Ki<= K2*i+2(Ki >= K2*i+1 且 Ki >= K2*i+2) i = 0,1,2…,则称这个堆为最小堆(或最大堆)。

这里写图片描述

最大堆、最小堆

最小堆:任一结点的关键码均小于等于它的左右孩子的关键码,位于堆顶结点的关键码最小。从根节点到每个节点的路径,都是递增的。

最大堆:任一结点的关键码均大于等于它的左右孩子的关键码,位于堆顶结点的关键码最大。从根节点到每个节点的路径,都是递减的。

节点的左右孩子

堆存储在下标为0开始计数的数组中,因此在堆中给定小标为i的结点时:
1、如果i=0,结点i是根节点,没有双亲节点;否则结点i的双亲结点为结点(i-1)/2
2、如果2 * i+1>n-1,则结点i无左孩子,否则结点i的左孩子为结点2 * i +1
3、如果2 * i+2>n-1,则结点i无右孩子,否则结点i的右孩子为结点2 * i+2

*堆的创建

由于堆的底层是由vector实现的。所以调整堆的时候,其实就是处理数组的下标。

由于要满足堆的特性,从最后一个非叶子节点,开始调整,让以该节点为根的子树,满足堆的特性,直到到第一个非叶子节点即:根节点。

最后一个非叶子节点的下标:(size -2 )/2

那么如何调整不满足堆特性的子树呢
(1)找到最小的孩子
(2)判断孩子与根节点的大小,如果是大堆的话,根节点小于孩子节点的话,就交换(默认less比较器);如果是小堆的话,根节点大于孩子节点的话,就交换。
(3)循环上面(1)(2),直到遇到最后一个孩子节点。

自顶向下的调整

void _AdjustDown(size_t root, size_t size) //参数:根节点的下标,堆的大小
    {
        size_t parent = root;
        size_t child = (parent * 2) + 1; // 默认最小孩子为左孩子
        while (child < size)
        {
            //找到最小的孩子结点,判断child+1是否越界

            if (child +1 < size && _heap[child + 1] < _heap[child])
            {
                child = child + 1;
            }
            //比较孩子和双亲结点

            if (_heap[child] < _heap[parent])
            {
                std::swap(_heap[child], _heap[parent]);
                parent = child;
                child = child * 2 + 1;
            }
            else
            {
                break;
            }
        }
    }

插入操作

堆的插入每次都在已经建成的而最小堆的后面插入,但插入之后,有可能破坏了堆的结构,这时就需要对堆进行重新调整。

这里写图片描述

自下而上 调整堆的结构

    void _AdJustUp(size_t size) // 插入 向上调整堆
    {
        size_t parent = (size - 2) >> 1;
        size_t child = size - 1;
        while (child != 0)
        {
            if (_heap[child] < _heap[parent])
            {
                std::swap(_heap[child], _heap[parent]);
                child = parent;
                parent = (child - 1) / 2;
            }
            else
                break;
        }
    }

删除操作

堆的删除,并不是真正意义上的删除

堆的删除是:从堆中删除堆顶元素。移除堆顶元素之后,用堆的最后一个节点填补取走的堆顶元素,并将堆的实际元素个数减1。但用最后一个元素取代堆顶元素之后有可能破坏堆,因此需要将对自顶向下调整,使其满足最大或最小堆。

这里写图片描述

代码实现

template<class T>
class Less
{
public:
    bool operator()(const T& left, const T& right)
    {
        return left <= right;
    }
};
template<class T>
class Greater
{
public:
    bool operator()(const T& left, const T& right)
    {
        return left >= right;
    }
};

template<class T,template<class> class Compare = Less> //模板的模板参数
class Heap
{
public:
    Heap()
    {}
    Heap(const T arr[], size_t size)
    {
        //把arr的数据保存到 _heap中
        //_heap.resize(size);
        for (size_t idx = 0; idx < size; ++idx)
        {
            //_heap[idx] = arr[idx];
            _heap.push_back(arr[idx]);
        }

        // 找到最后一个非叶子结点
        size_t root = (size - 2) / 2;
        for (int idx = root; idx >= 0; idx--)
        {
            _AdjustDown(idx, size);//调整堆
        }
    }
    void Insert(const T data)
        // 空 非空
    {
        _heap.push_back(data);
        size_t size = Size();
        if (size > 1)
        {
            _AdJustUp(size);
        }
    }
    void Remove() //把顶端的和最后一位互换,然后删除
    {
        assert(!Empty());
        size_t size = _heap.size();
        if (size > 1)
        {
            std::swap(_heap[0], _heap[size - 1]);
            _heap.pop_back();
            _AdjustDown(0, size - 1);
        }
        else
        {
            _heap.pop_back();
        }
    }
    bool Empty() const
    {
        return _heap.empty();
    }
    size_t Size()
    {
        return _heap.size();
    }
    T& Top()
    {
        return _heap[0];
    }
    ~Heap()
    {}

private:
    // 调整新堆
    void _AdjustDown(size_t root, size_t size) //参数:根节点的下标,堆的大小
    {
        size_t parent = root;
        size_t child = (parent * 2) + 1; // 默认最小孩子为左孩子
        while (child < size)
        {
            //找到最小的孩子结点,判断child+1是否越界

            if (child + 1 < size && Compare<T>()(_heap[child + 1], _heap[child]))
                //if (child +1 < size && _heap[child + 1] < _heap[child])
            {
                child = child + 1;
            }
            //比较孩子和双亲结点
            if (Compare<T>()(_heap[child], _heap[parent]))
                //if (_heap[child] < _heap[parent])
            {
                std::swap(_heap[child], _heap[parent]);
                parent = child;
                child = child * 2 + 1;
            }
            else
            {
                break;
            }
        }
    }

    void _AdJustUp(size_t size) // 插入 向上调整堆
    {
        size_t parent = (size - 2) >> 1;
        size_t child = size - 1;
        while (child != 0)
        {
            if (Compare<T>()(_heap[child], _heap[parent]))
                //if (_heap[child] < _heap[parent])
            {
                std::swap(_heap[child], _heap[parent]);
                child = parent;
                parent = (child - 1) / 2;
            }
            else
                break;
        }
    }

private:
    std::vector<T> _heap;
};
STL中的堆

make_heap、pop_heap、push_heap、sort_heap函数

STL中通过上面四个函数实现堆,使用的方法都是类似下面的声明。

template <class RandomAccessIterator>
  void make_heap ( RandomAccessIterator first, RandomAccessIterator last );

template <class RandomAccessIterator, class Compare>
  void make_heap ( RandomAccessIterator first, RandomAccessIterator last,
                   Compare comp );

注:sort_heap,排序后会破坏堆的结构。push_heap,要先对底层vector进行push_back;pop_heap函数,弹出后,要对vector进行pop_back

测试代码

int main () {
  int myints[] = {10,20,30,5,15};
  vector<int> v(myints,myints+5);

  make_heap (v.begin(),v.end());
  cout << "initial max heap   : " << v.front() << endl; //30

  pop_heap (v.begin(),v.end()); v.pop_back();
  cout << "max heap after pop : " << v.front() << endl;//20

  v.push_back(99); push_heap (v.begin(),v.end()); //99
  cout << "max heap after push: " << v.front() << endl;

  sort_heap (v.begin(),v.end());

  cout << "final sorted range :";
  for (unsigned i=0; i<v.size(); i++) cout << " " << v[i];

  cout << endl;

  return 0;
}
堆的应用

(1)实现priority_queue优先级队列。默认是最大堆,

STL中优先级队列

(2)堆排序

交换类排序:堆排序

(3)topK问题
找出最大的前K个数。

<think>我们正在讨论Java内存管理中的和栈。用户询问了内存的定义以及内存是否分和栈的情况。根据引用内容,我们可以总结如下: 1. 内存(Heap Memory): - 是用于动态内存分配的区域,在Java中,所有通过`new`关键字创建的对象都存储在内存中。 - 内存的大小在运行时动态分配,由Java垃圾收集器(GC)自动管理。 - 内存的分配速度相对较慢,并且可能产生内存碎片,但使用灵活,大小只受限于虚拟内存的大小。 - 内存是所有线程共享的,因此存在线程安全问题。 2. 栈内存(Stack Memory): - 栈用于存储局部变量、方法调用和部分结果(每个线程有自己的栈)。 - 栈中存储基本数据类型的值以及对象引用(即对象在内存中的地址)。 - 栈内存的存取速度较快,因为内存分配在编译时确定(固定大小),采用LIFO(后进先出)结构。 - 栈内存是线程私有的,因此不存在线程安全问题。 关于“内存是否分和栈”: - 内存本身是一个整体,但为了更高效的内存管理,Java虚拟机(JVM)将内存划分为不同的区域(分代)。常见的分代包括: - 年轻代(Young Generation):又分为Eden区和两个Survivor区(S0和S1)。 - 老年代(Old Generation)。 - 永久代(Permanent Generation)或元空间(Metaspace,在Java 8以后替代永久代)。 - 注意:这些划分是内部的划分,而栈是独立于的另一个内存区域。 - 因此,内存并不再分为“”和“栈”,而是内部有分代。栈是独立的内存区域。 引用内容支持: - 引用[2]提到:“内存是由new分配的内存,一般速度比较慢,而且容易产生内存碎片”。同时,引用[4]指出:“的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,java的垃圾收集器会自动收取这些不在使用的数据”。 - 引用[4]还提到:“栈中只保存基础数据类型的对象和自定义对象的引用(不是对象,是内存的地址),对象都存放在区中”。 总结: - 内存定义:用于存储对象实例的动态分配内存区域,由垃圾收集器管理。 - 内存内部有分代(如年轻代、老年代),但和栈是两个独立的内存区域,内存本身并不再分为和栈。 相关问题: 1. Java内存的分代结构具体如何工作? 2. 栈内存溢出和内存溢出分别是什么原因导致的? 3. 如何通过JVM参数调整和栈的大小? </think>### 内存的定义与结构分析 #### 一、内存的核心定义 内存(Heap Memory)是程序运行时动态分配的内存区域,具有以下本质特征: 1. **动态分配机制** 通过`new`、`malloc`等指令在运行时按需分配,内存大小不固定。例如Java中`Object obj = new Object()`会将对象实例存储在中[^2][^4]。 2. **生命周期管理** 对象生命周期不受作用域限制,由垃圾回收器(GC)统一管理。如下图示: ```mermaid graph LR A[创建对象 new Object] --> B[内存分配] B --> C[引用变量存入栈] C --> D[GC扫描无引用对象] D --> E[内存回收] ``` 3. **存储内容特点** | 内存区域 | 存储内容 | 管理方式 | |----------|--------------------------|---------------| | | 对象实例、数组 | GC自动回收 | | 栈 | 基本类型变量、对象引用 | 自动出栈销毁 | 例如:`int[] arr = new int[10]`中,数组实体在,引用`arr`在栈[^4]。 #### 二、内存的分区结构(分代模型) 内存并非单一连续区域,现代JVM采用**分代结构**: 1. **年轻代 (Young Generation)** - Eden区:新对象分配区 - Survivor区(S0/S1):存活对象过渡区 - 通过`Minor GC`回收短期对象 2. **老年代 (Old Generation)** - 存放长期存活对象 - 通过`Major GC/Full GC`回收 3. **元空间 (Metaspace)** - 存储类元数据(替代永久代) - 位于本地内存(非内存)[^2][^4] #### 三、与栈的本质区别 二者是独立但协作的内存区域: | 特性 | 内存 | 栈内存 | |--------------|----------------------------|----------------------------| | **分配方式** | 动态分配,大小不固定 | 编译时确定,大小固定 | | **存储内容** | 对象实体 | 基本类型值、对象引用地址 | | **访问速度** | 较慢(需指针寻址) | 极快(直接CPU缓存访问) | | **线程安全** | 全局共享(需同步机制) | 线程私有(天然安全) | | **碎片问题** | 存在内存碎片 | 无碎片(LIFO结构) | | **回收机制** | GC定期扫描 | 方法结束自动出栈 | #### 四、关键技术原理 1. **内存共享机制** 栈中引用指向中对象实例,多个栈引用可共享同一对象: ```java String s1 = "Hello"; // 栈保存引用,常量池在 String s2 = "Hello"; // s1与s2指向同一对象[^5] ``` 2. **溢出场景** 持续创建大对象导致耗尽: ```java while(true) { new byte[1024*1024]; // 每次分配1MB内存 } // 最终抛出OutOfMemoryError[^2] ``` 3. **栈溢出场景** 递归调用耗尽栈空间: ```java void stackOverflow() { stackOverflow(); } // 抛出StackOverflowError[^4] ``` ### 总结结论 1. **内存定义**:动态对象存储区,由GC管理生命周期,支持跨方法存在[^2][^4] 2. **分区结构**:内部分为年轻代(Eden+Survivor)和老年代,元空间独立存在 3. **栈关系**:栈存引用,存对象实体,二者协同工作但物理隔离[^3][^4] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值