堆排序 TopK 优先级队列的部分源码 JAVA对象的比较

文章详细介绍了如何利用堆排序算法对数组进行从小到大或从大到小的排序,包括建立大根堆和小根堆的步骤。同时,讨论了TopK问题的两种解决方案,一种是建立大堆并弹出元素,另一种是建立小堆并不断调整。文中还提到了PriorityQueue在处理此类问题时的作用,以及如何处理自定义元素的比较。
摘要由CSDN通过智能技术生成

一.堆排序:我们该如何借助堆来对数组的内容来进行排序呢?

假设我们现在有一个数组,要求从小到大进行排序,我们是需要进行建立一个大堆还是建立一个小堆呢?

1)我的第一步的思路就是建立一个小堆,因为每一次堆顶上面的元素就是最小的元素,直接按照顺序进行弹出堆顶元素不就可以了吗

2)但是当前我们要对数组整体本身进行排序,将来的数组,0下标就是最小的元素,不是每一次依次输出最小的元素,不是从小到大进行输出

总结:我们是将数组从小到大进行排序,那么我们建立一个大根堆,如果将数组按照从大到小进行排序,那么我们就建立一个小根堆,如果想让数组从小到大进行排序,那么就建立成一个大根堆

1)先进行把数组元素调整成大根堆,因为我们以后进行数据的交换的时候,总是要把最大的元素放到数组的最后一个位置

2)我们让堆的根节点也就是0小标和数组的最后一个位置未排序的元素进行交换即可,这样就可以保证数组的最后一个end位置一定是当前堆的最大元素,数组的最后一个位置一定是原数组中最大的元素

3)调整成大根堆,因为此时只有根节点的元素所在的位置不是一个大根堆,那么就向下进行调整这棵树

4)定义一个end值等于数组有效数据的长度,并且重复和堆顶元素进行交换,向下进行调整

5)例如我们想要对数组进行从大到小进行排序,那么我们要建立一个大堆;

建立大堆的时间复杂度是O(N),空间复杂度是O(1)

这个代码是错误的,自己去细细地品吧;
  public void sort()
    {
        int len=usedsize;
        while(len!=0)
        {
            int temp=array[len-1];
            array[len-1]=array[0];
            array[0]=temp;
            len--;
            adjustDown(0,len);

        }

  public void heartsort()
        {
            int index=usedsize-1;
            while(index!=0)//让顶上的元素与末尾元素交换,在对啊让arr1[0]进行向上调整
            {
                int temp=arr1[0];
                arr1[0]=arr1[index];
                arr1[index]=temp;
                downadjust(0,index);
                index--;
            }
        }

三:TopK问题

1)有十万个数据,你给我找前十个最大的数据?假设你的数据在内存中是可以进行存放的

思路1:建立一个大堆,弹出堆顶元素10次

就是将所有元素建立一个大堆,既然是找前10个最大的数据,那么就需要pop()10次就可以了(缺点:有多少个数据,堆就有多大),建堆的时间复杂度是O(N),每一次进行向下进行调整的时间复杂度是log(N),所以时间复杂度就是K*logN,其中的logN是向下进行调整的高度

思路2:建立一个大小为K的小堆

1)将前10个数据建立一个大小为10的小堆,遍历剩下的元素如果元素比堆顶元素大,就入堆,同时删除堆顶元素,再将剩下的元素调整成一个小堆;

2)当前我们找的是前十个最大的数据,那么我们为什么要建立一个小堆呢?

因为此时堆顶的元素,一定是当前K个元素中最小的元素,如果有元素比当前的堆顶元素大,那么这个元素很有可能就是Topk中的一个;

3)当我们要找第K大的元素的时候,首先我们要建立一个小堆,遍历剩下的元素,不断地进行比较,最终堆顶元素就是第K小的元素;

时间复杂度:我们要进行遍历这个数组中的所有元素,所以说时间复杂度是O(N),每当我们进行遍历到一个元素的时候,我们都要进行出堆顶元素,并且进行向下调整,每一次进行向下调整的次数是logK,所以说整个TopK问题的时间复杂度就是N*logK

思路:求前K大的元素 

1)先把前K个元素,建立成一个小根堆

