《数据结构和算法分析 C语言描述》笔记#7 排序算法

排序

这一章将围绕着数组元素的排序展开。为了减轻负担,所有的元素都为整数(其实前面几章也都是这样做的)。

  • 内部排序和外部排序

  • 内部排序:整个排序工作可以在主存中完成,元素个数一般小于 1 0 6 10^{6} 106

  • 外部排序:整个排序工作不能再主存中完成而必须在磁带或磁盘中完成。

另外,为了后面的叙述方面,我们假定这个数列的长度为 N N N

插入排序InsertionSort

插入排序的原理很简单。首先我们给出一个位置 P P P,假设在位置 P P P之前的数列都已经处于已排序状态,这样位置 P P P处的元素只需要在前面的数列找到自己合适的的位置即可。总的来说,我们要跑 N − 1 N-1 N1次,位置 0 0 0上的元素不需要排序,因为对于一个元素的数列来说,有序无序没有意义。

/**
书上给的样例,因为P前的元素都已经排序,那么我们只需要保证有足够的位置让P处的元素插入即可。
这样一来,我们仅仅目标位置右边到P位置的元素向右移动一个位置便能够得到足够的位置供P处元素插入。
**/
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]=tmp;
    }
}
一些简单排序算法的下界
  • 逆序(inversion)

    数组中具有 i < j i<j i<j但是 A [ i ] > A [ j ] A[i]>A[j] A[i]>A[j]的序偶1 ( A [ i ] , A [ j ] A[i],A[j] A[i],A[j])。一个排过序(升序)的数组没有逆序。

    我们假设不存在重复元素,并且所有的排列都是有可能的。可以得到下列两个定理:

    1. N N N个互异数的数组的平均逆序数是 N ( N − 1 ) / 4 N(N-1)/4 N(N1)/4

      证明可见

    2. 通过交换相邻元素进行排序的任何算法平均都需要 Ω ( N 2 ) \Omega (N^2) Ω(N2)时间。

      这个定理建立在前一个定理的基础上,我们交换的次数实际上就是逆序的个数。

希尔排序ShellSort

希尔排序名字源于它的发明者Donald Shell。它是冲破二次时间屏障的第一批算法之一,拥有亚二次时间界。由于它的工作原理,又被称为缩小增量排序(diminishing increment sort)。

在希尔排序中,我们取一系列的数值作为增量(increment),这一系列数值被称作增量序列(increment sequence)。增量序列由 h 1 h_1 h1开始逐步增大(这不一定是一个递增数列),也就是这样的序列: h 1 , h 2 . . . h t h_1,h_2...h_t h1,h2...ht。其中 h 1 h_1 h1等于1,在实际排序过程中,增量是由大到小最后到1的。

希尔排序是插入排序的更好版本,在这个排序过程中,我们将数组元素分为 h t h_t ht个小组,然后各个小组内进行插入排序;接着增量 h t h_t ht减少为 h t − 1 h_{t-1} ht1,重复前面的操作。在这个过程中,数组的有序性不断提高,最后我们就能够得到一个排序好的数组。

在这里插入图片描述

如图,希尔排序的一次排序中,进行了多次插入排序。

关于增量序列的选择

Shell建议的序列为: h t = N / 2 , h k = h k + 1 / 2 h_t = N/2, h_k=h_{k+1}/2 ht=N/2,hk=hk+1/2。这个序列很流行,但是并不好。在实际上,我们应该尽量减少增量间的公因子,最好的情况是增量互素。

堆排序heapSort

堆排序是建立在我们前几章提到的优先队列的基础上的,它利用了二叉堆的结构性和堆序性。

#define LeftChild(i) (2*(i)+1)

void percDown(elementType A[], int i, int N)
{
    int child;
    elementType tmp;

    for (tmp = A[i];LeftChild(i) < N; i = child)
    {
        child = LeftChild(i);
        if( child != N - 1 && A[child + 1] > A[child])
            child++;
        if ( tmp < A[child])
            A[i] = A[child];
        else
            break;
        
    }
    A[i] = tmp;
    
}

void heapSort(elementType A[], int N)
{
    int i;
    for (i = N/2; i >= 0; i--)//构建堆
        percDown(A, i , N);
    for (i = N-1; i > 0; i--)//删除最大值,但实际上并没有移除
    {
        swap(&A[0], &A[i]);
        percDown(A, 0, i);
    }
    
}
归并排序mergeSort

归并排序将两个已排序的合并到第三个表中。这涉及到三个数组和三个计数器(指针),前两个数组为长度相同的输入数组,后一个数组为输出数组。指向两个输入数组的指针指向的元素下标相同,选中相同下标的两个元素中较小的那个填入输出数组。

实际应用中,我们将一个数组分为前后两个输入数组进行归并排序(分治思想),见以下例程:


