PHP面试:尽量多的说出你知道的排序算法

预警

本文适合对于排序算法不太了解的新手同窗观看,大佬直接忽略便可。由于考虑到连贯性,因此篇幅较长。老铁们看完须要大概一个小时,可是从入门到彻底理解可能须要10个小时(哈哈哈,以我本身的经从来计算的),因此各位老铁能够先收藏下来,同步更新在Github,本文引用到的全部算法的实如今这个地址,天天抽点时间理解一个排序算法便可。php

排序和他们的类型

咱们的数据大多数状况下是未排序的,这意味着咱们须要一种方法来进行排序。咱们经过将不一样元素相互比较并提升一个元素的排名来完成排序。在大多数状况下,若是没有比较,咱们就没法决定须要排序的部分。在比较以后,咱们还须要交换元素,以便咱们能够对它们进行从新排序。良好的排序算法具备进行最少的比较和交换的特征。除此以外,还存在基于非比较的排序,这类排序不须要比较数据来进行排序。咱们将在这篇文章中为各位老铁介绍这些算法。如下是本篇文章中咱们将要讨论的一些排序算法:git

  • Bubble sort
  • Insertion sort
  • Selection sort
  • Quick sort
  • Merge sort
  • Bucket sort

以上的排序能够根据不一样的标准进行分组和分类。例如简单排序,高效排序,分发排序等。咱们如今将探讨每一个排序的实现和复杂性分析,以及它们的优缺点。github

时间空间复杂度以及稳定性

咱们先看下本文提到的各种排序算法的时间空间复杂度以及稳定性。各位老铁能够点击这里了解更多。算法

clipboard.png

冒泡排序

冒泡排序是编程世界中最常讨论的一个排序算法,大多数开发人员学习排序的第一个算法。冒泡排序是一个基于比较的排序算法,被认为是效率最低的排序算法之一。冒泡排序老是须要最大的比较次数,平均复杂度和最坏复杂度都是同样的。编程

冒泡排序中,每个待排的项目都会和剩下的项目作比较,而且在须要的时候进行交换。下面是冒泡排序的伪代码。数组

procedure bubbleSort(A: list of sortable items)
n = length(A)
for i = 0 to n inclusive do
 for j = 0 to n - 1 inclusive do
    if A[j] > A[j + 1] then
        swap(A[j + 1], A[j])
    end if
  end for
end for
end procedure
复制代码

正如咱们从前面的伪代码中看到的那样,咱们首先运行一个外循环以确保咱们迭代每一个数字,内循环确保一旦咱们指向某个项目,咱们就会将该数字与数据集合中的其余项目进行比较。下图显示了对列表中的一个项目进行排序的单次迭代。假设咱们的数据包含如下项目:20,45,93,67,10,97,52,88,33,92。第一次迭代将会是如下步骤:bash

clipboard.png

有背景颜色的项目显示的是咱们正在比较的两个项目。咱们能够看到,外部循环的第一次迭代致使最大的项目存储在列表的最顶层位置。而后继续,直到咱们遍历列表中的每一个项目。如今让咱们使用PHP实现冒泡排序算法。数据结构

咱们可使用PHP数组来表示未排序的数字列表。因为数组同时具备索引和值,咱们根据位置轻松迭代每一个项目,并将它们交换到适用的位置。app

function bubbleSort(&$arr) : void
{
	$swapped = false;
	for ($i = 0, $c = count($arr); $i < $c; $i++) {
		for ($j = 0; $j < $c - 1; $j ++) {
			if ($arr[$j + 1] < $arr[$j]) {
				list($arr[$j], $arr[$j + 1]) = array($arr[$j + 1], $arr[$j]);
			}
		}
	}
}
复制代码

冒泡排序的复杂度分析

