Java優先隊列PriorityQueue的各種打開方式以及一些你不知道的細節


Java優先隊列PriorityQueue的各種打開方式以及一些你不知道的細節

未經作者允許,不可轉載,如有錯誤,歡迎指正o( ̄▽ ̄)o

  • 首先我們知道用PriorityQueue這個類創建的對象是一個集合,然后調用api可以將一個個對象添加入集合,然后再通過api遍歷時,插入的元素就從小到大排序輸出,真的太神奇了!
  • 下面將介紹優先隊列的默認用法以及我想自己寫個類,然后扔到優先隊列中讓它也能從小到大排序怎么做?從大到小嘞?又或者我又想用系統的類比如String,但是我想按字典序從大到小能做嗎?
  • 題外話:每插入一個就會自動排序,這么強大的功能,效率一定不高吧?但如果你學過數據結構,想一下建堆的過程,以及堆排序中添加與調整堆的過程你就會發現,堆排序與優先隊列每次插入排序的功能是那么契合~
  • 下面的講解我嘗試用一種循序漸進的方式講述我知道的優先隊列,有經驗的讀者可能會覺得很啰嗦,但我還是覺得這能幫助你梳理一下知識點

優先隊列的默認用法—從小到大排序

我們先新建一個優先隊列,然后扔四個字符進去,然后用迭代器Iterator或者for-each方式遍歷打印,或者直接用println()打印,結果並非從小到大,而是(層序遍歷的堆,總之不是你要的~),事實上只有通過優先隊列定義的api才能按從小到大取出元素,比如remove方法

此時如果你對:

  1. 為什么能用for each形式可以打印集合有疑惑:這就要追根溯源到這個PriorityQueue類的來源,Java集合框架是一個大家族,它們由很多的接口定義了不同的功能,再由很多抽象類去逐步實現這些接口的功能,抽象類一代代繼承,最后得到如優先隊列這樣的最終實現類,而for each的遍歷方式是它的最上面的祖先Iterable接口中的一個方法,任何實現了Iterable接口的類都能用迭代器進行遍歷
  2. 為什么能System.out.println(XX);直接打印一個XX對象有疑惑:事實上只有實現了toString方法的類,才能在調用這個方法的時候轉化成字符串再打印

回到我們的程序

public class PriorityQueueTest {
    public static void main(String[] args) {
        //這里String類型默認實現Comparable接口的 就是按字典序排序 從小大到
        //上面這個注釋你可能不太明白,看下去就會明白了~,現在無視它
        var pq = new PriorityQueue<String>();
        pq.add("B");
        pq.add("D");
        pq.add("A");
        pq.add("C");
        //通過迭代器和for each,以及println輸出的順序是(層序遍歷的堆)
        System.out.println(pq);
        for (String s : pq)
            System.out.print(s);
        System.out.println();
        Iterator iter = pq.iterator();
        while (iter.hasNext())
            System.out.print(iter.next());
        System.out.println();
        //isEmpty方法是AbstractCollection抽象類中的
        while (!pq.isEmpty())
            //remove方法按照優先隊列中定義的每次返回最小的元素,並刪去該值
            //本例的最小是字典序最小,但是這個最小的概念是可以通過用戶自己定義的
            //怎么定義下面會講
            System.out.print(pq.remove());
        System.out.println();
        System.out.println(pq);
    }
}

輸出結果

[A, C, B, D]
ACBD
ACBD
//很顯然上三種打印的順序並非我們需要的(打印的是層序的堆),沒有實現從小到大排序
ABCD
[]

對String類用優先隊列從大到小排序

現在需要補充一個知識點:想要往優先隊列里放入一個對象,它就默認會去排序調整它在集合中的位置,與Java集合框架中的其他實現類一樣,而一切涉及排序功能的實現類,我們放入集合中的元素都必須實現了Comparable接口,或者在調用構造器時提供了Comparator對象為參數,這句話接下來會用示例講解~

  • 先來看一下String對象的源碼,它能直接放入PriorityQueue類是因為它實現了Comparable接口的唯一一個方法compareTo,定義了兩個String以何種規則進行比較大小
