Java使用Arrays.sort()方法實現給對象排序

使用Arrays.sort()方法給對象排序

當我們給一個整型數組或者浮點型之類的數組排序的時候,很簡單就可以達到我們排序的目的,無非是排序算法的問題。那麼,如果我們現在想根據對象的一個屬性值給一個對象數組進行排序呢?

假如我們現在有一個Car類型,Car類中有一個double型的speed屬性用來描述車輛的速度,現在我們想根據車速來對一個Car數組中的車輛進行排序,怎麼做呢?

public class Car{
	private double speed;//車輛的速度屬性
	public Car(double speed) {
		this.speed = speed;
	}
	public double getSpeed() {
		return speed;
	}
	public void setSpeed(double speed) {
		this.speed = speed;
	}	
}

麻煩的方法

我們可以通過比較對象的屬性值,根據比較結果,對對象數組進行排序,例如,假如我們使用的是冒泡排序的話,就需要寫成下面這樣:

for(int i=0;i<a.length-1;i++)//a為一個存儲瞭很多Car對象的數組
{
	for(int j=0;j<a.length-i-1;j++)
	{
		if(a[j].getSpeed()>a[j+1].getSpeed())//交換操作
		{
			Car temp=a[j];
			a[j]=a[j+1];
			a[j+1]=temp;
		}
	}
}

這樣寫確實能實現瞭效果,但是未必性能很好,由於是自己寫的,寫起來也會比較麻煩,下面的方法會更好

Arrays.sort()方法

我們先來看看用Array.sort()方法實現對車輛排序的代碼:

其中,Car這個類有兩種寫法:

第一種寫法:

public class Car implements Comparable<Car>{
	private double speed;
	public Car(double speed) {
		this.speed = speed;
	}
	public double getSpeed() {
		return speed;
	}
	public void setSpeed(double speed) {
		this.speed = speed;
	}
	@Override
	public int compareTo(Car newCar) {
		return Double.compare(this.getSpeed(),newCar.getSpeed());
	}	
}

第二種寫法

public class Car implements Comparable{
	private double speed;
	public Car(double speed) {
		this.speed = speed;
	}
	public double getSpeed() {
		return speed;
	}
	public void setSpeed(double speed) {
		this.speed = speed;
	}
	@Override
	public int compareTo(Object newCar) {
		return Double.compare(this.getSpeed(),((Car)newCar).getSpeed());
	}	
}

main方法:

public class Test8 {
	public static void main(String[] args) {
		Car[] carList = new Car[3];
		carList[0] = new Car(30);
		carList[1] = new Car(10);
		carList[2] = new Car(55);
		Arrays.sort(carList);
		
		for(Car c:carList)
		{
			System.out.println(c.getSpeed());
		}
	}	
}

輸出結果:

在這裡插入圖片描述

解析:

顧名思義,Arrays類的sort()方法是對一個數組進行排序的方法,sort()方法的參數是一個對象數組,也就是要排序的那個數組,但是有些特別的是,這個對象數組中存儲的對象的類必須實現瞭Comparable接口。

Comparable接口是用來比較大小的,要實現這個接口,我們必須重寫接口中的CompareTo()方法,這個方法用來比較兩個對象的大小,因為方法本身並不知道我我們會根據對象的哪個屬性來比較兩個對象的大小,因此在這個方法中,我們可以定義我們自己的比較規則。

在上述的代碼中,我們調用瞭Double類的比較方法來比較兩個對象的speed屬性的大小,如果調用方法的Car對象的speed屬性值比方法參數傳進來的Car對象的speed值大就返回1,相等就返回0,小於就返回-1。當然,我們也可以自己手寫這個部分,無非就是if else判斷的事。

之所以有兩種寫法,是因為第一種使用瞭泛型,第二種使用瞭對象的強制轉換,可以看到,當我們使用泛型的時候,在調用對象的時候就已經確定瞭使用接口的時候用的是哪一種對象,因此就不用把對象類型強制轉換為我們想要的類型瞭。而第二種我們沒有使用泛型,因此就需要把參數傳進來的對象強制轉換成我們想要的類型然後再進行操作。推薦使用第一種寫法。

接下來,我們在main方法中構建瞭一個Car類型的數組,然後直接調用Arrays.sort()方法傳進數組作為參數就可以對數組進行排序瞭。

這種方法比較方便,快捷,在性能方面也有不錯的表現,本人親測在排序20萬個對象的時候花費差不多200ms左右的時間。

當然,這種方法主要圖的是方便,當我們需要排序大量的數據的時候,還是應該結合自己數據的特性來具體選擇一種排序方式自己寫。

淺談Arrays.sort()原理

首先先來看一下Arrays.sort()使用的例子。

例子1

