注:關於排序算法,博主寫過【數據結構排序算法系列】數據結構八大排序算法,基本上把所有的排序算法都詳細的講解過,而之所以單獨將java集合中的排序算法拿出來講解,是因爲在阿里巴巴內推面試的時候面試官問過我,讓我說說java集合框架中用的哪種排序算法,當時回答錯了,(關於面試詳細過程請參看:【阿里內推一面】記我人生的處女面)面試結束後看了一下java源碼,用的是折半插入排序算法,本來早就打算寫此博客,但是因爲準備鵝廠的在線考試,而鵝廠在我心中的地位是最高的,爲了準備鵝廠的在線考試,自己基本上把所有事情都擱置起來了,把全部的精力都投入到複習中去了,所以一直沒動手寫。既然java的天才程序員都採用了折半插入排序,那麼“此人必有過人之處”,因此得好好了解一下折半插入排序。
我們先從c語言中的折半插入排序算法看起,在此基礎之上在來看java集合框架中的源碼。
#include<iostream>
using namespace std;
const int len=7;
void binaryInsertSort(int * array,int len)
{
for(int i=1;i<len;i++)//與普通的排序一樣,外層for循環用來控制排序趟數
{
int x=array[i];
int low=0,high=i-1;//low與high的初始化很重要,因爲i從1開始,所以low=0,high=i-1,這樣就能保證數組中的每一個
//元素參與排序,教材上的low=1是錯誤的,因爲教材上將數組中的第0位作爲監視哨而未參與排序。
while(low<=high)//尋找待插入的位置
{
int mid=(low+high)/2;
if(x<array[mid])
high=mid-1;
else
low=mid+1;
}
for(int j=i-1;j>=low;j--)//將記錄向後移動
{
array[j+1]=array[j];
}
array[low]=x;//插入記錄
}
}
int main()
{
int a[len]={7,0,4,5,1,2,3};
binaryInsertSort(a,len);
for(int i=0;i<len;i++)
cout<<a[i]<<' ';
cout<<endl;
}
可以看到折半插入排序的思想是基於折半查找的,即對有序表進行折半查找,其性能較好,所以可將折半查找的思路運用到排序中一個數組中的元素雖然剛開始不是有序的,但是可以通過折半查找的同時構造有序表,即折半插入排序算法即是通過折半查找構造有序序列,然後在已構造的部分有序序列中運用折半查找插入元素,最終直至整個表排好序爲止。
程序運行結果如下:
經過上述c語言代碼的講解,下面我們來看一下java的那些天才設計者們是如何java實現該算法的以及分析一個爲何那些天才們看上的不是我們普通程序員最喜歡的快速排序而是折半插入排序。
下面是java中TimSort類中的sort源碼,而java集合中的類調用的sort方法最終會調用它
static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
T[] work, int workBase, int workLen) {
assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;
int nRemaining = hi - lo;
if (nRemaining < 2)
return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
可以看到在TimSort類中最終會調用binarySort方法,即折半插入排序,我們來看一下其源碼:
/**
* Sorts the specified portion of the specified array using a binary
* insertion sort. This is the best method for sorting small numbers
* of elements. It requires O(n log n) compares, but O(n^2) data
* movement (worst case).
*
* If the initial part of the specified range is already sorted,
* this method can take advantage of it: the method assumes that the
* elements from index {@code lo}, inclusive, to {@code start},
* exclusive are already sorted.
*
* @param a the array in which a range is to be sorted
* @param lo the index of the first element in the range to be sorted
* @param hi the index after the last element in the range to be sorted
* @param start the index of the first element in the range that is
* not already known to be sorted ({@code lo <= start <= hi})
* @param c comparator to used for the sort
*/
@SuppressWarnings("fallthrough")
private static <T> void binarySort(T[] a, int lo, int hi, int start,
Comparator<? super T> c) {
assert lo <= start && start <= hi;
if (start == lo)
start++;
for ( ; start < hi; start++) {
T pivot = a[start];
// Set left (and right) to the index where a[start] (pivot) belongs
int left = lo;
int right = start;
assert left <= right;
/*
* Invariants:
* pivot >= all in [lo, left).
* pivot < all in [right, start).
*/
while (left < right) {
int mid = (left + right) >>> 1;
if (c.compare(pivot, a[mid]) < 0)
right = mid;
else
left = mid + 1;
}
assert left == right;
/*
* The invariants still hold: pivot >= all in [lo, left) and
* pivot < all in [left, start), so pivot belongs at left. Note
* that if there are elements equal to pivot, left points to the
* first slot after them -- that's why this sort is stable.
* Slide elements over to make room for pivot.
*/
int n = start - left; // The number of elements to move
// Switch is just an optimization for arraycopy in default case
switch (n) {
case 2: a[left + 2] = a[left + 1];
case 1: a[left + 1] = a[left];
break;
default: System.arraycopy(a, left, a, left + 1, n);
}
a[left] = pivot;
}
}
可以看到其實其代碼一點也不復雜,與我們上面分析的c語言代碼幾乎完全相同,只不過它所排序的元素不再是簡單的int型,比較規則也不再是簡單的比較數的大小,而是通過java中的Comparator接口來規定的,可以看到註釋遠遠多於代碼量,一方面這是因爲那些天才們用其高超的藝術大大的簡化了代碼,另一方面也是爲了解釋關於選擇折半插入排序的原因:
/**
* Sorts the specified portion of the specified array using a binary
* insertion sort. This is the best method for sorting small numbers
* of elements. It requires O(n log n) compares, but O(n^2) data
* movement (worst case).
/*
* The invariants still hold: pivot >= all in [lo, left) and
* pivot < all in [left, start), so pivot belongs at left. Note
* that if there are elements equal to pivot, left points to the
* first slot after them -- that's why this sort is stable.
* Slide elements over to make room for pivot.
*/
從我截取的這兩段註釋來看,可以知道:1折半插入排序是最好的算法對於排序小數量的元素This is the best method for sorting small numbers of elements.
2它只需要O(nlogn)的比較次數,但是其移動次數仍然爲 O(n^2)。It requires O(n log n) compares, but O(n^2) data movement (worst case).
3它是穩定的排序算法。that's why this sort is stable.而快速排序不是穩定的排序。
分析到這我們就可以知道爲何會選擇折半插入排序,其中1和3是最主要的原因。