对于第一遍,在最坏的状况下,咱们必须进行n-1比较和交换。 对于第2次遍历,在最坏的状况下,咱们须要n-2比较和交换。 因此,若是咱们一步一步地写它,那么咱们将看到:复杂度= n-1 + n-2 + ..... + 2 + 1 = n *(n-1)/ 2 = O(n2)。所以,冒泡排序的复杂性是O(n2)。 分配临时变量,交换,遍历内部循环等须要一些恒定的时间,可是咱们能够忽略它们,由于它们是不变的。如下是冒泡排序的时间复杂度表,适用于最佳,平均和最差状况:数据结构和算法

best time complexity Ω(n)
worst time complexity O(n2)
average time complexity Θ(n2)
space complexity (worst case) O(1)

尽管冒泡排序的时间复杂度是O(n2),可是咱们可使用一些改进的手段来减小排序过程当中对数据的比较和交换次数。最好的时间复杂度是O(n)是由于咱们至少要一次内部循环才能够肯定数据已是排好序的状态。

冒泡排序的改进

冒泡排序最重要的一个方面是,对于外循环中的每次迭代,都会有至少一次交换。若是没有交换,则列表已经排序。咱们能够利用它改进咱们的伪代码

procedure bubbleSort(A: list of sortable items)
    n = length(A)
    for i = 1 to n inclusive do
        swapped = false
        for j = i to n - 1 inclusive do
            if A[j] > A[j + 1] then
                swap(A[j], A[j + 1])
                swapped = true
            endif
        end for
        if swapped = false
            break
        endif
    end for
end procedure
    
复制代码

正如咱们所看到的,咱们如今为每一个迭代设置了一个标志为false,咱们指望在内部迭代中,标志将被设置为true。若是内循环完成后标志仍然为假,那么咱们能够打破外循环。

function bubbleSort(&$arr) : void {
	for ($i = 0, $c = count($arr); $i < $c; $i++) {
		$swapped = false;
		for ($j = 0; $j < $c - 1; $j++) {
			if ($arr[$j + 1] < $arr[$j]) {
				list($arr[$j], $arr[$j + 1]) = array($arr[$j + 1], $arr[$j]);
				$swapped = true;
			}
		}

		if (!$swapped) break; //没有发生交换,算法结束
	}
}
复制代码

咱们还发现,在第一次迭代中,最大项放置在数组的右侧。在第二个循环,第二大的项将位于数组右侧的第二个。咱们能够想象出来在每次迭代以后,第i个单元已经存储了已排序的项目,不须要访问该索引和 作比较。所以,咱们能够从内部迭代减小迭代次数并减小比较。这是咱们的第二个改进的伪代码

procedure bubbleSort(A: list of sortable items)
    n = length(A)
    for i = 1 to n inclusive do
        swapped = false
        for j = 1 to n - i - 1 inclusive do
            if A[j] > A[j + 1] then
                swap(A[j], A[j + 1])
                swapped = true
            endif
        end for
        if swapped = false
            break
        end if
    end for
end procedure
   
复制代码

下面的是PHP的实现

function bubbleSort(&$arr) : void {
	
	for ($i = 0, $c = count($arr); $i < $c; $i++) {
        $swapped = false;
		for ($j = 0; $j < $c - $i - 1; $j++) {
			if ($arr[$j + 1] < $arr[$j]) {
				list($arr[$j], $arr[$j + 1]) = array($arr[$j + 1], $arr[$j]);
				$swapped = true;
			}

			if (!$swapped) break; //没有发生交换,算法结束
		}
	}
}
复制代码

咱们查看代码中的内循环,惟一的区别是j <c - $i - 1;其余部分与第一次改进同样。所以,对于20、4五、9三、6七、十、9七、5二、8八、3三、92, 咱们能够很认为,在第一次迭代以后,顶部数字97将不被考虑用于第二次迭代比较。一样的状况也适用于93,将不会被考虑用于第三次迭代。

clipboard.png

咱们看看前面的图,脑海中应该立刻想到的问题是“92不是已经排序了吗?咱们是否须要再次比较全部的数字?是的,这是一个好的问题。咱们完成了内循环中的最后一次交换后能够知道在哪个位置,以后的数组已经被排序。所以,咱们能够为下一个循环设置一个界限,伪代码是这样的:

