搞定PHP面試 - 常見排序算法及PHP實現

常見排序算法及PHP實現

全文代碼使用PHP7.2語法編寫

流程圖生成工具:https://visualgo.net

0. 五種基礎排序算法對比

五種基礎排序算法對比

1. 冒泡排序(Bubble Sort)

冒泡排序 是一種交換排序,它的基本思想是:對待排序記錄從後往前(逆序)進行多遍掃描,當發現相鄰兩條記錄的次序與排序要求的規則不符時,就將這兩個記錄進行交換。這樣,值較小的記錄將逐漸從後面向前移動,就像氣泡在水中向上浮一樣。

算法描述

假設需要排序的記錄有 n 個,其值保存在數組 A 中,使用冒泡排序法,需對數組 A 進行 n-1 次掃描,完成排序操作。具體過程如下:

  1. 將 A[n-1] 與 A[n] 進行比較,若 A[n] < A[n-1] ,則交換兩元系的位置。
  2. 修改數組下標,使需要比較的兩個元素爲 A[n-1] 和 A[n-2] ,重複步驟(1),對這兩個元素進行比較。重複這個過程,直到對 A[1] 和 A[0] 進行比較完爲止。完成第1遍掃描。
  3. 經過第1遍掃描後,最小的元素已經像氣泡一樣“浮”到最上面,即位於元素 A[0] 中了。接下來重複前面的步驟,進行第2遍掃描,只是掃描結束位置到 A[2] 與 A[1] 進行比較完爲止(因爲A[0]中已經是最小的數據,不用再進行比較)。
  4. 通過 n-1 遍掃描,前 n-1 個數都已經排序完成,最後一個元素 A[n] 肯定就是最大的數了。至此,完成排序操作。

冒泡排序

代碼實現

/**
 * 冒泡排序
 * @param array $arr
 */
function bubbleSort(array &$arr) : void
{
    $length = count($arr);

    // 外層循環,從數組首部開始,每完成一次循環,可確定 $arr[$i] 位置的元素
    for ($i = 0; $i < $length; $i++){

        // 內層循環,$j 從後往前循環
        for ($j = $length - 1; $j > $i; $j--) {

            // 若前面的值大於後面的值,則互換位置
            if ($arr[$j] < $arr[$j - 1]) {
                // 互換數組兩個位置的值
                [$arr[$j], $arr[$j - 1]] = [$arr[$j - 1], $arr[$j]];
            }
        }
    }
}

2. 選擇排序(Selection Sort)

選擇排序是通過 n-i 次關鍵字間的比較,從 n-i+1 個記錄中選出關鍵字最小的記錄,並和第 i ( 1 <= i <= n ) 個記錄交換。

算法描述

  1. 維護數組中最小的前 n 個元素的已排序序列。
  2. 每次從剩餘未排序的元素中選取最小的元素,將其放在已排序序列的後面,作爲序列的第 n+1 個記元素。
  3. 以空序列作爲排序工作的開始,直到未排序的序列裏只剩一個元素時(它必然爲最大),只需直接將其放在已排序的記錄之後,整個排序就完成了。

選擇排序

代碼實現

/**
 * 選擇排序
 * @param array $arr
 */
function selectionSort(array &$arr) : void
{
    $length = count($arr);

    // 外層循環,從數組首部開始,每完成一次循環,可確定一個元素的位置
    for ($i = 0; $i < $length - 1; $i++) {
        // 選定的最小值的索引
        $minIdx = $i;
        // 從 $i + 1 位開始循環,判斷當前選定的元素是否是當次循環的最小值
        for ($j = $i + 1; $j < $length; $j++) {
            // 若出現比選定的值還小的值,則替換最小值的索引
            if ($arr[$minIdx] > $arr[$j]) {
                $minIdx = $j;
            }
        }
        // 互換數組兩個位置的值
        [$arr[$i], $arr[$minIdx]] = [$arr[$minIdx], $arr[$i]];
    }
}
/**
 * 選擇排序 - 方法2
 * @param array $arr
 */
