什么是算法的穩定性?
簡單的說就是一組數經過某個排序算法后仍然能保持他們在排序之前的相對次序就說這個排序方法是穩定的, 比如說,a1,a2,a3,a4四個數, 其中a2=a3,如果經過排序算法后的結果是 a1,a3,a2,a4我們就說這個算法是非穩定的,如果還是原來的順序a1,a2,a3,a4,我們就會這個算法是穩定的
1.選擇排序
選擇排序,顧名思義,在循環比較的過程中肯定存在着選擇的操作, 算法的思路就是從第一個數開始選擇,然后跟他后面的數挨個比較,只要后面的數比它還小,就交換兩者的位置,然后再從第二個數開始重復這個過程,最終得到從小到大的有序數組
算法實現:
public static void select(int [] arr){
// 選取多少個標記為最小的數,控制循環的次數
for (int i=0;i<arr.length-1;i++){
int minIndex = i;
// 把當前遍歷的數和它后面的數依次比較, 並記錄下最小的數的下標
for (int j=i+1;j<arr.length;j++){
// 讓標記為最小的數字,依次和它后面的數字比較,一旦后面有更小的,記錄下標
if (arr[minIndex]>arr[j]){
// 記錄的是下標,而不是着急直接交換值,因為后面可能還有更小的
minIndex=j;
}
}
// 當前最小的數字下標不是一開始我們標記出來的那個,交換位置
if (minIndex!=i){
int temp=arr[minIndex];
arr[minIndex]=arr[i];
arr[i]=temp;
}
}
}
}
時間復雜度: n + n-1 + n-2 + .. + 2 + 1 = n*(n+1)/2 = O(n^2)
穩定性: 比如: 5 8 5 2 7 經過一輪排序后的順序是2 8 5 5 7, 原來兩個5的前后順序亂了,因此它不穩定
推薦的使用場景: n較小時
輔助存儲: 就一個數組,結果是O(1)
2.插入排序
見名知意,在排序的過程中會存在插入的情況,依然是從小到大排序 算法的思路: 選取第二個位置為默認的標准位置,查看當前這個標准位置之前有沒有比它還大的元素,如果有的話就通過插入的方式,交換兩者的位置,怎么插入呢? 就是找一個中間變量記錄當前這個標准位置的值,然后讓這個標准位置前面的元素往前移動一個位置,這樣現在的標准位置被新移動過來的元素給占了,但是前面空出來一個位置, 於是將這個存放標准值的中間元素賦值給這個空出來的位置,完成排序
代碼實現:
/**
* 思路: 從數組的第二個位置開始,選定為標准位置,這樣開始的話,可以保證,從標准位置開始往前全部是有序的
* @param arr
*/
private static void insetSort(int[] arr) {
int temp;
// 從第二個開始遍歷index=1 , 一共length 個數,一直循環到,最后一個數參加比較, 就是 1 <-> length 次
for (int i=1;i<arr.length;i++){
// 判斷大小,要是現在的比上一個小的話,准備遍歷當前位置以前的有序數組
if (arr[i] < arr[i-1]){
// 存放當前位置的值
temp=arr[i];
int j;
// 循環遍歷當前位置及以前的位置的有序數組,只要是從當前位置開始,前面的數比當前位置的數大,就把這個大的數替插入到當前的位置
// 隨着j的減少,實際上每次循環都是前一個插到后一個的位置上
for (j=i-1;j>=0&&temp<arr[j];j--){
arr[j+1]=arr[j];
}
// 直到找出一個數, 不比原來存儲的那個當前的位置的大,就把存起來的數,插到這個數的前面
arr[j+1]=temp;
}
}
}
時間復雜度:
- 最好的情況就是: 數組本來就是有序的, 這樣算法從第2個位置開始循環n-1次, 時間復雜度就是 n
- 最壞的情況: 外層 n-1次, 內存分別是 1,2,3,4...n-2 ==> (n-2)(n-1)/2 = O(n^2)
- 平均時間復雜度: O(n^2)
穩定性: 從第2個位置開始遍歷,標准位之前數組一直是有序的,所以它肯定穩定
輔助存儲: O(1)
推薦的使用場景: n大部分有序時
3.冒泡排序
最熟悉的排序方式
- 從零位的數開始,比較總長度-1大輪
- 每一個大輪比較 總長度-1-當前元素的index 小輪
代碼實現:
private static void sort(int[] ints) {
System.out.println("length == "+ints.length);
// 控制比較多少輪 0- length-1 一共length個數,最壞比較lenth-1次
for (int i=0;i<ints.length-1;i++){
// 每次都從第一個開始比較,每次比較的時候,最多比較到上次比較的移動的最新的下標的位置,就是 length-0-i
for (int j=0;j<ints.length-1-i;j++){
int temp;
if (ints[j] > ints[j+1]) {
temp=ints[j];
ints[j]=ints[j+1];
ints[j+1]=temp;
}
}
}
}
時間復雜度:
- 做好的情況下: 數組本身就有序,執行的就是最外圈的for循環中的n次
- 最壞的情況: (n-1) + (n-2) + (n-3) +...+ 1 = n(n-1)/2 當n趨近於無限大時, 時間發雜度 趨近於n^2
穩定性: 穩定
輔助存儲空間: O(1)
推薦的使用場景: n越小越好
4.歸並排序
算法的思路: 先通過遞歸將一個完整的大數組[5,8,3,6]拆分成小數組, 經過遞歸作用,最終最小的數組就是[5],[8],[3],[6]
遞歸到最底層后就會有彈棧的操作,通過這時創建一個臨時數組,將這些小數組中的數排好序放到這個臨時數組中,再用臨時數組中的數替換待排序數組中的內容,實現部分排序, 重復這個過程
/**
* 為什么需要low height 這種參數,而不是通過 arr數組計算出來呢?
* --> 長個心,每當使用遞歸的時候,關於數組的這種信息寫在參數位置上
* @param arr 需要排序的數組
* @param low 從哪里開始
* @param high 排到哪里結束
*/
public static void mergeSort(int[] arr, int low, int high) {
int middle = (high + low) / 2;
if (low < high) {
// 處理左邊;
mergeSort(arr, low, middle);
// 處理右邊
mergeSort(arr, middle + 1, high);
// 歸並
merge(arr, low, middle, high);
}
}
/**
* @param arr
* @param low
* @param middle
* @param high
*/
public static void merge(int[] arr, 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 (arr[i] < arr[j]) {
temp[index] = arr[i];
i++;
} else {
// todo 在這里放個計數器++ , 可以計算得出 反序對的個數 (這樣的好處就是時間的復雜度是 nlogn)
temp[index] = arr[j];
j++;
}
index++;
}
while (j <= high) {
temp[index] = arr[j];
j++;
index++;
}
while (i <= middle) {
temp[index] = arr[i];
i++;
index++;
}
// 把臨時入數組中的數據重新存原數組
for (int x = 0; x < temp.length; x++) {
System.out.println("temp[x]== " + temp[x]);
arr[x + low] = temp[x];
}
}
按上面說的[5,8,3,6]來說,經過遞歸他們會被分成這樣
---------------壓棧--------------------
左 右
[5,8] [3,6]
[5] [8] [3] [6]
------------下面的彈棧-----------------
[5]和[8]歸並,5<8 得到結果: [5,8]
[3]和[6]歸並, 3<6 得到結果 [3,6]
於是經過兩輪歸並就得到了結果
[5,8] [3,6]
繼續歸並: 創建一個臨時數組 tmp[]
5>3 tmp[0]=3
5<6 tmp[1]=5
8>6 tmp[2]=6
tmp[3]=8
然后讓tmp覆蓋原數組得到最終結果
推薦使用的場景: n越大越好
時間復雜度: 最好,平均,最壞都是 O(nlogn) (這是基於比較的排序算法所能達到的最高境界)
穩定性能: 穩定
空間復雜度: 每兩個有序序列的歸並都需要一個臨時數組來輔助,因此是 O(N)
5.快速排序
是分支思想的體現, 算法的思路就是每次都選出一個標准值,目標是經過處理,讓當前數組中,標准值左邊的數都小於標准值, 標准值右邊的數都大於標准值, 重復遞歸下去,最終就能得到有序數組
實現:
// 快速排序
public static void quickSort(int[] arr, int start, int end) {
// 保證遞歸安全的進行
if (start < end) {
// 1. 找一個中間變量,記錄標准值,一般是數組的第一個,以這個標准的值為中心,把數組分成兩部分
int standard = arr[start];
// 2. 記錄最小的值和最大的值的下標
int low = start;
int high = end;
// 3. 循環比較,只要我的最開始的low下標,小於最大值的下標 就不停的比較
while (low < high) {
System.out.println("high== " + high);
// 從右邊開始,如果右邊的數字比標准值大,把下標往前動
while (low < high && standard <= arr[high]) {
high--;
}
// 右邊的最high的數字比 標准值小, 把當前的high位置的數字賦值給最low位置的數字
arr[low] = arr[high];
// 接着從做low 的位置的開始和標准值比較, 如果現在的low位置的數組比標准值小,下標往后動
while (low < high && arr[low] <= standard ) {
low++;
}
// 如果low位置的數字的比 標准值大,把當前的low位置的數字,賦值給high位置的數字
arr[high] = arr[low];
}
// 把標准位置的數,給low位置的數
arr[low] = standard ;
// 開始遞歸,分開兩個部分遞歸
// 右部分
quickSort(arr, start, low);
// 左部分
quickSort(arr, low + 1, end);
}
}
推薦使用場景: n越大越好
時間復雜度:
- 最壞: 其實大家可以看到,上面的有三個while,但是每次工作的最多就兩個,如果真的就那么不巧,所有的數兩兩換值,那么最壞的結果和冒泡一樣 O(n^2)
- 最好和平均都是: O(nlogn)
穩定性: 快排是不穩定的
6.希爾排序
希爾排序其實可以理解成一種帶步長的排序方式, 上面剛說了插入排序的實現方式,上面說我們默認從數組的第二個位置開始算,實際上就是說步長是1,下標的移動每次都是1
對於希爾排序來說,它默認的步長是 arr.length/2 , 每次步長都減少一半, 最終的步長也會是1
代碼實現:
/**
* 希爾排序,在插入排序的基礎上,添加了步長 ,
* // todo 只要在本步長范圍內,這些數字為組成一個組進行 插入排序
* 初始 步長=length/2
* 后來: 步長= 步長/2
* 直到最后: 步長=1; 正宗的插入排序
* @param ints
*/
private static void shellSort(int[] ints) {
// 記錄當前的步長
int step=ints.length/2;
// 步長==》控制遍歷幾輪, 並且每次步長都是上一次的一半大小
for (int i=step;i>0;i/=2){
// 遍歷當前步長下面的全部組,這里的j1, 就相當於插入排序中第一次開始的位置1前面的0下標
for (int j1=i;j1<ints.length;j1++){
// 遍歷本組中全部元素 == > 從第二個位置開始遍歷
// x 就相當於插入排序中第一次開始的位置1
for(int x=j1+i;x<=ints.length-1;x+=i){
// 從當前組的第二個元素開始,一旦發現它前面的元素比他小,
// 插入排序, 1. 把當前的元素存起來 2. 循環它前面的元素往前移 3. 把當前元素插入到合適的位置
if(ints[x]<ints[x-i]){
int temp=ints[x];
int j ;
for(j=x-i;j>=0&&ints[j]>temp;j=j-i)
{
ints[j+i]=ints[j];
}
ints[j+i]=temp;
}
}
}
}
}
空間復雜度: 和插入排序一樣都是O(1)
穩定性: 希爾排序由於步長的原因,而不向插入排序,一經開始標准位置前的數組即刻有序, 所以希爾排序是不穩定的
希爾排序的性能無法准確量化,跟輸入的數據有很大關系在實際應用中也不會用它,因為十分不穩定,雖然比傳統的插入排序快,但比快速排序等慢
其時間復雜度介於O(nlogn) 到 O(n^2) 之間
7.堆排序
堆排序是借助了堆這種數據結構完成的排序方式,堆有大頂堆和小頂堆, 將數組轉換成大頂堆然后進行排序的會結果是數組將從小到大排序,小頂堆則相反
什么是堆呢? 堆其實是可以看成一顆完全二叉樹的數組對象, 那什么是完全二叉樹呢? 比如說, 這顆數的深度是h,那么除了h層外, 其他的1~h-1
層的節點都達到了最大的數量
算法的實現思路: 通過遞歸,將數組看成一個堆,從最后一個非葉子節點開始,將這個節點轉換成大頂堆, 什么是大頂堆呢? 就是根節點總是大於它的兩個子節點, 重復這個過程一直遞歸到堆的根節點(此時根節點是最大值),此時整個堆為大頂堆, 然后交換根節點和最后一個葉子節點的位置,將最大值保存起來
例: 假設待排序序列是a[] = {7, 1, 6, 5, 3, 2, 4},並且按大頂堆方式完成排序
- 第一步(構造初始堆):
- 第二步(首尾交換,斷尾重構):
代碼實現:
public static void sort(int[] ints) {
// 開始位置是最后一個非葉子節點
int start = ints.length / 2-1;
// 調整成大頂堆
for (int i = start; i >= 0; i--) {
maxHeap(ints, ints.length, i);
}
// 調整第一個和最后一個數字, 在把剩下的轉換為大定堆, j--實現了,不再調整本輪選出的最大的數
for (int j = ints.length - 1; j > 0; j--) {
int temp = ints[0];
ints[0] = ints[j];
ints[j] = temp;
maxHeap(ints, j, 0);
}
}
/**
* 轉換為大頂堆, 其實就是比較根節點和兩個子節點的大小,調換他們的順序使得根節點的值大於它的兩個子節點
*
* @param arr
* @param size
* @param index 從哪個節點開始調整 (一開始轉換為大頂堆時,使用的是最后一個非夜之節點, 但是轉換完成之后,使用的就是0,從根節點開始調整)
*/
public static void maxHeap(int[] arr, int size, int index) {
// 當前節點的左子節點
int leftNode = 2 * index + 1;
// 當前節點的右子節點
int rightNode = 2 * index + 2;
// 找出 當前節點和左右兩個節點誰最大
int max = index;
if (leftNode < size && arr[leftNode] > arr[max]) {
max = leftNode;
}
if (rightNode < size && arr[rightNode] > arr[max]) {
max = rightNode;
}
// 交換位置
if (max != index) {
int temp = arr[index];
arr[index] = arr[max];
arr[max] = temp;
// 交換位置后,可能破壞之前的平衡(跟節點比左右的節點小),遞歸
// 有可能會破壞以max為定點的子樹的平衡
maxHeap(arr, size, max);
}
}
推薦的使用場景: n越大越好
時間復雜度: 堆排序的效率與快排、歸並相同,都達到了基於比較的排序算法效率的峰值(時間復雜度為O(nlogn))
空間復雜度: O(1)
穩定性: 不穩定
基數排序
算法思路:
看上圖中的綠色部分, 假設我們有下標從0-9,一共10個桶
第一排是給我們排序的一組數
我們分別對取出第一排數的個位數,放入到對應下標中的桶中,再依次取出,就得到了第三行的結果, 再取出三行的十位數,放入到桶中,再取出,就得到最后一行的結果
// 基數排序
// 創建10個數組,索引從0-9
// 第一次按照個位排序
// 第二次按照十位排序
// 第三次按照百位排序
// 排序的次數,就是數組中最大的數的位數
public static void radixSort(int[] arr){
int max = Integer.MIN_VALUE;
// 循環找到最大的數,控制比較的次數
for (int i : arr) {
if (i>max){
max=i;
}
}
System.out.println(" 最大值: "+max);
// 求最大數字的位數,獲取需要比較的輪數
int maxLength = (max+"").length();
System.out.println(" 最大串的長度: "+maxLength);
// 創建應用創建臨時數據的數組, 整個大數組中存放着10個小數組, 這10個小數組中真正存放的着元素
int [][] temp = new int[10][arr.length]; // 10個 長度為arr.length長度的數組
// todo 用於記錄在temp中相應的數組中存放的數字的數量
int [] counts = new int[10];
// 還需要添加另一個變量n 因為我們每輪的排序是從的1 - 10 - 100 - ... 開始求莫排序
for(int i=0,n=1;i<maxLength;i++,n*=10){
// 計算每一個數字的余數,遍歷數組,將符合要求的放到指定的數組中
for (int j=0;j<arr.length;j++){
int x = arr[j]/n%10;
// 把當前遍歷到的數據放入到指定位置的二維數組中
temp[x][counts[x]] = arr[j];
// 更新二維數組中新更改的數組后的 新長度
counts[x]++;
}
int index =0;
// 把存放進去的數據重新取出來
for (int y=0;y<counts.length;y++){
// 記錄數量的那個數組的長度不為零,我們才區取數據
if (counts[y]!=0){
// 循環取出元素
for (int z =0;z<counts[y];z++){
// 取出
arr[index++] = temp[y][z];
}
// 把數量置為0
counts[y]=0;
}
}
}
}
穩定性: 穩定
時間復雜度: 平均、最好、最壞都為O(k*n),其中k為常數,n為元素個數
空間復雜度: O(n+k)
桶排序
算法思路: 相對比較好想, 給我們一組數,我們在選出這組數中最大值和數組的length的長度,選最大的值當成新數組的長度,然后遍歷舊的數組,將舊數組中的值放入到新數組中index=舊數組中的值的位置
然后一次遍歷舊數組中的值,就能得到最終的結果
代碼實現:
private static void sort(int[] ints) {
int max = Integer.MIN_VALUE;
for (int anInt : ints) {
if (anInt>max)
max=anInt;
}
int maxLength = ints.length-max >0 ? ints.length:max;
int[] result = new int[maxLength];
for (int i=0;i<ints.length;i++){
result[ints[i]] +=1 ;
}
for(int i=0 ,index = 0;i<result.length;i++){
if (result[i]!=0){
for (int j=result[i];j>0;j--){
ints[index++] = i;
}
}
}
}
時間復雜度:
- 平均時間復雜度:O(n + k)
- 最佳時間復雜度:O(n + k)
- 最差時間復雜度:O(n^2)
空間復雜度:O(n * k)
穩定性:穩定
典型的用空間換時間的算法
計數排序
算法思路: 根據待排序集合中最大元素和最小元素的差值范圍,申請額外空間; 遍歷待排序集合,將每一個元素出現的次數記錄到元素值對應的額外空間內; 對額外空間內數據進行計算,得出每一個元素的正確位置; 將待排序集合每一個元素移動到計算得出的正確位置上。
代碼實現:
public static int[] sort(int[] A) {
//一:求取最大值和最小值,計算中間數組的長度:中間數組是用來記錄原始數據中每個值出現的頻率
int max = A[0], min = A[0];
for (int i : A) {
if (i > max) {
max = i;
}
if (i < min) {
min = i;
}
}
//二:有了最大值和最小值能夠確定中間數組的長度
//存儲5-0+1 = 6
int[] pxA = new int[max - min + 1];
//三.循環遍歷舊數組計數排序: 就是統計原始數組值出現的頻率到中間數組B中
for (int i : A) {
pxA[i - min] += 1;//數的位置 上+1
}
//四.遍歷輸出
//創建最終數組,就是返回的數組,和原始數組長度相等,但是排序完成的
int[] result = new int[A.length];
int index = 0;//記錄最終數組的下標
//先循環每一個元素 在計數排序器的下標中
for (int i = 0; i < pxA.length; i++) {
//循環出現的次數
for (int j = 0; j < pxA[i]; j++) {//pxA[i]:這個數出現的頻率
result[index++] = i + min;//原來原來減少了min現在加上min,值就變成了原來的值
}
}
return result;
}
也是典型的用空間換時間的算法
時間復雜度: O(n+k)
空間復雜度: O(n+k)
穩定性: 穩定