2)遍历后面的元素,只要当前遍历的元素比堆顶元素大,就进行弹出堆顶元素,将遍历到的这个元素这个元素入堆操作

3)将这个元素进行入堆之后,我们可以继续将这个堆中的元素,调整成一个小根堆

TopK问题的解决思路:比如说求前K个最小的元素

1)创建一个大小为K的大根堆

2)遍历数组中的元素,将前K个元素放入到队列里面

3)从K+1个元素开始,每一个元素都和堆顶元素进行比较,先弹出,后压入

 

总结:

1)如果说求前K个最大的元素,我们要构建一个小根堆,如果遍历的元素比堆顶的元素大,那么就将它入堆,重新调整成一个小根堆

2)如果说我们求前K个最小的元素,我们需要构建大根堆,如果说遍历的元素比堆顶的元素小,那么就将它入堆,重新调整成一个大根堆

3)如果需要求第K大的元素,建一个小堆,进行遍历原来的数组,最终得到的堆顶元素就是第K大的元素

4)PriorityQueue在进行插入元素的时候,是不可以进行插入null值的,因为我们要进行插入的元素要可以进行比较,况且要有可比较的能力,那么我们如何向优先级队列里面存放自定义元素呢?那么我们这个自定义的类必须事先Compareable接口或者是是Comparator接口

static void TopK(int[]arr1)
      {//创建一个大小为K的大根堆
          PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(3, new Comparator<Integer>() {
//他的意思就是说有一个类,实现了Comparator接口,并且重写了compare方法,相当于就是匿名内部类
              @Override
              public int compare(Integer o1, Integer o2) {
                  return o1-o2;//小堆是o1-o2,大堆是o2-o1
              }
          });
          for(int i=0;i<arr1.length;i++)
          {
//遍历数组中的元素,将前K个元素放到队列里面
              if(priorityQueue.size()<3)
              {
                  priorityQueue.add(arr1[i]);
              }
              else
              {
                  int num=priorityQueue.peek();
//代码走到这里面,说明队列中已经存放了三个元素了
//从第K+1个元素开始,每一个元素都要和当前堆顶元素进行比较
//先找到当前堆顶的元素,如果这个元素比数组元素大,就取出对顶元素,放数组元素入队列
                  if(arr1[i]>num)
                  {
//先进行弹出,在进行存入
                      priorityQueue.poll();
                      priorityQueue.add(arr1[i]);
                  }
              }
          }
          System.out.println(priorityQueue);
         int array[]=new int[k];
        for(int i=0;i<array.length;i++)
        {
            int data=queue.poll();
            array[i]=data;
        }
      
      }
    public static void main(String[] args) {
        int arr1[]={34,23,89,78,123,67,87,55};
//写一个topk问题,找到数组中8个数中前三个最大的
        TopK(arr1);

    }
 public static void main(String[] args) {
     PriorityQueue<Integer> queue=new PriorityQueue<>(new Comparator<Integer>() {
         @Override
         public int compare(Integer o1, Integer o2) {
             return o2-o1;
         }
     });
     int[] array={10,90,100,110,89,78};
     InsertQueue(array,queue);
     System.out.println(queue.poll());
     System.out.println(queue.poll());
     System.out.println(queue.poll());
    }

    private static void InsertQueue(int[] array, PriorityQueue<Integer> queue) {
        for(int i=0;i<array.length;i++){
            queue.offer(array[i]);
        }
    }

 

前K个最大的数对:力扣

这个题还是用TopK来进行解决,我们还是先要创建一个大堆,但是此时优先级队列中放的就不是一个数字了,他存放的是一个数对,也就是说,优先级队列中的每一个元素是一个List<Integer>,以后遍历元素的时候,我们就需要比较的值是每一个List中的两个元素的和;

 class Hello{
    public static void main(String[] args) {
      int arr1[]={1,7,11};
      int arr2[]={2,4,6};
      PriorityQueue<List<Integer>> priorityQueue=new PriorityQueue<>(3, 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);
        }
      });
      for(int i=0;i<arr1.length;i++){
        for(int j=0;j<arr2.length;j++) {
          if(priorityQueue.size()<3)
          {
            ArrayList list=new ArrayList<>();
            list.add(arr1[i]);
            list.add(arr2[j]);
            priorityQueue.add(list);
          }
          else
          {
            int top=priorityQueue.peek().get(0)+priorityQueue.peek().get(1);
            if(top>arr1[i]+arr2[j])
            {
              priorityQueue.poll();
              ArrayList list=new ArrayList<>();
              list.add(arr1[i]);
              list.add(arr2[j]);
              list.addAll(list);
            }
          }
        }
      }
      List<List<Integer>> list1=new ArrayList<>();
      for(int k=0;k<3&&!priorityQueue.isEmpty();k++)
      {
        list1.add(priorityQueue.poll());
      }
      System.out.println(list1);


    }