void Merge(elementType A[], elementType tmpArray[], int lPos, int rPos, int rightEnd)
{
    int i,leftEnd,numElements,tmpPos;

    leftEnd = rPos-1;
    tmpPos = lPos;
    numElements = rightEnd - lPos + 1;

    while(lPos <= leftEnd && rPos <= rightEnd)
    {
        if(A[lPos] <= A[rPos])
            tmpArray[tmpPos++] = A[lPos++];
        else
            tmpArray[tmpPos++] = A[rPos++];
    }

    while (lPos <= leftEnd)
    {
        tmpArray[tmpPos++] = A[lPos++];
    }
    
    while (rPos <= rightEnd)
    {
        tmpArray[tmpPos++] = A[rPos++];
    }

    for (i = 0; i < numElements; i++, rightEnd--)
    {
        A[rightEnd] = tmpArray[rightEnd];
    }
    
}

void mSort(elementType A[], elementType tmpArray[], int left, int right)
{
    int center;

    if (left < right)
    {
        center = (left + right)/2;
        mSort(A, tmpArray, left, center);
        mSort(A, tmpArray, center + 1, right);
        Merge(A, tmpArray, left, center + 1, right);
    }
    
}

void mergeSort(elementType A[], int N)
{
    elementType *tmpArray;

    tmpArray = malloc(N*sizeof(elementType));

    if(tmpArray != NULL)
    {
        mSort(A, tmpArray, 0, N-1);
        free(tmpArray);
    }
    else
        fatalError("No space available for tmpArray");
}
快速排序quickSort

快速排序是实践中已知的最快的排序算法,它是一种采用分治思想的递归算法。但值得一提的是,对于小数组的处理,快速排序并不是最好的选择,插入排序会更好。

快速排序可以简单地分为四个步骤:

  1. 如果S中元素个数是0或1,返回。
  2. 取S中任意一个元素 v v v,作为枢纽元(pivot)
  3. 将S中剩下的元素分为两个不相交的集合 S 1 = { x ∈ S ∣ x ≤ v } 和 S 2 = { x ∈ S ∣ x ≥ v } S_1=\{x\in S | x\leq v \}和S_2=\{x\in S | x\geq v \} S1={xSxv}S2={xSxv}
  4. 返回quickSort( S 1 S_1 S1),继而 v v v,然后quickSort( S 2 S_2 S2)。
枢纽元的选取

我们的输入很有可能是预排序的,为了保证 S 1 , S 2 S_1,S_2 S1,S2大小尽量相等,我们不要选择第一或者第二个元素作为枢纽元,这可能会让两边严重不平衡。

随机选取枢纽元是一个不错的想法,这个办法有一定的安全性,毕竟随机的枢纽元不会接连不断地产生劣质的分割。

还有一个更好的选择,三数中值分割法(Median-of-Three Partitioning)

我们首先选取第最左边的元素,中间位置的元素,和最右边的元素,对它们进行排序,选择中值作为枢纽元。这种方案不仅能一定消除坏情况,还能减少快排的运行时间。

如何分割

分割的策略选择是一个问题。原书使用了两个下标 i , j i,j i,j i i i从数组的第一个开始往后移动, j j j从数组的最后一个向前移动。在移动的过程中,当 i i i移动到大于枢纽元的元素就停下,当 j j j移动到小于枢纽元的元素就停下,这样一来,当 i , j i,j i,j都停下时, i i i指向一个大元素, j j j指向一个小元素,这个时候如果 i i i还在 j j j的左边,那么交换两者指向的元素。

我们不断重复这个过程,直到 i , j i,j i,j指向同一个元素。

然后我们进行分割的最后一步,将枢纽元与 i i i指向的元素交换位置。

这样一来我们便能保证枢纽元左边的元素都比它小,右边的元素比它大。

现在我们该开始考虑如何处理那些等于枢纽元的关键字。书上的建议是当遇到这种情况时,我们应该停下 i , j i,j i,j,直接进入下一个步骤。

不要用快速排序对小数组排序

当数组的长度 N ≤ 20 N\leq 20 N20时,选择插入排序会比快速排序更好。

例程
#define elementType int
#define Cuttoff ( 3 )

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]);

    swap(&A[center], &A[right-1]);
    return A[right-1];
    
}

void qSort(elementType A[], int left, int right)
{
    int i,j;
    elementType pivot;

    if(left + Cuttoff <= 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]);

        qSort(A, left, i - 1);
        qSort(A, i + 1, right);
        
    }
    else
        insertionSort(A + left, right - left + 1);
}

void quickSort(elementType A[], int N)
{
    qSort(A, 0, N - 1);
}
快速选择quickSelect

我们可以把快排的思想应用到解决选择问题上,即查找第 k k k个最小/大元。这只需要简单的将两次递归减少为一次递归(因为前文提到的分割策略)。如果你希望这个过程不破坏原来的排序,只需要在拷贝上进行这个操作。

针对大型结构

非基本类型,如结构体等等,我们可以通过比较它们某个特定的域来排序。但不需要交换他们的位置,只需要交换指向这些结构的指针即可。

决策树decision tree

在这里插入图片描述


  1. 序偶: ordered pair,用()包裹的一对数字,如(3,6),出自离散数学。 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值