procedure bubbleSort(A: list of sortable items)
    n = length(A)
    bound = n - 1
    for i = 1 to n inclusive do
        swapped = false
        bound = 0
        for j = 1 to bound inclusive do
            if A[j] > A[j + 1] then
                swap(A[j], A[j + 1])
                swapped = true
                newbound = j
            end if
        end for
        bound = newbound
        if swapped = false
            break
        endif
    end for
end procedure
   
复制代码

这里,咱们在每一个内循环完成以后设定边界,而且确保咱们没有没必要要的迭代。下面是PHP代码:

function bubbleSort(&$arr) : void {
	$swapped = false;
    $bound = count($arr) - 1;
	for ($i = 0, $c = count($arr); $i < $c; $i++) {
		for ($j = 0; $j < $bound; $j++) {
			if ($arr[$j + 1] < $arr[$j]) {
				list($arr[$j], $arr[$j + 1]) = array($arr[$j + 1], $arr[$j]);
				$swapped = true;
				$newBound = $j;
			}
		}
		$bound = $newBound;
		if (!$swapped) break; //没有发生交换,算法结束
	}
}
复制代码

选择排序

选择排序是另外一种基于比较的排序算法,它相似于冒泡排序。最大的区别是它比冒泡排序须要更少的交换。在选择排序中,咱们首先找到数组的最小/最大项并将其放在第一位。若是咱们按降序排序,那么咱们将从数组中获取的是最大值。对于升序,咱们获取的是最小值。在第二次迭代中,咱们将找到数组的第二个最大值或最小值,并将其放在第二位。持续到咱们把每一个数字放在正确的位置。这就是所谓的选择排序,选择排序的伪代码以下:

procedure  selectionSort( A : list of sortable items)
    n = length(A)
    for i = 1 to n inclusive do
        min  =  i
        for j = i + 1 to n inclusive do
            if  A[j] < A[min] then
                min = j 
            end if
        end  for

        if min != i
            swap(a[i], a[min])
        end if
    end  for
end procedure
复制代码

看上面的算法,咱们能够发现,在外部循环中的第一次迭代以后,第一个最小项被存储在第一个位置。在第一次迭代中,咱们选择第一个项目,而后从剩下的项目(从2到n)找到最小值。咱们假设第一个项目是最小值。咱们找到另外一个最小值,咱们将标记它的位置,直到咱们扫描了剩余的列表并找到新的最小最小值。若是没有找到最小值,那么咱们的假设是正确的,这确实是最小值。以下图:

clipboard.png

正如咱们在前面的图中看到的,咱们从列表中的第一个项目开始。而后,咱们从数组的其他部分中找到最小值10。在第一次迭代结束时,咱们只交换了两个地方的值(用箭头标记)。所以,在第一次迭代结束时,咱们获得了的数组中获得最小值。而后,咱们指向下一个数字45,并开始从其位置的右侧找到下一个最小的项目,咱们从剩下的项目中找到了20(如两个箭头所示)。在第二次迭代结束时,咱们将第二个位置的值和从列表的剩余部分新找到的最小位置交换。这个操做一直持续到最后一个元素,在过程结束时,咱们获得了一个排序的列表,下面是PHP代码的实现。

function selectionSort(&$arr) {
	$count = count($arr);

	//重复元素个数-1次
	for ($j = 0; $j <= $count - 1; $j++) {
		//把第一个没有排过序的元素设置为最小值
		$min = $arr[$j];
		//遍历每个没有排过序的元素
		for ($i = $j + 1; $i < $count; $i++) {
			//若是这个值小于最小值
			if ($arr[$i] < $min) {
				//把这个元素设置为最小值
				$min = $arr[$i];
				//把最小值的位置设置为这个元素的位置
				$minPos = $i;
			}
		}
		//内循环结束把最小值和没有排过序的元素交换
		list($arr[$j], $arr[$minPos]) = [$min, $arr[$j]];
	}
	
}
复制代码