Arrays.sort(int[] a)
 //註意一定要用Integer對象類
        Integer[] a1 = {34, 57, 46, 89, 98, 12, 55, 84, 29};
        Integer[] a2 = {34, 57, 46, 89, 98, 12, 55, 84, 29};
        //增序,Arrays.sort()默認升序
        Arrays.sort(a1);
        System.out.println("Arrays.sort()升序:");
        for (int i = 0; i < a1.length; i++) {
            System.out.print(a1[i] + " ");
        }
        //降序,可用Comparator()匿名內部類
        Arrays.sort(a2, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        System.out.println("\nArrays.sort()降序:");
        for (int i = 0; i < a2.length; i++) {
            System.out.print(a2[i]+ " ");
        }

基礎知識點

  • 若是基本類型,需要轉化為對應的對象類型(如:int轉化為Integer)Arrays.sort()可以排序基本對象類型,但是不可以使用基本數據類型。
  • Arrays.sort()默認的是升序排序,降序排序可采用Collection.sort()匿名內部類。
  • 數組與list一樣,需要遍歷出來。

運行結果如下:

Arrays.sort()升序: 12 29 34 46 55 57 84 89 98
Arrays.sort()降序: 98 89 84 57 55 46 34 29 12

例子2

Arrays.sort(int[] a, int fromIndex, int toIndex)
//註意一定要用Integer對象類
Integer[] a1 = {34, 57, 46, 89, 98, 12, 55, 84, 29};
 //對數組中的第四位到第7位(不包含第七位)(左閉右開原則)進行排序
 Arrays.sort(a1,3,6);
 System.out.println("Arrays.sort()升序:");
 for (int i = 0; i < a1.length; i++) {
     System.out.print(a1[i] + " ");
 }

運行結果如下:

在這裡插入圖片描述

結合文檔以及源代碼,我們發現,jdk中的Arrays.sort()的實現是通過所謂的雙軸快排的算法

雙軸快排

  • 快速排序使用的是分治思想,將原問題分成若幹個子問題進行遞歸解決。選擇一個元素作為軸(pivot),通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比軸元素小,另外一部分的所有數據都比軸元素大,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
  • 雙軸快排(DualPivotQuicksort),顧名思義有兩個軸元素pivot1,pivot2,且pivot ≤ pivot2,將序列分成三段:x < pivot1、pivot1 ≤ x ≤ pivot2、x >pivot2,然後分別對三段進行遞歸。這個算法通常會比傳統的快排效率更高,也因此被作為Arrays.java中給基本類型的數據排序的具體實現。

下面我們以JDK1.8中Arrays對int型數組的排序為例來介紹其中使用的雙軸快排:

1.判斷數組的長度是否大於286,大於則使用歸並排序(merge sort),否則執行2。

 // Use Quicksort on small arrays
    if (right - left < QUICKSORT_THRESHOLD) {
            sort(a, left, right, true);
            return;
    }
    // Merge sort
    ......

2.判斷數組長度是否小於47,小於則直接采用插入排序(insertion sort),否則執行3。

 // Use insertion sort on tiny arrays
    if (length < INSERTION_SORT_THRESHOLD) {
    // Insertion sort
    ......
    }

3.用公式length/8+length/64+1近似計算出數組長度的1/7。

 // Inexpensive approximation of length / 7
    int seventh = (length >> 3) + (length >> 6) + 1;

4.取5個根據經驗得出的等距點。

 /*
     * Sort five evenly spaced elements around (and including) the
     * center element in the range. These elements will be used for
     * pivot selection as described below. The choice for spacing
     * these elements was empirically determined to work well on
     * a wide variety of inputs.
     */
    int e3 = (left + right) >>> 1; // The midpoint
    int e2 = e3 - seventh;
    int e1 = e2 - seventh;
    int e4 = e3 + seventh;
    int e5 = e4 + seventh;

5.將這5個元素進行插入排序

// Sort these elements using insertion sort
    if (a[e2] < a[e1]) { int t = a[e2]; a[e2] = a[e1]; a[e1] = t; }
    if (a[e3] < a[e2]) { int t = a[e3]; a[e3] = a[e2]; a[e2] = t;
    if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }
    }
    if (a[e4] < a[e3]) { int t = a[e4]; a[e4] = a[e3]; a[e3] = t;
        if (t < a[e2]) { a[e3] = a[e2]; a[e2] = t;
            if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }
        }
    }
    if (a[e5] < a[e4]) { int t = a[e5]; a[e5] = a[e4]; a[e4] = t;
        if (t < a[e3]) { a[e4] = a[e3]; a[e3] = t;
            if (t < a[e2]) { a[e3] = a[e2]; a[e2] = t;
                if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }
            }
        }
    }

6.選取a[e2],a[e4]分別作為pivot1,pivot2。由於步驟5進行瞭排序,所以必有pivot1 <=pivot2。定義兩個指針less和great,less從最左邊開始向右遍歷,一直找到第一個不小於pivot1的元素,great從右邊開始向左遍歷,一直找到第一個不大於pivot2的元素。

 /*
         * Use the second and fourth of the five sorted elements as pivots.
         * These values are inexpensive approximations of the first and
         * second terciles of the array. Note that pivot1 <= pivot2.
         */
        int pivot1 = a[e2];
        int pivot2 = a[e4];
        /*
         * The first and the last elements to be sorted are moved to the
         * locations formerly occupied by the pivots. When partitioning
         * is complete, the pivots are swapped back into their final
         * positions, and excluded from subsequent sorting.
         */
        a[e2] = a[left];
        a[e4] = a[right];
        /*
         * Skip elements, which are less or greater than pivot values.
         */
        while (a[++less] < pivot1);
        while (a[--great] > pivot2);

