算法17:堆结构_相关面试题

什么是堆结构

1)堆结构就是用数组实现的完全二叉树结构
2)完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
3)完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
4)堆结构的heapInsert与heapify操作
5)堆结构的增大和减少
6)优先级队列结构,就是堆结构

 

如何构建堆结构?

package code2.排序_03.堆排序;

/**
 * 构建大根堆
 */
public class Code01_MyMaxHeap {

    private int limit;
    private int[] heap;
    private int size;

    public Code01_MyMaxHeap(int limit) {
        this.limit = limit;
        heap = new int[this.limit];
        size = 0;
    }

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

    //上移式构建大根堆
    public void heapInsert (int[] arr, int index) {
        while (index < this.limit && arr[index] > arr[(index-1)/2]) {
            swap(arr, index, (index-1)/2);
            index = (index-1)/2;
        }
    }

    //下层式构建大根堆
    public void heapify(int[] arr, int index, int length) {
        /**
         * 二叉树性质5:
         * 1. 如果index为0,则代表这是第一个节点,则无双亲节点
         * 2. 如果index*2 > n(n代表长度),则节点无左孩子。 java中下标是从0开始,因此 index*2 + 1 > n
         * 3. 如果index*2+1 >n, 则代表无右孩子。 java中下标是从0开始,因此 index*2 + 2 > n
         */
        int left = index * 2 + 1;
        while (left < length) {
            int bigger = (left + 1) < length && arr[left] < arr[left + 1] ? left + 1: left;
            if(arr[index] >= arr[bigger]) {
                break;
            }
            swap(arr, index, bigger);
            index = bigger;
            left = index * 2 + 1;
        }
    }

    public void push1(int value) {
        if (size == this.limit) {
            throw new RuntimeException("堆已满,无法添加新数据!");
        }
        heap[size] = value;
        heapInsert(heap, size++);
    }

    public void push2(int[] arr) {
        if (size == this.limit) {
            throw new RuntimeException("堆已满,无法添加新数据!");
        }
        heap = arr;
        for (int i = arr.length-1; i >=0; i--,size++) {
            heapify(heap, i, arr.length);
        }
    }

    public int pop() {
        if (size < 0) {
            throw new RuntimeException("堆为空,无法获取值");
        }
        int ans = heap[0];
        //swap(heap, 0, heap.length -1);
        /**
         * 好处:
         * 1. --size正好和数组长度相同
         * 2. 在heapify中,作为数组长度使用。变相的减少了1个长度。
         * 使最后的11不在作为父节点下沉的判断条件。参考代码36行
         */
        swap(heap, 0, --size);
        heapify(heap, 0, size);
        return ans;
    }

    public static void main(String[] args) {

       //case 1: 上移式构建大根堆,结果是11,7,8,6,4,7,0,3
    /*   int[] arr = {3,7,11,4,6,8,0,7};
        MyMaxHeap my = new MyMaxHeap(arr.length);
        for (int i =0; i < arr.length; i++) {
            my.push1(arr[i]);
        }*/


        //case 2: 下沉式构建大根堆。 大根堆的结果是11,7,8,7,6,3,0,4
        int[] arr = {3,7,11,4,6,8,0,7};
        Code01_MyMaxHeap my = new Code01_MyMaxHeap(arr.length);
        my.push2(arr);

        //case 3: 删除大根堆最大值,要求剩下的节点依旧维持大根堆的结构
        int value = my.pop();
        System.out.println("弹出值为:" + value);
    }
}

 

 上面的代码不难,相信阅读完代码以后,会对堆结构有一点了解。提到堆结构,离不开的就是堆排序问题了。

 

堆排序

package code2.排序_03.堆排序;

/**
 * 构建大根堆
 * 1,先让整个数组都变成大根堆结构,建立堆的过程:
 *     1)从上到下的方法,时间复杂度为O(N*logN)
 *     2)从下到上的方法,时间复杂度为O(N)
 * 2,把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,一直周而复始,时间复杂度为O(N*logN)
 * 3,堆的大小减小成0之后,排序完成
 */
public class Code02_HeapSort {

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