练习:前K个高频单词

题目关键:返回的答案应该按单词出现频率由高到低进行排序,咱们建的是小堆

如果有不同的单词按照相同的出现频率,应该按照字典顺序来进行排序

1)我们遍历原来的哈希表,我们创建一个HashMap里面进行记录每一个字符串出现的次数

2)相当于现在待排序序列是哈希表中的每一个(Key-Value)键值对,我们要根据写键值对来进行建立一个小堆(本质上来说是依靠单词出现的次数建立一个小堆)

3)频率高低为前提,频率高低相同比较字典大小,字典小的入堆


package Demo;

import java.util.*;

class Solution {
    public List<String> topKFrequent(String[] words, int k) {
       HashMap<String,Integer> map=new HashMap<>();
       for(int i=0;i<words.length;i++){
           if(!map.containsKey(words[i])){
               map.put(words[i],1);
           }else{
               int count=map.get(words[i]);
               count++;
               map.put(words[i],count);
           }
       }
       PriorityQueue<Map.Entry<String,Integer>> queue=new PriorityQueue<>(k, new Comparator<Map.Entry<String, Integer>>() {
           @Override
           public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
               if(o1.getValue().compareTo(o2.getValue())==0)
               {
                   return o2.getKey().compareTo(o1.getKey());//当放的元素小于堆的个数处理的时候变成大堆;a-2,b-2;
               }
               return o1.getValue()-o2.getValue();//核心是根据value值进行比较的
           }
       });
       for(Map.Entry<String,Integer> entry:map.entrySet()){
           if(queue.size()<k){
               queue.add(entry);//放入元素的时候会自动帮助我们建立一个大堆
           }else{
               Map.Entry<String,Integer> s1=queue.peek();
               if(entry.getValue().compareTo(s1.getValue())>0){//先看比较堆顶元素和即将要存放的元素出现频率大小
                   queue.poll();
                   queue.add(entry);
               }else if(entry.getValue().compareTo(s1.getValue())==0){//当频率相同的时候,比较长度
                   if(entry.getKey().compareTo(s1.getKey())<0) {//单次顺序在后面的入堆
                       queue.poll();
                       queue.add(entry);
                   }
               }

           }

       }
       List<String> list=new ArrayList<>();
       // System.out.println(queue);
        while(!queue.isEmpty()){
           Map.Entry<String,Integer> entry=queue.poll();
           list.add(entry.getKey());
       }
        Collections.reverse(list);
       return list;
    }
}

对象排序的比较以及PriorityQueue的部分源码

一:向优先级队列里面添加元素:offer的源码

public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
    }

优先级队列的扩容以及容量问题:

1)当你调用不带有参数的构造方法的时候,默认容量就是11,况且他默认传入的comparator比较器就是空

2)对于优先级队列的扩容,用grow函数进行扩容,如果原来数组的长度小于64,那么以原来的长度的2倍+2进行扩容,如果原来的容量不是小于64的,反之就要以1.5倍进行扩容

存放数据:

3)当我们进行第一次存放元素的时候,会先把第一个元素直接放到底层的queue的0下标、

4)当我们不是第一次存放的时候,会调用siftUp方法,第一个参数是即将要放到数组下标的位置,第二个参数就是要具体存放的引用(对象)

o1就是新存放的元素

 private void siftUp(int k, E x) {//k==2,e等于自定义类型
        if (comparator != null)
//这里面的comparotor是优先级队列中的一个属性,是我们需要在构造方法中进行传入的,在这里面会进行指定比较类型
            siftUpUsingComparator(k, x);
        else
//如果我们没有进行传入comparator比较器,那么最终会默认把我们传输过来的自定义类型强制转化成Comparable类型
            siftUpComparable(k, x);
    }
