算法导论(3) 快速排序、计数排序、基数排序

本文详细介绍了四种排序算法:快速排序、计数排序、基数排序和桶排序。重点讲解了每种算法的工作原理、实现方式及时间复杂度分析。快速排序采用分治策略,性能通常最佳;计数排序适用于特定范围内的整数排序;基数排序通过比较每一位达到排序目的;桶排序则适用于数据分布均匀的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.快速排序
快速排序也采用了分治思想:

  • 分解:数组A[p..r]划分为两个可能为空的子数组A[p..q-1]和A[q+1..r],前者均小于等于A[q],后者均大于等于A[q]。
  • 解决:递归调用,对两个子数组继续采用快速排序的方法。
  • 合并:数组是原址操作,不需要合并。

因为看过好多次快速排序的实现方式,因此学习了多种快速排序的实现方式,不过算法导论上的方法QuickSort2更容易理解以及记忆。其实只要会一种可以熟练应用就可以了。

template<class T>
void QuickSort(T a[], int left, int right)
{
    T key = a[(left + right) / 2];
    T temp=0;
    int l, r;
    l = left;
    r = right;
    while (l< r)
    {
        while (a[r] > key){
            r--;
        }
        while(a[l]<key){
            l++;
        }
        if (l<=r){
            temp = a[r];
            a[r] = a[l];
            a[l] = temp;
            r--;
            l++;
        }
        if (l == r){
            l++;
        }
    }
        if (left < r)
        {
            QuickSort(a, left, l-1);
        }
        if (right >l)
        {
            QuickSort(a, r+1, right);
        }

}

template<class T>
void QuickSort2(T a[], int left, int right)
{
    T temp, x = a[right];
    int i,j,q;
    if (left < right){
        i = left - 1;
        for (j = left; j < right; j++)
        {
            if (a[j] <= x)
            {
                i += 1;
                temp = a[j];
                a[j] = a[i];
                a[i] = temp;
            }
        }
        temp = a[right];
        a[right] = a[i + 1];
        a[i + 1] = temp;
        q = i + 1;
        QuickSort2(a, left, q - 1);
        QuickSort2(a, q + 1, right);
    }
}
template<class T>
void QuickSort3(T a[], int left, int right)
{
    T temp, x = a[left];
    int i, j, q;
    if (left < right){
        i = right+ 1;
        for (j = right; j > left; j--)
        {
            if (a[j] >= x)
            {
                i -= 1;
                temp = a[j];
                a[j] = a[i];
                a[i] = temp;
            }
        }
        temp = a[left];
        a[left] = a[i -1];
        a[i - 1] = temp;
        q = i - 1;
        QuickSort2(a, left, q - 1);
        QuickSort2(a, q + 1, right);
    }
}

template<class T>
void QuickSort4(T a[], int left, int right)
{
    if (left >= right)
    {
        return;
    }
    int first = left;
    int last = right;
    int key = a[first];

    while (first < last)
    {
        while (first < last && a[last] >= key)
        {
            --last;
        }

        a[first] = a[last];

        while (first < last && a[first] <= key)
        {
            ++first;
        }

        a[last] = a[first];

    }
    a[first] = key;
    QuickSort4(a, left, first - 1);
    QuickSort4(a, first + 1, right);
}

快速排序的运行时间依赖于划分是否平衡,如果平衡,其性能与归并排序一样,如果不平衡,其性能接近于插入排序。最坏情况下,子问题分别包含n-1和0个元素,其运行时间时O(n^2),性能还不如插入排序,因为假如数组已有序,插入排序O(n),而快速排序还是O(n^2)。最好情况下为Θ(nlog⁡n)。
快速排序的平均运行时间更接近与最好情况,而非最坏情况,即使每次划分的比例都是9:1,看似很不平衡,但其时间复杂度仍是O(nlog⁡n)。
目前为止这几种排序方法都是通过元素之间的比较进行排序的,因此被称为比较排序,在最坏情况下,任何比较排序算法都需要做Ω(nlog⁡n)次比较。因此归并排序和堆排序是渐进最优的。但快速排序通常是实际应用排序中最好的选择,因此其平均性能非常好。期望时间复杂度为Θ(nlog⁡n),并且Θ(nlog⁡n)其中隐含的常数因子非常小,能够进行原址排序。

