排序
實際開發中,爲什麼我們更傾向於使用插入排序而不是冒泡排序?
答:從代碼實現上來看,冒泡排序的數據交換要比插入排序的數據移動要複雜,冒泡排序需要3個賦值操作,而插入排序只需要1個,所以在對相同數組進行排序時,冒泡排序的運行時間理論上要長於插入排序。
排序算法的執行效率
1.最好情況,最壞情況,平均情況時間複雜度
2.時間複雜度的係數,常數,低階(在對同一階時間複雜度的排序性能對比的時候,我們就要把係數,常數,低階考慮進來)
3.比較次數和交換(移動)次數
排序算法的內存消耗
原地排序,原地排序算法特指空間複雜度爲O(1)的排序算法,
排序算法的穩定性
如果待排序的序列中存在值相等的元素,經過排序之後,相等元素之間原有的先後順序不變,穩定的就是經過排序之後,兩個相同元素之間的順序沒有變化。
冒泡排序
冒泡排序只會操作相鄰的兩個數據,每次冒泡操作都會對相鄰的兩個元素進行比較,看是否滿足大小關係要求,不滿足就互換,(想做魚吐泡泡,排序後的越靠後越大,想象泡泡上浮的過程,越變越大)
冒泡是原地排序算法,是穩定的,時間複雜度最好的是O(n),最差的就是O(n2)
package sort;
public class bubbleSort {
public static int[] bubbleSort(int[] array) {
if (array.length == 0) {
return array;
}
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j + 1] < array[j]) {
int tmp = array[j + 1];
array[j + 1] = array[j];
array[j] = tmp;
}
}
}
return array;
}
/*
修改後的冒泡排序
傳統的冒泡算法每次排序只確定了最大值,
我們可以在每次循環之中進行正反兩次冒泡,分別找到最大值和最小值,如此可使排序的輪數減少一半。*/
public static int[] bubbleSort2(int[] array) {
int low = 0;
int high = array.length - 1;
while (low < high) { //正向冒泡,確定最大值
for (int i = low; i < high; i++) { //如果前一位大於後一位,交換位置
if (array[i] > array[i + 1]) {
int temp = array[i];
array[i] = array[i + 1];
array[i + 1] = temp;
}
}
--high;
for (int j=high;j>low;--j){ //反向冒泡,確定最小值
if(array[j]<array[j-1]){ //如果前一位大於後一位,交換位置
int temp = array[j];
array[j] = array[j-1];
array[j-1] = temp;
}
}
++low;
}
return array;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
bubbleSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
int [] a = {1,5,4,11,2,20,18};
bubbleSort2(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + ",");
}
}
}
插入排序
思想就是動態的將有序集合中添加數據。
插入排序包含兩種操作,一種是元素的比較,一種是元素的移動,
插入排序是原地排序算法,是穩定的排序算法,最好的是時間複雜度爲O(n),最差爲O(n2);
package sort;
public class insertionSort {
public static int[] insertionSort(int[] array){
if(array.length == 0){
return array;
}
int current;
for(int i=0;i<array.length - 1;i++){
current = array[i+1];
int preIndex = i;
while(preIndex >=0 && current < array[preIndex]){
array[preIndex + 1] = array[preIndex];
preIndex--;
}
array[preIndex + 1] = current;
}
return array;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
insertionSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
選擇排序:
選擇排序每次會從未排序區間找到最小的元素,將其放到已排序區間的末尾
選擇排序也是一種原地排序算法,時間複雜度都爲O(n2);是不穩定的算法
package sort;
public class selectionSort {
public static int[] selectionSort(int[] array){
if(array.length == 0){
return array;
}
for(int i=0;i<array.length;i++){
int minIndex = i;
for(int j=i;j<array.length;j++){
if(array[j] < array[minIndex]) //找到最小的數
minIndex = j;
}
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
return array;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
selectionSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
歸併排序
如果要排序一個數組,我們先把數組從中間分成前後兩部分,然後對前後兩部分分別排序,然後將排好序的兩部分合併在一起,這樣整個數組就都有序了
使用的是分治思想,(分治是一種解決問題的處理思想,遞歸是一種編程技巧)
歸併排序是一個穩定的排序算法,時間複雜度是穩定的都爲O(nlogn),空間複雜度爲O(n)
package sort;
public class MergeSort {
static int number=0;
public static void main(String[] args) {
int[] a = {26, 5, 98, 108, 28, 99, 100, 56, 34, 1 };
printArray("排序前:",a);
MergeSort(a);
printArray("排序後:",a);
}
private static void printArray(String pre,int[] a) {
System.out.print(pre+"\n");
for(int i=0;i<a.length;i++)
System.out.print(a[i]+"\t");
System.out.println();
}
private static void MergeSort(int[] a) {
// TODO Auto-generated method stub
System.out.println("開始排序");
Sort(a, 0, a.length - 1);
}
private static void Sort(int[] a, int left, int right) {
if(left>=right)
return;
int mid = (left + right) / 2;
//二路歸併排序裏面有兩個Sort,多路歸併排序裏面寫多個Sort就可以了
Sort(a, left, mid);
Sort(a, mid + 1, right);
merge(a, left, mid, right);
}
private static void merge(int[] a, int left, int mid, int right) {
int[] tmp = new int[a.length];
int r1 = mid + 1;
int tIndex = left;
int cIndex=left;
// 逐個歸併
while(left <=mid && r1 <= right) {
if (a[left] <= a[r1])
tmp[tIndex++] = a[left++];
else
tmp[tIndex++] = a[r1++];
}
// 將左邊剩餘的歸併
while (left <=mid) {
tmp[tIndex++] = a[left++];
}
// 將右邊剩餘的歸併
while ( r1 <= right ) {
tmp[tIndex++] = a[r1++];
}
System.out.println("第"+(++number)+"趟排序:\t");
// TODO Auto-generated method stub
//從臨時數組拷貝到原數組
while(cIndex<=right){
a[cIndex]=tmp[cIndex];
//輸出中間歸併排序結果
System.out.print(a[cIndex]+"\t");
cIndex++;
}
System.out.println();
}
}
快速排序
快排是一種原地的,不穩定的排序算法,時間複雜度爲O(nlogn);
package sort;
public class QuickSort {
public static int[] QuickSort(int[] array, int start, int end) {
if (array.length < 1 || start < 0 || end >= array.length || start > end) {
return null;
}
int smallIndex = partition(array, start, end);
if (smallIndex > start)
QuickSort(array, start, smallIndex - 1);
if (smallIndex < end)
QuickSort(array, smallIndex + 1, end);
return array;
}
public static int partition(int[] array,int start,int end){
int pivot = (int) (start + Math.random() *(end - start + 1));
int smallIndex = start - 1;
swap(array,pivot,end);
for(int i=start;i<=end;i++){
if(array[i] <= array[end]){
smallIndex++;
if(i>smallIndex)
swap(array,i,smallIndex);
}
}
return smallIndex;
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
QuickSort(array,0,5);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
歸併排序與快速排序的區別
歸併和快排用的都是分治思想,遞推公式和遞歸代碼也非常相似,那它們的區別在哪裏呢?
1.歸併排序,是先遞歸調用,再進行合併,合併的時候進行數據的交換。所以它是自下而上的排序方式。何爲自下而上?就是先解決子問題,再解決父問題。
2.快速排序,是先分區,在遞歸調用,分區的時候進行數據的交換。所以它是自上而下的排序方式。何爲自上而下?就是先解決父問題,再解決子問題。
桶排序
將要排序的數據分到幾個有序的桶裏,每個桶裏的數據再單獨進行快速排序。
桶內排完序之後,再把每個桶裏的數據按照順序依次取出,組成的序列就是有序的了。
桶排序比較適合用在外部排序中。所謂的外部排序就是數據存儲在外部磁盤中,數據量比較大,內存有限,無法將數據全部加載到內存中。
應用案例
1)需求描述:
有10GB的訂單數據,需按訂單金額(假設金額都是正整數)進行排序但內存有限,僅幾百MB
2)解決思路:
掃描一遍文件,看訂單金額所處數據範圍,比如1元-10萬元,那麼就分100個桶。
第一個桶存儲金額1-1000元之內的訂單,第二個桶存1001-2000元之內的訂單,依次類推。
每個桶對應一個文件,並按照金額範圍的大小順序編號命名(00,01,02,…,99)。
將100個小文件依次放入內存並用快排排序。
所有文件排好序後,只需按照文件編號從小到大依次讀取每個小文件並寫到大文件中即可。
3)注意點:若單個文件無法全部載入內存,則針對該文件繼續按照前面的思路進行處理即可。
package sort;
public class HeapSort {
//聲明全局變量,用於記錄數組array的長度;
static int len;
public static int[] HeapSort(int[] array) {
len = array.length;
if (len < 1) return array;
//1.構建一個最大堆
buildMaxHeap(array);
//2.循環將堆首位(最大值)與末位交換,然後在重新調整最大堆
while (len > 0) {
swap(array, 0, len - 1);
len--;
adjustHeap(array, 0);
}
return array;
}
//建立最大堆
public static void buildMaxHeap(int[] array) {
//從最後一個非葉子節點開始向上構造最大堆
for (int i = (len/2 - 1); i >= 0; i--) {
adjustHeap(array, i);
}
}
//調整成爲最大堆
public static void adjustHeap(int[] array, int i) {
int maxIndex = i;
//如果有左子樹,且左子樹大於父節點,則將最大指針指向左子樹
if (i * 2 < len && array[i * 2] > array[maxIndex])
maxIndex = i * 2;
//如果有右子樹,且右子樹大於父節點,則將最大指針指向右子樹
if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex])
maxIndex = i * 2 + 1;
//如果父節點不是最大值,則將父節點與最大值交換,並且遞歸調整與父節點交換的位置。
if (maxIndex != i) {
swap(array, maxIndex, i);
adjustHeap(array, maxIndex);
}
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
HeapSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
計數排序
計數排序其實是桶排序的一種特殊情況。當要排序的n個數據,所處的範圍並不太大的時候,比如最大值是k,我們就可以把數據劃分爲k個桶,每個桶內的數據值都是相同的,省掉了桶內排序的時間
計數排序只能用在數據範圍不大的場景中,如果數據範圍k比要排序的數據n大很多,就不適合用計數排序了,而且,計數排序只能給非負整數排序,如果要排序的數據是其他類型的,要將其在不改變相對大小的情況下,轉化爲非負整數。
package sort;
import java.util.Arrays;
public class CountingSort {
/**
* 計數排序
*
* @param array
* @return
*/
public static int[] CountingSort(int[] array) {
if (array.length == 0) return array;
int bias, min = array[0], max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max)
max = array[i];
if (array[i] < min)
min = array[i];
}
bias = 0 - min;
int[] bucket = new int[max - min + 1];
Arrays.fill(bucket, 0);
for (int i = 0; i < array.length; i++) {
bucket[array[i] + bias]++;
}
int index = 0, i = 0;
while (index < array.length) {
if (bucket[i] != 0) {
array[index] = i - bias;
bucket[i]--;
index++;
} else
i++;
}
return array;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
CountingSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
基數排序
基數排序對要排序的數據是有要求的,需要可以分割出獨立的“位”來比較,而且位之間有遞進關係,如果a數據的高位比b數據大,那剩下的低位就不用比較了,除此之外,每一位的數據範圍不能太大,要可以用線性排序算法來排序,否則,基數排序的時間複雜度就補發做到O(n)了。
package sort;
import java.util.ArrayList;
public class RadixSort {
/**
* 基數排序
* @param array
* @return
*/
public static int[] RadixSort(int[] array) {
if (array == null || array.length < 2)
return array;
// 1.先算出最大數的位數;
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
RadixSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
排序優化
對於小規模數據進行排序,可以選擇時間複雜度爲O(n2)的算法,如果對大規模數據進行排序,時間複雜度爲O(nlogn)的算法更加高效,(歸併排序,快速排序,堆排序等等),java就是採用的堆排序實現排序函數,C語言使用快排實現排序函數
如何優選快速排序
快排最壞的情況是時間複雜度爲O(n2),如果數據原來就是有序的或者接近有序的,每次分區點都選擇最後一個數據,那塊拍就會退化爲O(n2),這種時間複雜度就是因爲分區點不合理。
最理想的分區點就是,被分區點分開的兩個分區中,數據的數量差不多
1.三數取中法
衝區間首尾中各取一個數,取這三個數的中間值作爲分區點。
2.隨機法
隨機法就是每次從要排序的區間中,隨機選擇一個元素作爲分區點,不能保證每次分區點都選的比較好,但是從概率來講,也不大可能會發生分區點都選得很差的情況。
快排使用遞歸實現的,遞歸要警惕堆棧溢出,避免堆棧溢出1.限制遞歸深度,一旦遞歸過深,超過了我們實現設定的閥值,就停止遞歸2.通過在堆上模擬實現一個函數調用棧,手動模擬遞歸壓棧出棧的過程