排序 | 冒泡 插入 希尔 选择 堆 快排 归并 非递归 计数 基数 排序(二)

简介: 排序 | 冒泡 插入 希尔 选择 堆 快排 归并 非递归 计数 基数 排序(二)

快速排序前后指针法

  • 快速排序的前后指针法(也称为Hoare分区方案)是另一种实现方式。在这个方法中,通过两个指针从数组的两端分别向中间移动,交换不符合排序条件的元素,最终将数组分为两个部分,左边部分小于基准元素,右边部分大于基准元素。

代码实现:

int PartSort3(int* a, int begin, int end)
{
  int midi = GetMidi(a, begin, end);
  Swap(&a[begin], &a[midi]);
  int prev = begin;
  int cur = prev + 1;
  int keyi = begin;
  while (cur <= end)
  {
    if (a[cur] < a[keyi] && ++prev != cur)
      Swap(&a[prev], &a[cur]);
    ++cur;
  }
  Swap(&a[keyi], &a[prev]);
  keyi = prev;
  return prev;
}
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
    return;
  // 小区间
  if (end - begin + 1 < 10)
  {
    InsertSort(a + begin, end - begin + 1);
  }
  else
  {
    int keyi = PartSort3(a, begin, end);
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
  }
}

优点:

  1. 原地排序: 前后指针法是一种原地排序算法,不需要额外的空间来存储临时数据,只需要一个常数级的辅助空间。
  2. 相对较好的性能: 在平均情况下,快速排序的前后指针法具有较好的性能,时间复杂度为O(n log n)。
  3. 不需要额外的空间: 与挖坑法相比,前后指针法在实际的操作中,不需要额外的元素用于填坑,从而减少了一些操作。

缺点:

  1. 不稳定性: 前后指针法是一种不稳定的排序算法,相等元素的相对位置可能发生变化,如果需要稳定性,可能需要额外的处理。
  2. 最坏情况下的性能: 在最坏情况下,即已经有序的序列,前后指针法的性能可能较差。这时的时间复杂度为O(n^2),因为每次分区只能使序列中的一个元素有序。
  3. 对于小规模数据性能较差: 在小规模数据的排序中,前后指针法的递归调用会增加额外的开销,性能可能不如一些简单的排序算法,如插入排序。

快速排序–非递归实现

  • 我们这里使用栈来解决这个问题~~
  • 先入栈,然后再进行分割
  • 注意: 如果是先入右后入左,那么出的时候就要先出左后出右
  • 栈不为空就继续,然后分割排左边和右边

代码实现:

#include"Stack.h"
// 快速排序 非递归实现 
void QuickSortNonR(int* a, int begin, int end)
{
  ST s;
  StackInit(&s);
  // 先入右后入左
  StackPush(&s, end);
  StackPush(&s, begin);
  while (!StackEmpty(&s))
  {
    // 先出左后出右
    int left = StackTop(&s);
    StackPop(&s);
    int right = StackTop(&s);
    StackPop(&s);
    // 排序
    int keyi = PartSort3(a, left, right);
    // [left keyi-1] keyi [keyi+1 right]
    if (left < keyi - 1)
    {
      StackPush(&s, keyi - 1);
      StackPush(&s, left);
    }
    if (keyi + 1 < right)
    {
      StackPush(&s, right);
      StackPush(&s, keyi + 1);
    }
  }
  StackDestroy(&s);
}

归并排序

  • 归并排序(Merge Sort)是一种分治算法,它的基本思想是将待排序的数组分成两个相等大小的子数组,然后分别对这两个子数组进行排序,最后将排序好的子数组合并成一个有序的数组。

代码实现:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
  if (begin >= end)
    return;
  // 分割  // 这里右移一位相当于 /2 
  int mid = (begin + end) >> 1; 
  // 递归
  _MergeSort(a, begin, mid, tmp);
  _MergeSort(a, mid + 1, end, tmp);
  // [begin mid][mid+1 end]  归并
  int begin1 = begin, end1 = mid;
  int begin2 = mid + 1, end2 = end;
  int i = begin;
  while (begin1 <= end1 && begin2 <= end2)
  {
    if (a[begin1] <= a[begin2])
    {
      tmp[i++] = a[begin1++];
    }
    else
    {
      tmp[i++] = a[begin2++];
    }
  }
  while (begin1 <= end1)
  {
    tmp[i++] = a[begin1++];
  }
  while (begin2 <= end2)
  {
    tmp[i++] = a[begin2++];
  }
  // 拷贝回原数组
  for (int i = begin; i <= end; ++i)
  {
    a[i] = tmp[i];
  }
  //memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin) + 1);
}
// 归并排序递归实现 
void MergeSort(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
    perror("malloc fail!\n");
    return;
  }
  _MergeSort(a, 0, n - 1, tmp);
  free(tmp);
}