    public void printArray(int[] arr) {
        if (arr == null) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

    public int[] heapSort(int[] arr)
    {
        if (arr == null || arr.length < 2) {
            return arr;  //不满足条件,就返回原数组
        }

        int size = arr.length;
        //首先构造大根堆, 从下网上,时间复杂度为O(N)
        for (int i = size-1; i>=0; i--) {
            heapify(arr, i, size);
        }
        //将最大值放在大根堆的最后
        swap(arr, 0, --size);
        //迭代,重新生成大根堆,并且将最大值放在最后.
        //因为第一次已经交换完毕,所以接下来的会逐个减少,知道最后一个是根元素为止
        while(size > 0) {
            /*for (int i = size; i>=0; i--) {
                heapify(arr, i, size);
            }*/
            //下面一行代码,等价与上方的一个for循环。 for循环是从数组的最后
            //一个元素开始下沉。而下方代码是基于上一次调整完成以后,新的数组的根节点开始下沉
            //从上往下,时间复杂度为O(N*logN)
            heapify(arr, 0, size);
            swap(arr, 0, --size);
        }

        return arr;
    }

    //下层式构建大根堆
    public void heapify(int[] arr, int index, int length) {
        /**
         * 二叉树性质5:
         * 1. 如果index为0,则代表这是第一个节点,则无双亲节点
         * 2. 如果index*2 > n(n代表长度),则节点无左孩子。 java中下标是从0开始,因此 index*2 + 1 > n
         * 3. 如果index*2+1 >n, 则代表无右孩子。 java中下标是从0开始,因此 index*2 + 2 > n
         */
        int left = index * 2 + 1;
        while (left < length) {
            int bigger = (left + 1) < length && arr[left] < arr[left + 1] ? left + 1: left;
            if(arr[index] >= arr[bigger]) {
                break;
            }
            swap(arr, index, bigger);
            index = bigger;
            left = index * 2 + 1;
        }
    }

    public static void main(String[] args) {

        //case 2: 下沉式构建大根堆。 大根堆的结果是11,7,8,7,6,3,0,4
        int[] arr = {3,7,11,4,6,8,0,7};
        Code02_HeapSort heap = new Code02_HeapSort();
        int[] sortArr = heap.heapSort(arr);
        heap.printArray(sortArr);
    }
}

了解堆结构和堆排序只是基础,有了以上的铺垫,才能更好的帮助我们理解堆相关的算法以及面试题。

 

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

这道题,其实想要考查的就是堆的相关知识。试想一下,如果k是一个比较小的数,例如3. 每个元素移动的距离一点不会超过3.  假设,每个数移动距离都是都是最大距离 3,那么如果数组下标为 0,1,2怎么办 ?必然只有往后移动才可以. 而从 k + 1开始,也就是从4开始,数据既可以往前移动,也可以往后移动。 于此同时,当 数据达到4个的时候,前 个数必然能确定一个最小值(或者最大值),因为每个元素移动的距离一定不超过k。 借助于堆结构对象 PriorityQueue 便能够很好的解决这个问题。 代码如下:

package code2.排序_03.堆排序;

import jdk.internal.org.objectweb.asm.util.CheckAnnotationAdapter;

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

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

    public static void printArray(int[] arr) {
        if (arr == null) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

    // for test
    public static int[] randomArrayNoMoveMoreK(int maxSize, int maxValue, int k) {
        double val = Math.random();
        int[] arr = new int[(int) ((maxSize + 1) * val)];
        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;
    }

    public static void sort (int[] arr, int k)
    {
        if (arr == null || arr.length < 2 || k == 0) {
            return ;
        }

        //利用系统提供的堆结构,可以直接实现数组的排序
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        int index = 0;
        /**
         * 每个数组的移动距离都不超过k, 也就是每个数组要么最多往前移动k个,
         * 要么最多往后移动k个数。
         * 从0 到 k-1, 如果要想移动k个数,那么只能往后移动,如果是往前移动
         * 那么丢在系统堆中会默认进行排序
         */
        for (;index <= k-1;index++) {
            heap.add(arr[index]);
        }

        int i = 0;
        /**
         * 从此时开始,数据存在往前移动或者往后移动 k 个数的情况
         * 因此,如果是往前移动 k 个,那么丢入堆中,poll()出来的必然是最新一次丢入
         * 的数据; 如果是往后移动k个,那么堆中的最小值肯定是之前丢进去的,直接拿出来即可
         *
         * 假设k =5, {6,5,4,3,2}, 那么如果出现 1, 那么poll出来的必然是1. 因为数组基本
         * 有序且最多移动不超过k, 那么从左到右出现 k+1个数的时候,此时必然存在最小数
         */
        for (; index < arr.length; index++, i++) {
            heap.add(arr[index]);
            arr[i] = heap.poll();
        }

        while (!heap.isEmpty()) {
            arr[i++] = heap.poll();
        }
    }

    public static void main(String[] args) {

        int maxSize = 100;
        int maxValue = 100;
        int k = 5;

        int[] arr = randomArrayNoMoveMoreK(maxSize,maxValue,k);
        //排序前
        printArray(arr);
        sort(arr, k);
        //排序后
        printArray(arr);
    }
}

 

算法二 :最大线段重合问题(用堆实现)