//假设我们在创建一个优先级队列的时候,既没有向里面传入一个比较器,自定义类型还没有实现Compareator接口,那么最后当我们向队列中存放第二个元素的时候,此时就会发生报错

由于我们使用的是无参的构造方法,没有进行传入一个比较器,所以在上面就会调用siftUpComparable()方法

  @SuppressWarnings("unchecked")
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
     int parent = (k - 1) >>> 1;//计算父亲节点的下标
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
//这个布尔表达式为假就进行交换,为真就不会进行交换
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

    @SuppressWarnings("unchecked")
    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
    }

1)如果说一个没有实现Comparator接口,构建优先级队列的时候还没有进行传一个比较器,那么代码就会抛出,这个类 cannot be cast to java.lang.Comparable

2)上述代码第一行会把对象转化成比较器,在while循环里面会调用compareTo方法

3)如果进入到循环里面,break出去,就不会进行交换,如果可以正确执行到break之后的代码,就可以正确的进行交换了

4)如果换成大堆,直接把compareTo方法的return顺序换一下,这个时候 PriorityQueue是大堆还是小堆完全取决于自定义类实现比较接口的compareTo的方法是怎么写的

1)我们在正常情况下priorityQueue使用的是Compareable接口,当我们没有传入任何的比较器的时候,默认的就是使用Compareable接口,会将元素转化成Compable类型,在进行向上调整的时候,使用了自定义类重写的CompareTo()方法,但是我们要对比较的类实现Compareable接口,这样对于类的侵入性太强了,一旦写好了根据那一种规则进行修改,就不可以进行发生变化了

public PriorityQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }
public PriorityQueue(int initialCapacity) {
        this(initialCapacity, null);
    }
public PriorityQueue(Comparator<? super E> comparator) {
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }

上面的默认两种方法都是基于compareable接口来进行实现的

2)我们自己可以写一个比较器,所以之前默认构造方法就不可以用了,咱们需要自己写一个比较器:

1)public PriorityQueue(Comparator<? super E> comparator) {
        this(DEFAULT_INITIAL_CAPACITY, comparator)
2)public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {}

里面的while循环都是用compare方法,建立大堆或者小堆都是依靠return语句里面的写法

class AgeComparator implements Comparator<Student>{
    public int compare(Student o1,Student o2){
         return o1.age-o2.age;
    }
}
class Student{
    public int age;
    public String username;
    public Student(int age,String username){
        this.age=age;
        this.username=username;
    }
}
public class Main{
    public static void main(String[] args) {
        AgeComparator comparator=new AgeComparator();
        PriorityQueue<Student> queue=new PriorityQueue<>(3,comparator);
    }   
}

4)自定义类型的比较,一定要实现比较接口;

5)父类的equals方法默认比较的是地址,但是在类中重写equals方法进行比较,比较的是对象里面的内容,先比较两个引用是不是同一种类型,在比较他们具体的内容;

创建小堆o1-o2;创建大堆o2-o1;

@Override
public boolean equals(Object o) {
    if (this == o) return true;//判断他们是否是地址相同,也就是说看看他们是否引用的是同一个对象
    if (o == null || getClass() != o.getClass()) return false;
//看看他们的类对象是否相同,是不是属于同一个类创建的两个对象,比较他们的类型

    //接下来我们要把Object类型转化成Student类型
    Student student = (Student) o;
//比较它们是否对象内容相同
    return age == student.age && Objects.equals(name, student.name);
}

对于String重写equals方法:

 public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;//判断他们是否是地址相同,也就是说看看他们是否引用的是同一个对象
        }
        if (anObject instanceof String) {
//如果说anObject也是一个字符串类型的,就进入到if语句里面进行下一步的比较
            String anotherString = (String)anObject;
//将Object类型转化成字符串类型
            int n = value.length;
            if (n == anotherString.value.length) //在这里面判断他们的长度是否相同
{
//在进行取出两个字符串的每一个字符一一进行比较,如果不相等,那么就直接返回false
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值