2.计数排序
假设n个输入元素中每一个都是在0到k区间的一个整数,其中k为某个整数。计数排序的基本思想是,对一个输入元素x,确定小于x的元素个数,利用这一信息将其放在对应的输出位置上。

void CountSort(int a[], int b[], int n,int k)
{
    vector<int> c;
    c.resize(k);
    for (int i = 0; i < k; i++)
    {
        c[i] = 0;
    }
    //记录某个元素出现了几次
    for (int j = 0; j < n; j++)
    {
        c[a[j]] += 1;
    }
    //累加,记录某个元素应该出现在哪个位置上
    for (int i = 1; i < k; i++)
    {
        c[i] += c[i - 1];
    }
    //将原数组中元素放到对应的位置上,c数组减一是为处理有相同值的情况
    for (int j = n-1; j>=0; j--)
    {
        b[c[a[j]]-1] = a[j];
        c[a[j]] -= 1;
    }
}

总得时间代价是Θ(k+n),实际工作中,如果k=O(n)时,一般会采用计数排序,运行时间为Θ(n)。计数排序是稳定的,具有相同值的元素在输入输出数组中的相对位置时一样的。个人感觉上只能用来处理整数排序,而且运行时间要取决于k值的大小。

3.基数排序
通过对数值一位一位(不是二进制是十进制)比较而达到排序的目的,通常采用按最低有效位来排序。一位数的排序算法必须是稳定的。感觉上也是针对整数的排序,但应该可以改写成针对其他类型数据的排序,时间复杂度上也可以达到线性的时间代价。

//求数据的最大位数
int maxbit(int data[], int n) 
{
    //保存最大的位数
    int d = 1; 
    int p = 10;
    for (int i = 0; i < n; i++)
    {
        while (data[i] >= p)
        {
            p *= 10;
            d++;
        }
    }
    return d;
}
//基数排序
void radixsort(int data[], int n) 
{
    int d = maxbit(data, n);
    int *tmp = new int[n];
    //计数器
    int *count = new int[10];
    int i, j, k;
    int radix = 1;
    //进行d次排序
    for (i = 1; i <= d; i++) 
    {
        //计数排序
        for (j = 0; j < 10; j++)

            count[j] = 0; 
        for (j = 0; j < n; j++)
        {
            k = (data[j] / radix) % 10; 
            count[k]++;
        }
        for (j = 1; j < 10; j++)
            count[j] = count[j - 1] + count[j]; 
        for (j = n - 1; j >= 0; j--) 
        {
            k = (data[j] / radix) % 10;
            tmp[count[k] - 1] = data[j];
            count[k]--;
        }
        for (j = 0; j < n; j++) 
            data[j] = tmp[j];
        radix = radix * 10;
    }
    delete[]tmp;
    delete[]count;
}

4.桶排序
算法导论中讲述的是假设要排序的数组均匀、独立分布在[0,1)之间。然后将[0,1)区间划分为n个大小相同的区间,称为桶,然后先对每个桶中的元素进行排序,最后按照次序将桶中元素列出即可。时间复杂度也是线性时间。
感觉类似于基数排序,但是是按照最高位进行排序,然后把相同最高位的分别进行排序,最后按照次序列出。这也说明基数排序也是可以修改为对浮点数等类型进行排序。这两种算法的基本思想就是一位一位进行排序,而书中针对一位数排序时,基数排序是让采用稳定的排序方法,而桶排序采用的是插入排序。这几种排序方法均可以在线性时间内完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值