优点:

  1. 稳定性: 归并排序是一种稳定的排序算法,即对于相等的元素,它们在排序后的相对位置保持不变。
  2. 适用于链表: 归并排序对于链表等非随机访问结构的数据也非常有效,因为它不涉及随机访问,只涉及指针操作。
  3. 适用于外部排序: 归并排序在外部排序(需要对外部存储进行排序的情况)中表现良好,因为它可以很容易地通过合并有序的外部文件来实现。
  4. 稳定的时间复杂度: 归并排序的时间复杂度是稳定的,不受输入数据的影响,总是O(n log n)。

缺点:

  1. 额外空间需求: 归并排序需要额外的内存空间来存储临时数组,这使得它的空间复杂度相对较高,是O(n)。
  2. 非原地排序: 归并排序是一种非原地排序算法,即它需要额外的空间来存储临时数组,而不是在原始数组上进行排序。这对于内存受限的情况可能不太理想。
  3. 常数因子较大: 归并排序的常数因子较大,因此在实际应用中可能被一些其他排序算法(如快速排序)超越。

归并排序非递归实现

  • 思想和上面的归并排序差不多~~

代码实现:

void MergeSortNonR(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  int gap = 1; // 每组数据个数
  while (gap < n)
  {
    for (int i = 0; i < n; i += 2 * gap)
    {
      // [i, i+gap-1] [i+gap,i+2*gap-1]
      int begin1 = i, end1 = i + gap - 1;
      int begin2 = i + gap, end2 = i + 2 * gap - 1;
      // 归并过程中右半区间可能就不存在
      if (begin2 >= n)
        break;
      // 归并过程中右半区间算多了, 修正一下
      if (end2 >= n)
      {
        end2 = n - 1;
      }
      int index = i;
      while (begin1 <= end1 && begin2 <= end2)
      {
        if (a[begin1] < a[begin2])
        {
          tmp[index++] = a[begin1++];
        }
        else
        {
          tmp[index++] = a[begin2++];
        }
      }
      while (begin1 <= end1)
      {
        tmp[index++] = a[begin1++];
      }
      while (begin2 <= end2)
      {
        tmp[index++] = a[begin2++];
      }
      // 拷贝回去
      for (int j = i; j <= end2; ++j)
      {
        a[j] = tmp[j];
      }
    }
    gap *= 2;
  }
  free(tmp);
}

非比较排序【计数排序】

  • 计数排序(Counting Sort)是一种非比较性的整数排序算法,它通过确定每个元素在输出序列中的位置来实现排序。

代码实现:

void CountSort(int* a, int n)
{
  int max = a[0], min = a[0];
  for (int i = 0; i < n; i++)
  {
    if (a[i] > max)
      max = a[i];
    if (a[i] < min)
      min = a[i];
  }
  int range = max - min + 1;
  int* count = (int*)malloc(sizeof(int) * range);
  if (count == NULL)
  {
    perror("malloc fail!\n");
    return;
  }
  memset(count, 0, sizeof(int) * range);
  //统计次数
  for (int i = 0; i < n; i++)
  {
    count[a[i] - min]++;
  }
  int i = 0;
  for (int j = 0; j < range; j++)
  {
    while (count[j]--)
    {
      a[i++] = j + min;
    }
  }
  free(count);
}

优点:

  1. 线性时间复杂度: 计数排序是一种具有线性时间复杂度的排序算法,其时间复杂度为O(n + k),其中n是输入元素的数量,k是输入范围的大小。在输入范围较小的情况下,计数排序通常比其他O(n log n)的排序算法更快。
  2. 稳定性: 计数排序是一种稳定的排序算法,即相等元素的相对顺序在排序后仍然保持不变。
  3. 适用于整数排序: 计数排序适用于整数排序,尤其是在知道输入范围不太大的情况下。它不依赖于比较操作,因此在某些情况下可能比基于比较的排序算法更高效。

