内部排序总结

1.插入排序

直接插入排序

每次从无序表中取出第一个元素,在有序表中从后往前找到合适的位置,并将该位置之后的元素往后移动一个元素,在该位置上插入元素,则插入后有序表增加一个元素,无序表减少一个元素。

代码如下:

template<class T>
void insertSort(vector<T> &nums) {
    for (size_t i = 1; i <nums.size(); ++i)
    {
        if (nums[i] < nums[i - 1])
        {
            T temp = nums[i];
            size_t j;
            for (j = i - 1; j >= 0 && nums[j] > temp; --j)
            {
                nums[j + 1] = nums[j];
            }
            nums[j + 1] = temp;
        }
    }
}

直接插入排序的平均时间复杂度最差时间复杂度均为 O(N^2)待排序线性表越有序,执行效率越好。其空间复杂度为 O(1)
直接插入排序的过程中,不需要改变相等数值元素的位置,所以它是稳定的算法。


希尔排序

希尔排序是一种基于插入排序的快速的排序算法。对于大规模乱序数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移动到另一端。例如,如果主键最小的元素正好在线性表的尾部,要将它挪到正确的位置就需要N-1次移动。

希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对线性表的局部进行排序,使线性表中任意间隔为 step (也称作步长)的元素都是有序的,并最终用插入排序将局部有序的数组排序。

实现希尔排序的一种方法是:

  • 将整个待排元素序列分割成若干个子序列(由相隔step的元素组成)分别进行直接插入排序
  • 依次缩减增量再进行排序,直到整个序列中的元素基本有序(增量足够小)
  • 对全体元素进行一次直接插入排序。

希尔排序的实现就转化为了一个类似于插入排序但使用不同增量的过程。希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都较短排序之后,子数组都是部分有序的,这两种情况都很适合插入排序。

那么如何选择希尔排序的步长?要回答这个问题并不简单。算法的性能不仅取决于 step,还取决于 step 之间的数学性质,比如它们的公因子等。有很多各种关于不同的步长序列的研究,但都无法证明某个是最好的。

代码如下:

template<class T>
void shellSort(vector<T> &nums) {
    int len = nums.size();
    for (int step = len / 2; step > 0; step /= 2)
    {
        if (step % 2 == 0)
        {
            --step;
        }
        for (int i = step; i < len; ++i)
        {
            T temp = nums[i];
            int j;
            for (j = i - step; j >= 0 && nums[j] > temp; j -= step)
            {
                nums[j + step] = nums[j];
            }
            nums[j + step] = temp;
        }
    }
}

在上面的实现中,step 的选择为 n/2, n/4, n/8, 直到减为1。希尔排序的时间复杂度的目前还不明确,最重要的结论是它的运行时间达不到平方级别。

由于希尔排序会有多个子数组排序独立进行排序,故希尔排序是不稳定的排序。


2.选择排序

直接选择排序

直接选择排序的思想是:

  • 找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。
  • 在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。
  • 如此往复,直到将整个数组排序。

这种方法叫做直接选择排序,因为它在不断地选择剩余元素之中的最小者。

代码如下:

template<class T>
void selectSort(vector<T> &nums) {
    int min = 0;
    for (size_t i = 0; i < nums.size() - 1; ++i)
    {
        min = i;
        for (size_t j = i + 1; j < nums.size(); ++j)
        {
            min = nums[min] > nums[j] ? j : min;
        }
        swap(nums[min], nums[i]);
    }
}

对于长度为N 的数组,选择排序需要大约 N^2/2 次比较和 N 次交换。因为每轮选择都让第 i 大的元素放在数组下标 i 的位置上。需要的比较次数分别为:N - 1,N - 2,N - 3,… 1,共 N(N-1)/2 次。

直接选择排序的时间复杂度为 O(N^2),空间复杂度为O(1)。直接选择排序是稳定的。


堆排序

当一棵二叉树的每个节点都大于等于它的两个子结点时,它被称为堆有序。根节点是堆有序二叉树中的最大结点。

二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级存储。在数组存储该完全二叉树时(下标为0 ~ N - 1),则下标为 i 的结点的左右子树根结点下标分别为 2 * i + 1, 2 * i + 2。

在堆调整算法中,已知记录 nums[begin . . end] 中除了 nums[begin] 之外均满足大根堆的定义,则需要将nums[begin] 与其左右子树根结点比较,若 nums[begin] 最大,则无需调整了;否则,将沿 key 较大的孩子结点向下调整,直至 nums[begin . . end] 均满足大根堆的定义。