 给定很多线段,每个线段都有两个数[start, end],表示线段开始位置和结束位置,左右都是闭区间

规定:
1)线段的开始和结束位置一定都是整数值
2)线段重合区域的长度必须>=1
返回线段最多重合区域中,包含了几条线段

 

 

 

解题思路:这个题中每个线段都有一个开始位置start 和 一个结束位置 end。 如果我们按照开始位置start进行初次排序,则会得到一个开始位置有序的线段数组,类似于 {{1,7}, {1,6}, {2,5}, {,63}......}. 然后再遍历数组每个线段的开始位置和结束位置。如果新线段开始的位置值(已经有序)大于堆结构中存储的线段末尾值,那么删除对中对应数据,并且将新线段的 end值存入数组中。为什么能这么操作?因为一开始我们便对线段的start进行了排序,以后的线段只能大于或者等于现在的线段的start值,这一点是非常重要。

package code2.排序_03.堆排序;

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

/**
 * 最大线段重合问题(用堆的实现)
 * 给定很多线段,每个线段都有两个数[start, end],
 * 表示线段开始位置和结束位置,左右都是闭区间
 * 规定:
 * 1)线段的开始和结束位置一定都是整数值
 * 2)线段重合区域的长度必须>=1
 * 返回线段最多重合区域中,包含了几条线段
 */
public class Code04_MaxLineSegment {

    class Line {
        public int start;
        public int end;

        Line(int start,int end) {
            this.start = start;
            this.end = end;
        }
    }

    class MyComparator implements Comparator<Line> {

        @Override
        public int compare(Line o1, Line o2) {
            return o1.start - o2.start;
        }
    }
    /**
     *  随机构造线段
     * @param N  代码线段的条数
     * @param L  代表线段的开始值
     * @param R  代表线段的结束值
     * @return
     */
    public int[][] generateLines(int N, int L, int R) {
        int size = (int) (Math.random() * N) + 1;
        int[][] ans = new int[size][2];
        for (int i = 0; i < size; i++) {
            int a = L + (int) (Math.random() * (R - L + 1));
            int b = L + (int) (Math.random() * (R - L + 1));
            if (a == b) {
                b = a + 1;
            }
            ans[i][0] = Math.min(a, b);
            ans[i][1] = Math.max(a, b);
        }
        return ans;
    }


    public int maxValue (int[][] segments)
    {
        Line[] lines = new Line[segments.length];
        for(int i =0; i < segments.length; i++) {
            int[] arr = segments[i];
             lines[i] = new Line(arr[0], arr[1]);
        }
        //按照Line对象的start进行排序
        //end > start, 所以此处的排序非常重要
        //比如, m = { {5,7}, {1,4}, {2,6} } 跑完如下的code之后变成:{ {1,4}, {2,6}, {5,7} }
        Arrays.sort(lines, new MyComparator());

        //小根堆,默认升序
        PriorityQueue<Integer> queue = new PriorityQueue();
        int max = 0;
        for (Line line : lines) {
            //正因为右了上方Arrays.sort的排序,我们才可以进行此处while的操作
            while (!queue.isEmpty() && queue.peek() <= line.start) {
                queue.poll();
            }
            queue.add(line.end);
            //这一段代码用的非常的巧妙。举个例子{{1,3},{1,4},{2,5},{4,8}}
            //{2,3}共出现了3次,{4,8}出现的时候会将1,3},{1,4}都删除。此时的size为2,
            //但是,我们依旧可以获得最大值为3
            max = Math.max(max, queue.size());
        }

        return max;
    }