function selectionSort2(array &$arr) : void
{
    $length = count($arr);
    
    // 外層循環,從數組首部開始,每完成一次循環,依次確定數組元素的位置
    for ($i = 0; $i < $length; $i++) {
        // 從 $i + 1 位開始循環,依次判定 $arr[$i] 與 $arr[$j] 的大小
        for ($j = $i + 1; $j < $length; $j++) {
            // 若 $arr[$i] 比 $arr[$j] 大,則互換兩個元素的位置
            if ($arr[$i] > $arr[$j]) {
                // 互換數組兩個位置的值
                [$arr[$j], $arr[$i]] = [$arr[$i], $arr[$j]];
            }
        }
    }
}

3. 插入排序(Insertion Sort)

插入排序 是通過構建有序序列,從未排序數據中選擇一個元素,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在從後向前掃描過程中,需要把已排序元素逐個向後移動,爲最新元素提供插入空間。

算法描述

  1. 對於第1個元素,因爲沒有比較,將其作爲已經有序的序列。
  2. 從數組中獲取下一個元素,在已經排序的元素序列中從後向前掃描,並進行判斷。
  3. 若排序序列的元素大於新元素,則將該元素向後移動一位。
  4. 重複步驟(3),直到在已排序的元素中找到小於或者等於新元素的元素,將新元素插入到該元素的後面。
  5. 重複步驟(2) ~ (4),直到完成排序。

插入排序

代碼實現

/**
 * 插入排序
 * @param array $arr
 */
function insertionSort(array &$arr) : void
{
    $length = count($arr);

    // 從數組首部開始排序,每完成一次循環,可確定一個元素的位置
    for ($i = 0; $i < $length - 1; $i++) {
        // 內層循環從 $i + 1 個元素開始,一位一位向前比較
        // 若前面的值比自己大,則替換,直到前面的值比自己小了,停止循環
        for ($j = $i + 1; $j > 0; $j--) {
            if ($arr[$j] >= $arr[$j - 1]) {
                break;
            }
            [[$arr[$j], $arr[$j - 1]]] = [[$arr[$j - 1], $arr[$j]]];
        }

    }
}
/**
 * 插入排序 - 方法2
 * @param array $arr
 */
function insertionSort2(array &$arr) : void
{
    $length = count($arr);

    // 從數組首部開始排序,每完成一次循環,可確定一個元素的位置
    for ($i = 0; $i < $length - 1; $i++) {
        // 從第二個元素開始,選擇固定位置的值作爲基準值
        $currentVal = $arr[$i + 1];
        // 初始鍵位於選定值的前一個位置
        $preIdx = $i;

        // 拿基準值一步一步向前比較,直到基準值比前面的值小,則兩值互換位置
        while ($preIdx >= 0 && $currentVal < $arr[$preIdx]) {
            $arr[$preIdx + 1] = $arr[$preIdx];
            $arr[$preIdx] = $currentVal;
            $preIdx--;
        }

    }
}

4. 快速排序(Quick Sort)

快速排序是通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。

算法描述

快速排序使用分治策略來把待排序數據序列分爲兩個子序列,具體步驟如下:

  1. 從數列中挑出一個元素,以該元素爲“基準”。
  2. 掃描一遍數列,將所有比“基準”小的元素排在基準前面,所有比“基準”大的元素排在基準後面。
  3. 通過遞歸,將各子序列劃分爲更小的序列,直到把小於基準值元素的子數列和大於基誰值元素的子數列排序。

快速排序

代碼實現

/**
 * 快速排序
 * @param $arr
 */
function quickSort(& $arr) : void
{
    $length = count($arr);

    // 若數組爲空,則不需要運行
    if ($length <= 1) {
        return;
    }

    $middle = $arr[0]; // 選定一箇中間值

    $left = []; // 接收小於中間值
    $right = [];// 接收大於中間值

    // 循環比較
    for ($i = 1; $i < $length; $i++) {

        if ($middle < $arr[$i]) {
            // 大於中間值
            $right[] = $arr[$i];
        } else {
            // 小於或等於中間值
            $left[] = $arr[$i];
        }
    }

    // 遞歸排序劃分好的左右兩邊
    quickSort($left);
    quickSort($right);

    $arr = array_merge($left, [$middle], $right);
}

5. 歸併排序(Merge Sort)

算法描述

歸併是一種典型的序列操作,其工作是把兩個或更多有序序列合併爲一個有序序列。基於歸併的思想也可以實現排序,稱爲歸併排序。基本方法如下:

  1. 初始時,把待排序序列中的 n 個元素看成 n 個有序子序列(因爲只有1個元素的序列總是排好序的),每個子序列的長度均爲1。
  2. 把序列組裏的有序子序列兩兩歸併,每完成一論歸併,序列組裏的序列個數減半,每個子序列的長度加倍。
  3. 對加長的有序子序列重複上面的操作,最終得到一個長度爲 n 的有序序列。