缺点:

  1. 空间复杂度: 计数排序的主要缺点是它需要额外的空间来存储计数数组。如果输入范围很大,可能需要较大的额外空间,这可能导致空间复杂度较高。
  2. 仅适用于整数: 计数排序仅适用于整数排序,因为它依赖于将元素映射到计数数组的索引。对于浮点数或其他数据类型,需要进行额外的转换。
  3. 对输入范围的限制: 计数排序要求输入的元素必须在已知范围内,否则需要进行范围的确定和调整,增加了实现的复杂性。

基数排序

  • 基数排序是一种非比较性的排序算法,它根据关键字的每一位来排序数据。

代码实现:

这里就先不实现,之后会开一个新文章来进行阐述,并且使用C++来写~~

优点:

  1. 稳定性: 基数排序是一种稳定的排序算法,即相等元素的相对顺序在排序后保持不变。
  2. 适用范围广: 基数排序对于数据的分布没有特殊的要求,适用于各种数据类型,包括整数、字符串等。
  3. 适用于大量数据: 在某些情况下,基数排序的性能可能比一些常见的比较性排序算法(如快速排序、归并排序)更好,尤其是当数据量非常大时。
  4. 不受输入数据范围限制: 基数排序不受输入数据范围的限制,可以处理负数和小数。

缺点:

  1. 空间复杂度较高: 基数排序的空间复杂度取决于数据的位数,如果数据位数很大,可能需要较大的辅助空间来存储中间结果。
  2. 不适用于小范围数据: 当数据范围比较小而位数较大时,基数排序可能不是最优选择,因为它需要较大的辅助空间。
  3. 只能处理正整数或字符串: 基数排序主要用于整数或字符串的排序,对于其他数据类型可能需要转换为整数或字符串形式,增加了额外的开销。
  4. 效率受制于位数: 基数排序的效率受制于位数,如果位数很大,可能需要进行多轮排序,导致时间复杂度较高。

排序算法复杂度及稳定性分析


相关文章
|
6月前
|
搜索推荐 Java
排序算法-冒泡、选择、堆、插入、归并、快速、希尔
排序算法-冒泡、选择、堆、插入、归并、快速、希尔
14 0
|
2天前
|
存储 搜索推荐 算法
排序 | 冒泡 插入 希尔 选择 堆 快排 归并 非递归 计数 基数 排序(一)
排序 | 冒泡 插入 希尔 选择 堆 快排 归并 非递归 计数 基数 排序(一)
|
2天前
|
存储 人工智能 搜索推荐
浅谈归并排序:合并 K 个升序链表的归并解法
在面试中遇到了这道题:如何实现多个升序链表的合并。这是 LeetCode 上的一道原题,题目具体如下:
45 0
浅谈归并排序:合并 K 个升序链表的归并解法
|
7月前
|
存储 搜索推荐 算法
插入/希尔/选择/堆/冒泡/快速/归并/计数排序 && 排序原理 && 搜索树原理 && 哈希概念
插入/希尔/选择/堆/冒泡/快速/归并/计数排序 && 排序原理 && 搜索树原理 && 哈希概念
47 0
|
9月前
|
搜索推荐
七个常用的排序算法---快排\归排\希尔\插入\选择\冒泡\堆排(一)
七个常用的排序算法---快排\归排\希尔\插入\选择\冒泡\堆排(一)
59 0
|
9月前
|
存储 搜索推荐 C语言
七个常用的排序算法---快排\归排\希尔\插入\选择\冒泡\堆排(二)
七个常用的排序算法---快排\归排\希尔\插入\选择\冒泡\堆排(二)
44 0
|
11月前
|
搜索推荐 算法
排序算法大总结(插入、希尔、选择、堆、冒泡、快速、归并、计数)(下)
排序算法大总结(插入、希尔、选择、堆、冒泡、快速、归并、计数)
52 0
|
11月前
|
搜索推荐 算法
排序算法大总结(插入、希尔、选择、堆、冒泡、快速、归并、计数)(上)
排序算法大总结(插入、希尔、选择、堆、冒泡、快速、归并、计数)
63 0
|
11月前
|
算法 搜索推荐
|
算法
数据结构与算法(六)排序 插入&希尔&归并
数据结构与算法(六)排序 插入&希尔&归并
74 0