在本文中,我们将对元素的数组排序问题做一些简单的归纳。为简单起见,在接下来的例子中假设数组只包含整数,虽然更复杂的结构显然也是可能的。本文的大部分内容,我们还假设整个排序工作能够在主存中完成。因此,元素的个数相对来说比较小(小于10^6)。当然,不能在主存中完成而必须在磁盘或磁带上完成的排序也相当重要。这种类型的排序叫做外部排序(external sorting),将在本文末尾进行讨论。
我们描述的算法都将是可以互换的,每个算法都将接收一个含有元素的数组和一个包含元素个数的整数。
我们将假设N是传递到我们的排序历程中的元素个数,它已经被检查过,是合法的。按照C的约定,对于所有的排序,数据都将在位置0处开始。
我们还假设“<”和“>”运算符存在,他们可以用于将相容的序放到输入中。除赋值运算符外,这两种运算是仅有的允许对输入数据进行的操作。在这些条件下的排序叫做基于比较的排序。
插入排序
最简单的排序算法之一是插入排序(insertion sort),插入排序由N-1趟(pass)排序组成。对于P = 1 趟到 P = N - 1 趟,插入排序保证从位置 0 到位置 P 上的元素为已排序状态。插入排序利用了这样的事实:对位置 P 处元素排序,则位置 0 到位置 P - 1上的元素都是已排过序的。
例:
void insertionSort (ElementType A[ ], int N)
{
int j, P;
ElementType Tmp;
for (P = 1; P < N; P++) {
Tmp = A[P];
for (j = P; j > 0 && A[j - 1] > Tmp; j--) {
A[j] = A[j - 1];
A[j - 1] = Tmp;
}
}
}
//php版
private function InsertSort(array $arr=[]){
$count = count($arr);
for($i=1;$i<$count;$i++){
$t = $arr[$i];
$j = $i-1;
while ($j>=0 && $arr[$j]>$t){
$arr[$j+1] = $arr[$j];
$j--;
}
$arr[$j+1] = $t;
}
return $arr;
}
分析:
由于嵌套循环的每一个都花费 N 次迭代,因此插入排序的时间复杂度为 O(N^2),而且这个界是精准的,因为以反序输入可以到达该界。另一方面,如果输入数据已预先排好序,那么运行时间为 O(N),因为内层for循环的检测总是立即判定不成立而终止。
选择排序
选择排序也是一种非常简单的排序算法,由 N - 1 趟循环组成。选择排序的原理是:遍历数组,将数组中最小的值换到第一个,然后再遍历数组剩余元素,将剩余元素中最小的值换到数组的第二个位置,以此类推…(对于P = 1 趟到 P = N - 1 趟,选择排序仍然保证从位置 0 到位置 P 上的元素为已排序状态。)
例:
void selectionSort (ElementType A[ ], int N)
{
int j, P;
ElementType Tmp;
for (P = 0; P < N - 1; P++) {
Tmp = A[P];
for (j = P + 1; j < N; j++) {
if (Tmp > A[j]) {
Tmp = A[j];
}
}
A[P] = Tmp;
}
}
//PHP版
private function selectSort (array $arr) : array
{
$count = count($arr);
for ($i = 0; $i < $count; $i++){
$k = $i;
for($j = $i + 1; $j < $count; $j++){
if ($arr[$j] < $arr[$k]){
$k = $j;
}
}
if ($i != $k){
$t = $arr[$i];
$arr[$i] = $arr[$k];
$arr[$k] = $t;
}
}
return $arr;
}
分析:由于嵌套循环的每一个都花费 N 次迭代,因此插入排序的时间复杂度总是 O(N^2),而且这个界是精准的,因为以反序输入可以到达该界。
冒泡排序
另一种非常简单的排序算法叫冒泡排序,由 N - 1 趟循环组成。冒泡排序的原理是:比较两个相邻的元素,将值大的元素交换到右边。(对于P = 1 趟到 P = N - 1 趟,每进行一趟排序,就会少比较一次,因为每进行一趟排序都会找出一个较大值。)
例:
void BubbleSort (ElementType A[ ], int N)
{
int j, P;
ElementType Tmp;
for (P = 0; P < N - 1; P++) {
for (j = 0; j < N - 1 - P; j++) {
if (A[j] > A[j+1]) {
Tmp = A[j];
A[j] = A[j+1];
A[j+1] = Tmp;
}
}
}
}
//PHP版
function bubbleSort($numbers) {
$cnt = count($numbers);
for ($i = 0; $i < $cnt - 1; $i++) {
for ($j = 0; $j < $cnt - $i - 1; $j++) {
if ($numbers[$j] > $numbers[$j + 1]) {
$temp = $numbers[$j];
$numbers[$j] = $numbers[$j + 1];
$numbers[$j + 1] = $temp;
}
}
}
return $numbers;
}
分析:时间复杂度为 O(N^2)。第一趟比较之后,数组中最大的那个数排到了最后,第二趟排序的时候,只需要比较除了最后一个数以外的其他的数,同样也能找出一个最大的数排在参与第二趟比较的数后面,第三趟比较的时候,只需要比较除了最后两个数以外的其他的数,以此类推……也就是说,每进行一趟比较,下一趟就少比较一次,一定程度上减少了算法的量。
希尔排序
希尔排序是冲破二次时间屏障的第一批算法,又称“缩小增量排序”。是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。它通过比较相距一定间隔的元素来工作;希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止(希尔排序的思想是使数组中任意间隔为h的元素都是有序的。)
例:
//根据当前增量进行插入排序
void shellInsert(int array[],int n,int dk)
{
int i,j,temp;
for(i=dk;i=i%dk)&&array[j]>temp;j-=dk)//比较与记录后移同时进行
array[j+dk]=array[j];
if(j!=i-dk)
array[j+dk]=temp;//插入
}
}
//计算Hibbard增量
int dkHibbard(int t,int k)
{
return (int)(pow(2,t-k+1)-1);
}
//希尔排序
void shellSort(int array[],int n,int t)
{
void shellInsert(int array[],int n,int dk);
int i;
for(i=1;i<=t;i++)
shellInsert(array,n,dkHibbard(t,i));
}
//此写法便于理解,实际应用时应将上述三个函数写成一个函数。
//PHP版
function shell_sort(&$arr) {
if(!is_array($arr)) return;
$n = count($arr);
for ($gap = floor($n/2); $gap > 0; $gap = floor($gap/=2)) {
for($i = $gap; $i < $n; ++$i) {
for($j = $i - $gap; $j >= 0 && $arr[$j + $gap] < $arr[$j]; $j -= $gap) {
$temp = $arr[$j];
$arr[$j] = $arr[$j + $gap];
$arr[$j + $gap] = $temp;
}
}
}
}
分析:
初始时,假设有一个大小为 10 的无序序列。
(1)在第一趟排序中,我们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组。
(2)接下来,按照直接插入排序的方法对每个组进行排序。
在第二趟排序中,我们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组。
(3)按照直接插入排序的方法对每个组进行排序。
(4)在第三趟排序中,再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组。
(5)按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。
堆排序
(堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。)
例:
#include
#include
void swap(int* a, int* b)
{
int temp = *b;
*b = *a;
*a = temp;
}
void max_heapify(int arr[], int start, int end)
{
//建立父节点指标和子节点指标
int dad = start;
int son = dad * 2 + 1;
while (son <= end) //若子节点指标在范围内才做比较
{
if (son + 1 <= end && arr[son] < arr[son + 1])
//先比较两个子节点大小,选择最大的
son++;
if (arr[dad] > arr[son]) //如果父节点大於子节点代表调整完毕,直接跳出函数
return;
else //否则交换父子内容再继续子节点和孙节点比较
{
swap(&arr[dad], &arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
//主例程
void heap_sort(int arr[], int len)
{
int i;
//初始化,i从最後一个父节点开始调整
for (i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
//先将第一个元素和已排好元素前一位做交换,再重新调整,直到排序完毕
for (i = len - 1; i > 0; i--)
{
swap(&arr[0], &arr[i]);
max_heapify(arr, 0, i - 1);
}
}
分析:
如图:(图片来源:https://github.com/hustcc/JS-Sorting-Algorithm)
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序是一种稳定的排序方法。
例:
void Merge(int sourceArr[],int tempArr[], int startIndex, int midIndex, int endIndex)
{
int i = startIndex, j=midIndex+1, k = startIndex;
while(i!=midIndex+1 && j!=endIndex+1)
{
if(sourceArr[i] > sourceArr[j])
tempArr[k++] = sourceArr[j++];
else
tempArr[k++] = sourceArr[i++];
}
while(i != midIndex+1)
tempArr[k++] = sourceArr[i++];
while(j != endIndex+1)
tempArr[k++] = sourceArr[j++];
for(i=startIndex; i<=endIndex; i++)
sourceArr[i] = tempArr[i];
}
//内部使用递归
void MergeSort(int sourceArr[], int tempArr[], int startIndex, int endIndex)
{
int midIndex;
if(startIndex < endIndex)
{
midIndex = startIndex + (endIndex-startIndex) / 2;//避免溢出int
MergeSort(sourceArr, tempArr, startIndex, midIndex);
MergeSort(sourceArr, tempArr, midIndex+1, endIndex);
Merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
}
}
/*** PHP版 ****/
//merge函数将指定的两个有序数组(arr1,arr2)合并并且排序
//我们可以找到第三个数组,然后依次从两个数组的开始取数据哪个数据小就先取哪个的,然后删除掉刚刚取过///的数据
function al_merge($arrA,$arrB)
{
$arrC = array();
while(count($arrA) && count($arrB)){
//这里不断的判断哪个值小,就将小的值给到arrC,但是到最后肯定要剩下几个值,
//不是剩下arrA里面的就是剩下arrB里面的而且这几个有序的值,肯定比arrC里面所有的值都大所以使用
$arrC[] = $arrA['0'] < $arrB['0'] ? array_shift($arrA) : array_shift($arrB);
}
return array_merge($arrC, $arrA, $arrB);
}
//归并排序主程序
function al_merge_sort($arr){
$len = count($arr);
if($len <= 1)
return $arr;//递归结束条件,到达这步的时候,数组就只剩下一个元素了,也就是分离了数组
$mid = intval($len/2);//取数组中间
$left_arr = array_slice($arr, 0, $mid);//拆分数组0-mid这部分给左边left_arr
$right_arr = array_slice($arr, $mid);//拆分数组mid-末尾这部分给右边right_arr
$left_arr = al_merge_sort($left_arr);//左边拆分完后开始递归合并往上走
$right_arr = al_merge_sort($right_arr);//右边拆分完毕开始递归往上走
$arr = al_merge($left_arr, $right_arr);//合并两个数组,继续递归
return $arr;
}
分析:归并排序是速度仅次于快速排序的一种算法。虽然归并排序的运行时间是O(N logN),但是它很难用于主存排序,主要问题在于合并两个排序的表需要线性附加内存,在整个算法中还要花费将数据拷贝到临时数组再拷贝回来这样一些附加的工作,其结果严重放慢了排序的速度。不过,这种合并的思想被大量用于外部排序算法中,详见下文 ’外部排序‘ 部分
快速排序
描述:正如他的名字一样,快速排序是在实践中最快的已知排序算法,他的平均运行时间是O(NlogN)。该算法之所以特别快,主要是由于非常精炼和高度优化的内部循环。它的最坏情形的性能为O(N^2).。不过对于很小的数组(N ≤ 20),快速排序不如插入排序好,不仅如此,因为快速排序是递归的,所以这样的情形还经常发生。通常的解决办法是对于小数组不递归地使用快速排序,而使用诸如插入排序这样的对小数组有效的排序算法。一种好的截至范围是 N = 10.
例:
ElementType
Median3(ElementType A[], int Left, int Right)
{
int Center = (Left + Right) / 2;
if (A[Left] > A[Center])
Swap(&A[Left],&A[Center]);
if (A[Left] > A[Right])
Swap(&A[Left],&A[Right]);
if (A[Center] > A[Right])
Swap(&A[Center],&A[Right]);
/** invariant: A[Left] ≤ A[Center] ≤ A[Right]*/
Swap(&A[Center],&A[Right - 1]); /** hide Pivot 防止越界*/
return A[Right - 1];
}
#define Cutoff(3)
Void
Qsort(ElementType A[], int Left, int Right)
{
int i, j;
ElementType Pivot;
if (Left + Cutoff <= Right)
{
Pivot = Median3(A, Left, Right);
i = Left; j = Right - 1;
for( ; ; ) {
while (A[++i] < Pivot){}
while (A[--j] < Pivot){}
if (i < j)
Swap(&A[i], &A[j]);
else
break;
}
Swap(&A[i], &A[Right - 1]); /** restore Pivot */
Qsort(A, Left, i - 1);
Qsort(A, i+1, Right);
} else {
InsertionSort(A + Left, Right - Left + 1); /** 使用前文的插入排序*/
}
}
//PHP版
private function QuickSort (array $arr) : array
{
$count = count($arr);
if ($count < 2){
return $arr;
}
$k = $arr[0]; //选择第一个元素作为基准点,实际应用中应该避免,详见下文 '选取枢纽元' 部分
$left = [];
$right = [];
for ( $i = 1; $i < $count; $i++){
if ($arr[$i] < $k){
array_push($left,$arr[$i]);
}else{
array_push($right,$arr[$i]);
}
}
$left = $this->QuickSort($left);
$right = $this->QuickSort($right);
return array_merge($left,[$k],$right);
}
分析:像归并排序一样,快速排序也是一种分治的递归算法。将数组S排序的基本算法由下列简单的四步组成:
1.如果S中元素个数是0或1,则返回。
2.取S中任一元素v,称之为枢纽元(pivot)。[枢纽元会影响算法的排序时间,详见下文解释]
3.将S - {v}(S中其余元素)分成两个不相交的集合(大于等于v的集合、小于等于v的集合)
4.返回{quicksort(S1)后,继随v,继而quicksort(S2)}。
选取枢纽元
枢纽元的选择会影响到算法排序的时间,所以一个合理的枢纽元是快速排序不可或缺的部分。最理想的情况是每一次都选择到集合的中位数,但往往这是不现实的,因为计算集合的中位数需要时间。虽然无论选择哪个数作为枢纽元都能完成排序工作,但是有些选择显然更优: