7.(数据结构)堆
7.1 相关概念
堆(Heap)在计算机科学中是一种特殊的数据结构,它通常被实现为一个可以看作完全二叉树的数组对象。以下是一些关于堆的基本概念:
数据结构:
堆是一个优先队列的抽象数据类型实现,通过完全二叉树的逻辑结构来组织元素。
完全二叉树意味着除了最后一层外,每一层都被完全填满,并且最后一层的所有节点都尽可能地集中在左边。
物理存储:堆用一维数组顺序存储,从索引0开始,每个节点的父节点和子节点之间的关系可以通过简单的算术运算确定。
堆的特性:堆序性质:对于任意节点i,其值(或关键字)满足与它的子节点的关系——在最大堆(大根堆)中,节点i的值大于或等于其两个子节点的值;在最小堆(小根堆)中,节点i的值小于或等于其两个子节点的值。
结构性:堆始终保持完全二叉树的状态,这意味着即使有节点删除或插入,堆也要经过调整以保持堆序性质。
操作:插入:新元素添加到堆中时,需要自下而上调整堆,以确保新的元素不会破坏堆的性质。
删除:通常从堆顶(根节点)删除元素(即最大堆中的最大元素或最小堆中的最小元素),然后将堆尾元素移动到堆顶,再自上而下调整堆。
查找:堆常用于快速找到最大或最小元素,但查找特定值的时间复杂度通常不优于线性时间,因为堆本身不具备随机访问的能力。
应用:堆常用于解决各种问题,如优先级队列、事件调度、图算法中的最短路径计算(Dijkstra算法)、求解Top K问题等。
分类:最常见的堆是二叉堆,包括大根堆和小根堆。
还有其他类型的堆,比如斐波那契堆,提供更高效的合并操作以及其他优化特性。
建堆算法:
1. Dijkstra算法:是一个使用优先队列(可以基于堆实现)的经典例子。在Dijkstra算法中,每次都会从未确定最短路径且当前距离已知最小的顶点开始,更新与其相邻顶点的距离。为了高效地找到下一个待处理的顶点(即当前已知最短路径的顶点),会用到一个能够根据顶点距离值进行快速插入和删除的优先队列,堆就是实现这种功能的理想数据结构之一。
2. 堆下沉”(Heap Sink),“堆下滤”(Heap Percolate Down):从根节点(非叶子节点中最高层的一个)开始。 检查该节点与其两个子节点的关系:在最大堆中,如果当前节点的值小于其任意一个子节点的值;在最小堆中,如果当前节点的值大于其任意一个子节点的值。 如果违反了堆的性质,则交换当前节点与其较大(对于最大堆)或较小(对于最小堆)子节点的值,并将当前节点移动到新位置(即原来子节点的位置)。 重复上述步骤,但这次以交换后的子节点作为新的当前节点,继续下潜至当前节点没有子节点(即成为叶子节点),或者当前节点及其子节点均满足堆的性质为止。
时间复杂度:
7.2 大顶堆
堆内比较重要的方法就是,建堆,堆下潜操作,堆上浮操作
/* * 大顶堆 * */ public class MaxHeap { public static void main(String[] args) { int[] array = {1, 2, 3, 4, 5, 6, 7}; MaxHeap maxHeap = new MaxHeap(array); System.out.println(Arrays.toString(MaxHeap.array)); } static int[] array; static int size; public MaxHeap(int[] array) { this.array = array; this.size = array.length; heapify(); } /* * 建堆: * 找到第一个非叶子结点 * 执行下潜,直到没有子节点 * */ private void heapify() { //最后一个非叶子结点 size/2-1 for (int i = size / 2 - 1; i >= 0; i--) { down(i); } } //执行下潜 parent 为需要下潜的索引,下潜到没有孩子,或者孩子都小于当前节点 private void down(int parent) { //左child索引 int leftChild = parent * 2 + 1; //右child索引 int rightChild = leftChild + 1; int max = parent; if (leftChild < size && array[max] < array[leftChild]) max = leftChild; if (rightChild < size && array[max] < array[rightChild]) max = rightChild; if (max != parent) { //下潜并再次调用 swap(parent, max); down(max); } } /* * 上浮操作 * */ public void up(int offered){ int child =size; //子元素索引等于0则到达了堆顶 while (child>0){ int parent=(child-1)/2; if (offered>array[parent]){ array[child]=array[parent]; }else { break; } child=parent; } array[child] = offered; } //交换两个元素 private void swap(int i, int j) { isEmptyeException(); int temp = array[i]; array[i] = array[j]; array[j] = temp; } public int peek(){ isEmptyeException(); return array[0]; } /* * 从第一个位置移除元素,交换头尾,在移除最后一个 * */ public int poll(){ isEmptyeException(); int temp=array[0]; //交换首位 swap(0,size-1); size--; down(0); return temp; } /* * 从指定位置移除元素,交换指定位置和尾,在移除最后一个 * */ public int poll(int index){ isEmptyeException(); int temp=array[index]; //交换首位 swap(index,size-1); size--; down(index); return temp; } /* * 替换堆顶部元素 * 1.替换堆顶元素 * 2.执行下潜,直到符合堆的特性 * */ public void replace(int replaced){ isEmptyeException(); array[0]=replaced; down(0); } /* * 堆尾部添加元素 * */ public boolean offered(int offered){ if(isFull()){ return false; } up(offered); size++; return true; } /* * 堆满异常 * */ public void isFulleException(){ if (isFull()){ throw new RuntimeException("堆已满"); } } /* * 堆空异常 * */ public void isEmptyeException(){ if (isEmpty()){ throw new RuntimeException("堆为空"); } } /* * 判满 * */ public boolean isFull(){ return size== array.length; } /* * 判空 * */ public boolean isEmpty(){ return size== 0; } }
7.3 堆排序
堆排序是一种基于比较的排序算法,它利用了完全二叉树(即堆)这种数据结构。堆通常可以分为两种:最大堆和最小堆。在最大堆中,父节点的值总是大于或等于其所有子节点的值;在最小堆中,父节点的值总是小于或等于其所有子节点的值。
堆排序的基本步骤如下:
构造初始堆:首先将待排序的序列构造成一个大顶堆(升序排序)或小顶堆(降序排序)。从最后一个非叶子节点开始,自底向上、自右向左进行调整。
堆调整:将堆顶元素(即当前未排序序列的最大元素或者最小元素)与末尾元素进行交换,然后移除末尾元素(因为它已经是最小或最大的),这样就完成了第一个元素的排序。此时,剩余未排序部分不再满足堆的性质,因此需要对剩余元素重新调整为堆。
重复步骤2:对剩余的n-1个元素重新构造堆,再将堆顶元素与末尾元素交换并移除末尾元素,直到整个序列都有序。
通过不断调整堆和交换堆顶元素,最终实现整个序列的排序。
堆排序的时间复杂度为O(nlogn),其中n为待排序元素的数量,空间复杂度为O(1)(原地排序)。由于其在最坏情况下的时间复杂度依然为O(nlogn),且不需要额外的存储空间,因此堆排序在处理大数据量时具有较好的性能表现。
public class HeapSort { public static void main(String[] args) { int[] array = {2, 3, 1, 7, 6, 4, 5}; HeapSort heapSort = new HeapSort(array); System.out.println(Arrays.toString(heapSort.array)); while (heapSort.size > 1) { heapSort.swap(0, heapSort.size-1); heapSort.size--; heapSort.down(0); } System.out.println(Arrays.toString(heapSort.array)); } int[] array; int size; public HeapSort(int[] array) { this.array = array; this.size = array.length; heapify(); } /* * 建堆: * 找到第一个非叶子结点 * 执行下潜,直到没有子节点 * */ private void heapify() { //最后一个非叶子结点 size/2-1 for (int i = size / 2 - 1; i >= 0; i--) { down(i); } } //执行下潜 parent 为需要下潜的索引,下潜到没有孩子,或者孩子都小于当前节点 private void down(int parent) { //左child索引 int leftChild = parent * 2 + 1; //右child索引 int rightChild = leftChild + 1; int max = parent; if (leftChild < size && array[max] < array[leftChild]) max = leftChild; if (rightChild < size && array[max] < array[rightChild]) max = rightChild; if (max != parent) { //下潜并再次调用 swap(parent, max); down(max); } } /* * 上浮操作 * */ public void up(int offered) { int child = size; //子元素索引等于0则到达了堆顶 while (child > 0) { int parent = (child - 1) / 2; if (offered > array[parent]) { array[child] = array[parent]; } else { break; } child = parent; } array[child] = offered; } //交换两个元素 private void swap(int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } }