Java 八種排序演算法總結
前言
好久沒複習基礎了,寫個氣泡排序都要想一會。感覺自己好像老了好多,今天手癢總結一下排序演算法。目前網上部落格普遍都有詳細介紹,寫的很清楚。說實話我是沒必要再寫一遍的,感覺就是在囉嗦、還是重複性的,但是如果只是單純看的話,不到3分鐘我就忘記了(可能是健忘症晚期)。所以還是自己親手“教訓”一下印象比較深刻。
一、簡介
排序(Sorting) 是計算機程式設計中的一種重要操作,它的功能是將一個資料元素(或記錄)的任意序列,重新排列成一個關鍵字有序的序列。
排序演算法在很多領域得到相當地重視,尤其是在大量資料的處理方面。一個優秀的演算法可以節省大量的資源。在各個領域中考慮到資料的各種限制和規範,要得到一個符合實際的優秀演算法,得經過大量的推理和分析。
那麼怎麼對比哪個排序演算法更好呢? 這時候就是要看誰執行的比較快。 哈哈 真是一句廢話,誰不知道呢
所以我們需要了解一下演算法複雜度。
二、演算法複雜度
時間複雜度是指執行演算法所需要的計算工作量;而空間複雜度是指執行這個演算法所需要的記憶體空間
(1)時間複雜度
- 時間複雜度可以認為是對排序資料的總的操作次數。反映當n變化時,操作次數呈現什麼規律。
- 常見的時間複雜度有:常數階O(1),對數階O(log2n),線性階O(n), 線性對數階O(nlog2n),平方階O(n2)。
- 時間複雜度O(1):演算法中語句執行次數為一個常數,則時間複雜度為O(1)。
(2)空間複雜度
- 空間複雜度是指演算法在計算機內執行時所需儲存空間的度量,它也是問題規模n的函式。
- 空間複雜度O(1):當一個演算法的空間複雜度為一個常量,即不隨被處理資料量n的大小而改變時,可表示為O(1)。
- 空間複雜度O(log2N):當一個演算法的空間複雜度與以2為底的n的對數成正比時,可表示為O(log2n) , ax=N,則x=logaN。
- 空間複雜度O(n):當一個演算法的空間複雜度與n成線性比例關係時,可表示為0(n)。
(3)排序演算法穩定性
假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序演算法是穩定的;否則稱為不穩定的。
穩定性的意義
1、如果只是簡單的進行數字的排序,那麼穩定性將毫無意義。
2、如果排序的內容僅僅是一個複雜物件的某一個數字屬性,那麼穩定性依舊將毫無意義
3、如果要排序的內容是一個複雜物件的多個數字屬性,但是其原本的初始順序毫無意義,那麼穩定性依舊將毫無意義。
4、除非要排序的內容是一個複雜物件的多個數字屬性,且其原本的初始順序存在意義,那麼我們需要在二次排序的基礎上保持原有排序的意義,才需要使用到穩定性的演算法,例如要排序的內容是一組原本按照價格高低排序的物件,如今需要按照銷量高低排序,使用穩定性演算法,可以使得想同銷量的物件依舊保持著價格高低的排序展現,只有銷量不同的才會重新排序。
太複雜了,我也只瞭解了點皮毛。就簡單介紹這些,要詳細瞭解的同學們還是自己看書吧!!!
三、常見演算法
(1)氣泡排序
1、簡介:
重複地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果他們的順序(如從大到小、首字母從A到Z)錯誤就把他們交換過來
2、步驟:
-
比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
-
對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
-
針對所有的元素重複以上的步驟,除了最後一個。
-
持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較
3、動圖:
4、程式碼:
public static void order1(int[] a) {
for(int x=0;x<a.length-1;x++) {
for (int y = 0; y < a.length-1-x; y++) {
if(a[y]>a[y+1]) {
int t = a[y];
a[y] = a[y+1];
a[y+1] = t;
}
}
}
}
(2)選擇排序
1、簡介:
每一次從待排序的資料元素中選出最小(或最大)的一個元素,存放在序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到全部待排序的資料元素排完。 選擇排序是不穩定的排序方法
2、步驟:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。
- 重複第二步,直到所有元素均排序完畢。
3、動圖:
4、程式碼:
public static void order2(int[] a) {
for (int i = 0; i < a.length-1; i++) {
for (int j = i + 1; j < a.length; j++) {
if (a[i] > a[j]) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
}
}
(3)插入排序
1、簡介:
插入演算法把要排序的陣列分成兩部分:第一部分包含了這個陣列的所有元素,但將最後一個元素除外(讓陣列多一個空間才有插入的位置),而第二部分就只包含這一個元素(即待插入元素)。在第一部分排序完成後,再將這個最後元素插入到已排好序的第一部分中。
2、步驟:
- 從第一個元素開始,該元素可以認為已經被排序
- 取出下一個元素,在已經排序的元素序列中從後向前掃描
- 如果該元素(已排序)大於新元素,將該元素移到下一位置
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
- 將新元素插入到下一位置中
- 重複步驟2~5
3、動圖:
4、程式碼:
public static void order3(int[] a) {
for (int i = 1; i < a.length; i++) {
int get = a[i];
int j = i-1;
while (j >= 0 && a[j] > get) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = get;
}
}
(4)歸併排序
1、簡介:
將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路歸併。
2、步驟:
- 把長度為n的輸入序列分成兩個長度為n/2的子序列。
- 對這兩個子序列分別採用歸併排序。
- 將兩個排序好的子序列合併成一個最終的排序序列。
3、動圖:
4、程式碼:
/**
* 歸併排序
*/
public static void order4(int[] a) {
sort(a,0,a.length-1);
}
public static void sort(int[] data,int left,int right) {
if(left >= right) return;
//找出中間索引
int center = (left + right)/2;
//對左邊陣列進行遞迴
sort(data, left, center);
//對右邊陣列進行遞迴
sort(data, center+1, right);
//合併
merge(data,left,center,right);
}
public static void merge(int[] data,int left, int center,int right) {
//臨時陣列
int[] tmpArr = new int[data.length];
//右陣列第一個元素索引
int mid = center + 1;
//third 記錄臨時陣列的索引
int third = left;
//快取左陣列第一個元素的索引
int tmp = left;
while (left <= center && mid <=right) {
//從兩個陣列中取出最小的放入臨時陣列
if (data[left] <= data[mid]) {
tmpArr[third++] = data[left++];
} else {
tmpArr[third++] = data[mid++];
}
}
//剩餘部分依次放入臨時陣列(實際上兩個while只會執行其中一個)
while(mid <= right) {
tmpArr[third++] = data[mid++];
}
while(left <= center) {
tmpArr[third++] = data[left++];
}
//將臨時陣列中的內容拷貝回原陣列中
while (tmp <= right) {
data[tmp] = tmpArr[tmp++];
}
}
(5)快速排序
1、簡介:
在陣列中隨機選一個數(預設陣列首個元素),陣列中小於等於此數的放在左邊,大於此數的放在右邊,再對陣列兩邊遞迴呼叫快速排序,重複這個過程。
2、步驟:
- 先從數列中取出一個數作為key值;
- 將比這個數小的數全部放在它的左邊,大於或等於它的數全部放在它的右邊;
- 對左右兩個小數列重複第二步,直至各區間只有1個數。
3、動圖:
4、程式碼:
/**
* 快速排序
*/
public static void order5(int[] a) {
quickSort(a,0,a.length-1);
}
private static void quickSort(int[] a, int left, int right) {
if(left < right) {
int i = getMiddle(a,left,right);
quickSort(a, left, i - 1);
quickSort(a, i + 1,right);
}
}
private static int getMiddle(int[] a, int low, int high) {
int pivot = a[low];
int i = low;
int j = high;
while(i < j) {
while(pivot <= a[j] && i < j) j--;
while(pivot >= a[i] && i < j) i++;
if(i < j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
a[low] = a[i];
a[i] = pivot;
return i;
}
(6)希爾排序
1、簡介:
希爾排序是插入排序改良的演算法,希爾排序步長從大到小調整,第一次迴圈後面元素逐個和前面元素按間隔步長進行比較並交換,直至步長為1,步長選擇是關鍵。
2、步驟:
先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,具體演算法描述:
- 選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列個數k,對序列進行k 趟排序;
- 每趟排序,根據對應的增量ti,將待排序列分割成若干長度為m 的子序列,分別對各子表進行直接插入排序。僅增量因子為1 時,整個序列作為一個表來處理,表長度即為整個序列的長度。
3、動圖:
4、程式碼:
/**
* 希爾排序
*/
public static void order6(int[] a) {
int len = a.length;//單獨把陣列長度拿出來,提高效率。
while(len != 0) {
len = len/2;
for (int i = 0; i < len; i++) {//分組
for (int j = i + 1; j < a.length; j+=len) {//元素從第二個開始
int k = j - len;//k為有序序列最後一位的位數
int temp = a[j];//要插入的元素
while (k >= 0 && temp < a[k]) {//從後往前遍歷
a[k + len] = a[k];
k -= len;//向後移動len位
}
a[k + len] = temp;
}
}
}
}
(7)基數排序
1、簡介:
基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優先順序排序。最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。
2、步驟:
- 取得陣列中的最大數,並取得位數;
- arr為原始陣列,從最低位開始取每個位組成radix陣列;
- 對radix進行計數排序(利用計數排序適用於小範圍數的特點);
3、動圖:
4、程式碼:
/**
* 基數排序
*/
public static void order7(int[] a) {
int max = a[0];
for (int i = 1; i < a.length; i++) {
if (a[i] > max) {
max = a[i];
}
}
int time = 0;
while (max > 0) {
max /= 10;
time++;
}
List<ArrayList<Integer>> queue = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++) {
ArrayList<Integer> queue1 = new ArrayList<Integer>();
queue.add(queue1);
}
for (int i = 0; i < time; i++) {
for (int j = 0; j < a.length; j++) {
int x = a[j] % (int) Math.pow(10, i+1)/(int)Math.pow(10, i);
ArrayList<Integer> queue2 = queue.get(x);
queue2.add(a[j]);
queue.set(x, queue2);
}
int count = 0;
for (int k = 0; k < 10; k++) {
while (queue.get(k).size()>0) {
ArrayList<Integer> queue3 = queue.get(k);
a[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
(8)堆排序
1、簡介:
堆排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
2、步驟:
- 將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆為初始的無序區;
- 將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
- 由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,……Rn-1)調整為新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數為n-1,則整個排序過程完成。
3、動圖:
4、程式碼:
/**
* 堆排序
*/
public static void order8(int[] a) {
int len = a.length;
for (int i = 0; i < len - 1; i++) {
buildMaxHeap(a,len - 1 - i);
swap(a,0,len - 1 - i);
}
}
private static void buildMaxHeap(int[] data, int lastIndex) {
//從lastIndex處節點(最後一個節點)的父節點開始
for (int i = (lastIndex - 1)/2; i >= 0; i--) {
//k儲存正在判斷的節點
int k = i ;
//如果當前K節點的子節點存在
while(k * 2 + 1 <= lastIndex) {
//k節點的左子節點的索引
int biggerIndex = 2 * k +1;
//如果biggerIndex小於lastIndex,即biggerIndex +1代表的K節點的右子節點存在
if(biggerIndex < lastIndex) {
//如果右子節點的值較大
if(data[biggerIndex] < data[biggerIndex + 1]) {
biggerIndex++;
}
}
//如果K節點的值小於其較大的子節點的值
if(data[k] < data[biggerIndex]) {
//交換他們
swap(data, k, biggerIndex);
k = biggerIndex;
} else {
break;
}
}
}
}
private static void swap(int[] a, int i, int j) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
四、總結
場景應用:
(1)若n較小(如n≤50),可採用直接插入或直接選擇排序。
當記錄規模較小時,直接插入排序較好;否則因為直接選擇移動的記錄數少於直接插人,應選直接選擇排序為宜。
(2)若檔案初始狀態基本有序(指正序),則應選用直接插人、冒泡或隨機的快速排序為宜;
(3)若n較大,則應採用時間複雜度為O(nlgn)的排序方法:快速排序、堆排序或歸併排序。
快速排序是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;
堆排序所需的輔助空間少於快速排序,並且不會出現快速排序可能出現的最壞情況。這兩種排序都是不穩定的。
若要求排序穩定,則可選用歸併排序。但本章介紹的從單個記錄起進行兩兩歸併的 排序演算法並不值得提倡,通常可以將它和直接插入排序結合在一起使用。先利用直接插入排序求得較長的有序子檔案,然後再兩兩歸併之。因為直接插入排序是穩定 的,所以改進後的歸併排序仍是穩定的。
果然演算法很複雜,以我智商都有點不夠用。分析的我腦殼有點疼,實在扯不下去。就寫這麼多了。見諒。。。
五、Demo地址
https://github.com/DayorNight/JavaOrderingMethod
六、參考文件
http://www.runoob.com/w3cnote/sort-algorithm-summary.html
https://jingyan.baidu.com/article/db55b609f856604ba30a2f18.html
https://blog.csdn.net/yushiyi6453/article/details/76407640
七、內容推薦
簡書:
上一篇:
如果你覺得我寫的不錯或者對您有所幫助的話
不妨頂一個【微笑】,別忘了點贊、收藏、加關注哈
您的每個舉動都是對我莫大的支援