与堆和堆排序相关的问题

堆排序,相关,问题 · 浏览次数 : 184

小编点评

**算法和数据结构笔记** *堆排序:**利用优先队列实现,不断弹出并加入元素。**时间复杂度:O(N*logK),其中K是堆元素的数量。**空间复杂度:O(K)。***堆排序算法的实现**:利用数组实现,并利用**"swap"**方法进行元素交换。 ***排序算法**:**利用数组实现,并利用**"swap"**方法进行元素交换。**时间复杂度:O(N),其中N是数组长度。**空间复杂度:O(1)。***排序算法的实现**:利用**"Arrays.sort(arr)"**方法。 ***数据结构**:****数组**:利用数组实现,并利用**"swap"**方法进行元素交换。**时间复杂度:O(N),其中N是数组长度。**空间复杂度:O(1)。***数组的实现**:利用**"int[] arr"**数组。 *****数据结构的实现**:******优先队列**:利用优先队列实现,并利用**"swap"**方法进行元素交换。**时间复杂度:O(N*logK),其中K是堆元素的数量。**空间复杂度:O(K)。*****优先队列的实现**:利用数组实现,并利用**"swap"**方法进行元素交换。 *****数据结构的实现**:********数组**:利用数组实现,并利用**"swap"**方法进行元素交换。**时间复杂度:O(N),其中N是数组长度。**空间复杂度:O(1)。*****数组的实现**:利用**"int[] arr"**数组。

正文

与堆和堆排序相关的问题

作者:Grey

原文地址:

博客园:与堆和堆排序相关的问题

CSDN:与堆和堆排序相关的问题

堆结构说明

堆结构就是用数组实现的完全二叉树结构,什么是完全二叉树?可以参考如下两篇博客:

使用二叉树的递归套路来解决的问题

快速求完全二叉树的节点个数

完全二叉树中如果每棵子树的最大值都在顶部就是大根堆;完全二叉树中如果每棵子树的最小值都在顶部就是小根堆。

Java 语言中的 java.util.PriorityQueue,就是堆结构。

因为是用用数组表示完全二叉树,所以有如下两个换算关系,也就是堆的两种表示情况:

情况一,如果使用数组 0 号位置,那么对于 i 位置来说,它的:

  • 左孩子下标:2 * i + 1

  • 右孩子下标: 2 * i + 2

  • 父节点下标: (i - 1)/ 2

情况二,如果不用数组 0 号位置,那么对于 i 位置来说,它的:

  • 左孩子下标:2 * i 即:i << 1

  • 右孩子下标:2 * i + 1 即:i << 1 | 1

  • 父节点下标:i / 2 即:i >> 1

如果是小根堆(下标从 0 开始),

对每个元素 A[i],都需要满足 A[i * 2 + 1] >= A[i]A[i * 2 + 2] >= A[i]

如果是小根堆(下标从 0 开始),

对每个元素 A[i],都需要满足 A[i * 2 + 1] <= A[i]A[i * 2 + 2] <= A[i]

大根堆同理。

堆的数据结构定义如下,以大根堆为例,以下是伪代码

  // 大根堆
  public static class MyMaxHeap {
    // 用于存堆的数据
    private int[] heap;
    // 堆最大容纳数据的数量
    private final int limit;
    // 堆当前的容量
    private int heapSize;
    
    // 堆初始化
    public MyMaxHeap(int limit) {
      heap = new int[limit];
      this.limit = limit;
      heapSize = 0;
    }
    // 判断堆是否为空
    public boolean isEmpty() {
      return heapSize == 0;
    }
    // 判断堆是否满
    public boolean isFull() {
      return heapSize == limit;
    }
    public void push(int value) {
      // TODO 入堆
      // 注意:入堆后,也要保持大根堆的状态
    }
    public int pop() {
      // TODO 最大值出堆
      // 注意:出堆后,也要保持大根堆的状态
    }
  }

由上述数据结构定义可知,核心方法就是 pushpop,在每次操作后,要动态调整堆结构,使之保持大根堆的结构。

要完成这两个操作,就需要利用到堆的两个基本操作:

一个是 HeapInsert,一个是 Heapify。

Heapify 操作

Heapify 就是堆化的过程,以小根堆为例,示例说明

假设原始数组为:{3,2,1,4,5},初始状态如下

image

首先从头结点 3 开始,先找到 3 的左右孩子中较小的一个进行交换,现在较小的是右孩子 1,交换后是如下情况

image

互换后,3 号结点已经没有左右孩子了,停止操作。

然后按顺序继续处理 2 结点,2 结点已经比左右孩子都小了,无需进行交换。

image

接下来是 4 结点和 5 结点,都没有左右孩子,就无需再做操作。

整个流程就是,每个结点(假设为 X )去找自己的左右孩子中较小的那个(加设为 Y),然后X 和 Y 交换位置,交换后,看 X 是否继续有孩子结点,往复这个过程,一直到整个二叉树遍历完成。

完整代码如下:

public class Solution {
  public static void heapify(int[] arr) {
    if (arr == null || arr.length <= 1) {
      return;
    }
    for (int i = arr.length - 1; i >= 0; i--) {
      heapify(arr, i, arr.length);
    }
  }
  private static void heapify(int[] arr, int i, int n) {
    int left = 2 * i + 1;
    while (left < n) {
      int min = left + 1 < n && arr[left + 1] < arr[left] ? left + 1 : left;
      if (arr[i] <= arr[min]) {
        break;
      }
      swap(arr, i, min);
      i = min;
      left = 2 * i + 1;
    }
  }

  private static void swap(int[] arr, int i, int j) {
    if (i != j) {
      arr[i] = arr[i] ^ arr[j];
      arr[j] = arr[i] ^ arr[j];
      arr[i] = arr[i] ^ arr[j];
    }
  }
}

测评链接:LintCode 130 · Heapify

HeapInsert 操作

整个过程如下,以小根堆为例,从数组最后一个元素 X 开始,一直找其父节点 A,如果X 比 A 小,X 就和 A 交换,然后来到父节点 A,继续往上找 A 的父节点 B,如果 A 比 B 小,则把 A 和 B 交换……一直找到某个结点的头结点不比这个结点大,这个节点就可以停止移动了。以一个示例说明

假设原始数组为:{3,2,1,4,5},初始状态如下

image

从最后一个元素 5 开始,5 的父节点是 2,正好满足,无需继续往上找父节点,然后继续找倒数第二个位置 4 的父节点,也比父节点 2 要大,所以 4 节点也不需要动。

image

接下来是 1 结点,其父结点是 3 结点,所以此时要把 3 和 1 交换,变成如下样子

image

然后是 2 结点,2 结点的父节点 是 1 ,无需交换,然后是 1 结点,头结点,停止遍历,整个过程完毕。

HeapInsert 操作的完整代码如下

    private void heapInsert(int[] arr, int i) {
      while (arr[i] > arr[(i - 1) / 2]) {
        // 一直网上找
        swap(arr, i, (i - 1) / 2);
        i = (i - 1) / 2;
      }
    }

无论是 HeapInsert 还是 Heapify,整个过程时间复杂度是 O(logN),N 是二叉树结点个数,其高度是 logN。

有了 Heapify 和 HeapInsert 两个过程,整个堆的 pop 操作和 push 操作都迎刃而解。

    public void push(int value) {
    // 堆满了,不能入堆
      if (heapSize == limit) {
        throw new RuntimeException("heap is full");
      }
      // 把最后一个位置填充上,然后往小做 heapInsert 操作
      heap[heapSize] = value;
      // value  heapSize
      heapInsert(heap, heapSize++);
    }

    public int pop() {
      // 弹出的值一定是头结点
      int ans = heap[0];
      // 头结点弹出后,直接放到最后一个位置,然后往上做 heapify
      // 由于 heapSize 来标识堆的大小,heapSize--,就等于把头结点删掉了。
      swap(heap, 0, --heapSize);
      heapify(heap, 0, heapSize);
      return ans;
    }

堆排序

了解了 HeapInsert 和 Heapify 过程,堆排序过程,也就是利用了这两个方法,流程如下

第一步:先让整个数组都变成大根堆结构,建立堆的过程:

如果使用从上到下的方法,时间复杂度为O(N*logN)

如果使用从下到上的方法,时间复杂度为O(N)

第二步:把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,一直周而复始,时间复杂度为O(N*logN)

第三步:把堆的大小减小成0之后,排序完成。

堆排序额外空间复杂度O(1)

堆排序完整代码如下

import java.util.Arrays;
import java.util.PriorityQueue;

public class Code_HeapSort {