堆调整代码如下:

template<class T>
void maxHeapFix(vector<T> &nums, int begin, int end)
{
    T temp = nums[begin];
    for (int i = 2 * begin + 1; i <= end; i = 2 * i + 1)
    {
        if (i < end && nums[i] < nums[i + 1])
            ++i;
        if (temp >= nums[i])            // i 为 temp 该放的位置
            break;
        nums[begin] = nums[i];
        begin = i;
    }
    nums[begin] = temp;
}

在堆排序算法中,先建一个大根堆,则最大元素在堆顶,将堆顶元素与最后一个元素交换,然后对数组的前 N - 1 个元素进行调整,使 nums[0 . . N - 1] 也满足大根堆,如此反复直达排序结束。

堆排序算法代码如下:

template<class T>
void heapSort(vector<T> &nums) {
    for (int i = nums.size() / 2 - 1; i >= 0; --i)
    {
        maxHeapFix(nums, i, nums.size() - 1);
    }
    for (int i = nums.size() - 1; i > 0; --i)
    {
        swap(nums[0], nums[i]);
        maxHeapFix(nums, 0, i - 1);
    }
}

堆排序的最坏时间复杂度为 O(N lgN),这是相对于快速排序来说,堆排序的最大优点,其平均时间复杂度也是 O(N lgN)。堆排序的空间复杂度为 O(1)。堆排序是稳定的。

堆排序可以很好地实现优先队列。在某些数据处理的例子里,如 TopK 和 Multiway,总数据量太大,无法排序(甚至无法全部装入内存)。如果你需要从 10 亿个元素中选出最大的十个,你可以不用对这个大规模的数组排序,只需要一个能存储10个元素的优先队列即可。


3.交换排序

冒泡排序

冒泡排序的过程较为简单:

  • 将第一个元素与第二个元素进行比较,若为逆序,则交换;将第二个元素与第三个元素比较,若为逆序,则交换,,,以此类推,直到第 N - 1 个元素与第 N 个元素比较交换为止。这个过程称为一趟冒泡,其结果是较小元素不断“上浮”,较大元素不断“下沉”,最终最大放在正确位置。
  • 进行第二趟冒泡, 对前 N - 1 个元素进行一趟冒泡,最终第二大的元素放在正确位置。
  • … …
  • 进行第 i 趟冒泡, 对前 N - i + 1 个元素进行一趟冒泡,最终第 i 大的元素放在正确位置。

若在某趟冒泡中,没有元素交换,则说明所有元素均在正确的位置,即排序已经完成。

冒泡排序的实现代码如下:

template<class T>
void bubbleSort(vector<T> &nums) {
    for (size_t i = 0; i < nums.size(); ++i)
    {
        bool exchange = false;
        for (size_t j = 0; j < nums.size() - i - 1; ++j)
        {
            if (nums[j] > nums[j + 1])
            {
                swap(nums[j], nums[j + 1]);
                exchange = true;
            }
        }
        if (!exchange)
        {
            break;
        }
    }
}

冒泡排序的时间复杂度为O(N^2),空间复杂度为O(1),冒泡排序是稳定的排序算法。


快速排序

快速排序是一种分治的排序算法。它将一个数组分成两个子数组,然后两个子数组独立的排序。

一般的实现是选取数组第一个元素作为枢轴,在一轮交换之后,比枢轴小的在其左边,比枢轴大的在其右边。然后递归的对两个子数组调用快速排序。具体每一轮的交换过程是:

  • 前后两个指向数组首尾的指针 first 和 last,选取枢轴 nums[first]
  • 从子数组最右边开始,依次比较,找到一个比枢轴小的,将它放到 first
  • 从子数组最左边开始,依次比较,找到一个比枢轴大的,将它放到 last
  • 继续从右找比枢轴小的,从左找比枢轴大的,重复上面的两步操作直至左右两个指针相遇
  • 最终将 key 放到 两个指针相遇处

对上述一轮交换后得到的枢轴左右两边的子数组分别调用快速排序,递归的出口是当数组中只剩下一个元素的时候。

快速排序的一个实现如下:

template<class T>
void quickSort(vector<T> &nums, int left, int right) {
    if (left >= right)
    {
        return;
    }
    T key = nums[left];
    int first = left;
    int last = right;
    while (first < last)            // 将比枢轴值小的交换到左边,比枢轴值大的交换到右边
    {
        while (first < last && nums[last] >= key) --last;
        if (first < last)
            nums[first] = nums[last];
        while (first < last && nums[first] <= key) ++first;
        if (first < last)
            nums[last] = nums[first];
    }
    nums[first] = key;              // 将枢轴放在正确的位置,此时,比枢轴小的在其左边,比枢轴大的在其右边

    quickSort(nums, left, first - 1);
    quickSort(nums, first + 1, right);
}

