文章目錄
排序方法 | 平均情況 | 最好情況 | 最壞情況 | 輔助空間 | 穩定性 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 穩定 |
簡單選擇排序 | O(n²) | O(n²) | O(n²) | O(1) | 穩定 |
直接插入排序 | O(n²) | O(n) | O(n²) | O(1) | 穩定 |
希爾排序 | O(nlogn)~O(n²) | O(n^1.3) | O(n²) | O(1) | 不穩定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不穩定 |
歸併排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 穩定 |
快速排序 | O(nlogn) | O(nlogn) | O(n²) | O(logn)~O(n) | 不穩定 |
1.冒泡排序
O(n²):從後往前稱之爲冒泡,兩兩比較相鄰記錄的關鍵字,如果反序則交換,直到沒有反序的記錄爲止,穩定排序
最好的情況:待排序表本身有序,O(n)
最壞的情況:待排序表爲逆序, O(n²)
//交換函數
function swap(&$arr,$i,$j){
$tmp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $tmp;
}
//主冒泡排序函數
function BubbleSort(&$arr){
$len = count($arr);
//冒泡排序優化:如果某一次循環,沒發生元素的交換,則整個數組已經有序
$flag = true;
for($i = 0;$i<$len-1 && $flag;++$i){
$flag = false;
for($j = $len-2;$j>=$i;--$j){
if($arr[$j] > $arr[$j+1]){
swap($arr,$j,$j+1);
$flag = true;
}
}
}
}
$arr = [9,1,5,8,3,0,7,4,6,2];
BubbleSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
2.簡單選擇排序
O(n²):通過n-i次關鍵字間的比較,從n-i+1個記錄中選出關鍵字最小的記錄,並和第i(1<=i<=n)個記錄交換之,先比較再交換,爲穩定排序
對於比較次數來說,最好的情況與最壞的情況一樣,都是需要比較n(n-1)/2 ,O(n²)
對於比較次數來說,最好的情況爲0次,即初始已排好序
最壞的情況爲n-1次,即初始逆序
//交換函數
function swap(&$arr,$i,$j){
$tmp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $tmp;
}
//主選擇排序函數
function SelectSort(&$arr){
$len = count($arr);
for($i=0;$i<$len-1;++$i){
//記錄第$i個元素後的元素的最小值下標
$min = $i;
for($j=$i+1;$j<$len;++$j){
if($arr[$j] < $arr[$min]){
$min = $j;
}
}
if($min != $i)
swap($arr,$min,$i);
}
}
$arr = [9,1,5,8,3,0,7,4,6,2];
SelectSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
3.直接插入排序
O(n²):從無序表中依次取出元素,把它插入到有序表的合適的位置,爲穩定排序
最好的情況:初始已排好序
最壞的情況:初始逆序
//主插入排序函數
function InsertSort(&$arr){
$len = count($arr);
for($i = 1;$i<$len;++$i){
$tmp = $arr[$i];//設置哨兵
//直到找到比哨兵小的元素,插入在它之前
for($j = $i-1;$j>=0 && $arr[$j]>$tmp;--$j)
$arr[$j+1] = $arr[$j];
$arr[$j+1] = $tmp;
}
}
$arr = [9,1,5,8,3,0,7,4,6,2];
InsertSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
4.希爾排序
O(nlogn~n²):在記錄數少的時候或者記錄本身基本有序的情況下,直接插入排序才比較高效。希爾排序是在插入排序的基礎上進行改進,分割成若干個子序列,此時子序列待排序的記錄數減少,然後在這些子序列內部分別進行直接插入排序,當整個序列基本有序時,再對全體記錄進行一次直接插入排序。爲不穩定排序。
採用跳躍分割的策略:將相距某個“增量”的記錄組成一個子序列,這樣才能保證在子序列內分別進行直接插入排序後得到的結果是基本有序的而不是局部有序的
//主希爾排序函數
function ShellSort(&$arr){
$len = count($arr);
$inc = $len;//增量
do{
$inc = ceil($inc/3);
for($i = $inc;$i<$len;++$i){
$tmp = $arr[$i]; //設置哨兵
//元素後移
for($j = $i-$inc;$j>=0 && $tmp<$arr[$j];$j-=$inc)
$arr[$j+$inc] = $arr[$j];
$arr[$j+$inc] = $tmp;
}
}while ($inc>1); //增量爲1時,停止循環
}
$arr = [9,1,5,8,3,0,7,4,6,2];
ShellSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
5.堆排序
O(nlogn):將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根結點。將它移走(其實就是將其與堆數組的末尾元素交換,此時末尾元素就是最大值),然後將剩餘的n-1個序列重新構造成一個堆,這樣就會得到n個元素中的次大值。如此反覆執行,便能得到一個有序序列。爲不穩定排序
最好,最壞,平均的時間複雜度,均爲O(nlogn)
由於初始構建堆所需的比較次數較多,堆排序並不適合待排序序列個數較少的情況
//交換函數
function swap(&$arr,$i,$j){
$tmp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $tmp;
}
//堆調整函數:保證堆始終是大頂堆
function HeapAdjust(&$arr,$s,$m){
$tmp = $arr[$s];
for($j = 2*$s;$j <= $m;$j *= 2){
if($j<$m && $arr[$j] < $arr[$j+1])
++$j;
if($tmp >= $arr[$j])
break;
$arr[$s] = $arr[$j];
$s = $j;
}
$arr[$s] = $tmp;
}
//主堆排序函數
function HeapSort(&$arr){
//堆排序與數組下標關聯很重要,因此不能有0下標,所以先插入頭元素,之後再去除即可
array_unshift($arr, 0);
$len = count($arr)-1;
//初始構造大頂堆
for($i = floor($len/2);$i>0;--$i){
HeapAdjust($arr,$i,$len);
}
//依次交換堆頂
for($i = $len;$i>1;--$i){
swap($arr,1,$i);
HeapAdjust($arr,1,$i-1);
}
array_shift($arr);
}
$arr = [9,1,5,8,3,0,7,4,6,2];
HeapSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
6.歸併排序
O(n²):利用歸併的思想實現的排序方法。假設初始序列含有n個記錄,則可以看成是n個有序的子序列,每個子序列的長度爲1,然後兩兩歸併,得到floor(n/2)個長度爲2或1的有序子序列;再兩兩歸併,……,如此重複,直至得到一個長度爲n的有序序列爲止,這種排序叫做2路歸併排序。爲穩定排序
最好,最壞,平均的時間複雜度,均爲O(nlogn)
(1)自頂向下(遞歸)
//數組合並重排過程
function Merge(&$arr,$start,$mid,$end){
$i = $k = $start;
$j = $mid+1;
$tmp = [];
while ($i != $mid+1 && $j != $end+1) {
if($arr[$i] >= $arr[$j])
$tmp[$k++] = $arr[$j++];
else
$tmp[$k++] = $arr[$i++];
}
//剩餘的數組,塞進臨時數組裏
while ($i != $mid+1)
$tmp[$k++] = $arr[$i++];
while ($j != $end+1)
$tmp[$k++] = $arr[$j++];
//重新複製原數組
for($i = $start;$i<=$end;++$i){
$arr[$i] = $tmp[$i];
}
}
//遞歸歸併
function MSort(&$arr,$start,$end){
//當子序列長度爲1時,$start = $end,不需再分組
if($start < $end){
$mid = floor(($start + $end)/2);
MSort($arr,$start,$mid);
MSort($arr,$mid+1,$end);
Merge($arr,$start,$mid,$end);
}
}
//主歸併排序函數
function MergeSort(&$arr){
$start = 0;
$end = count($arr)-1;
MSort($arr,$start,$end);
}
$arr = [9,1,5,8,3,0,7,4,6,2];
MergeSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
(2)自底向上(非遞歸)
非遞歸的迭代方法,避免了遞歸時深度爲log2n的棧空間,空間複雜度爲O(n),並且避免遞歸也在時間性能上有一定的提升,所以在使用歸併排序時,儘量考慮使用非遞歸方法
//數組合並重排過程
function Merge(&$arr,$start,$mid,$end){
$i = $k = $start;
$j = $mid+1;
$tmp = [];
while ($i != $mid+1 && $j != $end+1) {
if($arr[$i] >= $arr[$j])
$tmp[$k++] = $arr[$j++];
else
$tmp[$k++] = $arr[$i++];
}
//剩餘的數組,塞進臨時數組裏
while ($i != $mid+1)
$tmp[$k++] = $arr[$i++];
while ($j != $end+1)
$tmp[$k++] = $arr[$j++];
//重新複製原數組
for($i = $start;$i<=$end;++$i){
$arr[$i] = $tmp[$i];
}
}
//主歸併排序函數
function MergeSort2(&$arr){
$len = count($arr);
for($jump = 1;$jump<$len;$jump*=2){
$flag = 1; //標記位:如果沒有進行下方循環,則當前僅剩兩個子序列,進行最後的合併重拍
for($i = 0; ($i+2*$jump-1)<$len; $i+=2*$jump){
$flag = 0;
/**
* $i是下一個開始交換的點的下標, $i+2*$jump-1是結束的下標
* jump 1 2 4 8
* $start = $i; 0 2 4 6 8 10x 0 4 8 0 8 0
* $mid = $i+$jump-1; 0 2 4 6 8 10x 1 5 3
* $end = $i+2*$jump-1 1 3 5 7 9 3 7 7 x
*/
Merge($arr,$i,$i+$jump-1,$i+2*$jump-1);
}
if($flag){
Merge($arr,$i,$i+$jump-1,$len-1);
}
}
}
$arr = [9,1,5,8,3,0,7,4,6,2];
MergeSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
PHP的單鏈表的插入排序和歸併排序解法:【PHP解法==LeetCode鏈表類型(鏈表排序)】147.對鏈表進行插入排序 && 148.排序鏈表
7.快速排序
O(nlogn):通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。爲不穩定排序
最好的情況:、Partition每次都將序列劃分的很均勻
最壞的情況:待排序的序列爲正序或倒序,每次劃分只得到一個比上一次劃分少一個記錄的子序列
PS:Partition函數最終$low和$high都會指向分好的中間節點 圖解Partition
//交換函數
function swap(&$arr,$i,$j){
$tmp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $tmp;
}
//分區函數
function Partition(&$arr,$low,$high){
$pivotkey = $arr[$low];
while ($low<$high) {
while ($low<$high && $arr[$high] >= $pivotkey) {
$high--;
}
swap($arr,$low,$high);
while ($low<$high && $arr[$low] < $pivotkey) {
$low++;
}
swap($arr,$low,$high);
}
return $low;
}
//快速排序遞歸函數
function Qsort(&$arr,$low,$high){
if($low<$high){
$pivot = Partition($arr,$low,$high);
Qsort($arr,$low,$pivot-1);
Qsort($arr,$pivot+1,$high);
}
}
//快速排序主函數
function QuickSort(&$arr){
$low = 0;
$high = count($arr) - 1;
Qsort($arr,$low,$high);
}
$arr = [9,1,5,8,3,0,7,4,6,2];
QuickSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
快速排序優化方案
(1)優化選取中樞
三數取中法:取三個關鍵字先進行排序,將中間數作爲樞軸,一般是取左端,右端和中間三個數,也可以隨機選擇
//分區函數
function Partition(&$arr,$low,$high){
/***** 三數取中法 *****/
//start
$mid = $low + floor(($high - $low)/2); //計算數組中間的元素的下標
if($arr[$low] > $arr[$high])
swap($arr,$low,$high); //交換左右端元素,保證左端較小
if($arr[$mid] > $arr[$high])
swap($arr,$mid,$high); //交換中右端元素,保證中端較小
if($arr[$low] > $arr[$mid])
swap($arr,$mid,$high); //交換左中端元素,保證左端較小
//end
$pivotkey = $arr[$low]; //此時$arr[$low]已經爲三個關鍵字的中間值
while ($low<$high) {
while ($low<$high && $arr[$high] >= $pivotkey) {
$high--;
}
swap($arr,$low,$high);
while ($low<$high && $arr[$low] < $pivotkey) {
$low++;
}
swap($arr,$low,$high);
}
return $low;
}
也可以進行九數取中
(2)優化不必要的交換
swap只作替換作用,最終當low和high會合,即找到了樞軸位置,此時再將$pivotkey賦值給$arr[$low]
當中少了多次交換的操作,在性能上又得到了部分的提高
//分區函數
function Partition(&$arr,$low,$high){
/***** 此處省略三數取中法 *****/
$pivotkey = $arr[$low]; //樞軸值
while ($low<$high) {
while ($low<$high && $arr[$high] >= $pivotkey) {
$high--;
}
$arr[$low] = $arr[$high];
while ($low<$high && $arr[$low] < $pivotkey) {
$low++;
}
$arr[$high] = $arr[$low];
}
$arr[$low] = $pivotkey;
return $low;
}
(3)優化小數組時的排序方案
如果數組非常小時,直接插入排序比快速排序來的更好,直接插入排序是簡單排序算法中性能最好的
//快速排序遞歸函數
function Qsort(&$arr,$low,$high){
$max_length_insert_sort = 7; //定義數組長度閾值
if(($high-$low)>$max_length_insert_sort){
//進行快速排序
$pivot = Partition($arr,$low,$high);
Qsort($arr,$low,$pivot-1);
Qsort($arr,$pivot+1,$high);
}else{
//進行直接插入排序
InsertSort($arr);
}
}
(4)優化遞歸操作
遞歸對性能是有一定影響的,QSort函數在其尾部有兩次遞歸操作,每次遞歸都會耗費一定的棧空間。如果能減少,將會大大提高性能
//快速排序遞歸函數
function Qsort(&$arr,$low,$high){
$max_length_insert_sort = 7; //定義數組長度閾值
if(($high-$low)>$max_length_insert_sort){
while ($low<$high) {
$pivot = Partition($arr,$low,$high);
Qsort($arr,$low,$pivot-1);
$low = $pivot+1;
}
}else{
InsertSort($arr);
}
}
改成while後,因爲第一次遞歸以後,變量low就沒有用處了,所以可以將pivot+1賦值給low
再循環一次,此時Partition(&$arr,$low,$high)的執行效果等同於Qsort(&$arr,$pivot+1,$high)
結果相同,但因爲採用了迭代而不是遞歸的方法,可以縮減堆棧深度,從而提高整體性能
本博文參考書籍《大話數據結構》