//這是類的聲明部分,可以看到實現了Comparable接口,而這個接口就只有一個compareTo的抽象方法
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
//抽象方法的實現
public int compareTo(String anotherString) {
        byte v1[] = value;
        byte v2[] = anotherString.value;
        if (coder() == anotherString.coder()) {
            return isLatin1() ? StringLatin1.compareTo(v1, v2)
                              : StringUTF16.compareTo(v1, v2);
        }
        return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
                          : StringUTF16.compareToLatin1(v1, v2);
     }
  • 現在我們來實現將String類放入PriorityQueue,完成字典序從大到小排序,上面我們講了,要實現排序可以通過兩種方法,對放入PriorityQueue集合的類String實現接口(這是系統的類,final修飾,你想動你不給你機會動呀~,這里我們采用第二種方法,自定義一個Comparator對象傳入構造器,你可以理解為第一種方式需要放入的類自帶了排序規則,第二種方式是優先隊列定義了排序規則,必須有規則才能實現排序)

程序:

public class PriorityQueueTest {
    public static void main(String[] args) {
        //實例化一個比較器對象
        MyComparator myComparator = new MyComparator();

        //這里在優先隊列的構造方法中傳入比較器對象,設定排序規則
        var pq = new PriorityQueue<String>(myComparator);
        pq.add("B");
        pq.add("D");
        pq.add("A");
        pq.add("C");
        System.out.println(pq);
        //通過迭代器和for each輸出的順序是元素的層序的堆
        for (String s : pq)
            System.out.print(s);
        System.out.println();
        Iterator iter = pq.iterator();
        while (iter.hasNext())
            System.out.print(iter.next());
        System.out.println();
        //isEmpty方法是AbstractCollection抽象類中的
        while (!pq.isEmpty())
            //remove方法按照優先隊列中定義的每次返回最小的元素
            //但是我們做了點手腳,讓大小反轉了,Java依舊是輸出小的,但是我們重新定義了字典序大的就是小
            System.out.print(pq.remove());
        System.out.println();
        System.out.println(pq);
    }
}
//這個自定義的比較器為優先隊列設定新的大小規則,
class MyComparator implements Comparator<String> {
    //來一下正負反轉實現從大到小的優先隊列,意思是字典序大的字符串更小
    @Override
    public int compare(String o1, String o2) {
        return -1 * o1.compareTo(o2);
    }
}

輸出:

[D, C, A, B]
DCAB
DCAB
//分割線,下面就實現了從大到小的打印了,當然Java依舊遵守從小到大打印,只是你改動了其中的規則
DCBA
[]

通過自定義比較器對自定義的類進行從小到大排序

程序:對水果類用優先隊列排序,價格低的優先(更小),價格相同字典序小的優先(更小)

public class PriorityQueueTest {
    public static void main(String[] args) {
        MyComparator myComparator = new MyComparator();
        var pq = new PriorityQueue<Fruit>(myComparator);
        Fruit fruit1 = new Fruit(10, "Banana");
        Fruit fruit2 = new Fruit(10, "Peach");
        Fruit fruit3 = new Fruit(20, "Apple");
        Fruit fruit4 = new Fruit(30, "Apple");
        pq.add(fruit1);
        pq.add(fruit2);
        pq.add(fruit3);
        pq.add(fruit4);
        System.out.println(pq);
        while (!pq.isEmpty())
            System.out.print(pq.remove() + " ");
        System.out.println("\n" + pq);
    }
}

class Fruit {
    private int price;
    private String name;

    @Override
    public String toString() {
        return "Fruit{" +
                "price=" + price +
                ", name='" + name + '\'' +
                '}';
    }