选择排序的复杂度

选择排序看起来也相似于冒泡排序,它有两个for循环,从0到n。冒泡排序和选择排序的区别在于,在最坏的状况下,选择排序使交换次数达到最大n - 1,而冒泡排序能够须要 n * n 次交换。在选择排序中,最佳状况、最坏状况和平均状况具备类似的时间复杂度。

best time complexity Ω(n2)
worst time complexity O(n2)
average time complexity Θ(n2)
space complexity (worst case) O(1)

插入排序

到目前为止,咱们已经看到了两种基于比较的排序算法。如今,咱们将探索另外一个排序算法——插入排序。与刚才看到的其余两个排序算法相比,它有最简单的实现。若是项目的数量较小,插入排序优于冒泡排序和选择排序。若是数据集很大,就像冒泡排序同样就变得效率低下。插入排序的工做原理是将数字插入到已排序列表的正确位置。它从数组的第二项开始,并判断该项是否小于当前值。若是是这样,它将项目转移,并将较小的项目存储在其正确的位置。而后,它移动到下一项,而且相同的原理继续下去,直到整个数组被排序。

procedure insertionSort(A: list of sortable items)
    n length(A)
    for i=1 to n inclusive do
        key = A[i]
        j = i - 1
        while j >= 0 and A[j] > key do
            A[j+1] = A[j]
            j--
        end while
        A[j + 1] = key
    end for
end procedure
复制代码

假如咱们有下列数组,元素是:20 45 93 67 10 97 52 88 33 92。咱们从第二个项目45开始。如今咱们将从45的左边第一个项目开始,而后到数组的开头,看看左边是否有大于45的值。因为只有20,因此不须要插入,目前两项(20, 45)被排序。如今咱们将指针移到93,从它再次开始,比较从45开始,因为45不大于93,咱们中止。如今,前三项(20, 45, 93)已排序。接下来,对于67,咱们从数字的左边开始比较。左边的第一个数字是93,它较大,因此必须移动一个位置。咱们移动93到67的位置。而后,咱们移动到它左边的下一个项目45。45小于67,不须要进一步的比较。如今,咱们先将93移动到67的位置,而后咱们插入67的到93的位置。继续如上操做直到整个数组被排序。下图说明在每一个步骤中使用插入排序的直到彻底排序过程。

clipboard.png

function insertionSort(array &$arr) {
	$len = count($arr);
	for ($i = 1; $i < $len; $i++) {
		$key = $arr[$i];
		$j = $i - 1;

		while ($j >= 0 && $arr[$j] > $key) {
			$arr[$j + 1] = $arr[$j];
			$j--;
		}
		$arr[$j + 1] = $key;
	}
}
复制代码

插入排序的复杂度

插入排序具备与冒泡排序类似的时间复杂度。与冒泡排序的区别是交换的数量远低于冒泡排序。

best time complexity Ω(n)
worst time complexity O(n2)
average time complexity Θ(n2)
space complexity (worst case) O(1)

排序中的分治思想

到目前为止,咱们已经了解了每次对完整列表进行排序的一些排序算法。咱们每次都须要应对一个比较大的数字集合。咱们能够设法使数据集合更小,从而解决这个问题。分治思想对咱们有很大帮助。用这种方法,咱们将一个问题分红两个或多个子问题或集合,而后在组合子问题的全部结果以得到最终结果。这就是所谓的分而治之方法,分而治之方法可让咱们有效地解决排序问题,并下降算法的复杂度。最流行的两种排序算法是合并排序和快速排序,它们应用分治算法对数据进行排序,所以被认为是最好的排序算法。

归并排序

正如咱们已经知道的,归并排序应用分治方法来解决排序问题,咱们用法两个过程来解决这个问题。第一个是将问题集划分为足够小的问题,以便容易地求解,而后将这些结果结合起来。咱们将用递归方法来完成分治部分。下面的图显示了如何采用分治的方法。

