优先级队列(堆)、java 对象的比较

一、二叉树的顺序存储

  1. 存储方式
    使用数组保存二叉树结构,方式即将二叉树用层序遍历方式放入数组中。
    一般只适合表示完全二叉树,因为非完全二叉树会有空间的浪费
    这种方式的主要用法就是堆的表示

  2. 下标关系
    已知双亲(parent)的下标,则:
    左孩子(left)下标 = 2 * parent + 1
    右孩子(right)下标 = 2 * parent + 2

已知孩子(不区分左右)(child)下标,则:
双亲(parent)下标 = (child - 1) / 2


二、堆 heap

  • 堆逻辑上是一棵完全二叉树
  • 堆物理上是保存在数组中
  • 满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆
  • 反之,则是小堆,或者小根堆,或者最小堆
  • 堆的基本作用是,快速找集合中的最值

1、PriorityQueue 方法

默认是小堆
每次入队,需保证当前是大根堆或小根堆
每次弹出最小元素,依然是大根堆或小根堆

public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        // 每放一个元素 都得保证当前的堆 是大堆 或者是小堆
        priorityQueue.offer(1);
        priorityQueue.offer(2);
        priorityQueue.offer(3);

        System.out.println(priorityQueue.poll()); // 1 ->默认是小堆
        System.out.println(priorityQueue.peek()); // 2
    }
}

1.1、操作-调整为大根堆

从最后一颗子树出发,每棵子树都是向下调整的
在这里插入图片描述

public class TestHeap {
    
    
    public int[] elem;
    public int usedSize;

    public TestHeap() {
    
    
        this.elem = new int[10];
    }

    /**
     * 向下调整
     * @param parent 每棵树的根节点
     * @param len 每棵树的调整的结束位置
     */
    public void shiftDown(int parent, int len) {
    
    
        int child = 2 * parent + 1;
        // 判断有右孩子
        while(child < len) {
    
    
            if(child + 1 < len && this.elem[child] < this.elem[child + 1]) {
    
    
                child++; // child 指向左右孩子中最大值的那一个下标
            }
            if(this.elem[child] > this.elem[parent]) {
    
    
                int tmp = this.elem[child];
                this.elem[child] = this.elem[parent];
                this.elem[parent] = tmp;
                parent = child;
                child = 2 * parent + 1;
            } else {
    
    
                break;
            }
        }
    }

    public void createHeap(int[] array) {
    
    
        for (int i = 0; i < array.length; i++) {
    
    
            this.elem[i] = array[i];
            usedSize++;
        }
        // 双亲(parent)下标 = (child - 1) / 2
        for (int parent = (usedSize-1-1)/2; parent >= 0 ; parent--) {
    
    
            // 调整
            shiftDown(parent, usedSize);
        }
    }
}
public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        int[] array = {
    
    27,15,19,18,28,34,65,49,25,37};
        TestHeap testHeap = new TestHeap();
        testHeap.createHeap(array);
    }
}

时间复杂度分析:

T(n) = n - log(n + 1)
当 n 越来越大时,也就是T(n) = n
在这里插入图片描述


1.2、入队列-向上调整

private void shiftUp(int child) {
    
    
        int parent = (child - 1) / 2;
        while(child > 0) {
    
    
            if(elem[child] > elem[parent]) {
    
    
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child - 1) / 2;
            } else {
    
    
                break;
            }
        }
    }

    // 向上调整
    public void offer(int val) {
    
    
        if(isFull()) {
    
     // 扩容
            elem = Arrays.copyOf(elem, 2*elem.length);
        }
        this.elem[usedSize++] = val;
        shiftUp(usedSize - 1);
    }

    public boolean isFull() {
    
    
        return usedSize == this.elem.length;
    }

在这里插入图片描述

1.3、出队列

每次出队列,都要保证出最大的或最小的
1、交换0下标和最后一个元素
2、调整0下标

public int pop() {
    
    
        if(isEmpty()) {
    
    
            throw new RuntimeException("优先级队列为空");
        }
        // 1、交换0下标和最后一个元素
        int tmp = elem[0];
        elem[0] = elem[usedSize - 1];
        elem[usedSize - 1] = tmp;
        // 2、调整0下标
        shiftDown(0, usedSize);
        return tmp;
    }

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

在这里插入图片描述

1.4、获取队首元素

public int peek() {
    
    
        if(isEmpty()) {
    
    
            throw new RuntimeException("优先级队列为空");
        }
        return elem[0];
    }

2、堆的其他应用

2.1、TopK 问题

方法一:整体排序
方法二:建一个大根堆 弹出堆顶 k 个元素
方法三:建小根堆 出较小的的堆顶元素 得前 k个元素

