在java排序算法中,按時間複雜度分類的話可以分爲三類。如下:
(1)O(n*n)的排序算法,有插入排序、冒泡排序、選擇排序;
(2)O(n*lgn)的排序算法,有歸併排序、堆排序以及快速排序;
(3)O(n)的排序算法,有基數排序、計數排序及桶排序。
O(n*n)的排序算法
(1)插入排序
算法思路:首先以第一個數爲基數,取出第二個數和基數做對比,如果大於基數則放在基數的右邊,反之則放在基數的左邊。依次取出後面的數(j)則依次和0..j-1中的每個數做對比,找到某個位置使得此數大於左邊的數小於等於右邊的數。
代碼如下:
private void insertSort(int[] origin){
if(origin != null && origin.length > 1){
for(int i=1; i<origin.length - 1; i++){
int cur = origin[i];
int j = i - 1;
while(j >= 0 && cur < origin[j]){
int temp = origin[j];
origin[j] = cur;
origin[j + 1] = temp;
j--;
}
}
}
}
(2)冒泡排序
算法思路:所有的數據從左至右,兩兩比較,如果左數比右數大,則交換位置,反之則位置不變,每掃描一輪則會找到一個最大值排在最後面,重複n-1輪就會排序完畢。
代碼如下:
private void bubbleSort(int[] origin){
if(origin != null && origin.length > 1){
boolean isChanged = false;
for (int i = 1; i <= origin.length; i++) {
isChanged = false;
for (int j = 0; j < origin.length - i; j++) {
if (origin[j] > origin[j + 1]) {
int tmp = origin[j];
origin[j] = origin[j + 1];
origin[j + 1] = tmp;
isChanged = true;
}
}
if (!isChanged) {
break;
}
}
}
}
(3)選擇排序
排序思路:掃描所有數據,找到最大的和最右側的數據交換。或找到最小的和最左側的數據交換。
代碼如下:
public void selectSort(int[] origin){
if(origin != null && origin.length > 1){
for(int i=0; i<origin.length; i++){
int maxIndex = 0;
for(int j=1; j<origin.length - i; j++){
if(origin[j] > origin[maxIndex]){
maxIndex = j;
}
}
int lastIndex = origin.length - i - 1;
if(maxIndex != lastIndex){
int tmp = origin[lastIndex];
origin[lastIndex] = origin[maxIndex];
origin[maxIndex] = tmp;
}
}
}
}
O(n*lgn)的排序算法
(1)歸併排序(merge sort)
歸併排序採用的策略叫做“分治策略”,分治策略有下面三個步驟:
1. 分解原問題爲若干子問題;
2. 解決這些子問題,子問題較大就繼續分解子問題,遞歸求解各個子問題,當子問題足夠小,通常就可以直接解決;
3. 合併這些子問題的解,合併成爲原問題的解。
歸併排序:分解待排序的n個元素序列爲各有n/2個元素的兩個子序列,然後遞歸的分解左右兩個子序列,最後到無法再分解爲止,遞歸合併這些子序列以產生排序的最後結果。
代碼如下:
private void mergeSort(int[] origin){
if(origin != null && origin.length > 1){
mergeSortInternal(origin, 0, origin.length - 1);
}
}
private void mergeSortInternal(int[] origin, int left, int right){
if(left >= right){
return;
}
int center = (left + right) / 2;
//對左邊數組進行分治遞歸
mergeSortInternal(origin, left, center);
//對右邊數組進行分治遞歸
mergeSortInternal(origin, center + 1, right);
//對排序好的數組進行合併
merge(origin, left, center, right);
}
private void merge(int[] origin, int left, int center, int right){
int[] tempArr = new int[origin.length];
int tempStartIndex = leftIndex;
int midIndex = center + 1;
int leftIndex = left;
while(leftIndex <= center && midIndex <= right){
if(origin[leftIndex] > origin[midIndex]){
tempArr[tempStartIndex++] = origin[leftIndex++];
}else {
tempArr[tempArrIndex++] = origin[midIndex ++];
}
}
while(leftIndex <= center){
tempArr[tempArrIndex++] = origin[leftIndex++];
}
while(midIndex <= right){
tempArr[tempArrIndex++] = origin[midIndex++];
}
for(int i=left; i<=right; i++){
origin[i] = tempArr[i];
}
}
(2)堆排序(heap sort)
堆排序主要用到二叉樹的概念,堆是一棵順序存儲的完全二叉樹。
最大堆:其中每個節點都不小於其左右子節點,這樣的堆稱爲最大堆。
最小堆:其中的每個節點都不大於其左右子節點,這樣的堆稱爲最小堆。
一個堆可以用數組表示,某個節點i的左子數爲2*i + 1,右子樹爲2*i+2。某個節點i的父節點爲 (i - 1) / 2。
堆排序:
1. 首先要將數組R[0..n]構建成最大堆;
2. 然後將最大堆的R[0]和堆的最後一個交換位置即R[0]和R[n]交換位置;
3. 然後再將R[0..n-1]調整爲最大堆,再交換R[0]和R[n-1]的位置;
4. 如此反覆,知道交換了R[0]和R[1]爲止。
代碼如下:
private void maxHeapSort(int[] origin ){
if(origin != null && origin.length > 1){
//構建最大堆
buildMaxHeap(origin);
int swapCount = 1;
while(swapCount < origin.length){
int temp = origin[0];
origin[0] = origin[origin.length - swapCount];
origin[origin.length - swapCount] = temp;
maxHeap(origin, 0, origin.length - swapCount);
swapCount++;
}
}
}
private void buildMaxHeap(int[] origin){
for(int i = origin.length / 2; i>=0; i--){
maxHeap(origin, i, origin.length);
}
}
//將某個樹枝節點構建成最大堆,範圍在length。
private void maxHeap(int[] origin, int branch, int length){
int leftChild = 2 * branch + 1;
if(leftChild < length){
int swapChildIndex = leftChild;
int rightChild = 2 * branch + 2;
if(rightChild < length && origin[rightChild] > origin[leftChild]){
swapChildIndex = rightChild;
}
if(origin[swapChildIndex] > origin[branch]){
int tmp = origin[swapChildIndex];
origin[swapChildIndex] = origin[branch];
origin[branch] = tmp;
maxHeap(origin, swapChildIndex, length);
}
}
}
(3)快速排序(quick sort)
快速排序也採用了“分治策略”,步驟爲:
1. 從數組A[0..n]中找一個數作爲基數,以這個基數分爲兩個子數組,A[0..q]和A[q+1..n],使得A[0..q]都小於等於這個基數,A[q+1..n]都大於等於這個基數。
2. 使用遞歸對子數組分別進行如上分解。
代碼如下:
private void quickSort(int[] origin){
if(origin != null && origin.length > 1){
quick(origin, 0, origin.length-1);
}
}
private void quick(int[] origin, int start, int end){
int startIndex = start;
int endIndex = end;
int baseIndex = start;
int base = origin[baseIndex];
while(endIndex > startIndex){
//首先從後往前遍歷找到一個比base小的數然後再互換位置
for(; endIndex > startIndex; endIndex--){
if(base > origin[endIndex]){
int tmp = origin[endIndex];
origin[endIndex] = base;
origin[baseIndex] = tmp;
baseIndex = endIndex;
break;
}
}
//再從前往後遍歷找到一個比基數大的數,然後再和基數互換位置
for(; startIndex < endIndex; startIndex++){
if(base < origin[startIndex]){
int tmp = origin[startIndex];
origin[startIndex] = base;
origin[baseIndex] = tmp;
baseIndex = startIndex;
break;
}
}
if(baseIndex - start > 1){
quick(origin, start, baseIndex - 1);
}
if(end - baseIndex > 1){
quick(origin, baseIndex + 1, end);
}
}
}
O(n)的排序算法
計數排序、基數排序及桶排序這三種排序方法都基於對要排序對象的已知限制情況。比如:對一個數組排序,而且此數組的數字在0到1000之間;
對日期或時間進行排序:2017-05-17 21:37:51;
對2017年的高考成績進行排序。
(1)計數排序
計數排序的精髓就是數一數每一個被排序的數出現了幾次。
首先,計數排序要求待排序的所有元素只能取有限並且已知的若干值;
創建一個用於計數的表格,列出所有待排序元素的可能取值,這些可能的取值是已經排序過的,每個取值有一個計數器,計數器的初始值都是0;
對所有的待排序元素進行掃描,在計數表格中找到掃描的值,然後將其計數器加1;
輸出的時候只看該數值的計數器是幾,就在輸出序列中重複輸出幾遍即可,計數器爲0就不輸出。
例如有一個數組,已知這個數組中的數都是0..10的。請對此數組進行排序。
//已知是0..10的數字
private void countSort(int[] origin){
int[] countArr = new int[11];
for (int i : origin){
countArr[i] = ++countArr[i];
}
//重新賦值
int index = 0;
for (int i=0; i<countArr.length; i++){
if (countArr[i] > 0){
for (int j=0; j<countArr[i]; j++){
origin[index++] = i;
}
}
}
}
(2)桶排序
桶排序的精髓就是將符合某種條件的待排序元素放在以這個條件命名的一個桶裏面,
比如:對某次數學考試的成績排序,假設成績都是0到100之間的整數;
1. 首先創建101個考試分數的桶,從0分桶到100分桶,桶裏面初始都是空的;
2. 然後對要排序的已有評分的考試試卷,一張一張地去判斷,85分的,放到85分的桶裏,99分的放在99分的桶裏……;
3. 所有試卷都放到對應分數的桶裏之後,從100分桶、99分桶、98分桶……裏面依次往外取試卷。
(3)基數排序
基數排序的精髓就是按照數字天然的數基去排序,比如整數的“個位、十位、百位、千位……”,時間的“年、月、日、時、分、秒”
1. 基數排序可以從低位開始排,也可以從高位開始排
2. 如果從高位開始排,注意要對高位補0,比如:2017年3月7日,要先寫成“2017年03月07日”,或者123要寫成0000123,補多少0取決於待排序數最大的有多少位(如果根本不知道最大的有多少位,還是從低位開始排吧)