zoukankan      html  css  js  c++  java
  • 堆排序以及Top K问题-Java实现

    一.问题背景

      如果做过参加过面试或者做过一些面试题,应该知道特别经典的top K问题,比如“找出无序数组中的最大或者最小K个数”:

      这种题可以排序后再输出最大或者最小的几个。但是不论是使用快排还是归并排序,毫无疑问,空间和时间复杂度的开销都是不满足面试官的要求的;而使用“堆”这种数据结构就比较好的解决这种问题,空间开销O(1),时间开销O(N logK)。

      需要注意的是,这里说的“堆”不是指堆栈的堆,而是一种数据结构,更准确的说是“完全二叉树”。

      下面就详细对堆这种数据结构进行介绍,注:本文的内容是在学习浙江大学何钦铭教授的数据结构课程后整理的。

      标注原文地址:https://www.cnblogs.com/-beyond/p/13084115.html

    二.堆的介绍

    2.1数组和链表实现优先队列

      再说Top K之前,先说一下调度算法。学过操作系统就知道,进程调度有多种算法,而最简单的就是“先到先服务”算法,这种算法可以简单的使用队列来实现,但是存在一个问题就是无法根据进程的优先级调整执行顺序,比如有两个进程,一个进程只是连接打印机打印一张纸,另外一个进程负责核心功能处理,很明显,核心功能处理的进程优先级更高,但是操作系统按照先到先服务算法来调度时,核心功能处理的进程并不是优先调度;

      这个时候可以切换为“按照优先级”进行调度,只需要每次选择最高优先级的进程执行,自己进行实现的话,有多种方式:

      

      仔细想一下,Top K的问题,和这里说的调度优先级其实是一样的问题。

      

    2.2堆的介绍

      1.堆是一种树结构,准确的说是“二叉树”,更准确的说是“完全二叉树”,根据完全二叉树的特点,可以使用数组来存储堆。

      2.堆是有序的,任一节点的关键字是其子树所有节点的最大值或者最小值。

      3.如果根节点是最大值的堆,称为“最大堆”或者“大顶堆”、“大根堆”;

      4.如果根节点是最小值的堆,称为“最小堆”、“小顶堆”、“小根堆”;

       

    三.堆的各种操作

    3.1创建堆

      因为堆满足完全二叉树的特点,所以可以使用数组来存储堆;下面是代码:

    package cn.ganlixin.tree.heap;
    
    import java.util.Scanner;
    
    /**
     * 描述:
     * 数据结构-堆(此处为最大堆)
     *
     * @author ganlixin
     * @create 2020-06-09
     */
    public class MaxHeap {
    
        /**
         * 保存堆元素的数组
         */
        private int[] elements;
    
        /**
         * 堆的大小
         */
        private int size;
    
        /**
         * 堆的容量
         */
        private int capacity;
    
        public MaxHeap() {
            this.size = 0;
            this.capacity = 0;
        }
    
        /**
         * 建堆并调整堆
         */
        public void createMaxHeap() {
            Scanner scanner = new Scanner(System.in);
    
            System.out.print("请输入堆的最大容量:");
            this.capacity = scanner.nextInt();
            this.size = 0;
    
            // 数组长度为容量加1,0号元素为哨兵元素
            this.elements = new int[this.capacity + 1];
            this.elements[0] = Integer.MAX_VALUE;
    
            System.out.print("请输入元素个数:");
            this.size = scanner.nextInt();
    
            if (this.size > this.capacity) {
                throw new RuntimeException("元素个数不能超过最大容量!");
            }
    
            System.out.print("请依次输入" + this.size + "个元素:");
            for (int i = 1; i <= this.size; i++) {
                elements[i] = scanner.nextInt();
            }
    
            buildHeap(); // 构建堆(因为初始状态,数组并不满足堆的有序性特点,所以需要进行调整构建,后面会介绍)
            System.out.println("已经完成堆的建立和调整");
        }
    }
    

      

    3.2堆的插入

      新元素,插入堆时,默认是插入到最后一个位置,这样保证满足完全二叉树的特点,但是可能不满足有序性的特点,所以需要进行一些调整;

      对于最大堆来说,任一根节点都比子节点的值大,所以如果插入的元素(默认是在最后),就需要和其父节点进行比较,如果比父节点大,则需要与父节点交换位置;这是一次调整,但是调整完以后,新插入的节点也许还会比新的父节点大,所以还需要继续比较,直到父节点比自己大,才停止比较,此时才找到新增元素应该插入的位置。

    /**
     * 向最大堆中新增一个元素<br>
     * 空间复杂度O(1),时间复杂度O(logN)
     *
     * @param newItem 新增的元素值
     */
    public void insertElement(int newItem) {
        // 新元素插入的位置,默认为最后一个元素的后面
        int nextIndex = ++this.size;
    
        // 将新元素放到最后,可以满足完全二叉树的要求,但是有序性不一定能保证,所以需要和父节点进行比较(父节点就是index/2)
        while (elements[nextIndex / 2] < newItem) {
            // 当父节点比新插入的节点小的时候,将父节点移到新节点准备插入的位置
            elements[nextIndex] = elements[nextIndex / 2];
    
            // 修改新节点准备插入的位置(此时为父节点的旧位置)
            nextIndex /= 2;
        }
    
        // 到此,nextIndex就指向了应该插入的位置(比子节点都大,比父节点小)
        elements[nextIndex] = newItem;
    }
    

      

    3.3堆元素的删除

      堆元素的删除(最大堆),是指将堆的最大值删除(也就是对顶元素给删除),删除对顶元素后,需要进行调整,默认是使用最后一个元素来顶替对顶元素,这样可以满足完全二叉树的特点,但是不一定满足有序性,所以需要调整;

      调整的过程,就是比较堆顶节点(此时已经替换为最后一个节点值)与子节点,当根节点比子节点小的时候,就交换根节点和子节点的位置,知道根节点大于子节点(左右子节点),才能确定根节点应该插入的位置。

    /**
     * 删除最大堆的最大值(堆顶元素)
     *
     * @return 堆最大值
     */
    public int deleteMaxItem() {
        // 第一个元素就是最大值
        int maxItem = elements[1];
    
        // 将最后一个元素取出来(删除,size减一),用来替补第一个元素(被删除的最大值)
        int lastItem = elements[size];
        size--;
    
        // 最后一个元素存放的位置,默认为1,表示第一个位置
        int insertIndex = 1;
    
        // 最后一个元素不一定是最大的,放到堆顶不一定合适,所以需要调整
        while (insertIndex * 2 <= size) {
            // childIndex默认指向左孩子
            int childIndex = insertIndex * 2;
    
            // 如果父节点有右孩子,并且左孩子比右孩子小,则childIndex指向较大的元素(也就是右孩子)
            if (childIndex != size && elements[childIndex] < elements[childIndex + 1]) {
                childIndex++;
            }
    
            // 当最后一个元素大于指向的元素时,证明找到了插入位置,则中断循环
            if (lastItem >= elements[childIndex]) {
                break;
            } else {
                // 最后一个元素比子节点小(比较大的节点小),则交换较大节点和父节点的位置
                elements[insertIndex] = elements[childIndex];
            }
    
            // 父节点指向空出来的子节点位置
            insertIndex = childIndex;
        }
    
        elements[insertIndex] = lastItem;
        return maxItem;
    }
    

      

    3.3将无序数组调整堆

      就以top K的问题来说,只需要建立一个堆的数据结构,然后弹出堆顶的K个元素,就是top K。

      但现在的问题是,提供的数组是无序的,怎么讲无序数组转换为堆:

      1.一种方式是从空堆开始一个一个添加元素,添加元素过程中会进行调整,元素插入完毕,堆也就建好了,这样的时间复杂度是N log(N),比较低效;

      2.直接在无序数组上进行调整,将期调整为堆结构,时间复杂度为O(logN);

      下面就介绍一下第二种方式。

      直接在无序数组上调整,不是从对顶元素开始调整,而是从最后一个元素进行调整,调整的过程和插入的过程相似:找到节点的父节点,以父节点为根调整为最大堆(根节点与左右子节点选最大值作为根),如此反复

    /**
     * 建立最大堆<br>
     * 两种方案<br>
     * 方案一:建立空堆,N个数,N次插入,时间复杂度O(N*logN),舍弃!<br>
     * 方案二:先顺序输入,满足完全二叉树要求,再进行调整堆,满足有序性,时间复杂度O(N)
     */
    private void buildHeap() {
        for (int i = size / 2; i > 0; i--) { // size/2是最后一个元素的父节点位置
            adjustHeap(i);
        }
    }
    
    /**
     * 以index指向的节点作为根,将该子堆调整为最大堆
     *
     * @param root 子堆的根节点
     */
    private void adjustHeap(int root) {
    
        // 取出根节点存的值
        int rootVal = elements[root];
    
        // insertIndex指向根节点值应该插入的位置
        int insertIndex = root;
        while (insertIndex * 2 <= size) {
            int childIndex = insertIndex * 2;
            if (childIndex != size && elements[childIndex] < elements[childIndex + 1]) {
                childIndex++;
            }
    
            // 如果根节点的值大于子节点,则证明找到了插入的位置
            if (rootVal > elements[childIndex]) {
                break;
            } else {
                elements[insertIndex] = elements[childIndex];
            }
    
            insertIndex = childIndex;
        }
    
        elements[insertIndex] = rootVal;
    }
    

      

    四.完成代码

      封装在MaxHeap.java中(最大堆)

    package cn.ganlixin.tree.heap;
    
    import java.util.Scanner;
    
    /**
     * 描述:
     * 数据结构-堆(此处为最大堆)
     * 完全二叉树,使用数组存储
     *
     * @author ganlixin
     * @create 2020-06-09
     */
    public class MaxHeap {
    
        /**
         * 保存堆元素的数组
         */
        private int[] elements;
    
        /**
         * 堆的大小
         */
        private int size;
    
        /**
         * 堆的容量
         */
        private int capacity;
    
        public MaxHeap() {
            this.size = 0;
            this.capacity = 0;
        }
    
        /**
         * 建堆并调整堆
         */
        public void createMaxHeap() {
            Scanner scanner = new Scanner(System.in);
    
            System.out.print("请输入堆的最大容量:");
            this.capacity = scanner.nextInt();
            this.size = 0;
    
            // 数组长度为容量加1,0号元素为哨兵元素
            this.elements = new int[this.capacity + 1];
            this.elements[0] = Integer.MAX_VALUE;
    
            System.out.print("请输入元素个数:");
            this.size = scanner.nextInt();
    
            if (this.size > this.capacity) {
                throw new RuntimeException("元素个数不能超过最大容量!");
            }
    
            System.out.print("请输入" + this.size + "个元素:");
            for (int i = 1; i <= this.size; i++) {
                elements[i] = scanner.nextInt();
            }
    
            buildHeap();
            System.out.println("已经完成堆的建立和调整");
        }
    
        /**
         * 建立最大堆<br>
         * 两种方案<br>
         * 方案一:建立空堆,N个数,N次插入,时间复杂度O(N*logN),舍弃!<br>
         * 方案二:先顺序输入,满足完全二叉树要求,再进行调整堆,满足有序性,时间复杂度O(N)
         */
        private void buildHeap() {
            if (isEmpty()) {
                throw new RuntimeException("堆为空,无法完成建堆操作");
            }
    
            for (int i = size / 2; i > 0; i--) {
                adjustHeap(i);
            }
        }
    
        /**
         * 以index指向的节点作为根,将该子堆调整为最大堆
         *
         * @param root 子堆的根节点
         */
        private void adjustHeap(int root) {
    
            // 取出根节点存的值
            int parentVal = elements[root];
    
            // parentIndex指向根节点值应该插入的位置
            int parentIndex = root;
            while (parentIndex * 2 <= size) {
                int childIndex = parentIndex * 2;
                if (childIndex != size && elements[childIndex] < elements[childIndex + 1]) {
                    childIndex++;
                }
    
                // 如果根节点的值大于子节点,则证明找到了插入的位置
                if (parentVal > elements[childIndex]) {
                    break;
                } else {
                    elements[parentIndex] = elements[childIndex];
                }
    
                parentIndex = childIndex;
            }
    
            elements[parentIndex] = parentVal;
        }
    
        /**
         * 向最大堆中新增一个元素<br>
         * 空间复杂度O(1),时间复杂度O(logN)
         *
         * @param newItem 新增的元素值
         */
        public void insertElement(int newItem) {
            if (isFull()) {
                throw new RuntimeException("堆已满,无法再添加元素");
            }
    
            // 新元素插入的位置,默认为最后一个元素的后面
            int nextIndex = ++this.size;
    
            // 将新元素放到最后,可以满足完全二叉树的要求,但是有序性不一定能保证,所以需要和父节点进行比较(父节点就是index/2)
            while (elements[nextIndex / 2] < newItem) {
                // 当父节点比新插入的节点小的时候,将父节点移到新节点准备插入的位置
                elements[nextIndex] = elements[nextIndex / 2];
    
                // 修改新节点准备插入的位置(此时为父节点的旧位置)
                nextIndex /= 2;
            }
    
            // 到此,nextIndex就指向了应该插入的位置(比子节点都大,比父节点小)
            elements[nextIndex] = newItem;
        }
    
        /**
         * 删除最大堆的最大值(堆顶元素)
         *
         * @return 堆最大值
         */
        public int deleteMaxItem() {
            if (isEmpty()) {
                throw new RuntimeException("堆为空,不能进行删除操作");
            }
    
            // 第一个元素就是最大值
            int maxItem = elements[1];
    
            // 将最后一个元素取出来(删除,size减一),用来替补第一个元素(被删除的最大值)
            int lastItem = elements[size];
            size--;
    
            // 最后一个元素存放的位置,默认为1,表示第一个位置
            int insertIndex = 1;
    
            // 最后一个元素不一定是最大的,放到堆顶不一定合适,所以需要调整
            while (insertIndex * 2 <= size) {
                // childIndex默认指向左孩子
                int childIndex = insertIndex * 2;
    
                // 如果父节点有右孩子,并且左孩子比右孩子小,则childIndex指向较大的元素(也就是右孩子)
                if (childIndex != size && elements[childIndex] < elements[childIndex + 1]) {
                    childIndex++;
                }
    
                // 当最后一个元素大于指向的元素时,证明找到了插入位置,则中断循环
                if (lastItem >= elements[childIndex]) {
                    break;
                } else {
                    // 最后一个元素比子节点小(比较大的节点小),则交换较大节点和父节点的位置
                    elements[insertIndex] = elements[childIndex];
                }
    
                // 父节点指向空出来的子节点位置
                insertIndex = childIndex;
            }
    
            elements[insertIndex] = lastItem;
            return maxItem;
        }
    
        /**
         * 打印排序后的堆
         */
        public void printSortedHeap() {
            for (int i = 1; i <= this.size; i++) {
                System.out.print(elements[i] + " ");
            }
            System.out.println();
        }
    
        /**
         * 判断堆是否已经满了(size>=capacity)
         *
         * @return true堆已满;false堆未满
         */
        public boolean isFull() {
            return this.size >= capacity;
        }
    
        /**
         * 判断堆是否为空
         *
         * @return true堆为空;false堆不为空
         */
        public boolean isEmpty() {
            return this.size == 0;
        }
    }

      测试:

    package cn.ganlixin.tree.heap;
    
    /**
     * 描述:
     * 测试最大堆
     *
     * @author ganlixin
     * @create 2020-06-09
     */
    public class Main {
        public static void main(String[] args) {
            MaxHeap maxHeap = new MaxHeap();
    
            // 输入元素,并调整堆
            maxHeap.createMaxHeap();
    
            System.out.print("输出堆:");
            maxHeap.printSortedHeap();
    
            int deleteMaxItem = maxHeap.deleteMaxItem();
            System.out.println("删除堆中最大元素:" + deleteMaxItem);
            System.out.print("输出堆:");
            maxHeap.printSortedHeap();
        }
    }
    

      输出:

    请输入堆的最大容量:10
    请输入元素个数:6
    请依次输入6个元素:8 5 9 6 4 2
    已经完成堆的建立和调整
    输出堆:9 6 8 5 4 2 
    删除堆中最大元素:9
    输出堆:8 6 2 5 4 
    

      

    五.再说Top K问题

      其实上面介绍完堆的各种操作后,对于Top K的问题已经能够解决了,此处以最大的top K问题为例:

      需要注意的是,在建堆的时候,并不是将整个数组的N个元素都调整,而是只调整前K个元素,让前K个元素保持堆的结构,也就是说,堆的容量,是K,而不是N。步骤如下:

      1.将前K个元素调整为最小堆(也称“小顶堆”、“小根堆”);

      2.依次将K+1后面的元素(看做新元素),与堆顶元素(堆的最小值)进行比较:

    a.如果堆顶元素比新元素要大,则新元素不用入堆,忽略;

    b.如果堆顶元素比新元素要小,则将新元素替换掉堆顶元素,然后进行调整堆(始终保证堆顶元素是堆中元素的最小值);

      3.不断重复步骤2,直至比较完N-K个元素;

      4.比较完后,堆中的元素就是最大的K个元素。

      说直白点,就是进行N-K+1次调整堆,整个流程的时间复杂度为O(N logK)

      如果需要按照排序输出K个元素,则进行K次删除堆顶元素即可(每次删除都会调整堆,保证堆顶最小)。

      下面是代码:

    package cn.ganlixin.tree.heap;
    
    import java.util.Scanner;
    
    /**
     * 描述:
     *
     * @author ganlixin
     * @create 2020-06-10
     */
    public class TopK {
    
        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
    
            System.out.print("请输入元素总个数:");
            int capacity = scanner.nextInt();
    
            System.out.print("请输入要找最大的几个数:");
            int k = scanner.nextInt();
    
            // 申请一个K+1的数组(因为建立的堆包含K个元素,而不是N个元素,0号元素用来做哨兵)
            int[] arr = new int[k + 1];
            arr[0] = Integer.MAX_VALUE;
    
            System.out.print("请输入全部元素:");
    
            // 先前K个元素进行建堆调整
            for (int i = 1; i <= k; i++) {
                arr[i] = scanner.nextInt();
            }
    
            // 先将前K个元素进行调整为最小堆(小顶堆)
            adjustHeap(arr, 1, k);
    
            // 继续处理后面的n-k个元素,和堆顶元素进行比较,如果比堆顶元素大,则替换堆顶元素,并进行调整堆
            for (int i = k + 1; i <= capacity; i++) {
                int newItem = scanner.nextInt();
                if (newItem > arr[1]) {
                    arr[1] = newItem; // 替换为新元素
    
                    // 每次整个堆都调整
                    adjustHeap(arr, 1, k);
                }
            }
    
            System.out.print("最大的" + k + "个数是:");
            for (int i = 1; i <= k; i++) {
                System.out.print(arr[i] + " ");
            }
        }
    
        /**
         * 将数组调整为满足最小堆的结构
         *
         * @param arr   要调整的数组
         * @param start 要调整的开始位置(index)
         * @param end   要调整的结束为止(index)
         */
        private static void adjustHeap(int[] arr, int start, int end) {
            for (int i = end / 2; i > 0; i--) { // end/2是最后一个节点的父节点
                int parentVal = arr[i];
                int parentIndex = i;
    
                while (parentIndex * 2 <= end) {
                    int childIndex = parentIndex * 2; // 左孩子节点
    
                    // childIndex指向两个子节点中较小的一个
                    if (childIndex != end && arr[childIndex] > arr[childIndex + 1]) {
                        childIndex++;
                    }
    
                    // 比较父节点和较大一个子节点的值,小顶堆需要父节点比子节点小
                    if (parentVal < arr[childIndex]) {
                        break;
                    } else {
                        arr[parentIndex] = arr[childIndex];
                    }
    
                    parentIndex = childIndex;
                }
    
                arr[parentIndex] = parentVal;
            }
        }
    }
    

      测试:

    请输入元素总个数:14
    请输入要找最大的几个数:5
    请输入全部元素:20 5 2 8 10 3 23 5 99 24 0 -7 8 100
    最大的5个数是:20 23 100 24 99 
    

      

  • 相关阅读:
    要搜索内容
    .net core 过滤器
    C# => 写法
    js 数组的forEach 函数
    .net core 下载文件 其他格式
    win10 1903 更改文字大小
    fetch 写法
    C# 匿名对象 增加属性
    ping —— 虚拟机
    selenium验证车贷计算器算法
  • 原文地址:https://www.cnblogs.com/-beyond/p/13084115.html
Copyright © 2011-2022 走看看