找前3个最大的元素:
1、把前3个元素,建小根堆
2、堆顶的元素是当前k个元素中的最小值
3、如果下一个X元素比堆顶元素大,出堆顶元素到最后,入这个X元素,然后再次调整堆顶元素为小根堆

优点:
堆的大小只有 k
时间复杂度:O(n*logk)
堆调整为堆的高度logk

总结:

  • 如果要求前 k 个最大的元素,要建一个大根堆,堆内元素就是前k大
  • 如果要求前 k 个最小的元素,要建一个小根堆,堆内元素就是前k小
    堆顶元素比下一个大,出堆顶元素,调整
  • 第 k 大的元素,建一个小堆,堆顶元素就是第 k 大的元素
  • 第 k 小的元素,建一个大堆,堆顶元素就是第 k 小的元素

- 求数组当中的前K个最小的元素

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

public class TopK {
    
    
    // n*logn
    public static int[] topK(int[] array, int k) {
    
    
        // 1、创建一个大小为 K 的大根堆
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
    
    
            @Override
            public int compare(Integer o1, Integer o2) {
    
    
                return o2 - o1; // 大根堆
            }
        });
        // 2、遍历数组当中的元素,前K个元素放到队列当中
        for (int i = 0; i < array.length; i++) {
    
    
            if(maxHeap.size() < k) {
    
    
                maxHeap.offer(array[i]);
            } else {
    
    
                // 3、从第 k+1 个元素开始,每个元素和堆顶元素进行比较
                int top = maxHeap.peek();
                if(top > array[i]) {
    
    
                    // 3.1、先弹出
                    maxHeap.poll();
                    //3.2、后存入
                    maxHeap.offer(array[i]);
                }
            }
        }
        // 4、存储前 K 个元素,返回
        int[] ret = new int[k];
        for (int i = 0; i < k; i++) {
    
    
            ret[i] = maxHeap.poll();
        }
        return ret;
    }

    public static void main(String[] args) {
    
    
        int[] array = {
    
    18,21,8,10,34,12};
        int[] ret = topK(array, 3);
        System.out.println(Arrays.toString(ret));
    }
}

- LeetCode 373. 查找和最小的 K 对数字

373. 查找和最小的 K 对数字

class Solution {
    
    
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
    
    
        // 创建大小为 K 的大根堆
        PriorityQueue<List<Integer>> maxHeap = new PriorityQueue<>(k, new Comparator<List<Integer>>() {
    
    
            @Override
            public int compare(List<Integer> o1, List<Integer> o2) {
    
    
                return (o2.get(0) + o2.get(1)) - (o1.get(0) + o1.get(1)); // 大根堆
            }
        });
        // 加入 K 组序列,然后比较堆顶元素
        for (int i = 0; i < Math.min(nums1.length, k); i++) {
    
    
            for (int j = 0; j < Math.min(nums2.length, k); j++) {
    
    
                if(maxHeap.size() < k) {
    
    
                    List<Integer> list = new ArrayList<>();
                    list.add(nums1[i]);
                    list.add(nums2[j]);
                    maxHeap.offer(list);
                } else {
    
    
                    // 堆顶元素大 poll
                    int top = maxHeap.peek().get(0) + maxHeap.peek().get(1);
                    if(top > nums1[i] + nums2[j]) {
    
    
                        maxHeap.poll();
                        List<Integer> list = new ArrayList<>();
                        list.add(nums1[i]);
                        list.add(nums2[j]);
                        maxHeap.offer(list);
                    }
                }
            }
        }
        // 存储 k 组序列
        List<List<Integer>> ret = new ArrayList<>();
        for (int i = 0; i < k && !maxHeap.isEmpty(); i++) {
    
     // 没有k组 空指针异常
            ret.add(maxHeap.poll());
        }

        return ret;
    }
}

2.2、堆排序

堆一组数据从小到大排序,建大根堆还是小根堆?
应该是小根堆,因为不是每次弹出堆顶元素,而是对此数组排序
在这里插入图片描述

	/**
     * 堆排序
     * 1、0下标和最后一个未排序的小标交换
     * 2、end--
     */
    public void heapSort() {
    
    
        for (int end = usedSize - 1; end > 0; end--) {
    
    
            int tmp = elem[0];
            elem[0] = elem[end];
            elem[end] = tmp;
            shiftDown(0, end);
        }
    }

TestHeap.java

import java.util.Arrays;

public class TestHeap {
    
    
    public int[] elem;
    public int usedSize;

    public TestHeap() {
    
    
        this.elem = new int[10];
    }

    /**
     * 向下调整
     * @param parent 每棵树的根节点
     * @param len 每棵树的调整的结束位置
     */
    public void shiftDown(int parent, int len) {
    
    
        int child = 2 * parent + 1;
        // 判断有右孩子
        while(child < len) {
    
    
            if(child + 1 < len && this.elem[child] < this.elem[child + 1]) {
    
    
                child++; // child 指向左右孩子中最大值的那一个下标
            }
            if(this.elem[child] > this.elem[parent]) {
    
    
                int tmp = this.elem[child];
                this.elem[child] = this.elem[parent];
                this.elem[parent] = tmp;
                parent = child;
                child = 2 * parent + 1;
            } else {
    
    
                break;
            }
        }
    }

    public void createHeap(int[] array) {
    
    
        for (int i = 0; i < array.length; i++) {
    
    
            this.elem[i] = array[i];
            usedSize++;
        }
        // 双亲(parent)下标 = (child - 1) / 2
        for (int parent = (usedSize-1-1)/2; parent >= 0 ; parent--) {
    
    
            // 调整
            shiftDown(parent, usedSize);
        }
    }


    private void shiftUp(int child) {
    
    
        int parent = (child - 1) / 2;
        while(child > 0) {
    
    
            if(elem[child] > elem[parent]) {
    
    
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child - 1) / 2;
            } else {
    
    
                break;
            }
        }
    }

    // 入队列  向上调整
    public void offer(int val) {
    
    
        if(isFull()) {
    
     // 扩容
            elem = Arrays.copyOf(elem, 2*elem.length);
        }
        this.elem[usedSize++] = val;
        shiftUp(usedSize - 1);
    }

    public boolean isFull() {
    
    
        return usedSize == this.elem.length;
    }


    // 出队列
    public int pop() {
    
    
        if(isEmpty()) {
    
    
            throw new RuntimeException("优先级队列为空");
        }
        // 1、交换0下标和最后一个元素
        int tmp = elem[0];
        elem[0] = elem[usedSize - 1];
        elem[usedSize - 1] = tmp;
        // 2、调整0下标
        shiftDown(0, usedSize);
        return tmp;
    }

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

    public int peek() {
    
    
        if(isEmpty()) {
    
    
            throw new RuntimeException("优先级队列为空");
        }
        return elem[0];
    }


    /**
     * 堆排序
     * 1、0下标和最后一个未排序的小标交换
     * 2、end--
     */
    public void heapSort() {
    
    
        for (int end = usedSize - 1; end > 0; end--) {
    
    
            int tmp = elem[0];
            elem[0] = elem[end];
            elem[end] = tmp;
            shiftDown(0, end);
        }
    }
}

三、java 对象的比较

1、问题提出

优先级队列在插入元素时有个要求:插入的元素不能是null或者元素之间必须要能够进行比较,为了简单起见,我们只是插入了Integer类型,那优先级队列中能否插入自定义类型对象?

class Card {
    
    
    public int rank; // 数值
    public String suit; // 花色

    public Card(int rank, String suit) {
    
    
        this.rank = rank;
        this.suit = suit;
    }
}

public class TestPriorityQueue {
    
    
    public static void TestPriorityQueue() {
    
    
        PriorityQueue<Card> p = new PriorityQueue<>();
        p.offer(new Card(1, "♠"));
        p.offer(new Card(2, "♠"));
    }
    public static void main(String[] args) {
    
    
        TestPriorityQueue();
    }
}

优先级队列底层使用堆,而向堆中插入元素时,为了满足堆的性质,必须要进行元素的比较,而此时Card是没有办法直接进行比较的,因此抛出异常
在这里插入图片描述

2、覆写基类的equal

在这里插入图片描述

  • 如果没有重写equals方法,默认调用的是object
    在这里插入图片描述

  • 重写equals方法:

在这里插入图片描述

public class Card {
    
    
    public int rank; // 数值
    public String suit; // 花色

    public Card(int rank, String suit) {
    
    
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public boolean equals(Object o) {
    
    
        // 自己和自己比较
        if (this == o) return true;
        
        // o如果是null对象,或者o不是Card的子类
        // getClass() != o.getClass()比较是不是同一个类型
        if (o == null || getClass() != o.getClass()) return false;

        // 强转为Card类型
        Card c = (Card) o;
        
        // 比较数值和花色是否一样
        return rank == card.rank && Objects.equals(suit, card.suit);
    }

	@Override
    public int hashCode() {
    
    
        return Objects.hash(rank, suit);
    }
}

基本类型可以直接比较,但引用类型最好调用其equal方法

注意: 一般覆写 equals 的套路就是上面演示的