    public Fruit(int price, String name) {
        this.price = price;
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

class MyComparator implements Comparator<Fruit> {

    @Override
    public int compare(Fruit o1, Fruit o2) {
        //價格相同就定義水果的名稱的字典序小的優先級更高(或者說更小)
        if (o1.getPrice() == o2.getPrice())
            return o1.getName().compareTo(o2.getName());
        //價格不同就價格小的優先級更高(更小),這里是>,返回值正數表示這種情況下
            // 你定義左側比右側優先級更低(更大),而優先隊列永遠是“小”的先輸出
        else
            return o1.getPrice() > o2.getPrice() ? 1 : -1;
    }
}

輸出:

[Fruit{price=10, name='Banana'}, Fruit{price=10, name='Peach'}, Fruit{price=20, name='Apple'}, Fruit{price=30, name='Apple'}]
//分割線
Fruit{price=10, name='Banana'} Fruit{price=10, name='Peach'} Fruit{price=20, name='Apple'} Fruit{price=30, name='Apple'} 
[]

通過自定義的類實現Comparable接口進行從大到小排序

程序:對水果類用優先隊列排序,價格大的優先,價格相同,字典序大的優先

public class PriorityQueueTest {
    public static void main(String[] args) {
        var pq = new PriorityQueue<Fruit>();
        Fruit fruit1 = new Fruit(10, "Banana");
        Fruit fruit2 = new Fruit(10, "Peach");
        Fruit fruit3 = new Fruit(20, "Apple");
        Fruit fruit4 = new Fruit(30, "Apple");
        pq.add(fruit1);
        pq.add(fruit2);
        pq.add(fruit3);
        pq.add(fruit4);
        System.out.println(pq);
        while (!pq.isEmpty())
            System.out.print(pq.remove() + " ");
        System.out.println("\n" + pq);
    }
}

class Fruit implements Comparable<Fruit>{
    private int price;
    private String name;

    @Override
    public String toString() {
        return "Fruit{" +
                "price=" + price +
                ", name='" + name + '\'' +
                '}';
    }

    public Fruit(int price, String name) {
        this.price = price;
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int compareTo(Fruit o) {
        if (this.getPrice() == o.getPrice())
            return this.getName().compareTo(o.getName()) * -1;
        else
            return this.getPrice() > o.getPrice() ? -1 : 1;
    }
}

輸出:

[Fruit{price=30, name='Apple'}, Fruit{price=20, name='Apple'}, Fruit{price=10, name='Peach'}, Fruit{price=10, name='Banana'}]
//分割線
Fruit{price=30, name='Apple'} Fruit{price=20, name='Apple'} Fruit{price=10, name='Peach'} Fruit{price=10, name='Banana'} 
[]

用lambda表達式優化比較器的使用

補充一個知識點吧:對於那些只帶有一個抽象方法的接口,又被稱之為函數式接口,所有能用函數式接口對象的地方,都能用lambda表達式代替(寫起來快一點,有時還能解耦合?)

舉個上面的栗子,Comparator接口就是一個函數式接口,下面是它的接口的聲明,@后面的寫的很清楚了,告訴你它是一個函數式接口~,所以偶爾看看這些類的實現對我們學習Java會有很大幫助

@FunctionalInterface
public interface Comparator<T>

下面用lambda表達式對上面那個,通過構造Comparator比較器對象,實現水果按價格和字典序從小到大排序例子進行優化

程序:

public class PriorityQueueTest {
    public static void main(String[] args) {
        //通過lambda表達式創建比較器接口對象
        Comparator<Fruit> comparator = (o1, o2) -> {
            //價格相同就定義水果的名稱的字典序小的優先級更高(或者說更小)
            if (o1.getPrice() == o2.getPrice())
                return o1.getName().compareTo(o2.getName());
                //價格不同就價格小的優先級更高(更小),這里是>,返回值正數表示這種情況下
                // 你定義左側比右側優先級更低(更大),而優先隊列永遠是“小”的先輸出
            else
                return o1.getPrice() > o2.getPrice() ? 1 : -1;
        };
        //這里和之前的比較器用法相同,函數式接口完美兼容lambda表達式
        var pq = new PriorityQueue<Fruit>(comparator);
        Fruit fruit3 = new Fruit(20, "Apple");
        Fruit fruit4 = new Fruit(30, "Apple");
        Fruit fruit1 = new Fruit(10, "Banana");
        Fruit fruit2 = new Fruit(10, "Peach");
        pq.add(fruit1);
        pq.add(fruit2);
        pq.add(fruit3);
        pq.add(fruit4);
        System.out.println(pq);
        while (!pq.isEmpty())
            System.out.print(pq.remove() + " ");
        System.out.println("\n" + pq);
    }
}

class Fruit {
    private int price;
    private String name;

    @Override
    public String toString() {
        return "Fruit{" +
                "price=" + price +
                ", name='" + name + '\'' +
                '}';
    }

    public Fruit(int price, String name) {
        this.price = price;
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

輸出:

[Fruit{price=10, name='Banana'}, Fruit{price=10, name='Peach'}, Fruit{price=20, name='Apple'}, Fruit{price=30, name='Apple'}]
//分割線
Fruit{price=10, name='Banana'} Fruit{price=10, name='Peach'} Fruit{price=20, name='Apple'} Fruit{price=30, name='Apple'} 
[]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM