八大排序算法(C语言实现)

简介: 八大排序算法(C语言实现)

1.排序的概念

1.排序:

排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作

2.稳定性:

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法稳定的;否则称为不稳定的

3.内部排序:

数据元素全部放在内存中的排序

4.外部排序:

数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序

2.常见八大排序算法

常见排序算法图示:

173d6f8e6412f7e51df6fa3fe5f88ad8.png

3.插入排序

基本思想:

插入排序可以分为直接插入排序和希尔排序,其区别就是希尔排序在直接插入排序的基础上加入了预排序的过程,基本思想都是把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

3.1直接插入排序

基本思想:

从第二个数开始将此数据插入到前面已经排好的有序序列中(一个数算有序),得到一个新的有序数列,依次向后取数字向前插入,直到数据全部有序为止

动图展示:

b8252f5ae26b4b1886fabb2aefa0c7d5.gif

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RG37NaWo-1673177660020)(https://cjc-wqr.oss-cn-nanjing.aliyuncs.com/动画.gif)]

代码实现:

//直接插入排序
void InsertSort(int* a, int n)
{
  for (int i = 0; i < n - 1; ++i)
  {
    int end = i;
    int tmp = a[end + 1];//记录后一个位置的值
    while (end >= 0)
    {
      if (tmp < a[end])//比较
      {
        a[end + 1] = a[end];
        --end;
      }
      else
      {
        break;
      }
    }
    a[end + 1] = tmp;
  }
}

复杂度及稳定性:

时间复杂度:

  • 最坏:数据为一个逆序的序列 O(N^2)
  • 最好:数据为一个顺序有序序列 O(N)
    元素集合越接近有序,直接插入排序算法的时间效率越高

空间复杂度:

  • 只需一个tmp变量 O(1)

稳定性:

  • 稳定,两个相同数据比较时,本轮排序会停止,两个相同数据相对顺序不变

3.2希尔排序

希尔排序是对直接插入排序进行优化后的一种排序,优化分为两步:

  1. 预排序:使得数数据更加接近于有序
  2. 直接插入排序:在预排序过后再进行直接插入排序,使得能更加快速的完成排序

基本思路:

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数区间gap1+gap*n的数据可以分为一组,对每一组内的数据进行排序。每完成一次分组排序后gap就会缩小,然后重复上述分组和排序的工作。直到gap = 1时,进行一次直接插入排序完成整个希尔排序

  • gap越大,大的数可以更快到后面,小的数可以更快到前面,越不接近有序
  • gap越小,数据跳动越慢,越接近有序

图示流程:

d8e5a984dfe5a6d4ea5af6d7b42912d8.png

代码实现:

//希尔排序
void ShellSort(int* a, int n)
{
  //预排序 分成gap组
  //gap > 1  预排序
  //gap = 1  直接插入排序
  int gap = n;
  while (gap > 1)
  {
    //gap = gap / 2;
    gap = gap / 3 + 1; //保证除到最后一次gap一定是1
    for (int i = 0; i < n - gap; ++i) //gap组并排比较
    {
      int end = i;
      int tmp = a[end + gap];
      while (end >= 0)
      {
        if (tmp < a[end])
        {
          a[end + gap] = a[end];
          end -= gap;
        }
        else
        {
          break;
        }
      }
      a[end + gap] = tmp;
    }
  }
}

复杂度及稳定性:

时间复杂度:

希尔排序的时间复杂度的计算过程很复杂,这里直接记结论就好:O(N^1.3)

空间复杂度:

只需一个tmp变量,O(1)

稳定性:

不稳定,两个相同的数在不同的组中发生交换时相对位置可能会发生变化

4.选择排序

基本思想:

选择排序可以分为直接选择排序和之前讲过的堆排序,基本思想都是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完

4.1直接选择排序

基本思想:

在元素集合中选取最大的元素,若其不是这组元素的最后一个元素,则将其与这组元素的最后一个元素end交换,然后让最后一个元素向前移动,重复上述步骤直到排序结束

优化后:

在元素集合中选取最大与最小的数据元素,若其不是这组元素的最后一个(第一个)元素,则将其与这组元素的最后一个元素end和第一个元素begin交换,然后让第一个元素向后移动,最后一个元素向前移动,重复上述步骤,直到集合剩余1个元素则排序结束

动图展示:

image.gif

此动图是优化前的流程图,优化后的基本思想与其类似这里我就不再画了,相信大佬们看了这个图就能妥妥的理解了

代码实现:

//交换函数
void Swap(int* e1, int* e2)
{
  int tmp = *e1;
  *e1 = *e2;
  *e2 = tmp;
}
//直接选择排序
void SelectSort(int* a, int n)
{
  int begin = 0, end = n - 1;
  while (begin < end)
  {
    int mini = begin, maxi = begin;
    for (int i = begin + 1; i <= end; ++i)
    {
      if (a[i] < a[mini])
      {
        mini = i;
      }
      if (a[i] > a[maxi])
      {
        maxi = i;
      }
    }
    Swap(&a[begin], &a[mini]);
    //如果begin和maxi重叠,第一步交换后maxi的位置会变
    if (maxi == begin)
    {
      maxi = mini;
    }
    Swap(&a[end], &a[maxi]);
    begin++;
    end--;
  }
}

注意:

交换beginmini的值后,如果beginmaxi的位置重叠,那么就要将maxi赋值为mini的值,避免交换后导致maxi的位置发生改变

复杂度及稳定性:

时间复杂度:

每次比较都要遍历一遍数据,O(N^2)

空间复杂度:

只需两个变量遍历更新来进行交换,O(1)

稳定性:

不稳定,在进行选择时可能会把相同数中的后者选择到前面,导致相对位置发生改变

4.2.堆排序

基本思想:

堆排序在我前面的文章中有详细的讲解,这里我就只附上原码不另外再讲思路了,友友们们可以直接点击跳转观看堆排序和TopK问题(点击跳转)

代码实现:

升序--建大堆:

//交换函数
void Swap(int* p1, int* p2)
{
  int tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
//向下调整(建大堆)
void AdjustDown(int* a, int n, int parent)
{
  int child = parent * 2 + 1;//假定左孩子
  while (child < n)
  {
    //大堆:保证child指向大的那个孩子
    if (a[child + 1] > a[child] && child + 1 < n)
    {
      child++;
    }
    //大堆:孩子大于父亲就交换,并继续向下比较调整,反之则调整结束
    if (a[child] > a[parent])
    {
      Swap(&a[child], &a[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
//堆排序
//升序:建大堆
void HeapSort(int* a, int n)
{
  //建堆算法
  //从最后一个元素的父节点开始依次向前可以遍历到每颗子树的父节点
  for (int i = (n - 1 - 1) / 2; i >= 0; --i)
  {
    AdjustDown(a, n, i);
  }
  int end = n - 1;
  while (end > 0)
  {
    //交换首尾数据
    Swap(&a[0], &a[end]);
    //从首元素开始向下调整
    AdjustDown(a, end, 0);
    --end;
  }
}

**降序--建小堆:**

//交换函数
void Swap(int* p1, int* p2)
{
  int tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
//向下调整(建小堆)
void AdjustDown(int* a, int n, int parent)
{
  int child = parent * 2 + 1;//假定左孩子
  while (child < n)
  {
    //小堆:保证child指向小的那个孩子
    if (a[child + 1] < a[child] && child + 1 < n)
    {
      child++;
    }
    //小堆:孩子小于父亲就交换,并继续向下比较调整,反之则调整结束
    if (a[child] < a[parent])
    {
      Swap(&a[child], &a[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
//堆排序
//降序:建小堆
void HeapSort(int* a, int n)
{
  //建堆算法
  //从最后一个元素的父节点开始依次向前可以遍历到每颗子树的父节点
  for (int i = (n - 1 - 1) / 2; i >= 0; --i)
  {
    AdjustDown(a, n, i);
  }
  int end = n - 1;
  while (end > 0)
  {
    //交换首尾数据
    Swap(&a[0], &a[end]);
    //从首元素开始向下调整
    AdjustDown(a, end, 0);
    --end;
  }
}

复杂度及稳定性:

时间复杂度:

向下调整建堆加上数据交换,O(N * logN)

空间复杂度:

只需一个交换变量,O(1)

稳定性:

不稳定,当两个相同的数值分别位于数组的首尾时,向下调整会使两数的相对位置发生改变

5.交换排序

基本思想:

所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动

5.1冒泡排序

基本思想:

冒泡排序的基本思想在我之前的文章中也有讲过,友友们可以直接点击跳转观看,C语言数组详解(点击跳转)

动图展示:

image.gif

代码实现:

这里代码实现部分相较于之前的会有一丢丢的改进,一趟冒泡排序中,如果没有发生交换,说明已经有序了,不需要再处理,可以直接结束排序

//冒泡排序
void BubbleSort(int* a, int n)
{
  //总趟数
  for (int i = 0; i < n - 1; ++i)
  {
    int exchange = 0;
    //每趟交换次数
    for (int j = 0; j < n - 1 - i; ++j)
    {
      if (a[j] > a[j + 1])
      {
        Swap(&a[j], &a[j + 1]);
        exchange = 1;
      }
    }
    //一趟冒泡排序中,如果没有发生交换,说明已经有序了,不需要再处理
    if (exchange == 0)
    {
      break;
    }
  }
}

复杂度及稳定性:

时间复杂度:

冒泡排序的遍历是一个等差数列,O(N^2)

空间复杂度:

只需要一个变量来辅助交换,O(1)

稳定性:

稳定,两个相同的数相遇时则不需要再进行交换了,相对位置没有发生改变

5.2快速排序

本篇文章的大哥快排来了,实现方法和优化思路都是非常重要的,友友们可要打起精神了!

基本思路:

任取待排序元素序列中的某元素作为基准值key,按照该基准值将待排序集合分割成两个子序列,左子序列中所有元素均小于key,右子序列中所有元素均大于key,然后在左右子序列中重复该过程,直到所有元素都排列在相应位置上为止

注意:

左边做key,右边先走,右边做key,左边先走 保证相遇位置的值比key要小

相遇的情况:

1.right停住,left遇到right,相遇位置就是right停住的位置,此时的值比key

2.left停住,right遇到left,由于是right先走的,此时left就是起始没动的位置key

5.2.1快排递归实现

代码实现:

//快速排序(递归版)
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  int left = begin, right = end;
  int keyi = left;
  while (left < right)
  {
    //右边先走,找小于key的数
    while (left < right && a[right] >= a[keyi])
    {
      --right;
    }
    //左边再走,找大于key的数
    while (left < right && a[left] <= a[keyi])
    {
      ++left;
    }
    Swap(&a[left], &a[right]);
  }
  Swap(&a[left], &a[keyi]);
  keyi = left;
  //此时key左边区间比key小,右边区间比key大
  //区间划分:[begin,keti-1]  keyi  [keyi+1,end]
  QuickSort(a, begin, keyi - 1);
  QuickSort(a, keyi + 1, end);
}

除了以上的普通实现以外,我们常见的快排实现方法有三种,分别是:Hoare法(霍尔法)挖坑法双指针法

5.2.1.1Hoare法(霍尔法)

基本思想:

快排的则整体思想上面已经讲述过了,这里来讲解一下Hoare法的具体步骤:

  1. 选取数据序列最左边的值为key
  2. 先从序列最右边向左走,找到<=key的值后停下,然后从序列最左边向右走,找到>key的值后停下
  3. 此时交换两处位置的值,并重复上述步骤直到左右相遇为止
  4. 然后后交换相遇位置的值与key位置的值即可达到一趟快排的目的
  5. 最后分别递归左右区间即可完成本次快排
  6. 动图展示:
  7. image.gif
  8. 以上是单趟Hoare法快排的动态流程图,相信观看过后整体的快排也难不倒你

代码展示:

//Hoare法
int PartSort1(int* a, int begin, int end)
{
  int mid = GetMidIndex(a, begin, end);
  Swap(&a[begin], &a[mid]);
  int left = begin, right = end;
  int keyi = left;
  while (left < right)
  {
    //右边先走,找小
    while (left < right && a[right] >= a[keyi])
    {
      --right;
    }
    //左边先走,找大knm
    while (left < right && a[left] <= a[keyi])
    {
      ++left;
    }
    Swap(&a[left], &a[right]);
  }
  Swap(&a[left], &a[keyi]);
  keyi = left;
  return keyi;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  // 小区间用直接插入替代,减少递归调用次数
  if ((end - begin + 1) < 15)
  {
    InsertSort(a + begin, end - begin + 1);
  }
  else
  {
    int keyi = PartSort1(a, begin, end);
    //区间划分:[begin, keyi-1] keyi [keyi+1, end]
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
  }
}

5.2.1.2挖坑法

基本思想:

挖坑法相较于Hoare法引入了的概念更加方便理解,具体步骤如下:

  1. 选取序列最左边的值为key,并在此位置挖坑
  2. 然后先从序列最右边向左边走,找到<=key的值,就将次值填到中,然后从序列最左边向右边走,找到>key的值,就将此值填入
  3. 重复上述步骤,直到左右相遇为止
  4. 此时相遇点必然是一个,将key填入其中本趟的快排就结束了

动图展示:

image.gif

以上是单趟挖坑法快排的动态流程图,相信观看过后整体的快排也难不倒你

代码实现:

//挖坑法
int PartSort2(int* a, int begin, int end)
{
  int mid = GetMidIndex(a, begin, end);
  Swap(&a[begin], &a[mid]);
  int left = begin, right = end;
  int key = a[left];
  int hole = left;
  while (left < right)
  {
    //右边找小,填到左坑
    while (left < right && a[right] >= key)
    {
      --right;
    }
    a[hole] = a[right];
    hole = right;
    //左边找大,填到右坑
    while (left < right && a[left] <= key)
    {
      ++left;
    }
    a[hole] = a[left];
    hole = left;
  }
  a[hole] = key;
  return hole;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  // 小区间用直接插入替代,减少递归调用次数
  if ((end - begin + 1) < 15)
  {
    InsertSort(a + begin, end - begin + 1);
  }
  else
  {
    int keyi = PartSort2(a, begin, end);
    //区间划分:[begin, keyi-1] keyi [keyi+1, end]
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
  }
}

5.2.1.3双指针法

基本思想:

双指针法快排的实现就与上面的两种方法有些不同了,下面我们来看具体步骤:

  1. 选取序列最左边的值为key
  2. 首先定义两个指针prev指针指向序列的开头,cur指针指向prev的后一个位置
  3. 判断cur指向的数据是否小于key,若小于则先将prev后移一位,再交换prev与cur位置的值,然后将cur后移一位,反之只将cur后移一位重复上述步骤,直到cur越界,此时将prev处的值与key交换就完成了本趟快排

动图展示:

bf1770c0ebaa4b4c8e8fdef9e22fc25e.gif以上是单趟双指针法快排的动态流程图,相信观看过后整体的快排也难不倒你

代码实现:

//双指针法
int PartSort3(int* a, int begin, int end)
{
  int mid = GetMidIndex(a, begin, end);
  Swap(&a[begin], &a[mid]);
  int keyi = begin;
  int prev = begin, cur = begin + 1;
  while (cur <= end)
  {
    //找到比key小的值,跟++prev位置交换,小的往前翻,大的往后翻
    if (a[cur] < a[keyi] && ++prev != cur)
      Swap(&a[prev], &a[cur]);
    ++cur;
  }
  Swap(&a[prev], &a[keyi]);
  keyi = prev;
  return keyi;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  if ((end - begin + 1) < 15)
  {
    // 小区间用直接插入替代,减少递归调用次数
    InsertSort(a + begin, end - begin + 1);
  }
  else
  {
    int keyi = PartSort3(a, begin, end);
    //区间划分:[begin, keyi-1] keyi [keyi+1, end]
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
  }
}

5.2.2快排迭代实现

递归版本的快排虽然实现起来更加简单,但是如果数据量过大,就可能出现栈溢出的问题,这时我们就要考虑使用迭代版本的快排了,迭代版的快排是利用这一数据结构来实现的

基本思想:

无论是递归还是迭代我们每一步的主要目的都是需要完成区间划分,利用先进后出的特点,我们用以下步骤来完成目的:

  1. 先将数据序列的首尾元素beginend入栈
  2. 进行出栈操作得到一个区间[left,righr](由于栈的特性,这里的left对应的是原end的值,另一个同理)
  3. 根据得到的区间利用双指针法进行选key区间划分,并记录key的位置
  4. 判断key-1是否大于left,若是则说明左半区间合法,就可以将区间边界leftkey-1入栈;同理判断key+1是否小于right,若是则说明右半区间合法,就可以将区间边界入栈了
  5. 区间被一直划分这就与递归的思想有一点类似了,一直重复上述步骤直到所有区间都非法,即空了,则整个迭代版本的快排就结束了
  6. 代码实现

为了方便观看和理解,代码中涉及到的栈的部分接口的实现我省略掉了,想了解的友友们可以去看我之前的文章栈(C语言实现)(点击跳转)

//快速排序(迭代实现)
void QuickSortNonR(int* a, int begin, int end)
{
  ST st;
  StackInit(&st);
  //首尾入栈
  StackPush(&st, begin);
  StackPush(&st, end);
  while (!StackEmpty(&st))
  {
    int right = StackTop(&st);
    StackPop(&st);
    int left = StackTop(&st);
    StackPop(&st);
    //利用双指针法选key
    int keyi = PartSort3(a, left, right);
    //区间划分:[left, keyi-1] keyi [keyi+1, right]
    //判断是否符合入栈条件
    if (keyi + 1 < right)
    {
      StackPush(&st, keyi + 1);
      StackPush(&st, right);
    }
    if (left < keyi - 1)
    {
      StackPush(&st, left);
      StackPush(&st, keyi - 1);
    }
  }
  StackDestroy(&st);
}

5.3快排优化

重难点来了!细细品味!

这里提供三步优化方法:

优化一:三数取中 / 随机选key

如果数据接近有序或者逆序,对于快排来说时间复杂度是较高的,因为快排选key始终是最左或者最右的,就有可能选到最大数或者最小数导致要把所有数据遍历一遍,这里采用

  • 三数取中:取三个数的中间大小的值,例如3、1、2取2
  • 随机选key:随机选取数据序列中的数字做key

对于一些特殊测试用例,如果我们严格走三数取中,可能大量区间选key会选到比较小或者比较大的值,导致性能下降。这时我们就可以结合随机选key来优化,不过因为是随机的除了特殊用例,整体上还是三数取中比较好

代码实现:

三数取中:

//三数取中
//begin < mid < end 取mid
int GetMidIndex(int* a, int begin, int end)
{
  int mid = (begin + end) / 2;
  if (a[mid] > a[begin])
  {
    if (a[mid] < a[end])
    {
      return mid;
    }
    else if (a[begin] < a[end])
    {
      return begin;
    }
    else
    {
      return end;
    }
  }
  else //a[mid] < a[begin]
  {
    if (a[end] < a[mid])
    {
      return mid;
    }
    else if (a[begin] < a[end])
    {
      return begin;
    }
    else
    {
      return end;
    }
  }
}

随机选key:

//随机选key
int GetMidIndex(int* a, int begin, int end)
{
  int mid = begin + rand() % (end - begin);//保证随机选的key在区间内
  if (a[begin] > a[mid])
  {
    if (a[begin] < a[end])
    {
      return begin;
    }
    else if (a[end] < a[mid])
    {
      return mid;
    }
    else
    {
      return end;
    }
  }
  else //a[begin] < a[mid]
  {
    if (a[mid] < a[end])
    {
      return mid;
    }
    else if (a[end] < a[begin])
    {
      return begin;
    }
    else
    {
      return end;
    }
  }
}

优化二:小区间优化

对于递归来说,越是递归到最后的小区间,耗费的时间就越长,这个时候我们选择在小区间使用直接插入排序进行代替可以很好的弥补递归快排在小区间排序中的缺陷,大大减少了递归次数,这里我们将15定为小区间

代码实现:

void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  // 优化一:小区间用直接插入代替,减少递归调用次数
  if ((end - begin + 1) < 15)
  {
    InsertSort(a + begin, end - begin + 1);
  }
  else
  {
    // 优化二:三数取中,避免选的key是最大或最小遍历次数太多
    int mid = GetMidIndex(a, begin, end);
    Swap(&a[begin], &a[mid]);
    int left = begin, right = end;
    int keyi = left;
    while (left < right)
    {
      //右边先走,找小于key的数
      while (left < right && a[right] >= a[keyi])
      {
        --right;
      }
      //左边再走,找大于key的数
      while (left < right && a[left] <= a[keyi])
      {
        ++left;
      }
      Swap(&a[left], &a[right]);
    }
    Swap(&a[left], &a[keyi]);
    keyi = left;
    //此时key左边区间比key小,右边区间比key大
    //区间划分:[begin,keti-1]  keyi  [keyi+1,end]
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
  }
}

优化三:三路划分

如果数据序列中与key相同的值太多,比如数值全等于key,算法效率就会退化到O(N^2),这时我们使用三路划分来进行优化,就能很好的弥补这一缺陷,具体步骤如下:

  1. 与key相等的数往后推
  2. 小于key的数甩到左边
  3. 大于key的数甩到右边
  4. 与key相等的数就在中间部分了

代码实现:

void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  // 优化一:小区间用直接插入代替,减少递归调用次数
  if ((end - begin + 1) < 15)
  {
    InsertSort(a + begin, end - begin + 1);
  }
  else
  {
    // 优化二:三数取中,避免选的key是最大或最小遍历次数太多
    int mid = GetMidIndex(a, begin, end);
    Swap(&a[begin], &a[mid]);
    int left = begin, right = end;
    int key = a[left];
    int cur = begin + 1;
    while (cur <= right)
    {
      if (a[cur] < key)
      {
        Swap(&a[cur], &a[left]);
        cur++;
        left++;
      }
      else if (a[cur] > key)
      {
        Swap(&a[cur], &a[right]);
        --right;
      }
      else //a[cur] == key
      {
        cur++;
      }
    }
    //三路划分优化后
    //三个区间分别是 <key, =key, >key 1[begin,left-1] 2[left,right] 3[right+1,end]
    //此时只用递归快排1、3区间即可
    QuickSort(a, begin, left - 1);
    QuickSort(a, right + 1, end);
  }
}

复杂度及稳定性(优化后):

时间复杂度:

O(N*logN)

空间复杂度:

O(logN)

稳点性:

不稳定,两个相同的数,后者与key交换后相对位置就会发生改变


6.归并排序

基本思想

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

归并流程图

597df752fdd2eff45c35a9563dff69de.png

6.1归并递归实现

基本思路

我们需要得到有序子序列,再将其合并得到新的有序序列,这就要用到递归来实现了,具体步骤如下:

  1. 将原序列分解为左右两个区间
  2. 为了使区间内数据有序,我们需要依靠递归,不断递出分解区间直到区间内数据只剩一个(一个数据是被认为有序的)
  3. 再执行合并操作,向上回归合并,使得每次合并的区间内数据都是有序的,当回归结束后归并排序也就结束了,数据整体就有序了

友友们可以看看上面的归并流程图,更好理解

动图展示:

a69fd9d00d7a4cfd9e9c77e522d7074b.gif

代码实现:

//归并排序(递归实现)子函数
void _MergeSort(int* a, int begin, int end, int* tmp)
{
  if (begin >= end)
    return;
  int mid = (begin + end) / 2;
  //递归使子区间有序: [begin,mid] [mid+1,end] 
  _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++];
  }
  //将归并排序后的数组拷贝回原数组
  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("fail malloc");
    exit(-1);
  }
  _MergeSort(a, 0, n - 1, tmp);
  free(tmp);
  tmp = NULL;
}

复杂度及稳定性:

时间复杂度:

归并排序就是分治的思想,O(N*logN)

空间复杂度:

归并需要开辟额外的空间,O(N)

稳定性:

稳定,两个相同的数,在合并数组的过程中,前者总是会比后者先合并进数组,相对位置并不会发生改变

6.2归并迭代实现

基本思想:

归并的迭代实现只需定义一个范围rangeN(默认为1),遍历整个数据序列对rangeN范围内的数据进行合并,使rangeN逐渐扩大,直到rangeN > n(数据个数),此时的归并范围超出循环停止,整个迭代版本的归并排序也就完成了

注意!

归并排序的迭代实现的边界问题很难控制,一不小心就会发生越界

有以下三种越界情况:

d2028523c52d29a8e53477b263588f96.png

下面我们来用两种方法进行控制:

方法一:直接跳出

我们将合并排序后的数组拷贝回原数组有两种方法:一是归并一部分拷贝一部分,二是归并完成后整体拷贝

使用直接跳出的方法来控制范围,在拷贝时只能归并一部分拷贝一部分

//1.end1 begin2 end2 越界
if (end1 >= n)
{
  break;
}
//2.begin2 end2 越界
else if (begin2 >= n)
{
  break;
}
//3.end2 越界
else if (end2 >= n)
{
  //修正到区间末尾
  end2 = n - 1;
}

方法二:区间修正

使用区间的方法来控制范围,在拷贝时使用归并一部分拷贝一部分或者整体拷贝都是可行的

//1.end1 begin2 end2 越界
if (end1 >= n)
{
  //修正到区间末尾
  end1 = n - 1;
  //修正到不存在的区间(下面的归并循环不会进去)
  begin2 = n;
  end2 = n - 1;
}
//2.begin2 end2 越界
else if (begin2 >= n)
{
  //修正到不存在的区间
  begin2 = n;
  end2 = n - 1;
}
//3.end2 越界
else if (end2 >= n)
{
  //修正到区间末尾
  end2 = n - 1;
}

这里我们把两种拷贝方法和两种控制界限的方法都在代码中体现一下

代码实现:

整体归并完了再拷贝&区间修正:

//归并排序迭代实现
//整体归并完了再拷贝
void MergeSortNonR(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  //归并每组数据个数,从每组1个数开始,因为1个数可以认为是有序的,可以直接归并
  int rangeN = 1;
  while (rangeN < n)
  {
    for (int i = 0; i < n; i += 2 * rangeN)
    {
      //[begin1,end1] [begin2,end2] 归并
      int begin1 = i, end1 = i + rangeN - 1;
      int begin2 = i + rangeN, end2 = i + rangeN * 2 - 1;
      //打印一下归并区间,便于观察
      printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
      int j = i;
      //对于三种越界情况来修区间 --> 拷贝数据:整体归并完了拷贝 or 归并一部分拷贝一部分
      //1.end1 begin2 end2 越界
      if (end1 >= n)
      {
        //修正到区间末尾
        end1 = n - 1;
        //修正到不存在的区间(下面的归并循环不会进去)
        begin2 = n;
        end2 = n - 1;
      }
      //2.begin2 end2 越界
      else if (begin2 >= n)
      {
        //修正到不存在的区间
        begin2 = n;
        end2 = n - 1;
      }
      //3.end2 越界
      else if (end2 >= n)
      {
        //修正到区间末尾
        end2 = n - 1;
      }
      //两个区间取小的数归并尾插
      while (begin1 <= end1 && begin2 <= end2)
      {
        if (a[begin1] <= a[begin2])
        {
          tmp[j++] = a[begin1++];
        }
        else
        {
          tmp[j++] = a[begin2++];
        }
      }
      //一个区间遍历完了,另一个还没有
      while (begin1 <= end1)
      {
        tmp[j++] = a[begin1++];
      }
      while (begin2 <= end2)
      {
        tmp[j++] = a[begin2++];
      }
    }
    //整体归并完了再拷贝
    memcpy(a, tmp, sizeof(int) * (n));
    rangeN *= 2;
  }
  free(tmp);
  tmp = NULL;
}

归并一部分拷贝一部分&直接跳出:

//归并排序迭代实现
//归并一部分拷贝一部分
void MergeSortNonR(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  //归并每组数据个数,归并每组从1开始,因为一个数可以认为是有序的,可以直接归并
  int rangeN = 1;
  while (rangeN < n)
  {
    for (int i = 0; i < n; i += rangeN * 2)
    {
      //[begin1,end1],[begin2,end2] 归并
      int begin1 = i, end1 = i + rangeN - 1;
      int begin2 = i + rangeN, end2 = i + rangeN * 2 - 1;
      //打印区间,便于观察
      printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
      int j = i;
      //对于三种越界情况来修区间 --> 拷贝数据:整体归并完了拷贝 or 归并一部分拷贝一部分
      //1.end1 begin2 end2 越界
      if (end1 >= n)
      {
        break;
      }
      //2.begin2 end2 越界
      else if (begin2 >= n)
      {
        break;
      }
      //3.end2 越界
      else if (end2 >= n)
      {
        //修正到区间末尾
        end2 = n - 1;
      }
      //两个区间取小的数尾插
      while (begin1 <= end1 && begin2 <= end2)
      {
        if (a[begin1] <= a[begin2])
        {
          tmp[j++] = a[begin1++];
        }
        else
        {
          tmp[j++] = a[begin2++];
        }
      }
      //如果一个区间遍历完了,另一个还没有
      while (begin1 <= end1)
      {
        tmp[j++] = a[begin1++];
      }
      while (begin2 <= end2)
      {
        tmp[j++] = a[begin2++];
      }
      //归并一部分,拷贝一部分
      memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
    }
    rangeN *= 2;
  }
  free(tmp);
  tmp = NULL;
}

复杂度及稳定性:

同归并排序的递归实现

7.计数排序

基本思想:

计数排序主要是利用映射来完成的,具体步骤如下:

  1. 统计相同元素出现的次数
  2. 将待排序数据序列映射到计数空间中对应下标的位置,该位置对应的值就是该数据出现的次数
  3. 映射完成后,遍历计数空间,将有值的位置根据次数重新赋值到原有序序列中,计数空间遍历完后计数排序也就完成了

优化:

上面的映射方式属于绝对映射:开辟的计数空间大小:最大值 + 1,也就是每个数据映射到自己对应的下标。如果数据序列中都是一些很大的值,这样开辟的计数空间就会造成大量浪费,针对这一问题,我们将其优化为相对映射:开辟的计数空间大小 = 最大值 - 最小值 + 1就能很好的解决

计数排序流程图:

df642373d74a7125b43b5dbdee0919c4.png

代码实现:

//计数排序
void CountSort(int* a, int n)
{
  int max = a[0], min = a[0];
  for (int i = 1; i < n; ++i)
  {
    if (a[i] < min)
      min = a[i];
    if (a[i] > max)
      max = a[i];
  }
  //相对映射开辟空间
  int range = max - min + 1;
  int* countA = (int*)calloc(range, sizeof(int));
  if (countA == NULL)
  {
    perror("calloc fail");
    exit(-1);
  }
  //1.统计数据出现次数
  for (int i = 0; i < n; ++i)
  {
    countA[a[i] - min]++;
  }
  //2.放回原数据序列排序
  int k = 0;
  for (int j = 0; j < range; ++j)
  {
    while (countA[j]--)
    {
      a[k++] = j + min;
    }
    free(countA);
  }
}

复杂度及稳定性:

时间复杂度:

先遍历了一遍原数据,再遍历了一遍计数空间,O(N + Range)

空间复杂度:

需要开辟计数空间,O(max - min)

稳定性:

稳定,计数排序是直接覆盖原数组

注意:

计数排序虽然是个效率很高的排序,但是它只适合范围集中的数据,且只适合整形

8.八大排序总结表

排序方法 时间复杂度 空间复杂度 稳定性

直接插入排序 O(N^2) O(1) 稳定

希尔排序 O(N^1.3) O(1) 稳定

直接选择排序 O(N^2) O(1) 不稳定

堆排序 O(N*logN) O(1) 不i稳定

冒泡排序 O(N^2) O(1) 稳定

快速排序(递归) O(N*logN) O(logN) 不稳定

归并排序(递归) O(N*logN) O(N) 稳定

计数排序 O(N) O(max-min) 稳定

八大排序的实现到这里就介绍结束了,期待大佬们的三连!你们的支持是我最大的动力!

文章有写的不足或是错误的地方,欢迎评论或私信指出,我会在第一时间改正。

目录
相关文章
|
29天前
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
52 4
|
4月前
|
存储 算法 C语言
"揭秘C语言中的王者之树——红黑树:一场数据结构与算法的华丽舞蹈,让你的程序效率飙升,直击性能巅峰!"
【8月更文挑战第20天】红黑树是自平衡二叉查找树,通过旋转和重着色保持平衡,确保高效执行插入、删除和查找操作,时间复杂度为O(log n)。本文介绍红黑树的基本属性、存储结构及其C语言实现。红黑树遵循五项基本规则以保持平衡状态。在C语言中,节点包含数据、颜色、父节点和子节点指针。文章提供了一个示例代码框架,用于创建节点、插入节点并执行必要的修复操作以维护红黑树的特性。
110 1
|
13天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
37 1
|
1月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
84 8
|
1月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
79 7
|
6月前
|
存储 算法 C语言
二分查找算法的概念、原理、效率以及使用C语言循环和数组的简单实现
二分查找算法的概念、原理、效率以及使用C语言循环和数组的简单实现
|
6月前
|
算法 C语言
C语言----判断n是否是2的次方数,利用到按位与&,算法n&(n-1)
C语言----判断n是否是2的次方数,利用到按位与&,算法n&(n-1)
|
6月前
|
机器学习/深度学习 算法 C语言
详细介绍递归算法在 C 语言中的应用,包括递归的基本概念、特点、实现方法以及实际应用案例
【6月更文挑战第15天】递归算法在C语言中是强大力量的体现,通过函数调用自身解决复杂问题。递归涉及基本概念如自调用、终止条件及栈空间管理。在C中实现递归需定义递归函数,分解问题并设定停止条件。阶乘和斐波那契数列是经典应用示例,展示了递归的优雅与效率。然而,递归可能导致栈溢出,需注意优化。学习递归深化了对“分而治之”策略的理解。**
123 7
|
6月前
|
算法 Java C语言
Java中的算法与C语言中的函数
Java中的算法与C语言中的函数
46 2
|
6月前
|
存储 算法 搜索推荐
【数据结构和算法】--- 基于c语言排序算法的实现(2)
【数据结构和算法】--- 基于c语言排序算法的实现(2)
38 0