算法概述
算法分類
十種常見排序算法可以分為兩大類:
- 比較類排序:通過比較來決定元素間的相對次序,由於其時間復雜度不能突破O(nlogn),因此也稱為非線性時間比較類排序。
- 非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱為線性時間非比較類排序。
算法復雜度
相關概念
- 穩定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不穩定:如果a原本在b的前面,而a=b,排序之后 a 可能會出現在 b 的后面。
- 時間復雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什么規律。
- 空間復雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。
冒泡排序(Bubble Sort)
冒泡排序是一種簡單的排序算法。它重復地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重復地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。
算法描述
- 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對,這樣在最后的元素應該會是最大的數;
- 針對所有的元素重復以上的步驟,除了最后一個;
- 重復步驟1~3,直到排序完成。
動圖演示
代碼實現
package cn.itcast.algorithm; import java.util.Arrays; public class BubbleSort { public static void main(String[] args) { int arr[] = {119, 34, 1, 101}; bubbleSort(arr); System.out.println("排序后:" + Arrays.toString(arr)); } public static void bubbleSort(int[] arr) { int temp = 0;//臨時變量
boolean flag = false;//標識變量,表示是否進行過交換
for (int i = 0; i < arr.length - 1; i++) {//遍歷數組長度-1次
for (int j = 0; j < arr.length - 1 - i; j++) {//j < arr.length - 1 - i (-i是因為每遍歷一趟,確定下來一個值,也就是循環的值每次-i) //如果前面的數比后面的數大,則交換
if (arr[j] > arr[j + 1]) { flag = true;//表示進行過交換 //交換
temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } System.out.println("第" + (i + 1) + "趟排序后的數組");//(i+1)因為i是從0開始的
if (!flag) {//在一趟排序中,一次交換都沒有發生過
break; } else { flag = false;//重置flag,進行下次判斷
} } } }
代碼優化:
package cn.itcast.algorithm; import java.util.Arrays; public class BubbleSort { public static void main(String[] args) { int arr[] = {119, 34, 1, 101}; bubbleSort(arr); System.out.println("排序后:" + Arrays.toString(arr)); } public static void bubbleSort(int[] arr) { int temp = 0;//臨時變量
for (int i = 0; i < arr.length - 1; i++) {//遍歷數組長度-1次
boolean flag = false;//標識變量,表示是否進行過交換
for (int j = 0; j < arr.length - 1 - i; j++) {//j < arr.length - 1 - i (-i是因為每遍歷一趟,確定下來一個值,也就是循環的值每次-i) //如果前面的數比后面的數大,則交換
if (arr[j] > arr[j + 1]) { flag = true;//表示進行過交換 //交換
temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } if (!flag) {//在一趟排序中,一次交換都沒有發生過,if里本身要是true才會往下執行 break;//跳出最近的循環 } } } }
選擇排序(Selection Sort)
選擇排序(Selection-sort)是一種簡單直觀的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再從剩余未排序元素中繼續尋找最小(大)元素,然后放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
算法描述
n個記錄的直接選擇排序可經過n-1趟直接選擇排序得到有序結果。具體算法描述如下:
- 初始狀態:無序區為R[1..n],有序區為空;
- 第i趟排序(i=1,2,3…n-1)開始時,當前有序區和無序區分別為R[1..i-1]和R(i..n)。
該趟排序從當前無序區中-選出關鍵字最小的記錄 R[k],將它與無序區的第1個記錄R交換,使R[1..i]和R[i+1..n)分別變為記錄個數增加1個的新有序區 和 記錄個數減少1個的新無序區;
- n-1趟結束,數組有序化了。
動圖演示
代碼實現
package cn.itcast.algorithm; import java.util.Arrays; public class SelectSort { public static void main(String[] args) { int arr[] = {119, 34, 1, 101}; selectSort(arr); System.out.println("排序后:" + Arrays.toString(arr)); } public static void selectSort(int[] arr) { for (int i = 0; i < arr.length - 1; i++) {//遍歷長度-1次
int minIndex = i; int min = arr[i]; for (int j = i + 1; j < arr.length; j++) { if (min > arr[j]) {//假定的最小值,不一定是最小
min = arr[j];//重置min,並不改變arr[]數組內的值
minIndex = j;//重置minIndex
} } if (minIndex != i) {//最小值下標不是i,表示最小值不是它自己,則進行下面的交換 arr[minIndex] = arr[i];//將當前輪下標i(當前輪次第一個)對應的值賦給最小值對應的下標的值,覆蓋原來的值
arr[i] = min;//讓之前拿到的最小值min賦給最小值的當前輪下標i對應的值
} } } }
插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從后向前掃描,找到相應位置並插入。
算法描述
一般來說,插入排序都采用in-place在數組上實現。具體算法描述如下:
- 從第一個元素開始,該元素可以認為已經被排序;
- 取出下一個元素,在已經排序的元素序列中從后向前掃描;
- 如果該元素(已排序)大於新元素,將該元素移到下一位置;
- 重復步驟3,直到找到已排序的元素小於或者等於新元素的位置;
- 將新元素插入到該位置后;
- 重復步驟2~5。
動圖演示
package cn.itcast.algorithm; import java.util.Arrays; public class InsertSort { public static void main(String[] args) { int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
System.out.println(Arrays.toString(arr)); insertSort(arr); } public static void insertSort(int[] arr) { for (int i = 1; i < arr.length; i++) {//不需要-1,因為i是從1開始的 //定義待插入的數,先假定一個是有序的
int insertVal = arr[i]; //待插入數前面那個數的下標
int insertIndex = i - 1; //數組不越界 並且 待插入的數小於待插入前面那個數(還沒有找到插入位置)
while (insertIndex >= 0 && insertVal < arr[insertIndex]) { arr[insertIndex + 1] = arr[insertIndex];//將待插入數前面那個數 覆蓋 它后面那個數(arr[indexIndex]后移)
insertIndex--;//為了找插入位置,不斷往前遍歷
} arr[insertIndex + 1] = insertVal;//把之前存起來要插入的數插入到對應位置。 insertIndex+1:順序正確時,+1保持值不變 | 順序不正確時(已經進了while循環減過1了):+1后就是要插入的位置 } } }
希爾排序(Shell Sort)
簡單插入排序,當需要插入的數是較小的數時,后移的次數明顯增多,對效率有影響.
1959年Shell發明,第一個突破O(n2)的排序算法,是簡單插入排序的改進版。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。
算法描述
先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,具體算法描述:
- 選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列個數k,對序列進行k 趟排序;
- 每趟排序,根據對應的增量ti,將待排序列分割成若干長度為m 的子序列,分別對各子表進行直接插入排序。僅增量因子為1 時,整個序列作為一個表來處理,表長度即為整個序列的長度。
當需要插入的數是較小的數時,后移的次數明顯增多,對效率有影響.
動圖演示
代碼實現
package cn.itcast.algorithm; import java.util.Arrays; public class ShellSort { public static void main(String[] args) { int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0}; shallSort2(arr); System.out.println(Arrays.toString(arr)); } public static void shallSort2(int[] arr) { //增量gap,並逐步的縮小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) { //從第gap個元素,逐個對其所在的組進行直接插入排序
for (int i = gap; i < arr.length; i++) { int j = i;//待插入位置的下標
int temp = arr[j];//臨時變量記錄要插入的數 //如果待插入的數 小於 自己組里待插入數之前的數
if (arr[j] < arr[j - gap]) { //進行插入排序
while (j - gap >= 0 && temp < arr[j - gap]) {//j-gap:是因為每個組里都有初始增量gap,最外層for循環每次遍歷,gap都會改變 //移動
arr[j] = arr[j - gap]; j -= gap; } //當退出while循環后,就給temp找到了位置
arr[j] = temp; } } } } }
快速排序(Quick Sort)
快速排序的基本思想:通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。
算法描述
快速排序使用分治法來把一個串(list)分為兩個子串(sub-lists)。具體算法描述如下:
- 從數列中挑出一個元素,稱為 “基准”(pivot);
- 重新排序數列,所有元素比基准值小的擺放在基准前面,所有元素比基准值大的擺在基准的后面(相同的數可以到任一邊)。在這個分區退出之后,該基准就處於數列的中間位置。這個稱為分區(partition)操作;
- 遞歸地(recursive)把小於基准值元素的子數列和大於基准值元素的子數列排序。
動圖演示
代碼實現
package cn.itcast.algorithm; import java.util.Arrays; public class QuickSort { public static void main(String[] args) { int[] arr = {-9, 78, 6, 23, -30, 70}; QuickSort(arr, 0, arr.length - 1); System.out.println(Arrays.toString(arr)); } public static void QuickSort(int[] arr, int left, int right) { int l = left;//左下標
int r = right;//右下標 //中軸的值
int pivot = arr[(l + r) / 2]; int temp = 0; //while循環的目的是讓比pivot 值小的放到左邊 //比pivot 值大的放到右邊
while (l < r) { //在pivot的左邊一直找,直到找到大於等於pivot的值,退出循環
while (arr[l] < pivot) { l += 1; } //在pivot的右邊一直找,直到找到小於等於pivot的值,退出循環
while (arr[r] > pivot) { r -= 1; } //如果l>=r,說明pivot左右兩邊的值,已經按照左邊全部是小於等於pivot的值,右邊全部是大於等於pivot的值
if (l >= r) { break; } //交換
temp = arr[l]; arr[l] = arr[r]; arr[r] = temp; //如果交換完后,發現這個arr[l]==pivot值 相等 r--,前移一位
if (arr[l] == pivot) { r -= 1; } //如果交換完后,發現這個arr[r]==pivot值 相等 l++,后移一位
if (arr[r] == pivot) { l += 1; } } //如果l==r,必須l++,r++,否則出現棧溢出,導致死循環
if (l == r) { l += 1; r -= 1; } //向左遞歸
if (left < r) { QuickSort(arr, left, r); } //向右遞歸
if (right > l) { QuickSort(arr, l, right); } } }
歸並排序(Merge Sort)
歸並排序是建立在歸並操作上的一種有效的排序算法。該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為2-路歸並。
算法描述
- 把長度為n的輸入序列分成兩個長度為n/2的子序列;
- 對這兩個子序列分別采用歸並排序;
- 將兩個排序好的子序列合並成一個最終的排序序列。
動圖演示
代碼實現
package cn.itcast.algorithm; import java.util.Arrays; public class MergeSort { public static void main(String[] args) { int arr[] = {8, 4, 5, 7, 1, 3, 6, 2}; int temp[] = new int[arr.length]; mergeSort(arr, 0, arr.length - 1, temp); System.out.println(Arrays.toString(arr)); } //分+合方法
public static void mergeSort(int[] arr, int left, int right, int[] temp) { if (left < right) { int mid = (left + right) / 2;//中間索引 //向左遞歸分解
mergeSort(arr, left, mid, temp); //向右遞歸分解
mergeSort(arr, mid + 1, right, temp); //合並
merge(arr, left, mid, right, temp); } } /** * 合並的方法 * * @param arr 排序的原始數組 * @param left 左邊有序序列的初始索引 * @param mid 中間索引 * @param right 右邊索引 * @param temp 臨時存儲的中轉數組 */
public static void merge(int[] arr, int left, int mid, int right, int[] temp) { int i = left;//初始化i,左邊有序序列的初始索引
int j = mid+1;//中間索引
int t = 0;//指向temp的中間索引 //(-)、先把左右兩邊有序的數據按照規則填充到temp數組,直到左右兩邊的有序序列,有一邊處理完畢為止
while (i <= mid && j <= right) { //如果左邊的有序序列的當前元素小於等於右邊有序序列的當前元素 //即將左邊有序序列的當前元素填充到temp數組 //然后t++,i++
if (arr[i] <= arr[j]) { temp[t] = arr[i]; t += 1; i += 1; //反之,將右邊有序序列的當前元素填充到temp數組
} else { temp[t] = arr[j]; t += 1; j += 1; } } //(二)、把有剩余元素的一邊的數據依次全部填充到temp數組中 //左邊的有序序列還有剩余的元素,就全部填充到temp數組
while (i <= mid) { temp[t] = arr[i]; t += 1; i += 1; } //右邊的有序序列還有剩余的元素,就全部填充到temp數組
while (j <= right) { temp[t] = arr[j]; t += 1; j += 1; } //(三)、將temp數組里的有序元素拷貝回arr數組 //從左邊開始拷貝, 注意:不是每次都拷貝所有
t = 0; int tempLeft = left; //第一次合並:templeft = 0,right = 1。 第二次合並:templeft = 2,right = 3。 最后一次:templeft = 0,right = 7
while (tempLeft <= right) { arr[tempLeft] = temp[t]; t += 1; tempLeft += 1; } } }
基數排序(Radix Sort)
基數排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最后的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。
算法描述
- 取得數組中的最大數,並取得位數;
- arr為原始數組,從最低位開始取每個位組成radix數組;
- 對radix進行計數排序(計數排序就是計算在當前一維數組有多少個數據,利用計數排序適用於小范圍數的特點);
動圖演示
代碼實現package cn.itcast.algorithm;
import java.util.Arrays; /** * 典型的空間換時間的排序算法 */ public class RadixSort { public static void main(String[] args) { int arr[] = {53, 3, 542, 748, 14, 214}; radixSort(arr); } public static void radixSort(int[] arr) { //得到數組中最大數 int max = arr[0];//假設第一個數就是最大數 for (int i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } //得到最大數是幾位數 int maxLength = (max + "").length(); //二維數組包含10個一維數組,每個一維數組大小只能為arr.length(空間換時間) int[][] bucket = new int[10][arr.length]; //定義一個一維數組來記錄各個桶放入的數據個數, bucketElementCounts[0],記錄的就是bucket[0]這個桶放入的數據個數 int[] bucketElementCounts = new int[10]; for (int i = 0, n = 1; i < maxLength; i++, n *= 10) { for (int j = 0; j < arr.length; j++) { //個位的值,也是對應桶下標的值 int digitOfElement = arr[j] / n % 10; //放入對應的桶中, bucketElementCounts[digitOfElement]為(digitOfElement例如值為第3個桶)里數的個數
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; bucketElementCounts[digitOfElement]++; } //按照這個桶的順序(一維數組的下標依次取出,放回原數組) int index = 0; //遍歷每一個桶,並將桶中的數據放入原數組 //bucketElementCounts.length = 10 for (int k = 0; k < bucketElementCounts.length; k++) { // !0說明有數據,才將數據放入原數組,bucketElementCounts[k]為第k個桶 if (bucketElementCounts[k] != 0) { //循環該桶,即第k個桶(第k個一維數組),放入 for (int l = 0; l < bucketElementCounts[k]; l++) { //取出元素放入arr中 arr[index] = bucket[k][l]; index++; } } //一輪過后,需要將bucketElementCounts[k]=0; bucketElementCounts[k] = 0; } System.out.println("第" + (i+1) + "次,取個位 " + Arrays.toString(arr)); } } }
bucketElementCounts[digitOfElement]為(digitOfElement)里數的個數