這種歸併方法也稱爲簡單的二路歸併排序,其中每次操作都是把兩個有序序列合併爲一個有序序列。也可考慮三路歸併或更多路的歸併。

歸併排序

代碼實現

/**
 * 歸併排序
 * @param array $arr
 * @return array
 */
function mergeSort(array $arr)
{
    // 計算數組長度,若長度不大於1,則不需要排序
    $length = count($arr);
    if ($length <= 1) {
        return $arr;
    }

    // 獲取數組中間位置的索引
    $midIdx = floor($length / 2);

    // 把數組從中間拆分成左右兩部分
    $left = mergeSort(array_slice($arr, 0, $midIdx));
    $right = mergeSort(array_slice($arr, $midIdx));

    // 合併兩部分,同時進行排序
    return merge($left, $right);
}

/**
 * 合併數組,同時進行排序
 * @param array $left
 * @param array $right
 * @return array
 */
function merge(array $left, array $right)
{
    // 分別計算左右兩數組的長度
    $lLength = count($left);
    $rLength = count($right);

    // 左右兩數組的索引
    $l = $r = 0;

    $lists = [];

    // 只有左右兩數組都未遍歷完成時,纔有必要繼續遍歷
    // 當其中一個數組的元素遍歷完成,說明另一個數組中未遍歷過的值比遍歷過的值都大
    while ($l < $lLength && $r < $rLength) {
        // 比較 $left[$l] 和 $right[$r],取其中較小的值加入到 $lists 數組中
        if ($left[$l] < $right[$r]) {
            $lists[] = $left[$l];
            $l++;
        } else {
            $lists[] = $right[$r];
            $r++;
        }
    }

    // 合併 $lists 和 $left、$right 中剩餘的元素
    return array_merge($lists, array_slice($left, $l), array_slice($right, $r));
}
/**
 * 合併數組,同時進行排序 - 方法2
 * @param array $left
 * @param array $right
 * @return array
 */
function merge2(array $left, array $right)
{
    // 分別計算左右兩數組的長度
    $lLength = count($left);
    $rLength = count($right);

    // 左右兩數組的索引
    $l = $r = 0;

    $lists = [];

    // 只有左右兩數組都未遍歷完成時,纔有必要繼續遍歷
    // 當其中一個數組的元素遍歷完成,說明另一個數組中未遍歷過的值比遍歷過的值都大
    while ($l < $lLength && $r < $rLength) {
        // 比較 $left[$l] 和 $right[$r],取其中較小的值加入到 $lists 數組中
        if ($left[$l] < $right[$r]) {
            $lists[] = $left[$l];
            // PHP 中 unset 掉數組中的元素後,其他元素的鍵名不變
            unset($left[$l]);
            $l++;
        } else {
            $lists[] = $right[$r];
            unset($right[$r]);
            $r++;
        }
    }

    // 合併 $lists 和 $left、$right 中剩餘的元素
    return array_merge($lists, $left, $right);
}
/**
 * 合併數組,同時進行排序 - 方法3
 * @param array $left
 * @param array $right
 * @return array
 */
function merge3(array $left, array $right)
{
    // 分別計算左右兩數組的長度
    $lLength = count($left);
    $rLength = count($right);

    $lists = [];

    // 只有左右兩數組都未遍歷完成時,纔有必要繼續遍歷
    // 當其中一個數組的元素遍歷完成,說明另一個數組中未遍歷過的值比遍歷過的值都大
    while ($lLength > 0 && $rLength > 0) {
        // 比較 $left[$l] 和 $right[$r],取其中較小的值加入到 $lists 數組中
        if ($left[0] < $right[0]) {
            $lists[] = $left[0];
            // PHP中 unset 掉數組中的元素後,其他元素的鍵名不變
            unset($left[0]);
            // 重建數組索引,始終讓比較的值在第一位
            $left = array_values($left);
            $lLength--;
        } else {
            $lists[] = $right[0];
            unset($right[0]);
            $right = array_values($right);
            $rLength--;
        }
    }

    // 合併 $lists 和 $left、$right 中剩餘的元素
    return array_merge($lists, $left, $right);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章