  1. 如果指向同一个对象,返回 true
  2. 如果传入的为 null,返回 false
  3. 如果传入的对象类型不是 Card,返回 false
  4. 按照类的实现目标完成比较,例如这里只要花色和数值一样,就认为是相同的牌
  5. 注意下调用其他引用类型的比较也需要 equals,例如这里的 suit 的比较
    覆写基类equal的方式虽然可以比较,但缺陷是:equal只能按照相等进行比较,不能按照大于、小于的方式进行比较。

3、基于Comparble接口类的比较

public interface Comparable<E> {
    
    
	// 返回值:
	// < 0: 表示 this 指向的对象小于 o 指向的对象
	// == 0: 表示 this 指向的对象等于 o 指向的对象
	// > 0: 表示 this 指向的对象等于 o 指向的对象
	int compareTo(E o);
}

对用用户自定义类型,如果要想按照大小与方式进行比较时:在定义类时,实现Comparble接口即可,然后在类中重写compareTo方法

class Card implements Comparable<Card>{
    
    
    public int rank; // 数值
    public String suit; // 花色
    public Card(int rank, String suit) {
    
    
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public int compareTo(Card o) {
    
    
        return this.rank - o.rank;
    }

    @Override
    public String toString() {
    
    
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }
}

public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        // 默认是小根堆
        PriorityQueue<Card> priorityQueue = new PriorityQueue<>();
        priorityQueue.offer(new Card(1, "♥")); // 直接放到底层的queue数组的0下标
        priorityQueue.offer(new Card(2, "♥"));
        System.out.println(priorityQueue); 
        // 结果:[Card{rank=1, suit='♥'}, Card{rank=2, suit='♥'}]

        // priorityQueue.offer(null); // err ->NullPointerException
    }
}
public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        Card card1 = new Card(2, "♥");
        Card card2 = new Card(1, "♦");

        System.out.println(card1.compareTo(card2)); // 1 -> 第一张牌大
    }
}

在这里插入图片描述

缺点:对类的侵入性太高,一旦写好根据哪种规则比较,就不能轻易修改

4、基于比较器比较

class Card {
    
    
    public int rank; // 数值
    public String suit; // 花色
    public Card(int rank, String suit) {
    
    
        this.rank = rank;
        this.suit = suit;
    }
}

class RankComparator implements Comparator<Card> {
    
    
    @Override
    public int compare(Card o1, Card o2) {
    
    
        return o1.rank - o2.rank;
    }
}

public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        Card card1 = new Card(1, "♥");
        Card card2 = new Card(2, "♦");

        RankComparator rankComparator = new RankComparator();
        int ret = rankComparator.compare(card1, card2);
        System.out.println(ret); // -1
    }
}

比较队列:

class Card {
    
    
    public int rank; // 数值
    public String suit; // 花色
    public Card(int rank, String suit) {
    
    
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
    
    
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }
}

class RankComparator implements Comparator<Card> {
    
    
    @Override
    public int compare(Card o1, Card o2) {
    
    
        return o1.rank - o2.rank;
    }
}

public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        Card card1 = new Card(1, "♥");
        Card card2 = new Card(2, "♦");

        RankComparator rankComparator = new RankComparator();

        PriorityQueue<Card> priorityQueue = new PriorityQueue<>(rankComparator);
        priorityQueue.offer(card1);
        priorityQueue.offer(card2);
        System.out.println(priorityQueue);
        // [Card{rank=1, suit='♥'}, Card{rank=2, suit='♦'}]
    }
}

匿名内部类:

class Card {
    
    
    public int rank; // 数值
    public String suit; // 花色
    public Card(int rank, String suit) {
    
    
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
    
    
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }
}

public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        Card card1 = new Card(2, "♥");
        Card card2 = new Card(1, "♦");
        // 1、
        PriorityQueue<Card> priorityQueue =  new PriorityQueue<>(new Comparator<Card>() {
    
    
            @Override
            public int compare(Card o1, Card o2) {
    
    
                return o1.rank - o2.rank;
            }
        });

        // 2、lambda 表达式 -> 可读性非常差
        // PriorityQueue<Card> priorityQueue = new PriorityQueue<>((x, y) -> {return x.rank - y.rank;});

        priorityQueue.offer(card1);
        priorityQueue.offer(card2);
        System.out.println(priorityQueue); // [Card{rank=1, suit='♦'}, Card{rank=2, suit='♥'}]
    }
}

5、三种方式对比

覆写的方法 说明
Object.equals 因为所有类都是继承自 Object 的,所以直接覆写即可,不过只能比较相等与
Comparable.compareTo 需要手动实现接口,侵入性比较强,但一旦实现,每次用该类都有顺序,属于
内部顺序
Comparator.compare 需要实现一个比较器对象,对待比较类的侵入性弱,但对算法代码实现侵入性

猜你喜欢

转载自blog.csdn.net/qq_56884023/article/details/122644142