clipboard.png

基于前面的图像,咱们如今能够开始准备咱们的代码,它将有两个部分。

/**
 * 归并排序
 * 核心:两个有序子序列的归并(function merge)
 * 时间复杂度任何状况下都是 O(nlogn)
 * 空间复杂度 O(n)
 * 发明人: 约翰·冯·诺伊曼
 * 速度仅次于快速排序,为稳定排序算法,通常用于对整体无序,可是各子项相对有序的数列
 * 通常不用于内(内存)排序,通常用于外排序
 */

function mergeSort($arr)
{
    $lenght = count($arr); 
    if ($lenght == 1) return $arr;
    $mid = (int)($lenght / 2);

    //把待排序数组分割成两半
    $left = mergeSort(array_slice($arr, 0, $mid));
    $right = mergeSort(array_slice($arr, $mid));

    return merge($left, $right);
}

function merge(array $left, array $right)
{
    //初始化两个指针
    $leftIndex = $rightIndex = 0;
    $leftLength = count($left);
    $rightLength = count($right);
    //临时空间
    $combine = [];

    //比较两个指针所在的元素
    while ($leftIndex < $leftLength && $rightIndex < $rightLength) {
        //若是左边的元素大于右边的元素,就将右边的元素放在单独的数组,并将右指针向后移动
        if ($left[$leftIndex] > $right[$rightIndex]) {
            $combine[] = $right[$rightIndex];
            $rightIndex++;
        } else {
            //若是右边的元素大于左边的元素,就将左边的元素放在单独的数组,并将左指针向后移动
            $combine[] = $left[$leftIndex];
            $leftIndex++;
        }
    }

    //右边的数组所有都放入到了返回的数组,而后把左边数组的值放入返回的数组
    while ($leftIndex < $leftLength) {
        $combine[] = $left[$leftIndex];
        $leftIndex++;
    }

    //左边的数组所有都放入到了返回的数组,而后把右边数组的值放入返回的数组
    while ($rightIndex < $rightLength) {
        $combine[] = $right[$rightIndex];
        $rightIndex++;
    }

    return $combine;
}
复制代码

咱们划分数组,直到它达到1的大小。而后,咱们开始使用合并函数合并结果。在合并函数中,咱们有一个数组来存储合并的结果。正由于如此,合并排序实际上比咱们迄今所看到的其余算法具备更大的空间复杂度。

归并排序的复杂度

因为归并排序遵循分而治之的方法,因此咱们必须解决这两个复杂问题。对于n个大小的数组,咱们首先须要将数组分红两个部分,而后合并它们以获得n个大小的数组。咱们能够看下面的示意图

clipboard.png

解决每一层子问题须要的时间都是cn,假设一共有l层,那么总的时间复杂度会是ln。由于一共有logn + 1 层,那么结果就是 cn(logn + 1)。咱们删除常数阶和线性阶,最后的结果能够得出时间复杂度就是O(nlog2n)。

best time complexity Ω(nlogn)
worst time complexity O(nlogn)
average time complexity Θ(nlogn)
space complexity (worst case) O(n)

快速排序

快速排序是对冒泡排序的一种改进。它的基本思想是:经过一趟排序将要排序的数据分割成独立的两部分,其中一部分的全部数据都比另一部分的全部数据都要小,而后再按此方法对这两部分数据分别进行快速排序,整个排序过程能够递归进行,以此达到整个数据变成有序序列。

function qSort(array &$arr, int $p, int $r) {
	if ($p < $r) {
		$q = partition($arr, $p, $r);
		qSort($arr, $p, $q);
		qSort($arr, $q + 1, $r);
	}
}

function partition(array &$arr, int $p, int $r) {
	$pivot = $arr[$p];
	$i = $p - 1;
	$j = $r + 1;

	while (true) {
		do {
			$i++;
		} while ($arr[$i] < $pivot);

		do {
			$j--;
		} while ($arr[$j] > $pivot);

		if ($i < $j) {
			list($arr[$i], $arr[$j]) = [$arr[$j], $arr[$i]];
		} else {
			return $j;
		}

	}
}
复制代码

