首先,我們先說說排序的分類和特性:
一、排序的分類
1. 內部排序和外部排序
(1) 內部:待排序記錄存放在計算機隨機存儲器(內存)中進行的排序過程。
(2) 外部:待排序記錄的數量很大,以致於內存不能一次容納全部記錄,所以在排序過程中需要對外存進行訪問的排序過程。
2. 比較和非比較排序
(1)比較類:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此也稱爲非線性時間比較類排序。
(2)非比較類:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱爲線性時間非比較類排序。
二、特性
3. 穩定和非穩定性
(1)穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面。
(2)非穩定:如果a原本在b的前面,而a=b,排序之後 a 可能會出現在 b 的後面,也即原來的相對位置改變。
4. 複雜度:
(1)時間複雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什麼規律。
(2)是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。
三、八大排序講解
好了,不再多說了,開始入正題:
首先,看八大排序,每個算法的特性:
交換排序:冒泡和快排
1.冒泡排序
(1) 動畫演示:
(2) 算法思路分析:
1>相鄰兩個數兩兩相比,n[i]跟n[j+1]比,如果n[i]>n[j+1],則將連個數進行交換,
2> j++, 重複以上步驟,第一趟結束後,最大數就會被確定在最後一位,這就是冒泡排序又稱大(小)數沉底,
3>i++,重複以上步驟,直到i=n-1結束,排序完成。
(3) 複雜度分析:
1> 時間複雜度:
不管原始數組是否有序,時間複雜度都是O(n2),因爲沒一個數都要與其他數比較一次,(n-1)2次,分解:n2+2n-1, 去掉低次冪和常數,剩下n2,所以最後的時間複雜度是n2。
2>空間複雜度:因爲只定義了一個輔助變量,與n的大小無關,所以空間複雜度爲O(1)。
(4) java代碼:
import java.util.Scanner;
public class Bubbling {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int temp;
int [] data = new int [len];
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
//排序
for(int i = 0; i < len; i++) {
for(int j = i + 1; j < len - 1; j++) {
if(data[i] > data[j]) {
temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
}
//輸出
for(int i = 0; i < len; i++) {
System.out.print(data[i]);
}
sca.close();
}
}
- 快速排序
(1) 動畫演示:
(2) 算法思路分析:
快速排序的思想就是,選一個數作爲基數(這裏我選的是第一個數),大於這個基數的放到右邊,小於這個基數的放到左邊,等於這個基數的數可以放到左邊或右邊,看自己習慣,這裏我是放到了左邊,一趟結束後,將基數放到中間分隔的位置,第二趟將數組從基數的位置分成兩半,分割後的兩個的數組繼續重複以上步驟,選基數,將小數放在基數左邊,將大數放到基數的右邊,在分割數組,直到數組不能再分爲止,排序結束。
例如從小到大排序:
1>第一趟,第一個數爲基數temp,設置兩個指針left = 0,right = n.length,
①從right開始與基數temp比較,如果n[right]>基數temp,則right指針向前移一位,繼續與基數temp比較,直到不滿足n[right]>基數temp
②將n[right]賦給n[left]
③從left開始與基數temp比較,如果n[left]<=基數temp,則left指針向後移一位,繼續與基數temp比較,直到不滿足n[left]<=基數temp
④將n[left]賦給n[rigth]
⑤重複①-④步,直到left==right結束,將基數temp賦給n[left]
2> 第二趟,將數組從中間分隔,每個數組再進行第1步的操作,然後再將分隔後的數組進行分隔再快排,
3>遞歸重複分隔快排,直到數組不能再分,也就是只剩下一個元素的時候,結束遞歸,排序完成
下面用圖,演示第一趟執行流程:
(3)複雜度分析:
1>時間複雜度:
最壞情況就是每一次取到的元素就是數組中最小/最大的,這種情況其實就是冒泡排序了(每一次都排好一個元素的順序)
這種情況時間複雜度就好計算了,就是冒泡排序的時間複雜度:T[n] = n * (n-1) = n^2 + n。
最好情況下是O(nlog2n),推導過程如下:(遞歸算法的時間複雜度公式:T[n] = aT[n/b] + f(n) )
所以平均時間複雜度爲O(nlog2n)
2>空間複雜度:快速排序使用的空間是O(1)的,也就是個常數級;而真正消耗空間的就是遞歸調用了,因爲每次遞歸就要保持一些數據:
最優的情況下空間複雜度爲:O(log2n);每一次都平分數組的情況
最差的情況下空間複雜度爲:O( n );退化爲冒泡排序的情況
所以平均空間複雜度爲O(log2n)
(4) java代碼:
import java.util.Scanner;
//冒泡排序演變而來
public class 快速 {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
quickSort(data, 0, data.length - 1);//快排
for(int i = 0; i < data.length; i++) {
System.out.print(data[i] + " ");
}
sca.close();
}
public static void quickSort(int [] data, int left, int right) {
int f, t;
int rtemp, ltemp;
ltemp = left; //左指針
rtemp = right; //右指針
f = data[(left + right) / 2]; //傳來的每一個子表的中間值
while(ltemp < rtemp) { //左指針只要比右邊小就開始循環
while(data[ltemp] < f) ++ltemp; //從左邊開始,只要當前數小於中間值,保留右移
while(data[rtemp] > f) --rtemp; //從右邊開始,只要當前數大於中間值,保留左移
if(ltemp <= rtemp) { //左指針小於等於右指針(等於 ==>是爲避免子表只有倆元素)
t = data[ltemp];
data[ltemp] = data[rtemp];
data[rtemp] = t;
--rtemp;
++ltemp;
}
}
if(ltemp == rtemp) ltemp++; //左右指針相等,左指針右移(左右指針都移到了中間)
if(left < rtemp) quickSort(data, left, ltemp - 1); //右指針沒有到達左頭部,以左指針爲右頭部形成左子表
if(ltemp < right) quickSort(data, rtemp + 1, right);//左指針沒有到達右頭部,以右指針爲左頭部形成右子表
}
}
插入排序:簡單插入排序和希爾排序
1. 簡單插入排序:
(1) 動畫演示:
(2) 算法思路分析:
如:從小到大排序:
1> 從第二位開始遍歷,
2> 當前數(第一趟是第二位數)與前面的數依次比較,如果前面的數大於當前數,則將這個數放在當前數的位置上,當前數的下標-1,
3> 重複以上步驟,直到當前數不大於前面的某一個數爲止,這時,將當前數,放到這個位置,
**注:**1-3步就是保證當前數的前面的數都是有序的,內層循環的目的就是將當前數插入到前面的有序序列裏
4> 重複以上3步,直到遍歷到最後一位數,並將最後一位數插入到合適的位置,插入排序結束。
下面用圖,模擬算法每一趟執行流程:
(3) 複雜度分析:
1>時間複雜度:插入算法,就是保證前面的序列是有序的,只需要把當前數插入前面的某一個位置即可。所以如果數組本來就是有序的,則數組的最好情況下時間複雜度爲O(n) 如果數組恰好是倒=倒序,比如原始數組是5 4 3 2 1,想要排成從小到大,則每一趟前面的數都要往後移,一共要執行n-1 + n-2 + … + 2 + 1 = n * (n-1) / 2 = 0.5 * n2 - 0.5 * n次,去掉低次冪及係數,所以最壞情況下時間複雜度爲O(n2)。
平均時間複雜度(n+n2 )/2,所以平均時間複雜度爲O(n2)
2>空間複雜度:
插入排序算法,只需要兩個變量暫存當前數,以及下標,與n的大小無關,所以空間複雜度爲:O(1)
(4) java代碼:
import java.util.Scanner;
public class Insert{
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
int j, t;
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
for(int i = 1; i < data.length; i++) {
t = data[i];
j = i - 1;
while(j >= 0 && t < data[j]) { //比當前掃描位置小 並且沒有到數組頭部,繼續往前掃描
data[j + 1] = data[j]; //讓上一個單元,等於當前掃描單元的值
j--; //繼續往前掃描
}
data[j + 1] = t; //讓當前掃描位置的上一個單元等於所要插入的元素
}
for(int i = 0; i < data.length; i++) {
System.out.print(data[i] + " ");
}
sca.close();
}
}
2.希爾排序:
(1) 動畫演示:
(2) 算法思想分析:
希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。
簡單插入排序很循規蹈矩,不管數組分佈是怎麼樣的,依然一步一步的對元素進行比較,移動,插入,比如[5,4,3,2,1,0]這種倒序序列,數組末端的0要回到首位置很是費勁,比較和移動元素均需n-1次。
而希爾排序在數組中採用跳躍式分組的策略,通過某個增量將數組元素劃分爲若干組,然後分組進行插入排序,隨後逐步縮小增量,繼續按組進行插入排序操作,直至增量爲1。希爾排序通過這種策略使得整個數組在初始階段達到從宏觀上看基本有序,小的基本在前,大的基本在後。然後縮小增量,到增量爲1時,其實多數情況下只需微調即可,不會涉及過多的數據移動。
那麼來看下希爾排序的基本步驟,在此選擇增量gap=length/2,縮小增量繼續以gap = gap/2的方式,這種增量選擇可以用一個序列來表示,{n/2,(n/2)/2…1},稱爲增量序列。希爾排序的增量序列的選擇與證明是個數學難題,選擇的這個增量序列是比較常用的,也是希爾建議的增量,稱爲希爾增量,但其實這個增量序列不是最優的。此處做示例使用希爾增量。
(3) 複雜度分析:
1> 時間複雜度:最壞情況下,每兩個數都要比較並交換一次,則最壞情況下的時間複雜度爲O(n2), 最好情況下,數組是有序的,不需要交換,只需要比較,則最好情況下的時間複雜度爲O(n)。
2> 希爾排序,只需要一個變量用於兩數交換,與n的大小無關,所以空間複雜度爲:O(1)。
選擇排序:簡單選擇排序和堆排序
(4) java代碼:
import java.util.Scanner;
//插入排序演變而來,縮小增量
public class 希爾 {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
int i, j;
int r, temp;
for(i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
for(r = data.length / 2; r >= 1; r /= 2) { //增量 每次縮減一半
for(i = r; i < data.length; i++) { //讓這個增量所在的序列對進行比較(序列對排序就是插入排序)
temp = data[i];
j = i - r; //每次 + 1 的在 '本序列對' 進行掃描
while(j >= 0 && temp < data[j]) {
data[j + r] = data[j];
j -= r;
}
data[j + r] = temp;
}
}
for(int k = 0; k < data.length; k++) {
System.out.print(data[k] + " ");
}
sca.close();
}
}
1.簡單選擇排序:
(1)動畫演示:
(2) 算法思路分析:
1> 第一個跟後面的所有數相比,如果小於(或小於)第一個數的時候,暫存較小數的下標,第一趟結束後,將第一個數,與暫存的那個最小數進行交換,第一個數就是最小(或最大的數)
2> 下標移到第二位,第二個數跟後面的所有數相比,一趟下來,確定第二小(或第二大)的數
3> 重複以上步驟,直到指針移到倒數第二位,確定倒數第二小(或倒數第二大)的數,那麼最後一位也就確定了,排序完成。
(3) 複雜度分析:
1>時間複雜度:不管原始數組是否有序,時間複雜度都是O(n2),因爲沒一個數都要與其他數比較一次,(n-1)2次,分解:n2-2n+1, 去掉低次冪和常數,剩下n2,所以最後的時間複雜度是n2。
2>空間複雜度:因爲只定義了兩個輔助變量,與n的大小無關,所以空間複雜度爲O(1)。
(4) java代碼:
import java.util.Scanner;
public class Choice {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
int index, temp;
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
for(int i = 0; i < len - 1; i++) {
index = i;
for(int j = i + 1; j < len; j++) {
if(data[j] < data[index]) {
index = j;
}
}
if(index != i) {
temp = data[i];
data[i] = data[index];
data[index] = temp;
}
}
for(int i = 0; i < len; i++) {
System.out.print(data[i] + " ");
}
sca.close();
}
}
- 堆排序:
(1)動畫演示:
(2)讓我們先了解堆結構:
先來了解下堆的相關概念:堆是具有以下性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。如下圖:
然後,我們對堆中的節點,按層進行編號,將這種邏輯結構映射到數組中,如:
該數組從邏輯上講就是一個堆結構,我們用簡單的公式來描述一下堆的定義就是:
大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
接下來看看堆排序的基本思想及基本步驟:
堆排序的基本思想是:將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就爲最大值。然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反覆執行,便能得到一個有序序列了
步驟一:
構造初始堆。將給定無序序列構造成一個大頂堆(一般升序採用大頂堆,降序採用小頂堆)。
1>.假設給定無序序列結構如下
2>.此時我們從最後一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。
3>.找到第二個非葉節點4,由於[4,9,8]中9元素最大,4和9交換。
這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。
此時,我們就將一個無需序列構造成了一個大頂堆。
步驟二 將堆頂元素與末尾元素進行交換,使末尾元素最大。然後繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反覆進行交換、重建、交換。
a.將堆頂元素9和末尾元素4進行交換
b.重新調整結構,使其繼續滿足堆定義
c.再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.
後續過程,繼續進行調整,交換,如此反覆進行,最終使得整個序列有序
(3) 再簡單總結下堆排序的基本思路:
a.將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
b.將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
c.重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
(4) 複雜度分析:
1> 時間複雜度:堆排序是一種選擇排序,整體主要由構建初始堆+交換堆頂元素和末尾元素並重建堆兩部分組成。其中構建初始堆經推導複雜度爲O(n),在交換並重建堆的過程中,需交換n-1次,而重建堆的過程中,根據完全二叉樹的性質,[log2(n-1),log2(n-2)…1]逐步遞減,近似爲nlogn。所以堆排序時間複雜度最好和最壞情況下都是O(nlogn)級。
2> 堆排序不要任何輔助數組,只需要一個輔助變量,所佔空間是常數與n無關,所以空間複雜度爲O(1)。
(5) java代碼:
import java.util.Arrays;
import java.util.Scanner;
//選擇排序思想演變而來
public class Heap{
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
heapSort(data);
System.out.println(Arrays.toString(data));
sca.close();
}
public static void heapSort(int [] data) {
int len = data.length;
//開始位置是最後一個非葉子節點,即最後一個節點的父節點(二叉樹特性)
int start = (len - 1) / 2;
//初始化一個大頂堆(以每一個節點形成的堆都是一個大頂堆)
for(int i = start; i >= 0; i--) {
creatMaxHeap(data, len, i);
}
//先把數組中的第0個和堆中的最後一個數交換位置,再把前面的處理爲大頂堆
for(int i = len - 1; i > 0; i--) {
int temp = data[0];
data[0] = data[i];
data[i] = temp;
creatMaxHeap(data, i, 0);
}
}
public static void creatMaxHeap(int [] data, int len, int index) {
//左子節點
int leftNode = 2 * index + 1;
//右子節點
int rightNode = 2 * index + 1;
int max = index;
//和兩個子節點分別對比,找出最大的節點
if(leftNode < len && data[leftNode] > data[max]) max = leftNode;
if(rightNode < len && data[rightNode] > data[max]) max = rightNode;
//交換位置
if(max != index) {
int temp = data[index];
data[index] = data[max];
data[max] = temp;
//交換位置以後,可能會破壞之前排好的堆,所以,之前排好的堆需要重新調整
creatMaxHeap(data, len, max);
}
}
}
歸併排序:二路歸併和多路歸併(這裏不再敘述)
1.二路歸併
(1) 動畫演示:
(2) 算法思想分析:
歸併排序就是遞歸得將原始數組遞歸對半分隔,直到不能再分(只剩下一個元素)後,開始從最小的數組向上歸併排序
1> 向上歸併排序的時候,需要一個暫存數組用來排序,
2> 將待合併的兩個數組,從第一位開始比較,小的放到暫存數組,指針向後移,
3> 直到一個數組空,這時,不用判斷哪個數組空了,直接將兩個數組剩下的元素追加到暫存數組裏,
4> 再將暫存數組排序後的元素放到原數組裏,兩個數組合成一個,這一趟結束。
根據思路分析,每一趟的執行流程如下圖所示:
(3)算法複雜度分析:
1>時間複雜度:遞歸算法的時間複雜度公式:T[n] = aT[n/b] + f(n)
無論原始數組是否是有序的,都要遞歸分隔並向上歸併排序,所以時間複雜度始終是O(nlog2n)2>空間複雜度:
每次兩個數組進行歸併排序的時候,都會利用一個長度爲n的數組作爲輔助數組用於保存合併序列,所以空間複雜度爲O(n)。
(4) java代碼:
import java.util.Arrays;
import java.util.Scanner;
public class Merge {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
System.out.println(Arrays.toString(data));
mergeSort(data, 0, data.length - 1);
System.out.println(Arrays.toString(data));
sca.close();
}
public static void mergeSort(int [] data, int low, int high) {
int middle = (low + high) / 2;
//如果最後只剩下一個不再遞歸
if(low < high) {
//處理左邊
mergeSort(data, low, middle);
//處理右邊
mergeSort(data, middle + 1, high);
//歸併
merge(data, low, middle, high);
}
}
public static void merge(int [] data, int low, int middle, int high) {
//用於存儲歸併後的臨時數組
int [] temp = new int [high - low + 1];
//記錄第一個數組中需要遍歷的下標
int i = low;
//記錄第二個數組中需要遍歷的下標
int j = middle + 1;
//記錄臨時數組的下標
int index = 0;
//遍歷兩個數組取出小的數字,放入臨時數組
while(i <= middle && j <= high) {
if(data[i] < data[j]) {
//把小的數放到數組中
temp[index] = data[i];
//下標移向後一位
i++;
}else {
temp[index] = data[j];
j++;
}
index++;
}
while(i <= low) {
temp[index] = data[i];
i++;
index++;
}
while(j <= high) {
temp[index] = data[j];
j++;
index++;
}
//把臨時數組的中的數據重新存入原數組
for(int k = 0; k < temp.length; k++) {
data[k + low] = temp[k];
}
}
}
非比較排序:基排序、(桶排序、計數排序這倆個不再敘述)
(1) 動畫演示:
(2) 算法思想分析:
基數排序第i趟將待排數組裏的每個數的i位數放到tempj(j=1-10)隊列中,然後再從這十個隊列中取出數據,重新放到原數組裏,直到i大於待排數的最大位數。
1.數組裏的數最大位數是n位,就需要排n趟,例如數組裏最大的數是3位數,則需要排3趟。
2.若數組裏共有m個數,則需要十個長度爲m的數組tempj(j=0-9)用來暫存i位上數爲j的數,例如,第1趟,各位數爲0的會被分配到temp0數組裏,各位數爲1的會被分配到temp1數組裏…
3.分配結束後,再依次從tempj數組中取出數據,遵循先進先進原則,例如對數組{1,11,2,44,4},進行第1趟分配後,temp1={1,11},temp2={2},temp4={44,4},依次取出元素後{1,11,2,44,4},第一趟結束
4.循環到n趟後結束,排序完成。
根據思路分析,每一趟的執行流程如下圖所示:
通過基數排序對數組{53, 3, 542, 748, 14, 214, 154, 63, 616}:
(3) 複雜度分析:
1>時間複雜度:每一次關鍵字的桶分配都需要O(n)的時間複雜度,而且分配之後得到新的關鍵字序列又需要O(n)的時間複雜度。假如待排數據可以分爲d個關鍵字,則基數排序的時間複雜度將是O(d2n) ,當然d要遠遠小於n,因此基本上還是線性級別的。係數2可以省略,且無論數組是否有序,都需要從個位排到最大位數,所以時間複雜度始終爲O(dn) 。其中,n是數組長度,d是最大位數。
2>空間複雜度:
基數排序的空間複雜度爲O(n+k),其中k爲桶的數量,需要分配n個數。
(4) java代碼:
import java.util.Arrays;
import java.util.Scanner;
public class Base {
public static void main(String[] args) {
Scanner sca = new Scanner(System.in);
int len = sca.nextInt();
int [] data = new int [len];
for(int i = 0; i < len; i++) {
data[i] = sca.nextInt();
}
System.out.println(Arrays.toString(data));
radixSort(data);
System.out.println(Arrays.toString(data));
sca.close();
}
public static void radixSort(int [] data) {
//存放數組中最大的數字
int max = Integer.MIN_VALUE;
for(int i = 0; i < data.length; i++) {
if(data[i] > max) {
max = data[i];
}
}
//計算最大數字是幾位
int maxLength = (max + "").length();
//用於臨時存儲數據的數組
int [][] temp = new int [10][data.length];
//用於記錄在temp中相應的數組中存放的數字數量
int [] counts = new int [10];
//根據最大長度的數,決定比較的次數
for(int i = 0, n = 1; i < maxLength; i++, n *= 10) {
//把每一個數字分別計算餘數
for(int j = 0; j < data.length; j++) {
//計算餘數
int ys = data[j] / n % 10;
//把當前遍歷的數據放入指定的數組中
temp[ys][counts[ys]] = data[j];
//記錄數量
counts[ys]++;
}
//記錄取出的元素需要放的位置
int index = 0;
//把數字取出來
for(int k = 0; k < counts.length; k++) {
//記錄數量的數組中當前餘數記錄的數量不爲0
if(counts[k] != 0) {
//循環取出元素
for(int h = 0; h < counts[k]; h++) {
//取出元素
data[index] = temp[k][h];
//記錄下一個位置
index++;
}
//把數量置爲0
counts[k] = 0;
}
}
}
}
}
如果此博客對你學習八大排序算法,有一點小幫助,點個贊吧!啊哈!!!!!!