九大排序算法

       排序算法一直都是各个公司年年都会拿出的面试题,作为初入社会寻找工作的小白,掌握各个排序算法是及其重要的。

1、冒泡排序(时间复杂度O(N^2),空间复杂度O(1),稳定)

       先从简单的来,冒泡排序是学习C语言是必学的一种排序算法,它的思想也很容易理解。
       给一组数据,双重循环。用一个指针指向第一个数据(且向后移动),另一个指针移动,拿第二个指针指向的数据挨个与第一个指针指向的数据比较,将大的数据向后交换。

void BubbleSort(int *a, int n)
{
    assert(a);

    for (int i = 0; i < n; i++)
    {
        for (int j = i; j < n; j++)
        {
            if (a[i] > a[j])
                swap(a[i], a[j]);
        }
    }
}

2、选择排序(时间复杂度O(N^2),空间复杂度(O(1)),不稳定)

       双重循环,内循环负责找出最小的那个数据,找到之后与外循环指向的数据进行交换。

void SelectSort(int *a, int n)
{
    assert(a);

    for (int i = 0; i < n - 1; i++)
    {
        int minIndex = i;
        for (int j = i + 1; j < n; j++)
        {
            if (a[j] < a[minIndex])
                minIndex = j;
        }
        if (minIndex != i)
        swap(a[i], a[minIndex]);
    }
}

       同时找出最大与最小的数据,分别与数组两头数据交换。

void SelectSort(int *a, int n)
{
    assert(a);
    int i = 0;
    int j = n - 1;
    for (; i <= j; i++, j--)
    {
        int begin = i ;
        int end = j - 1;
        int minIndex = i;
        int maxIndex = j;
        while (begin <= j || end >= i)
        {
            if (a[begin] < a[minIndex])
                minIndex = begin;
            if (a[end] > a[maxIndex])
                maxIndex = end;
            begin++;
            end--;
        }
        swap(a[i], a[minIndex]);
        swap(a[j], a[maxIndex]);
    }
}

3、直接插入排序(时间复杂度O(N^2),空间复杂度(O(1)),稳定)

       双重循环。数据被分为两部分,始终保持前面部分有序。
举一个栗子:

       2        2        4        9        3        6        34        7        2        5

2 2 4 9还是有序的,当end指向9,tmp指向3时。此时需要将3向前移动,挨个与9 4 2比较。我们需要将3插入2与4之间,依次将9 4向后移动一位,因为3已结保存在tmp中,不用担心被覆盖。最后将3放在原本4的位置就好了。

void InsertSort(int *a, int n)
{
    assert(a);

    for (int i = 0; i < n - 1; i++)
    {
        int end = i;
        int tmp = a[i + 1];

        while (end >= 0)
        {
            if (tmp < a[end])
            {
                a[end + 1] = a[end];
                end--;
            }
            else
                break;
        }
        a[end + 1] = tmp;
    }
}

4、希尔排序(时间复杂度O(N)~O(N^2),空间复杂度(O(1)),不稳定)

       与直接插入排序原理一致。定义一个gap,直接插入排序相当于gap等于1的情况,即每次比较的范围为gap,gap逐渐缩小,直到变为1。