快速排序的复杂度

最坏状况下快速排序具备与冒泡排序相同的时间复杂度,pivot的选取很是重要。下面是快速排序的复杂度分析。

best time complexity Ω(nlogn)
worst time complexity O(n2)
average time complexity Θ(nlogn)
space complexity (worst case) O(logn)

对于快速排序的优化,有兴趣的老铁能够点击这里查看。

桶排序

桶排序 (Bucket sort)或所谓的箱排序,工做的原理是将数组分到有限数量的桶里。每一个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。

/** * 桶排序 * 不是一种基于比较的排序 * T(N, M) = O(M + N) N是带排序的数据的个数,M是数据值的数量 * 当 M >> N 时,须要考虑使用基数排序 */

function bucketSort(array &$data) {
    $bucketLen = max($data) - min($data) + 1;
    $bucket = array_fill(0, $bucketLen, []);

    for ($i = 0; $i < count($data); $i++) {
        array_push($bucket[$data[$i] - min($data)], $data[$i]);
    }

    $k = 0;

    for ($i = 0; $i < $bucketLen; $i++) {
        $currentBucketLen = count($bucket[$i]);

        for ($j = 0; $j < $currentBucketLen; $j++) {
            $data[$k] = $bucket[$i][$j];
            $k++;
        }
    }
}
复制代码

基数排序的PHP实现,有兴趣的同窗一样能够访问这个页面来查看。

快速排序的复杂度

桶排序的时间复杂度优于其余基于比较的排序算法。如下是桶排序的复杂性

best time complexity Ω(n+k)
worst time complexity O(n2)
average time complexity Θ(n+k)
space complexity (worst case) O(n)

PHP内置的排序算法

PHP有丰富的预约义函数库,也包含不一样的排序函数。它有不一样的功能来排序数组中的项目,你能够选择按值仍是按键/索引进行排序。在排序时,咱们还能够保持数组值与它们各自的键的关联。下面是这些函数的总结

函数名 功能
sort() 升序排列数组。value/key关联不保留
rsort() 按反向/降序排序数组。index/key关联不保留
asort() 在保持索引关联的同时排序数组
arsort() 对数组进行反向排序并维护索引关联
ksort() 按关键字排序数组。它保持数据相关性的关键。这对于关联数组是有用的
krsort() 按顺序对数组按键排序
natsort() 使用天然顺序算法对数组进行排序,并保持value/key关联
natcasesort() 使用不区分大小写的“天然顺序”算法对数组进行排序,并保持value/key关联。
usort() 使用用户定义的比较函数按值对数组进行排序,而且不维护value/key关联。第二个参数是用于比较的可调用函数
uksort() 使用用户定义的比较函数按键对数组进行排序,而且不维护value/key关联。第二个参数是用于比较的可调用函数
uasort() 使用用户定义的比较函数按值对数组进行排序,而且维护value/key关联。第二个参数是用于比较的可调用函数

对于sort()、rsort()、ksort()、krsort()、asort()以及 arsort()下面的常量可使用

  • SORT_REGULAR - 正常比较单元(不改变类型)
  • SORT_NUMERIC - 单元被做为数字来比较
  • SORT_STRING - 单元被做为字符串来比较
  • SORT_LOCALE_STRING - 根据当前的区域(locale)设置来把单元看成字符串比较,能够用 setlocale() 来改变。
  • SORT_NATURAL - 和 natsort() 相似对每一个单元以“天然的顺序”对字符串进行排序。 PHP 5.4.0 中新增的。
  • SORT_FLAG_CASE - 可以与 SORT_STRING 或 SORT_NATURAL 合并(OR 位运算),不区分大小写排序字符串。

完整内容

本文引用到的全部算法的实如今这个地址,主要内容是使用PHP语法总结基础的数据结构和算法。欢迎各位老铁收藏~