    //仅仅为了测试而准备
    public int maxCover1(int[][] lines) {
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < lines.length; i++) {
            min = Math.min(min, lines[i][0]);
            max = Math.max(max, lines[i][1]);
        }
        int cover = 0;
        for (double p = min + 0.5; p < max; p += 1) {
            int cur = 0;
            for (int i = 0; i < lines.length; i++) {
                if (lines[i][0] < p && lines[i][1] > p) {
                    cur++;
                }
            }
            cover = Math.max(cover, cur);
        }
        return cover;
    }

    public static void main(String[] args) {
        /**
         * 首先测试简单case
         * {{1,5},{2,9},{1,4},{1,3},{1,,6}} ==>  我们知道重合最多的的是{2,3},共计5次
         */
        Code04_MaxLineSegment test = new Code04_MaxLineSegment();
        //int[][] arr = {{1,5},{2,9},{1,4},{1,3},{1,6}};
        int[][] arr = {{1,3},{1,4},{2,5},{4,8}};
        int max = test.maxValue(arr);
        System.out.println("最多处包含的线段为: " + max);


        //下面进行一些大批量随机数的测试
        int N = 100;
        int L = 0;
        int R = 200;
        int testTimes = 200000;
        System.out.println("test end");
        for (int i = 0; i < testTimes; i++) {
            int[][] lines = test.generateLines(N, L, R);
            int ans1 = test.maxCover1(lines);
            int ans2 = test.maxValue(lines);
            if (ans1 != ans2) {
                System.out.println("Oops!");
            }
        }
        System.out.println("test end");
    }
}

 

算法三:手写加强堆

系统提供的堆无法做到的事情:
1)已经入堆的元素,如果参与排序的指标方法变化,
系统提供的堆无法做到时间复杂度O(logN)调整!都是O(N)的调整!
2)系统提供的堆只能弹出堆顶,做不到自由删除任何一个堆中的元素,
或者说,无法在时间复杂度O(logN)内完成!一定会高于O(logN)
根本原因:无反向索引表

 

所以,有了加强堆,我们便可以新增很多新的功能。比如,删除堆中的任意一个元素,修改任意一个元素的值等等。所以,手写加强对非常的重要。

package code2.排序_03.堆排序;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;

/**
 * 系统提供的堆无法做到的事情:
 * 1)已经入堆的元素,如果参与排序的指标方法变化,
 * 系统提供的堆无法做到时间复杂度O(logN)调整!都是O(N)的调整!
 * 2)系统提供的堆只能弹出堆顶,做不到自由删除任何一个堆中的元素,
 * 或者说,无法在时间复杂度O(logN)内完成!一定会高于O(logN)
 * 根本原因:无反向索引表
 */
public class Code05_EnhanceHeap<T> {

    private ArrayList<T> heap;   //动态数组替换 int[]
    private HashMap<T, Integer> indexMap;  //索引表
    private int heapSize;
    private Comparator<? super T> comp;    //比较器,可以根据客户提供的比较器进行升序和降序排序

    Code05_EnhanceHeap(Comparator<? super T> comp) {
        heap = new ArrayList<>();
        indexMap = new HashMap<>();
        heapSize = 0;
        this.comp = comp;
    }

    private void swap(int i, int j) {
        T o1 = heap.get(i);
        T o2 = heap.get(j);
        heap.set(i, o2);
        heap.set(j, o1);
        indexMap.put(o2, i);
        indexMap.put(o1, j);
    }

    private void resign(T obj) {
        //下面两个方法只会执行其中的一个
        heapInsert(indexMap.get(obj));
        heapify(indexMap.get(obj));
    }

    private void heapInsert (int index) {
        //大根堆還是小根堆,完全取決於比較器。
        //如果比較器默認是升序,則生成大根堆
        //如果比較器默認是降序,則生成小根堆
        while (comp.compare(heap.get(index), heap.get((index-1)/2)) < 0) {
            swap(index, (index-1)/2);
            index = (index-1)/2;
        }
    }

    private void heapify(int index) {
        /**
         * 二叉树性质5:
         * 1. 如果index为0,则代表这是第一个节点,则无双亲节点
         * 2. 如果index*2 > n(n代表长度),则节点无左孩子。 java中下标是从0开始,因此 index*2 + 1 > n
         * 3. 如果index*2+1 >n, 则代表无右孩子。 java中下标是从0开始,因此 index*2 + 2 > n
         */
        int left = index * 2 + 1;
        while (left < heapSize) {
            int best = (left + 1) < heapSize && comp.compare(heap.get(left+1), heap.get(left)) < 0 ? left + 1: left;
            //父节点小於等于子节点,无需下沉
            if(comp.compare(heap.get(index), heap.get(best)) <= 0) {
                break;
            }
            swap(index, best);
            index = best;
            left = index * 2 + 1;
        }
    }