  public static void heapSort(int[] arr) {
    if (arr == null || arr.length < 2) {
      return;
    }
    // O(N*logN)
    //  for (int i = 0; i < arr.length; i++) { // O(N)
    //   heapInsert(arr, i); // O(logN)
    //  }
    // O(N)
    for (int i = arr.length - 1; i >= 0; i--) {
      heapify(arr, i, arr.length);
    }
    int heapSize = arr.length;
    swap(arr, 0, --heapSize);
    // O(N*logN)
    while (heapSize > 0) { // O(N)
      heapify(arr, 0, heapSize); // O(logN)
      swap(arr, 0, --heapSize); // O(1)
    }
  }

  // arr[index]刚来的数,往上
  public static void heapInsert(int[] arr, int index) {
    while (arr[index] > arr[(index - 1) / 2]) {
      swap(arr, index, (index - 1) / 2);
      index = (index - 1) / 2;
    }
  }

  // arr[index]位置的数,能否往下移动
  public static void heapify(int[] arr, int index, int heapSize) {
    int left = index * 2 + 1;
    while (left < heapSize) {
      int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
      largest = arr[largest] > arr[index] ? largest : index;
      if (largest == index) {
        break;
      }
      swap(arr, largest, index);
      index = largest;
      left = index * 2 + 1;
    }
  }

  public static void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }
}

与堆排序相关的一个问题

题目描述

已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离一定不超过k,并且k相对于数组长度来说是比较小的,请选择一个合适的排序策略,对这个数组进行排序。(从小到大)

本题的主要思路就是利用堆排序:

先把 k 个数进堆,然后再加入一个,弹出一个(加入和弹出过程一定不会超过 k 次),最后堆里面剩下的继续弹出即可。

时间复杂度是O(N*logK)

完整代码如下(含对数程序)

import java.util.Arrays;
import java.util.PriorityQueue;

public class Code_DistanceLessK {
  public static void sortedArrDistanceLessK(int[] arr, int k) {
    k = Math.min(arr.length - 1, k);
    PriorityQueue<Integer> heap = new PriorityQueue<>();
    int i = 0;
    for (; i < k + 1; i++) {
      heap.offer(arr[i]);
    }
    int index = 0;
    for (; i < arr.length; i++) {
      heap.offer(arr[i]);
      arr[index++] = heap.poll();
    }
    while (!heap.isEmpty()) {
      arr[index++] = heap.poll();
    }
  }

  // for test
  public static void comparator(int[] arr, int k) {
    Arrays.sort(arr);
  }

  // for test
  public static int[] randomArrayNoMoveMoreK(int maxSize, int maxValue, int K) {
    int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
    for (int i = 0; i < arr.length; i++) {
      arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
    }
    // 先排个序
    Arrays.sort(arr);
    // 然后开始随意交换,但是保证每个数距离不超过K
    // swap[i] == true, 表示i位置已经参与过交换
    // swap[i] == false, 表示i位置没有参与过交换
    boolean[] isSwap = new boolean[arr.length];
    for (int i = 0; i < arr.length; i++) {
      int j = Math.min(i + (int) (Math.random() * (K + 1)), arr.length - 1);
      if (!isSwap[i] && !isSwap[j]) {
        isSwap[i] = true;
        isSwap[j] = true;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
      }
    }
    return arr;
  }

  // for test
  public static int[] copyArray(int[] arr) {
    if (arr == null) {
      return null;
    }
    int[] res = new int[arr.length];
    for (int i = 0; i < arr.length; i++) {
      res[i] = arr[i];
    }
    return res;
  }

  // for test
  public static boolean isEqual(int[] arr1, int[] arr2) {
    if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
      return false;
    }
    if (arr1 == null) {
      return true;
    }
    if (arr1.length != arr2.length) {
      return false;
    }
    for (int i = 0; i < arr1.length; i++) {
      if (arr1[i] != arr2[i]) {
        return false;
      }
    }
    return true;
  }

  // for test
  public static void printArray(int[] arr) {
    if (arr == null) {
      return;
    }
    for (int j : arr) {
      System.out.print(j + " ");
    }
    System.out.println();
  }

  // for test
  public static void main(String[] args) {
    System.out.println("test begin");
    int testTime = 500000;
    int maxSize = 100;
    int maxValue = 100;
    boolean succeed = true;
    for (int i = 0; i < testTime; i++) {
      int k = (int) (Math.random() * maxSize) + 1;
      int[] arr = randomArrayNoMoveMoreK(maxSize, maxValue, k);
      int[] arr1 = copyArray(arr);
      int[] arr2 = copyArray(arr);
      sortedArrDistanceLessK(arr1, k);
      comparator(arr2, k);
      if (!isEqual(arr1, arr2)) {
        succeed = false;
        System.out.println("K : " + k);
        printArray(arr);
        printArray(arr1);
        printArray(arr2);
        break;
      }
    }
    System.out.println(succeed ? "Nice!" : "Fucking fucked!");
  }
}