void ShellSort(int *a, int n)
{
    assert(a);

    int gap = n;
    while (gap > 1)
    {
        gap = gap / 3 + 1;
        for (int i = 0; i < n - gap; i++)
        {
            int end = i;
            int tmp = a[i + gap];

            while (end >= 0)
            {
                if (tmp < a[end])
                {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else
                    break;
            }
            a[end + gap] = tmp;
        }
    }
}

5、堆排序(时间复杂度O(NlogN),空间复杂度(O(1)),不稳定)

       将存放数据的数组看做一个完全二叉树的结构。将大的数据向下调整,并逐渐缩小调整范围。

这里写图片描述

void _AdjustDown(int *a, int i, int n)
{
    assert(a);
    int parent = i;
    int child = parent * 2 + 1;
    while (child < n)
    {
        if (child + 1 < n && a[child] < a[child + 1])
        {
            child++;
        }
        if (a[child] > a[parent])
            swap(a[child], a[parent]);
        parent = child;
        child = child* 2 + 1;
    }
}

void HeapSort(int *a, int n)
{
    assert(a);
    for (int i = n-1; i >= 0; i--)
    {
        _AdjustDown(a, i, n);
    }

    for (int i = n-1; i > 0; --i)
    {
        swap(a[0], a[i]);
        _AdjustDown(a, 0, i);
    }
}

6、快速排序(时间复杂度O(NlogN),空间复杂度(O(logN)),不稳定)

       快速排序这里介绍三种方法:左右指针法、前后指针法和挖坑法。
       统一都用到三数取中法,取到数组中第一个数据、最后一个数据和中间的数据,比较大小,将中间的数作为key。

下面都只介绍递归一次的过程。

int GetMidIndex(int *a, int left, int right)
{
    int mid = left + (right - left) >> 1;
    if (a[left] < a[mid])
    {
        if (a[mid] < a[right])
        {
            return mid;
        }
        else if (a[left] < a[right])
        {
            return right;
        }
        else
            return left;
    }
    else
    {
        if (a[right] < a[mid])
        {
            return mid;
        }
        else if (a[left]>a[right])
        {
            return right;
        }
        else
            return left;
    }
}
6.1 左右指针法

       递归。同时找出比key大和比key小的数,将小的数放在key的左边,大的数放在key的右边,递归缩小范围继续之前的操作。

这里写图片描述

int PartSort1(int *a, int left, int right)
{
    int mid = GetMidIndex(a, left, right);
    swap(a[mid], a[right]);
    int key = a[right];
    int begin = left;
    int end = right;

    while (begin < end)
    {
        while (begin < end && a[begin] < key)
            ++begin;
        while (begin<end && a[end]  >key)
            --end;
        swap(a[begin], a[end]);
    }
    swap(a[begin], key);
    return begin;
}
6.2 前后指针法

这里写图片描述

       cur先走,直到cur指向的数据小于key,++prev如果不等于cur则交换prev与cur指向的数据,这样一来就将晓得数据换到了前面。到cur走到数组末尾之后,递归缩小范围继续交换。

int PartSort3(int *a, int left, int right)
{
    int mid = GetMidIndex(a, left, right);
    swap(a[mid], a[right]);
    int key = a[right];
    int cur = left;
    int prev = left - 1;

    while (cur < right)
    {
        if (a[cur] < key && ++prev != cur)
        {
            swap(a[cur], a[prev]);
        }
        cur++;
    }
    return prev;
}
6.3 挖坑法

       挖坑法就是不断找新坑的过程。先begin从左到右寻找比key大的值,找到之后begin指向的值为新坑(即key值),再end从右到左寻找比key小的值,找到之后end指向的值为新坑。最后将最初的key值赋值给begin指向的值(此时begin和end必定相等)。

这里写图片描述

int PartSort2(int *a, int left, int right)
{
    int mid = GetMidIndex(a, left, right);
    swap(a[mid], a[right]);
    int key = a[right];
    int begin = left;
    int end = right;

    while (begin < end)
    {
        while (begin < end && a[begin] <= key)
            ++begin;
        if (begin < end)
            a[end] = a[begin];

        while (begin < end && a[end] >= key)
            --end;
        if (begin < end)
            a[begin] = a[end];
    }
    a[begin] = key;
    return begin;
}

       函数调用过程:

void QuickSort(int *a, int left, int right)
{
    assert(a);

    if (left >= right)
        return;
    int div = PartSort1(a, left, right);
    QuickSort(a, left, div - 1);
    QuickSort(a, div + 1, right);
}

7、归并排序(时间复杂度O(NlogN),空间复杂度(O(N)),稳定)

       归并排序是建立在归并思想上的有效排序算法,主要采用的是分治法,将两个有序的序列归并为一个有序的序列的过程。
       基本思想:设置begin、end、index三个指针,分别指向第一个有序序列、第二个有序序列、记录归并结果的暂存空间tmp,合并是依次比较a[begin]和a[end]的关键字,将较大(或较小)的值复制到tmp[index]中,然后将被复制关键字的指针(begin或end)加1,且index加1.重复这个过程直到两个有序序列有一个被复制完毕,此时将剩下的序列中的数据依次复制到tmp中即可。

举一个栗子:
       假设我们有一个待排序列(14,12,15,13,11,16),我们将其分割为一个一个的有序序列,然后单个有序序列挨个归并为一个有序序列,具体分割、归并过程见下图:

这里写图片描述

void MergeArray(int *a, int left, int mid, int right, int *tmp)
{
    int index = 0;
    int begin = left;
    int end = right;
    int m = mid + 1;
    while (begin <= mid && m <= right)
    {
        if (a[begin] <= a[m])
            tmp[index++] = a[begin++];
        else
            tmp[index++] = a[m++];
    }

    while (begin <= mid)
        tmp[index++] = a[begin++];
    while (m <= right)
        tmp[index++] = a[m++];

    for (int i = 0; i < index; i++)
        a[left + i] = tmp[i];
}

void Merge(int *a, int left, int right, int *tmp)
{ 
    if (left >= right)
        return;
    int mid = (left + right) / 2;
    Merge(a, left, mid, tmp);//分割
    Merge(a, mid + 1, right, tmp);

    MergeArray(a, left, mid, right, tmp);//归并

}

void MergeSort(int *a, int n)
{
    int *p = new int[n];
    Merge(a, 0, n - 1, p);
    delete[] p;
}

8、计数排序(时间复杂度O(N),空间复杂度(O(最大数-最小数)),不稳定)

       计数排序就是利用一个数组来记录每个数出现的次数,数组的下标表示数据。
举一个栗子:

       待排序列为2 5 7 1 2 3 5 7,那么我们可以开一个大小为8的数组来存放每一个数据出现的次数。如下图:

这里写图片描述

       上面的数据小而且数据与数据之间相差的也不大,我们可以直接确定要开多大的数组,但如果你不知道给出的待排序列有多少,数据有多大这种方法就不行了。所以这里我们可以做一些优化,我们可以先找出待排序列中的最大值和最小值,然后开辟最大值-最小值这么大的数组,计数的时候减去最小值之后再看应该把哪一个下标对应的值加1。

int GetMax(int *a, int n)
{
    int Max = 0;
    for (int i = 0; i < n; i++)
    {
        if (a[i]>a[Max])
            a[Max] = a[i];
    }
    return a[Max];
}

int GetMin(int *a, int n)
{
    int Min = 0;
    for (int i = 0; i < n; i++)
    {
        if (a[i]<a[Min])
            a[Min] = a[i];
    }
    return a[Min];
}

void CountSort(int *a, int n)
{
    int Max = GetMax(a, n);
    int Min = GetMin(a, n);
    int len = Max - Min + 1;

    int *count = new int[len];
    for (int i = 0; i < len; i++)
        count[i] = 0;

    for (int i = 0; i < n; i++)
    {
        count[a[i] - Min]++;
    }

    for (int i = 0; i < len; i++)
    {
        while (count[i])
        {
            if (count[i] > 0)
                cout << i + Min << " ";
            count[i]--;
        }
    }
}

9、基数排序(时间复杂度O(N*位数),空间复杂度O(N),稳定)

       按照平日里我们比较两个数的大小,我们一定是从高位开始比较,高位相同则比较次高位,直到比较出大小。那么排序算法里面我们也可以采用这种思想来实现排序。
基数排序还分为LSD和MSD两种,LSD指从数据的低位开始比较,MSD则是从数据的高位开始比较。当数据的位数比较少时,我们可以采用LSD,位数较多时MSD的效率会更高。
       基数排序就是“分配”和“收集”的过程。
假设一组数据:
       73        22         93        43        55        14        28        65        39        81
       那么首先根据个位数的数值的大小,将这10个数据分配到对应的数组桶中,如下图

这里写图片描述

       分配结束后,根据数组桶中的顺序将数据收集出来,得到半有序的数据(个位有序)
       81        22        73        93        43        14        55        65        28        39
       同理再对十位上的数值进行以上相同的操作,如下图
这里写图片描述

       则最后收集的数据序列就是有序的序列了
       14        22        28        39        43        55        65        73        81        93

int GetDigit(int *a, int n)//获得位数
{
    int digit = 1;
    int base = 10;
    for (int i = 0; i < n; i++)
    {
        while (a[i] >= base)
        {
            ++digit;
            base *= 10;
        }
    }
    return digit;
}

void RadixSort(int *a, int n)
{
    int digit = GetDigit(a, n);
    int *bucket = new int[n];
    int base = 1;

    for (int j = 0; j < digit; j++)
    {
        int *counts = new int[10];
        for (int i = 0; i < 10; i++)
            counts[i] = 0;
        for (int i = 0; i < 10; i++)
        {
            int m = (a[i] / base) % 10;
            counts[m]++;
        }

        int *starts = new int[10];
        for (int i = 0; i < 10; i++)
            starts[i] = 0;
        for (int i = 1; i < 10; i++)
        {
            starts[i] = starts[i - 1] + counts[i - 1];
        }

        for (int i = 0; i < n; i++)
        {
            int m = (a[i] / base) % 10;
            bucket[starts[m]++] = a[i];
        }
        memcpy(a, bucket, sizeof(int)*n);
        base *= 10;
        delete[] counts;
        delete[] starts;
    }
    delete[] bucket;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值