快速排序的平均时间复杂度为O(N lgN),最坏时间复杂度为O(N^2),最坏情况下,是整个数组都已经有序且完全倒序,此时,快速排序退化为冒泡排序,要比较n*(n-1)/2次才能完成。

快速排序的平均空间复杂度为O(lgN),最坏空间复杂度为O(N)。空间复杂度为栈的深度,平均情况下,栈的深度为O(lgN),最坏情况下,栈的深度为O(N)。


4.归并排序

归并排序是又一类不同的排序算法。归并即将两个或两个以上的有序表组合成一个新的有序表。无论是顺序表还是链表存储结构,都可在 O(m + n) 的时间量级上实现。

利用归并思想容易实现归并排序。假设初始序列含有 N 个记录,则可看成是 N 个有序的子序列,每个子序列长度为 1,然后两两归并,得到⌈N / 2⌉ 个长度为 2 或 1 的有序子序列;再两两归并,……,如此重复,直到得到一个长度为 N 的有序子序列为止。这种排序即为 2 - 路归并排序。

归并两个有序子序列的一个实现如下,两个子序列的下标范围分别是 nums[lbegin, lend] 和 nums[lend + 1, rend]:

template<class T>
void merge(vector<T> &nums, int lbegin, int lend, int rend, vector<T> &temp)
{
    int rbegin = lend + 1;
    int index = lbegin;
    int i = lbegin;
    if (nums[lend] <= nums[rbegin])
    {
        return;
    }
    while (lbegin <= lend && rbegin <= rend)
    {
        if (nums[lbegin] <= nums[rbegin])
            temp[index++] = nums[lbegin++];
        else
            temp[index++] = nums[rbegin++];
    }
    while (lbegin <= lend)
        temp[index++] = nums[lbegin++];
    while (rbegin <= rend)
        temp[index++] = nums[rbegin++];
    for (; i <= rend; ++i)
    {
        nums[i] = temp[i];
    }
}

完整的2 - 路归并排序的实现如下:

template<class T>
void mergeSort(vector<T> &nums) {
    vector<T> temp(nums.size());
    for (size_t step = 1; step <= nums.size(); step *= 2)
    {
        size_t lbegin = 0;
        size_t lend = lbegin + step - 1;
        size_t rend = lend + step < nums.size() - 1 ? lend + step : nums.size() - 1;;
        while (lbegin < nums.size() - step)
        {
            merge(nums, lbegin, lend, rend, temp);
            lbegin += 2 * step;
            lend = lbegin + step - 1;
            rend = lend + step;
            rend = rend < nums.size() - 1 ? rend : nums.size() - 1;
        }
    }
}

归并排序的时间复杂度为O(N lgN),空间复杂度为O(N)。归并排序是稳定的排序。


5.基数排序

基数排序是和上面的各类排序算法都不相同的一种排序算法,上面的各种算法中,都是基于关键字的比较和移动元素来实现,而基数排序不需要进行关键字之间的比较。基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。

先求得元素位数的最大值,该值加上 1 则一定能存下所有元素。

int getRadix(const vector<int> &nums)
{
    int radix = 1;
    int p = 10;
    for (size_t i = 0; i < nums.size(); ++i)
    {
        while (nums[i] >= p)
        {
            p *= 10;
            ++radix;
        }
    }
    return radix;
}

基数排序的步骤为:

  • 按照最低位(个位)将元素分别分配到 10 个链中,每条链中元素的个位数相同
  • 依次收集上述10条链中的元素,以便下次分配
  • 依此类推,直到最大元素中的最高位。

基数排序的一个实现(LSD)如下:

void radixSort(vector<int> &nums) {
    int radix = getRadix(nums);
    vector<vector<int> > temp(10);
    for (int i = 0, div = 1; i < radix; ++i, div *= 10)
    {
        for (size_t j = 0; j < nums.size(); ++j)
        {
            temp[(nums[j] / div) % 10].push_back(nums[j]);
        }
        int index = 0;
        for (int j = 0; j < 10; ++j)
        {
            for (size_t k = 0; k < temp[j].size(); ++k)
                nums[index++] = temp[j][k];
            temp[j].clear();
        }
    }
}

基数排序的时间复杂度为O(d · N),空间复杂度为O(rd + N)。基数排序是稳定的排序。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值