排序算法 Java篇
最近再對數(shù)據(jù)結構進行復習武契,在以前的學習中還存在很多疑問募判,所以想通過這次復習將其弄懂,所以花了點時間進行整理咒唆,如若碰到不足之處届垫,望指出。
閱讀本節(jié)你將了解到以下內(nèi)容:
冒泡排序算法思路與代碼實現(xiàn)(以及優(yōu)化)
選擇排序算法思路與代碼實現(xiàn)
插入排序算法思路與代碼實現(xiàn)
直接插入
二分插入
Shell排序(主要邏輯還是插入排序)
快速排序算法思路與代碼實現(xiàn)
歸并排序算法思路與代碼實現(xiàn)
基數(shù)排序算法思路與代碼實現(xiàn)
堆排序算法思路與代碼實現(xiàn)
各個算法之間的對比
排序算法 | 時間復雜度 (平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩(wěn)定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 穩(wěn)定 |
選擇排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不穩(wěn)定 |
直接插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 穩(wěn)定 |
二分插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 穩(wěn)定 |
Shell排序 | O(nlog2n) | O(n^2) | O(n) | O(1) | 不穩(wěn)定 |
快速排序 | O(nlog2n) | O(n2) | O(nlog2n) | O(nlog2n) | 不穩(wěn)定 |
歸并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 穩(wěn)定 |
基數(shù)排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(n+r) | 穩(wěn)定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不穩(wěn)定 |
1. 冒泡排序
時間復雜度 (平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩(wěn)定性 |
---|---|---|---|---|
O(n^2) | O(n^2) | O(n) | O(1) | 穩(wěn)定 |
時間復雜度為O(n)是進行過優(yōu)化的情況全释∽按Γ可以查看優(yōu)化的代碼
冒泡排序是一種理解起來比較簡單的排序算法,兩兩比較浸船,將最大(或最型ā)的數(shù)不斷后面進行放置即可。
代碼示例
/**
* 1. 冒泡排序
* 思路:
* 1. 每次前后兩個數(shù)進行比較李命,大的數(shù)往后面放
* 2. 不斷的執(zhí)行上面的過程
*/
public static int[] bubbleSort(int[] array){
//中間變量 用來幫助交換兩個數(shù)的值
int tmp = 0;
//要比較多少輪
//時間復雜度O(n*n)
for (int i = 0; i < array.length; i++) {
//每輪需要比較的次數(shù)
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j+1]) {
tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
}
}
}
return array;
}
代碼示例優(yōu)化
public static int[] bubbleSort(int[] array){
//中間變量 用來幫助交換兩個數(shù)的值
int tmp = 0;
boolean flag;
//要比較多少輪
//時間復雜度O(n*n)
for (int i = 0; i < array.length; i++) {
flag = false;
//每輪需要比較的次數(shù)
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j+1]) {
tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flag = true;
}
}
//當?shù)竭@里 flag 為false的時候 說明其后面的元素是序的不需要在繼續(xù) 了
if(false == flag){
return;
}
}
return array;
}
2.選擇排序
時間復雜度 (平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩(wěn)定性 |
---|---|---|---|---|
O(n^2) | O(n^2) | O(n^2) | O(1) | 不穩(wěn)定 |
選擇排序的思路就是每次記錄最大值的下標登淘,在完成該次循環(huán)后再將這個數(shù)放到相對(每次進行比較元素最后的位置)后的位置
代碼示例
/**
*2. 選擇排序
* 思路:
* 1. 找出最大數(shù)的索引
* 2. 將最大數(shù)的索引與最后一個數(shù)進行交換
* @param array
* @return
*/
public static int[] selectSort(int[] array){
//每輪只交換一次 性能高于冒泡 雖然時間復雜度為O(n*n)
for (int i = 0; i < array.length; i++) {
//最大數(shù)的下標 默認為0
int maxNumIndex = 0;
//這里需要注意的就是 array的取值范圍 用array.length - i表示
for (int j = 0; j < array.length - i; j++) {
//如果碰到更大的數(shù) 則更新下標
if (array[j] > array[maxNumIndex]) {
maxNumIndex = j;
}
}
//將其與最后一個數(shù)交換位置
int tmp = array[maxNumIndex];
//這里最后一個元素為array.length - i - 1
array[maxNumIndex] = array[array.length - i - 1];
array[array.length - i - 1] = tmp;
}
return array;
}
3.直接插入排序
時間復雜度 (平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩(wěn)定性 |
---|---|---|---|---|
O(n^2) | O(n^2) | O(n) | O(1) | 穩(wěn)定 |
這個比前兩個復雜一點:
需要排序的數(shù):4,3封字,8形帮,2,7周叮,1
選擇第一個數(shù)放入集合 也就是4 結果如下
4 3,8界斜,2仿耽,7,1
再次從數(shù)組中取出一個數(shù) 這里為 3 將其與前面已經(jīng)放入集合且排好序的元素從后往前進行比較
4 比 3大各薇,所以得到的結果如下
3项贺,4 8,2峭判,7开缎,1
重復上面的操作得到
3,4林螃,8 2奕删,7,1
從上面可以看出當想新加入一個數(shù)放置的位置是當其大于集合中的某一個數(shù)的時候就放在其后面疗认,開始在這個數(shù)后面的數(shù)順位往后面移動完残。
具體可以結合代碼進行理解
代碼示例
/**
* 3. 直接插入排序
* 思路:
* 1. 取出一個數(shù)伏钠,這里假設有一個集合為m(已經(jīng)是有序),讓這個數(shù)與集合中的數(shù)做對比
* 2. 將這個數(shù)從后往前做對比谨设,當這個數(shù)小于集合中的數(shù)熟掂,則集合中的數(shù)往后移動,如果小于扎拣,則這里為存放這個數(shù)的位置
* 3. 重復1赴肚,2
*
* @param array
* @return
*/
public static int[] insertSort(int[] array){
//算法時間復雜度為O(n * n)
for (int i = 1; i < array.length; i++) {
//記錄下一個需要放入集合的數(shù)
int tmp = array[i];
int j;
for (j = i - 1; j >= 0; j--) {
if(tmp < array[j]){
//當前比較的數(shù)大于 下一個需要插入的數(shù) 則將當前比較的數(shù)往后移動一個位置
array[j + 1] = array[j];
}
//如果當前比較的數(shù)小于下一個需要插入的數(shù) 則退出循環(huán)
else{
break;
}
}
//由于退出循環(huán)時 j-- 了一次 因此在存放要插入的數(shù)據(jù)的時候j需要進行加一次
array[j + 1] = tmp;
}
return array;
}
4.二分插入排序
時間復雜度 (平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩(wěn)定性 |
---|---|---|---|---|
O(n^2) | O(n^2) | O(n) | O(1) | 穩(wěn)定 |
思路和上面直接插入大概一致,只是其在往集合中插入元素的時候使用的是二分查找插入二蓝,而不是一個個比較插入誉券。具體可以結合代碼
代碼示例
/**
* 4. 二分法插入排序
* 思路:其與直接插入排序 相似 只是在插入的時候采用二分法進行查找插入的位置
* @param array
* @return
*/
public static int[] binaryInsertSort(int[] array){
for (int i = 1; i < array.length; i++) {
//保存要插入的數(shù)
int tmp = array[i];
//需要查找使用二分法查找的數(shù)組的最左方
int left = 0;
//最右方
int right = i - 1;
int mid;
while(left<=right){
mid = (left + right)/2;
//如果中間的值小于 要插入的值 說明這個數(shù)據(jù)處于 這個數(shù)組的后方法
if(array[mid] < tmp){
left = mid + 1;
}
//如果中間的值大于會等于要插入的值 說明這個數(shù)處于 這個數(shù)組的前方
else{
right = mid - 1;
}
}
//不管找到與否 這個數(shù)都應該在left后面的位置 所以這里需要將left及其后面的數(shù)向后移動
for (int j = i - 1; j >= left ; j--) {
array[j + 1] = array[j];
}
array[left] = tmp;
}
return array;
}
上面的幾種實現(xiàn)其實不需要return 對于數(shù)組沒有必要,可以選擇刪除侣夷。
5.Shell(希爾)排序
時間復雜度 (平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩(wěn)定性 |
---|---|---|---|---|
O(nlog2n) | O(n^2) | O(n) | O(1) | 不穩(wěn)定 |
Shell排序的主要邏輯還是插入排序的思想横朋。
不同點在于其有一個分量(自己看情況而定),比如下面一組數(shù):
3百拓,23琴锭,5,7衙传,2决帖,8,32蓖捶,23地回,1,6
當分量為4(隔4個位置分為一組)的時候其可以分為下面幾組數(shù)據(jù):
3俊鱼,2刻像,1 結果:1,2并闲,3 (這里主要還是交換了他們的位置)细睡,
23,8帝火,6 結果:6溜徙,8,23
5犀填,32 結果:5蠢壹,32
7,23 結果:23九巡,7
這個分量完成后的結果為:
分量為4的一輪排序結果:1图贸,6,5,23求妹,2乏盐,8,32制恍,7父能,3,23,可以看出每組的最小元素都排到了前面净神。
再將分量進行細分何吝,知道分量為1,即可以理解為上面的插入排序
代碼示例
/**
* 5. 希爾排序
* 思想:
* 給定一個分量鹃唯,每次對這個分量倍數(shù)上面的數(shù)進行排序
* 比如:當存在11個元素時爱榕,我們給定分量 為11/2 = 5
* 則后面的步驟為
* 對第 0,5坡慌,10位置元素進行插入排序 對1黔酥,6位置的元素進行選擇排序 對2,7位置的元素進行排序 以此類推
* 后面對分量在進行細分 這里選擇再除2 則為5/2=2
* 則其后面的步驟為
* 對0洪橘,2跪者,4,6熄求,8渣玲,10位置的元素進行插入排序 對1,3弟晚,5忘衍,7,9位置的元素進行插入排序
* 后面再將分量細分 再出二得到 2/2 = 1
* 即對所有位置的數(shù)進行插入排序
* 插入結束
*/
public static void shellSort(int[] array){
//用這個表示數(shù)組的長度 這樣寫的原因使得其不需要每次都在循環(huán)中調用array.length 處于加快程序運行速度考慮
int arrayLength = array.length;
//用length表示每次的分量
int length = array.length;
while (true){
//這里的length表示上面說的分量
length /= 2;
//這個循環(huán)表示再每一個循環(huán)下需要執(zhí)行多少次插入排序
for (int i = 0; i < length ; i++) {
//下面為插入排序的邏輯
for (int j = i+length; j < arrayLength; j+=length) {
//利用一個局部變量保存需要加入到隊列的數(shù)
int tmp = array[j];
int k;
for (k = j - length; k >= i; k-=length) {
//如果下一個要插入的數(shù)據(jù)小于數(shù)組中的數(shù) 則將數(shù)組中的數(shù)往后移動一個位置
if(tmp < array[k]){
array[k + length] = array[k];
}
//如果大于則直接退出
else{
break;
}
}
//由于退出循環(huán)時k-=m因此這里需要對其進行加上并將tmp放在這個位置
array[k + length] = tmp;
}
}
//這里需要設置退出循環(huán)所需要的條件卿城,當length=1時枚钓,即分量為1的插入排序。其就是一個插入排序
//再完成分量為1的插入排序后 說明數(shù)組已經(jīng)排好了順序 因此需要退出循環(huán)
if(length == 1){
break;
}
}
}
6.快速排序
時間復雜度 (平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩(wěn)定性 |
---|---|---|---|---|
O(nlog2n) | O(n2) | O(nlog2n) | O(nlog2n) | 不穩(wěn)定 |
快速排序簡單來說就是選擇一個基準點瑟押,一般選擇第一個元素作為基準點搀捷。
將比這個基準點小的數(shù)放在這個數(shù)的左邊,大的數(shù)放在這個基準數(shù)的右邊勉耀。
可以結合下面的圖片進行理解,但需要注意的是需要從右變開始蹋偏,否則會出現(xiàn)錯誤的情況便斥。
-
哨兵j出發(fā),比基準點大繼續(xù)走威始,比基準點小則停止枢纠。
- 當j停止后即其碰到一個比基準點小的數(shù),其暫停了黎棠,這個時候i開始走晋渺,當其遇到一個比基準點大的數(shù)的時候i也停止镰绎。
-
當i,j同時停止的時候木西,說明其可以將使得自己停止的數(shù)進行交換畴栖。
- 通過上面的循環(huán)步驟就可以將所有的比基準點大的數(shù)據(jù)放在一邊,小的數(shù)放在另一邊八千。
- 將這個動作一值做下去直到其只有自生一個元素則停止吗讶。
- 結合代碼理解更美味。
代碼示例
/**
* 6. 快速排序
* @param array
* @param left
* @param right
* @return
*/
public static void quickSort(int[] array,int left, int right){
if(left > right){
return;
}
//選取一個哨兵
int i = left;
int j = right;
int tmp = 0;
while(i != j){
//這里先要判斷這個 再判斷下面的部分 否則會出現(xiàn)錯誤
if(array[left] <= array[j] && i < j){
j--;
}else if(array[left] >= array[i] && i < j){
i++;
}else{
//這個是當array[i] > array[left] && array[j] < array[left]執(zhí)行
//所以這里交換這兩個的值
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
}
}
//再上面進行大部分交換完后 將第一個元素放到相對的中間去
tmp = array[left];
array[left] = array[i];
array[i] = tmp;
//這里還需要對左右兩邊的數(shù)組進行
quickSort(array,left,i-1);
quickSort(array,i+1,right);
}
7.歸并排序
時間復雜度 (平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩(wěn)定性 |
---|---|---|---|---|
O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 穩(wěn)定 |
歸并排序總共分為兩個步驟:
1. 將數(shù)組進行分割
2. 對已經(jīng)排好序的數(shù)組進行合并(有興趣的可以嘗試將兩個有序的鏈表進行合并為一個有序的鏈表)
3. 分別完成上面兩個步驟應該難度不大恋捆。對于歸并排序自己感覺存在問題的就是對于參數(shù)的把握照皆,而這個需要進行更為細節(jié)的考慮。
4. 參考下面代碼以及注釋應該就可以很好的理解了沸停。
代碼示例
/**
* 歸并排序:
* 思想:
* 1.對數(shù)組遞歸分割膜毁。
* 2.將拍好序的數(shù)組進行合并這里使用的是mergeArray()方法。
* 3.將排的數(shù)組放回原數(shù)組中愤钾。
* @param array
* @param start
* @param end
*/
public static void mergeSort(int[] array,int start,int end){
if(start >= end){
return;
}
int mid = (start + end)/2;
mergeSort(array,start,mid);
mergeSort(array,mid+1,end);
mergeArray(array,start,mid,end);
}
/**
* 這個函數(shù)需要將 array數(shù)組指定范圍(start-end)內(nèi)的元素進行排序
* @param array
* @param start
* @param mid
* @param end
*/
public static void mergeArray(int[] array,int start,int mid,int end){
//需要用一個數(shù)組來存放 歸并后的元素
int[] tmp = new int[end - start + 1];
//第一個數(shù)組的第一個元素
int i = start;
//第二個數(shù)組的第一個元素
int j = mid + 1;
//用于tmp存放的下標索引
int count = 0;
while(i <= mid && j <= end ){
//如果 前面的那段小于后面那段 則將其 放入tmp中
if(array[i] < array[j]){
tmp[count] = array[i];
i++;
}
//否則 則將后面那段放入 tmp中
else{
tmp[count] = array[j];
j++;
}
count++;
}
//這里需要判斷是誰越界退出的循環(huán)
//如果check == 1則說明是因為length1全部完成 否則 length2完成
int check = (i == mid + 1)? 1 : 2;
if (check == 1){
for (int k = j; k <= end; k++) {
tmp[count] = array[k];
count++;
}
}else{
for (int k = i; k <= mid ; k++) {
tmp[count] = array[k];
count++;
}
}
copyArray(array,tmp,start);
}
/**
* 將array數(shù)組從位置start開始將tmp里面的值復制過去
* @param array
* @param tmp
* @param start
*/
public static void copyArray(int[] array,int[] tmp,int start){
int length = tmp.length;
for (int i = 0; i < length; i++) {
array[start] = tmp[i];
start++;
}
}
8.基數(shù)排序
時間復雜度 (平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩(wěn)定性 |
---|---|---|---|---|
O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(n+r) | 穩(wěn)定 |
1. 獲得是個桶骄蝇,其對應著0,1猎醇,2顺饮,3,4劲装,5胧沫,6,7占业,8绒怨,9。
2. 按順序得到每個數(shù)的個位谦疾,將這個數(shù)放入與這個數(shù)個位相等的桶里(如果這個數(shù)為99南蹂,其個位為9 ,則將99這個數(shù)放入9號桶里)念恍。
3. 將放入桶里的數(shù)按從0號-9號桶的按順序取出六剥。
4. 再將上面取出的數(shù)獲得其十位(怎么獲得其十位上的數(shù)自己可以想想,可參考代碼)峰伙。后面的步驟和2疗疟,3一樣。
5. 如果有百位瞳氓,千位策彤,處理方式相同
代碼示例
/**
* 基數(shù)排序 :
* 思想:
* 1. 先獲得10個桶(也就是數(shù)組)對應著0,1,2店诗,3裹刮,4,5庞瘸,6捧弃,7,8恕洲,9塔橡。10個數(shù)字
* 2. 循環(huán)將每一個數(shù)字進行個位、十位霜第、百位.....進行逐個求出并放入對應的桶中
* 3. 每對一個位進行求和完畢則將數(shù)組取出后重新放入
*
*
* 基數(shù)排序不能處理負數(shù)的情況
* 代價大的解決方案:找出最小的負數(shù) 對其數(shù)組里面的每個元素加上一個絕對值大于最小負數(shù)的正數(shù)
* 使得數(shù)組里面的數(shù)全為正數(shù)后排序完成再減去這個正數(shù)
* @param array
*/
public static void radixSort(int[] array){
//先找出最大的數(shù) 看其總共需要求幾輪值
int max = Integer.MIN_VALUE;
for (int i = 0; i < array.length; i++) {
if(max < array[i]){
max = array[i];
}
}
//獲取最大值的長度
int maxLength = (max + "").length();
//數(shù)組的長度
int arrayLength = array.length;
//定義桶 桶只有10個 其容量最多只有 arrayLength個
int[][] tmp = new int[10][arrayLength];
//記錄每一個桶中存的元素的個數(shù) 總共需要記錄十個位置
int[] count = new int[10];
//最多循環(huán)最大值長度次循環(huán)
for (int i = 0,n = 1; i < maxLength; i++,n*=10) {
//這里需要對每個數(shù)進行個位 葛家、十位、百位進行求值并放入數(shù)組中
for (int j = 0; j < arrayLength; j++) {
//對每個數(shù)進行除n%10可以求出其對應位的值
int ys = array[j]/n%10;
//求出余數(shù)說明其是放在 第ys個桶的位置 這里需要一些桶 同時需要這個桶中存了多少個數(shù)
tmp[ys][count[ys]] = array[j];
//桶中存放的數(shù)多了一個
++count[ys];
}
int pos = 0;
//執(zhí)行過一個位的存放后需要對其按順序取出并放入原數(shù)組中
for (int j = 0; j < tmp.length; j++) {
//每個桶中存放元素的個數(shù)
for (int k = 0; k < count[j]; k++) {
array[pos] = tmp[j][k];
//將當前位置的值置零以便下次循環(huán)
tmp[j][k] = 0;
pos++;
}
//將當前位置的元素個數(shù)置零 以便下次循環(huán)使用
count[j] = 0;
}
}
}
9.堆排序
時間復雜度 (平均) | 時間復雜度(最壞) | 時間復雜度(最好) | 空間復雜度 | 穩(wěn)定性 |
---|---|---|---|---|
O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不穩(wěn)定 |
堆排序比較復雜泌类,上圖(從別人哪里tou來的癞谒,嘻嘻(源地址)):
堆排序是選擇排序的一種,通過建立堆將其最大(最腥姓ァ)的元素找出弹砚。
其過程主要也分為兩個步驟:
- 建堆
- 調整堆
這個單純靠我這里的這部分可能還不能夠理解(可以取查找單獨對堆排序進行講解的教程進行參考 堆排序視頻)。
代碼示例
/**
* 堆排序
* @param array
*/
public static void heapSort(int[] array){
//數(shù)組的長度
int length = array.length;
int index = (length - 2) / 2;
for (int i = index; i >= 0; i--) {
//各個局部建堆 最后形成一個大堆
maxHeap(array,length,i);
}
//不斷將對頂?shù)脑睾投盐驳脑剡M行交換 這個過程中需要調整需要加入堆中的元素的個數(shù)
for (int i = length - 1; i > 0; i--) {
int tmp = array[i];
array[i] = array[0];
array[0] = tmp;
//將最大的數(shù)移到最后面之后就不需要再將其進行比較了 因此size 變小了
//但由于這次交換 堆被破壞了 因此需要從被破壞的地方重新進行堆調整
maxHeap(array,i,0);
}
}
/**
*
* @param array 要建立堆的數(shù)組
* @param size 參與建堆的數(shù)組大小 前多少個
* @param index 開始調整的位置
*/
public static void maxHeap(int[] array,int size,int index){
//當前位置的左邊元素的索引 這個是完全二叉樹的特性
//左邊節(jié)點的下標等于 父節(jié)點的 2*fatherIndex + 1
int lIndex = 2 * index + 1;
//右邊元素的下標
int rIndex = 2 * index + 2;
//下面段代碼的目的在于找到 父 左 右三個節(jié)點中最大的元素來當父親
int max = index;
//這里判斷size是保證其后面不會出現(xiàn)越界的情況
if(lIndex < size && array[max] < array[lIndex]){
max = lIndex;
}
if(rIndex < size && array[max] < array[rIndex]){
max = rIndex;
}
//如果位置改變了 則交換位置
//沒改變則沒有必要
if(max != index){
//將最大的元素放到父親的位置
//保存父節(jié)點的位置
int tmp = array[index];
//將最大元素覆蓋父節(jié)點的位置
array[index] = array[max];
array[max] = tmp;
//當因為調節(jié)了前面的元素導致了后面的堆進行了破壞 需要對后面的堆再進行調整
maxHeap(array,size,max);
}
}
10.總結
其實各個排序算法理解起來并不算太難枢希,自己再這次復習的過程中首先是從簡單的排序進行理解桌吃,其中包括冒泡排序,直接選擇排序苞轿,直接插入排序茅诱,二分插入排序。在這段時間接觸了許多關于遞歸(牛課劍指offer)的問題搬卒,所以對復雜的排序算法理解也不算太難瑟俭。總的來說對排序算法的學習需要一個過程契邀,靜心多練習摆寄。
11.參考鏈接
https://blog.csdn.net/sd09044901guic/article/details/80613053
https://blog.csdn.net/qq_41891257/article/details/85245127
('')第一篇博客,不喜勿噴坯门。