排序算法
1.什么叫排序?
排序前:3,1,6,9,8,9
排序后:1,3,5,6,8,9
排序無處不在
十大排序算法:
2 算法介紹
什么是排序算法的穩定性?
回答:如果相等的2個元素,在排序前后的相對位置保持不變,那么這是穩定的排序算法
舉例:
排序前:5,1,3(a), 4,7,3(b)
穩定的排序: 1,3(a),3(b),4,5,7
不穩定的排序: 1,3(b),3(a),4,5,7
在對自定義對象進行排序時,穩定性會影響最終的排序效果
什么是原地算法(In-place Algorithm)?
回答:不依賴額外的資源或者依賴少數的額外資源,僅依靠輸出覆蓋輸入,空間復雜度為0(1)的可以認為是原地算法
ps.(代碼實現用到的方法定義,本文統一以升序為例子)
cmp(index0,index1)
cmp方法傳入二個數組索引比較兩個索引所指元素的大小
如果index0索引的元素大于index1索引的元素則返回 一個大于0 的整數
反之則返回小于0的整數 ,相等返回0。
swp(index0,index1)
swp方法傳入二個數組索引交換索引所指元素的位置
2.1冒泡排序
冒泡排序也叫起泡排序
-
執行流程
① 從頭開始比較相鄰每一對元素,如果第1個比第2個大,就交換它們的位置,執行完一輪后,最末尾那個元素就是最大的元素
② 忽略①中曾經找到的最大元素,重復執行步驟1,知道全部元素有序
代碼實現:
for (int end = array.length - 1; end > 0 ; end --){
for (int begin = 1; begin <= end ; begin ++){
if (cmp(begin, begin - 1) < 0) {
swap(begin, begin - 1)
}
}
}
冒泡排序優化①
在冒泡排序的過程中會出現這么一種情況在某一輪的排序過程中根本就沒有調用swap方法說明此時的數組已經是完全有序了但是按照之前的做法我們的程序還是要執行下去直到所有輪次結束,這樣后面的代碼都是在做無用功大大的浪費了性能,所以我們可以在檢測到數組已經提前有序的情況下及時的結束方法提高效率。
代碼實現:
for (int end = array.length - 1; end > 0 ; end --){
bool sorted = true;
for (int begin = 1; begin <= end ; begin ++){
if (cmp(begin, begin - 1) < 0) {
swap(begin, begin - 1)
sorted = false;
}
}
if (sorted) break
}
從代碼中可以看出 如果swap 在一輪排序中始終沒有調用,那么sorted變量則為true
發現sorted為true時,我們直接結束循環。在實際應用中很少會出現①中提前有序的情況,所以第一種優化的能用到的情況不會太大。
冒泡排序優化②
如果序列尾部已經局部有序,我們可以記錄最后一次交換的位置,減少比較次數
什么意思?
舉個例子:有無序列表 a = [2,1,8,9,7,10,11,12,13,14]
通過觀察可以發現a的尾部從10元素開始后面都是有序的,如果通過我們之前的代碼排序在交換了9 和 7 的位置之后 我們還是會去判斷10之后的元素的大小關系但是它們已經是有序的了。所以后面我們幾輪的排序都會做一些無用的判斷。這個時候我們可以記錄下我們最后一次交換元素的位置 7 的位置 此時 a = [1,2,8,7,9,10,11,12,13,14]
當我們下一輪排序時到7 的位置我們就直接結束排序因為后面的元素已經是有序的了。這樣可以優化我們判斷次數。
代碼實現:
for (int end = array.length - 1; end > 0 ; end --){
int sortedIndex = 1;
for (int begin = 1; begin <= end ; begin ++){
if (cmp(begin, begin - 1) < 0) {
swap(begin, begin - 1)
sortedIndex = begin;
}
}
end = sortedIndex
}
- 設置 sortedIndex 的初始值為1可以完美的兼容整個序列提前有序的情況
- sortedIndex 的值會不斷被begin的值覆蓋 單一輪排序完成時 sortedIndex的值必然是最后一次交換元素的位置
2.2 選擇排序
-
執行流程
① 遍歷一邊序列找出最大的那個元素,然后與末尾的元素交換位置
② 忽略①中找到的最大元素,重復執行步驟 ①
代碼實現:
for (int end = array.length - 1; end > 0 ; end --){ int maxIndex = 0; for (int begin = 1; begin <= end ; begin ++){ if (cmp(max, begin ) <= 0) { max = begin; } } swap(max,end); }
選擇排序的交換次數遠遠少于冒泡排序,平均性能優于冒泡排序
最好,最壞,平均時間復雜度:O(n2)
2.3 插入排序
- 執行流程
① 在執行過程中,插入排序會將序列分為2部分,頭部是已經排好序的,尾部是待排序的
② 從頭開始掃描每一個元素,每當掃描到一個元素,就將它插入到頭部合適的位置,使得頭部的數據依次保持有序
代碼實現:
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
while (cur > 0 && amp(cur, cur -1) < 0) {
swap(cur, cur - 1);
cur--
}
}
插入排序的時間復雜度與逆序對的數量成正比關系
逆序對的數量越多,插入排序的時間復雜度越高
(ps.什么是逆序對? 舉個例子:數組<2,3,8,6,1>的逆序對為: <2,1><3,1><8,1><8,6><6,1>,一共5個逆序對)最壞,平均時間復雜度::O(n2)
最好時間復雜度: O(n)
空間復雜度:O(1)
屬于穩定排序
插入排序優化1
- 思路將【交換】轉為【挪移】
① 先將待插入的元素備份
②頭部有序數據中比待插入元素大的,都朝尾部方向挪動一個位置
③將待插入元素放入最終合適的位置
代碼實現:
for (int begin = 1 ; begin < array.length; begin++){
int cur = begin;
T value = array[cur];//備份元素
while (cur > 0 && cmp(v, array[cur - 1]) < 0) {
array[cur] = array[cur - 1];
cur --;
}
array[cur] = v;
}
插入排序優化2
- 思路在優化1的基礎上 優化確定位置的過程優化1是一個一個比較來確定,所以可以通過二分法直接求出要插入的位置。從而減少比較次數
代碼實現:
for (int i = 1; i < array.length; i ++ ){
//找出插入位置
int index = -1;
int begin = 0;
int end = I;
while (begin < end) {
int mid = (begin + end) >> 1;
if (cmp(i ,mid) < 0 ){
end = mid;
}else {
begin = mid + 1;
}
}
index = begin;
//備份元素
T value = array[I];
for (int j = i; j > index; j --) {
array[j] = array[j - 1];
}
array[index] = value;
}
2.4 歸并排序
-
執行流程
① 不斷地將當前序列平均分割成2個子序列,直到不能分割(序列中只剩一個元素)
② 不斷地將2個子序列合并成一個有序序列直到最終只剩下1個有序序列
代碼實現
// 準備一段臨時的數組空間,在合并操作中使用
leftArray = (T[ ]) new Object[array.lengtn >> 1];
sort(0,array.length);
private void sort(int begin, int end) {
// 至少需要有2個元素
if (end - begin < 2) return;
int mid = (begin + end) >> 1;
sort(begin,mid);
sort(mid,end);
merge(begin,mid,end);
}
private void merge(int begin, int mid ,int end) {
int li = 0;
int le = mid - begin; // 左邊數組(基于leftArray)
int ri = mid;
int re = end;//右邊數組(基于array)
int ai = begin;//array 的索引
for (int I = li; i < le; i ++){//拷貝左邊數組到leftArray
leftArray[I] = array[begin + I];
}
while (li < le) {
if (ri < re && amp(array[ri]),leftArray[li] < 0) {
array[ai ++] = array[ri ++];// 拷貝右邊數組到array
} else {
array[ai ++] = leftArray[li ++];//拷貝左邊數組到array
}
}//cmp位置改為 <= 會失去穩定性
}
歸并排序 - 復雜度分析
- 歸并排序花費的時間
T(n) = 2 * T(n/2) + 0(n)
T(1) = O(1)
T(n)/n = T(n/2)/(n/2) + O(1)
令S(n) = T(n)/n
S(1) = O(1)
S(n) = S(n/2) + O(1) = S(n/4) + 0(2) = S(n/8) + 0(3) = S(n/2k) + O(k) = S(1) + O(logn) = O(logn)
T(n) = n* S(n) = 0(nlogn)
- 由于歸并排序總是平均分割子序列,所以最好,最壞,平均時間復雜度都是O(nlogn)
常見的遞推式與復雜度
2.5 快速排序
- 執行流程
① 從序列中選擇一個軸點元素,一般每次選擇0位置的元素為軸點元素
② 利用軸點將序列分割成2個子序列,將小于軸點元素的元素放在軸點前面(左側)
將大于軸點元素的元素放在軸點的后面(右側)
③ 對子序列進行①②操作知道不能再分割(子序列中只剩下1個元素)
代碼實現
private void sort(int begin, int end) {
//至少要用2個元素
if (end - begin < 2) return;
int middle = privotIndex(begin,end);
sort(begin,middle);
sort(middle + 1, end);
}
private int privotIndex(int begin, int end) {
T privot = array[begin];
end --;//end指向最后一個元素
while (begin < end) {
while (begin < end) {
if (cmp(privot, array[end]) < 0) {
end --;
} else {
array[begin ++] = array[end];
break;
}
}
while (begin < end) {
if (cmp(privot, array[begin]) > 0) {
begin ++;
} else {
array[end --] = array[begin];
break;
}
}
}
array[begin] = pivot;
return begin;
}