    public boolean isEmpty () {
        return heapSize == 0;
    }

    public int size () {
        return heapSize;
    }

    public boolean contains(T obj) {
        return indexMap.containsKey(obj);
    }

    public T peek() {
        return heap.get(0);
    }

    public void push(T obj) {
        heap.add(obj);
        indexMap.put(obj, heapSize);
        heapInsert(heapSize++);
    }

    //删除堆顶
    public T pop() {
        T ans = heap.get(0);
        swap(0, heapSize-1);
        indexMap.remove(ans);
        heap.remove(--heapSize);  //此处删除,实际上heap中元素就被删除了,长度需要减1
        heapify(0);
        return ans;
    }

    // 请返回堆上的所有元素
    public List<T> getAllElements() {
        List<T> ans = new ArrayList<>();
        for (T c : heap) {
            ans.add(c);
        }
        return ans;
    }

    public void remove(T obj) {
        T replace = heap.get(heapSize - 1); //记录最后一个元素
        int index = indexMap.get(obj);      //获取要删除数的索引
        indexMap.remove(obj);               //从索引表中删除当前数
        heap.remove(--heapSize);            //从堆中删除最后一个数
        if (obj != replace) {
            heap.set(index, replace);
            indexMap.put(replace, index);
            resign(replace);
        }
    }

    public static void printArray(List list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        for (int i = 0; i < list.size(); i++) {
            Customer c = (Customer) list.get(i);
            System.out.println(c.toString());
        }
        System.out.println();
    }

    static class Customer {
        int id;
        int age;
        String name;
        Customer(int id, int age, String name) {
           this.id = id;
           this.name = name;
           this.age = age;
        }

        @Override
        public String toString() {
            return "id :" + id + " age: " + age + " name " + name;
        }
    }

    static  class MyIdComprator implements Comparator<Customer> {

        /**
         * 統一規範
         * 如果為負數, 認為第一個數排前面
         * 如果整數,認為第二個數排前面
         * 0 無所謂
         * @param o1
         * @param o2
         * @return
         */
        @Override
        public int compare(Customer o1, Customer o2) {
            //按照id排序,如果ID相同则按照年龄进行排序
            return o1.id - o2.id;
        }
    }

    public static void main(String[] args) {
        //case1: 测试加强堆的构建
        Code05_EnhanceHeap test = new Code05_EnhanceHeap(new MyIdComprator());
        test.push(new Customer(1, 11, "zhangsan"));

        Customer c2 = new Customer(5, 12, "lisi");
        test.push(c2);

        test.push(new Customer(2, 16, "malong"));
        test.push(new Customer(3, 11, "haoy"));
        test.push(new Customer(4, 13, "pp"));
        Customer c6 = new Customer(6, 12, "cy");
        test.push(c6);
        test.push(new Customer(2, 15, "leo"));
        test.push(new Customer(7, 12, "kiki"));
        printArray(test.getAllElements());

        //case2:测试常用方法
        System.out.println("============case 2=============");
        System.out.println(test.isEmpty());
        System.out.println(test.contains(c2));
        System.out.println(test.size());
        System.out.println(test.peek());  //peek是获取不删除
        System.out.println(test.size());

        //case3 测试删除一个节点,并且还是保大根堆结构
        System.out.println("============case 3=============");
        test.pop();
        printArray(test.getAllElements());

        System.out.println("============case 4=============");
        test.push(new Customer(9, 12, "yy"));
        printArray(test.getAllElements());

        System.out.println("============case 5=============");
        test.remove(c6);
        printArray(test.getAllElements());
        System.out.println(test.contains(c6));

        System.out.println("============case 6 取出全部元素,看看是否有序=============");
        int i =0;
        while (!test.isEmpty()) {
            System.out.println(" 第 " + i + " 次:" + test.pop());
        }
    }
}

 

posted @ 2023-02-15 22:16  街头小瘪三  阅读(22)  评论(0编辑  收藏  举报