更多

算法和数据结构笔记

参考资料

算法和数据结构体系班-左程云

与与堆和堆排序相关的问题相似的内容:

与堆和堆排序相关的问题

与堆和堆排序相关的问题 作者:Grey 原文地址: 博客园:与堆和堆排序相关的问题 CSDN:与堆和堆排序相关的问题 堆结构说明 堆结构就是用数组实现的完全二叉树结构,什么是完全二叉树?可以参考如下两篇博客: 使用二叉树的递归套路来解决的问题 快速求完全二叉树的节点个数 完全二叉树中如果每棵子树的最

加强堆结构说明

加强堆结构说明 作者:Grey 原文地址: 博客园:加强堆结构说明 CSDN:加强堆结构说明 关于堆和堆排序的说明 可以参考这篇博客:与堆和堆排序相关的问题 基础的堆结构可以实现数据入堆和出堆以后(即: 调用堆的 pop 和 push 方法),使用O(logN)的时间复杂度可以将堆调整好,如果使用的

C#堆排序算法

前言 堆排序是一种高效的排序算法,基于二叉堆数据结构实现。它具有稳定性、时间复杂度为O(nlogn)和空间复杂度为O(1)的特点。 堆排序实现原理 构建最大堆:将待排序数组构建成一个最大堆,即满足父节点大于等于子节点的特性。 将堆顶元素与最后一个元素交换:将最大堆的堆顶元素与堆中的最后一个元素交换位

[转帖]【JVM】堆内存与栈内存详解

堆和栈的定义 java把内存分成栈内存和堆内存。 (1)栈内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。 当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另

DevSecOps 需要知道的十大 K8s 安全风险及建议

Kubernetes (K8s)是现代云原生世界中的容器管理平台。它实现了灵活、可扩展地开发、部署和管理微服务。K8s 能够与各种云提供商、容器运行时接口、身份验证提供商和可扩展集成点一起工作。然而 K8s 的集成方法可以在任何基础设施上运行任何容器化应用程序,这使得围绕 K8s 和其上的应用程序堆

JVM内存学习 2.0

先说一下结果 1. Linux的内存分配是惰性分配的. APP申明了 kernel并不会立即进行初始化和使用. 2. JVM的内存主要分为, 堆区, 非堆区, 以及jvm使用的其他内存. 比如直接内存等. 3. top看到的内存与pmap 查询出来的内存基本一样. top的RES和pmap的RSS基

5.1 缓冲区溢出与攻防博弈

在黑客安全圈子中,基于内存攻击技术的攻击手段在随着时代的变化而不断发展着,内存攻击是指通过利用软件的安全漏洞,构造恶意的输入,从而使正常程序造成拒绝服务或者是远程获得控制权,内存攻击技术中最先登上历史舞台的就是缓冲区溢出漏洞,时至今日能够被广泛利用的并具有较大破坏性的高危漏洞(CVE)几乎都属于缓冲区溢出。首先读者应该明白缓冲区溢出(Buffer Overflow),它分为栈溢出与堆溢出,此类漏洞

[转帖]12.JVM运行时数据区之虚拟机栈概述

`https://blog.csdn.net/u011069294/article/details/107050001` 目录 1. 内存中的栈与堆2.栈的优点 1. 内存中的栈与堆 栈是运行时单位,堆是存储的单位。 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。 堆解决的是数据存储的问

[转帖]fastJson与一起堆内存溢出'血案'

https://www.jianshu.com/p/876d443c2162 现象 QA同学反映登录不上服务器 排查问题1--日志级别 查看log,发现玩家登录的时候抛出了一个java.lang.OutOfMemoryError 大概代码是向Redis序列化一个PlayerMirror镜像数据,但是

驱动开发:内核远程堆分配与销毁

在开始学习内核内存读写篇之前,我们先来实现一个简单的内存分配销毁堆的功能,在内核空间内用户依然可以动态的申请与销毁一段可控的堆空间,一般而言内核中提供了`ZwAllocateVirtualMemory`这个函数用于专门分配虚拟空间,而与之相对应的则是`ZwFreeVirtualMemory`此函数则用于销毁堆内存,当我们需要分配内核空间时往往需要切换到对端进程栈上再进行操作,接下来`LyShark