7.接著定義指針k從less-1開始向右遍歷至great,把小於pivot1的元素移動到less左邊,大於pivot2的元素移動到great右邊。這裡要註意,我們已知great處的元素小於pivot2,但是它於pivot1的大小關系,還需要進行判斷,如果比pivot1還小,需要移動到到less左邊,否則隻需要交換到k處。

/*
 * Partitioning:
 *
 *   left part           center part                   right part
 * +--------------------------------------------------------------+
 * |  < pivot1  |  pivot1 <= && <= pivot2  |    ?    |  > pivot2  |
 * +--------------------------------------------------------------+
 *               ^                          ^       ^
 *               |                          |       |
 *              less                        k     great
 *
 * Invariants:
 *
 *              all in (left, less)   < pivot1
 *    pivot1 <= all in [less, k)     <= pivot2
 *              all in (great, right) > pivot2
 *
 * Pointer k is the first index of ?-part.
 */
        outer:
        for (int k = less - 1; ++k <= great; ) {
            int ak = a[k];
            if (ak < pivot1) { // Move a[k] to left part
                a[k] = a[less];
                /*
                 * Here and below we use "a[i] = b; i++;" instead
                 * of "a[i++] = b;" due to performance issue.
                 */
                a[less] = ak;
                ++less;
            } else if (ak > pivot2) { // Move a[k] to right part
                while (a[great] > pivot2) {
                    if (great-- == k) {
                        break outer;
                    }
                }
                if (a[great] < pivot1) { // a[great] <= pivot2
                    a[k] = a[less];
                    a[less] = a[great];
                    ++less;
                } else { // pivot1 <= a[great] <= pivot2
                    a[k] = a[great];
                }
                /*
                 * Here and below we use "a[i] = b; i--;" instead
                 * of "a[i--] = b;" due to performance issue.
                 */
                a[great] = ak;
                --great;
            }
        }

8.將less-1處的元素移動到隊頭,great+1處的元素移動到隊尾,並把pivot1和pivot2分別放到less-1和great+1處。

// Swap pivots into their final positions
        a[left]  = a[less  - 1]; a[less  - 1] = pivot1;
        a[right] = a[great + 1]; a[great + 1] = pivot2;

9.至此,less左邊的元素都小於pivot1,great右邊的元素都大於pivot2,分別對兩部分進行同樣的遞歸排序。

// Sort left and right parts recursively, excluding known pivots
        sort(a, left, less - 2, leftmost);
        sort(a, great + 2, right, false);

10.對於中間的部分,如果大於4/7的數組長度,很可能是因為重復元素的存在,所以把less向右移動到第一個不等於pivot1的地方,把great向左移動到第一個不等於pivot2的地方,然後再對less和great之間的部分進行遞歸排序。

/*
         * If center part is too large (comprises > 4/7 of the array),
         * swap internal pivot values to ends.
         */
        if (less < e1 && e5 < great) {
            /*
             * Skip elements, which are equal to pivot values.
             */
            while (a[less] == pivot1) {
                ++less;
            }
            while (a[great] == pivot2) {
                --great;
            }
        }
        ......
        // Sort center part recursively
        sort(a, less, great, false);

另外參考瞭其他博文,算法思路如下

算法步驟

1.對於很小的數組(長度小於47),會使用插入排序。

2.選擇兩個點P1,P2作為軸心,比如我們可以使用第一個元素和最後一個元素。

3.P1必須比P2要小,否則將這兩個元素交換,現在將整個數組分為四部分:

(1)第一部分:比P1小的元素。

(2)第二部分:比P1大但是比P2小的元素。

(3)第三部分:比P2大的元素。

(4)第四部分:尚未比較的部分。

在開始比較前,除瞭軸點,其餘元素幾乎都在第四部分,直到比較完之後第四部分沒有元素。

4.從第四部分選出一個元素a[K],與兩個軸心比較,然後放到第一二三部分中的一個。

5.移動L,K,G指向。

6.重復 4 5 步,直到第四部分沒有元素。

7.將P1與第一部分的最後一個元素交換。將P2與第三部分的第一個元素交換。

8.遞歸的將第一二三部分排序。

**總結:**Arrays.sort對升序數組、降序數組和重復數組的排序效率有瞭很大的提升,這裡面有幾個重大的優化。

1.對於小數組來說,插入排序效率更高,每次遞歸到小於47的大小時,用插入排序代替快排,明顯提升瞭性能。

2.雙軸快排使用兩個pivot,每輪把數組分成3段,在沒有明顯增加比較次數的情況下巧妙地減少瞭遞歸次數。

3.pivot的選擇上增加瞭隨機性,卻沒有帶來隨機數的開銷。

4.對重復數據進行瞭優化處理,避免瞭不必要交換和遞歸。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: