排序算法大总结(插入、希尔、选择、堆、冒泡、快速、归并、计数)(下)

简介: 排序算法大总结(插入、希尔、选择、堆、冒泡、快速、归并、计数)

挖坑法

人们所熟知的快排就是这个思想,比霍尔法更容易清晰理解。先将第一个数放在临时变量key中,此时形成一个坑位,然后右边先走找小,遇到小的停下来,将该值赋给坑位,并形成一个新的坑位。并且左边找大,赋值,形成新的坑位,直至两边相遇,将key值赋给左边。

//快速排序(挖坑法)
void QuickSort_dig(int* a, int begin, int end)
{
  //左边作key,右边先走找小,找到了停下
  //左边再走找大,找到停下,交换
  //重复
  if (begin >= end)
  {
    return;
  }
  //一次排序
  int left = begin;
  int right = end;
  int keyi = GetMidIndex(a,left,right);
  int key = a[keyi];
  while (left < right)
  {
    //找小
    while (left < right && a[keyi] <= a[right])
    {
      --right;
    }
    a[keyi] = a[right];
    keyi = right;  //right变为坑
    //找大
    while (left < right && a[left] <= a[keyi])
    {
      ++left;
    }
    a[keyi] = a[left];
    keyi = left;
  }
  a[keyi] = key;
  //递归
  QuickSort_old_hoare(a, begin, keyi - 1);
  QuickSort_old_hoare(a, keyi + 1, end);
}

函数分开版本:

void QuickSort(int* a, int begin,int end)
{
  if (begin >= end)
  {
    return;
  }
  if (end-begin>10)
  {
    int keyi = Partition2(a, begin, end);
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi+1, end);
  }
  else
  {
    InsertSort(a + begin, end - begin + 1);
  }
}
//挖坑版本
int Partition2(int* a, int begin, int end)
{
  int left = begin;
  int right = end;
  //int keyi = begin;//坑在起始位置
  int keyi = GetMidIndex(a,begin,end);//坑在起始位置
  int key = a[keyi];
  while (left < right)
  {
    //找小
    while (left < right &&a[right] >= key)
    {
      --right;
    }
    a[keyi] = a[right];//填坑
    keyi = right;//换坑
    //找大
    while (left < right && a[left] <= key)
    {
      ++left;
    }
    a[keyi] = a[left];//填坑
    keyi = left;//换坑
  }
  a[keyi] = key;
  return keyi;
}

双指针版本

这个版本利用双‘指针’,优化了代码结构,使代码变得更加简洁,但是代码的可读性变差。快指针的作用是找小,慢指针的作用是保留边界,两个指针中间是大数,以翻跟头的形式往前走。

//双指针版本
int Partition3(int* a, int begin, int end)
{
  //prev在起始位置,cur在下一个位置
  //判断cur,若cur小于prev,则prev+1交换
  //大于prev,cur++
  int prev = begin;
  int cur = prev + 1;
  int key = a[begin];
  while (cur <= end)
  {
    当prev+1 == cur时,如果再交换的话就浪费了
    //if (a[cur] < key )
    //{
    //  ++prev;
    //  Swap(&a[cur], &a[prev]);
    //}
    //++cur;
    //当prev+1 == cur时,如果再交换的话就浪费了
    if (a[cur] < key&&++prev!=cur)
    {
      Swap(&a[cur], &a[prev]);
    }
    ++cur;
  }
  Swap(&a[begin], &a[prev]);
  return prev;
}
void QuickSort(int* a, int begin,int end)
{
  if (begin >= end)
  {
    return;
  }
  if (end-begin>10)
  {
    int keyi = Partition2(a, begin, end);
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi+1, end);
  }
  else
  {
    InsertSort(a + begin, end - begin + 1);
  }
}

快排优化

为了防止最坏情况下爆栈,我们需要使用三数取中法来优化选择keyi的位置。这样递归的深度接近logN。

//为了防止最坏情况爆栈,我们可以用三数取中法来优化,keyi
// 三数取中法
int GetMidIndex(int* a, int begin, int end)
{
  int mid = (begin + end) / 2;
  if (a[begin] > a[mid])
  {
    if (a[mid] > a[end])
    {
      return mid;
    }
    else
    {
      if (a[end] > a[begin])
      {
        return begin;
      }
      else
      {
        return end;
      }
    }
  }
  else
  {
    if (a[end] < a[begin])
    {
      return begin;
    }
    else
    {
      if (a[end] > a[mid])
      {
        return mid;
      }
      else
      {
        return end;
      }
    }
  }
}

快速排序非递归

因为快排的思想接近于二叉树的先序遍历,因此我们可以考虑使用栈来模拟递归的过程。核心思想就是:利用栈来保存每次快排的区间。有点类似于层序遍历的方法。

//快排非递归(需要使用栈)
// 要求掌握,递归改非递归
// 递归大问题,极端场景下面,如果深度太深,会出现栈溢出
// 1、直接改循环 -- 比如斐波那契数列、归并排序
// 2、用数据结构栈模拟递归过程
void QuickSort_nonrecursive(int* a, int begin, int end)
{
  //先将左右入栈
  //出栈,调用快排函数
  //调用完了再入栈
  Stack q;
  StackInit(&q);
  StackPush(&q, end);
  StackPush(&q, begin);
  //栈不空
  while (!StackEmpty(&q))
  {
    int left = StackTop(&q);
    StackPop(&q);
    int right = StackTop(&q);
    StackPop(&q);
    如果左小于右说明可以划分,就继续入栈
    //if (left < right)
    //{
    //  int keyi = Partition2(a, left, right);
    //  StackPush(&q, keyi - 1);
    //  StackPush(&q, left);
    //  StackPush(&q, right);
    //  StackPush(&q, keyi + 1);
    //}
    int keyi = Partition2(a, left, right);
    //小区间能继续划分,就入栈
    if (left < keyi - 1)
    {
      StackPush(&q, keyi - 1);
      StackPush(&q, left);
    }
    if (keyi + 1 < right)
    {
      StackPush(&q, right);
      StackPush(&q, keyi + 1);
    }
  }
  StackDestroy(&q);
}

特性总结:

1.快排,顾名思义综合性能最好,适应场景最多

2.时间复杂度:O(NlogN)

3.空间复杂度:O(logN)

4.稳定性:不稳定

5. 归并排序

归并排序是建立在归并操作上的一种有效的排序算法,该算法采用分治法(Divide and Conquer)。将已有序的子序列合并,得到完全有序的序列;即先保证子序列有序,然后将两个子序列合并成一个有序序列,称为二路归并。核心步骤:先分解、后合并。

归并递归版本

// 归并排序递归
void MergeSort(int* a, int n)
{
  int* tem = (int*)malloc(sizeof(int) * n);
  if (tem == NULL)
  {
    printf("malloc failed");
    exit(0);
  }
  _MergeSort(a, 0, n - 1, tem);
  free(tem);
}
void _MergeSort(int* a, int begin, int end, int* tem)
{
  //begin和end左闭右闭区间
  if (begin >= end) //只有一个元素就不需要归并了
  {
    return;
  }
  //1.分解
  int mid = (begin + end) / 2;
  _MergeSort(a, begin, mid, tem);
  _MergeSort(a, mid+1, end, tem);
  //2.合并(都是闭区间)
  int begin1 = begin, end1 = mid;
  int begin2 = mid + 1, end2 = end;
  int j = begin;
  //开始合并
  while (begin1 <= end1 && begin2 <= end2)
  {
    if (a[begin1] < a[begin2])
    {
      tem[j++] = a[begin1++];
    }
    else
    {
      tem[j++] = a[begin2++];
    }
  }
  while (begin1 <= end1)
  {
    tem[j++] = a[begin1++];
  }
  while (begin2 <= end2)
  {
    tem[j++] = a[begin2++];
  }
  //合并好一部分就拷贝回去,左闭右闭加1
  memcpy(a + begin, tem + begin, sizeof(int) * (end - begin + 1));
}

归并非递归版本

因为归并的递归类似于后续遍历,因此不适合用栈来模拟实现,会导致丢失区间问题。因此我们考虑使用循环来修改。利用gap来遍历数组,模拟二路归并的过程,注意区间的控制问题(很棘手)

归并一次后同意合并:

// 归并排序非递归
//后序遍历用栈不好改非递归
//考虑用循环
//第一轮,一个一个排,第二轮两个两个排,
void MergeSortNonR(int* a, int n)
{
  int begin = 0;
  int end = n - 1;
  int* tem = (int*)malloc(sizeof(int) * n);
  assert(tem);
  memset(tem, 0, sizeof(int) * n);
  //排序
  int gap = 1;
  while (gap < n)
  {
    //printf("gap = %d: ", gap);
    for (int i = 0; i < n; i = i + 2 * gap)
    {
      int begin1 = i, end1 = i + gap - 1;
      int begin2 = i + gap, end2 = begin2 + gap - 1;
      //防止越界,修正边界
      //end1越界
      if (end1 >= n)
      {
        //把end1修成最后一个数
        end1 = n - 1;
        begin2 = n;
        end2 = n - 1;
      }
      else if(begin2 >= n)// begin2越界
      {
        begin2 = n;
        end2 = n - 1;
      }
      else if (end2 >= n) // end2越界
      {
        end2 = n - 1;
      }
      //printf("[%d,%d] [%d,%d]->", begin1, end1, begin2, end2);
      //往tem,排序
      int j = i;//比较位置开始,即从begin1
      while (begin1 <= end1 && begin2 <= end2)
      {
        if (a[begin1] <= a[begin2])
        {
          tem[j++] = a[begin1++];
        }
        else
        {
          tem[j++] = a[begin2++];
        }
      }
      while (begin1 <= end1)
      {
        tem[j++] = a[begin1++];
      }
      while (begin2 <= end2)
      {
        tem[j++] = a[begin2++];
      }
    }
    //排完全部的再拷贝
    //合并
    //printf("\n");
    memcpy(a, tem, sizeof(int) * (end - begin + 1));
    gap = gap * 2;
  }
}

归并一个区间就合并:

// 归并排序非递归
//后序遍历用栈不好改非递归
//考虑用循环
//第一轮,一个一个排,第二轮两个两个排,
void MergeSortNonR(int* a, int n)
{
  int begin = 0;
  int end = n - 1;
  int* tem = (int*)malloc(sizeof(int) * n);
  assert(tem);
  memset(tem, 0, sizeof(int) * n);
  //排序
  int gap = 1;
  while (gap < n)
  {
    //printf("gap = %d: ", gap);
    for (int i = 0; i < n; i = i + 2 * gap)
    {
      int begin1 = i, end1 = i + gap - 1;
      int begin2 = i + gap, end2 = begin2 + gap - 1;
      //防止越界,修正边界
      //end1越界
      if (end1 >= n)
      {
        //把end1修成最后一个数
        end1 = n - 1;
        begin2 = n;
        end2 = n - 1;
      }
      else if (begin2 >= n)// begin2越界
      {
        begin2 = n;
        end2 = n - 1;
      }
      else if (end2 >= n) // end2越界
      {
        end2 = n - 1;
      }
      //记录每次归并几个,就拷贝几个
      int count = end2 - begin1 + 1;
      //printf("[%d,%d] [%d,%d]->", begin1, end1, begin2, end2);
      //往tem,排序
      int j = i;//比较位置开始,即从begin1
      while (begin1 <= end1 && begin2 <= end2)
      {
        if (a[begin1] <= a[begin2])
        {
          tem[j++] = a[begin1++];
        }
        else
        {
          tem[j++] = a[begin2++];
        }
      }
      while (begin1 <= end1)
      {
        tem[j++] = a[begin1++];
      }
      while (begin2 <= end2)
      {
        tem[j++] = a[begin2++];
      }
      //排完一部分就拷贝
      memcpy(a + i, tem + i, sizeof(int) * count);
    }
    gap = gap * 2;
  }
}

特性总结

1.缺点空间复杂度为O(N),该算法主要用于外部排序的思想。

2.时间复杂度:O(NlogN)

3.空间复杂度:O(N)

4.稳定性:稳定

6.计数排序

计数排序又称鸽巢原理,类似于哈希定址法:第一次统计数值出现的次数,根据统计的结果将序列回收到原来的序列中。

//计数排序
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 count = max - min + 1;
  int* tem = (int*)malloc(sizeof(int) * count);
  assert(tem);
  memset(tem, 0, sizeof(int) * count);
  //遍历数组记录数据出现次数
  for (int i = 0; i < n; i++)
  {
    tem[a[i] - min]++;
  }
  int j = 0;
  //排序,tem中出现几次就往a写几个
  for (int i = 0; i < count; i++)
  {
    //只要tem[i]不为0,就会进入循环
    while (tem[i]--)
    {
      a[j++] = i + min;
    }
  }
}

特性总结

1.计数排序在数据范围集中时,效率很高,但不适合浮点数,数据范围相差太大的数据。

2.时间复杂度:O(max(N,range))

3.空间复杂度:O(range)

4.稳定性:稳定

以上就是本文对排序知识的总结,如有问题,欢迎在评论区讨论。

目录
相关文章
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
62 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
数据可视化 搜索推荐 Python
Leecode 刷题笔记之可视化六大排序算法:冒泡、快速、归并、插入、选择、桶排序
这篇文章是关于LeetCode刷题笔记,主要介绍了六大排序算法(冒泡、快速、归并、插入、选择、桶排序)的Python实现及其可视化过程。
13 0
|
1月前
|
搜索推荐 算法
排序算法---冒泡&选择&插入总结
排序算法---冒泡&选择&插入总结
14 0
|
3月前
|
搜索推荐 算法
十大排序算法-快排-希尔-堆排-归并-冒泡-桶排-选择-插入-计数-基数
十大排序算法-快排-希尔-堆排-归并-冒泡-桶排-选择-插入-计数-基数
十大排序算法-快排-希尔-堆排-归并-冒泡-桶排-选择-插入-计数-基数
|
3月前
|
数据采集 算法
基于PSO粒子群算法的三角形采集堆轨道优化matlab仿真
该程序利用PSO算法优化5个4*20矩阵中的模块采集轨迹,确保采集的物品数量及元素含量符合要求。在MATLAB2022a上运行,通过迭代寻优,选择最佳模块组合并优化轨道,使采集效率、路径长度及时间等综合指标最优。具体算法实现了粒子状态更新、需求量差值评估及轨迹优化等功能,最终输出最优轨迹及其相关性能指标。
|
3月前
|
数据采集 搜索推荐 算法
【高手进阶】Java排序算法:从零到精通——揭秘冒泡、快速、归并排序的原理与实战应用,让你的代码效率飙升!
【8月更文挑战第21天】Java排序算法是编程基础的重要部分,在算法设计与分析及实际开发中不可或缺。本文介绍内部排序算法,包括简单的冒泡排序及其逐步优化至高效的快速排序和稳定的归并排序,并提供了每种算法的Java实现示例。此外,还探讨了排序算法在电子商务、搜索引擎和数据分析等领域的广泛应用,帮助读者更好地理解和应用这些算法。
41 0
|
24天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
9天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
|
10天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
11天前
|
存储 算法 决策智能
基于免疫算法的TSP问题求解matlab仿真
旅行商问题(TSP)是一个经典的组合优化问题,目标是寻找经过每个城市恰好一次并返回起点的最短回路。本文介绍了一种基于免疫算法(IA)的解决方案,该算法模拟生物免疫系统的运作机制,通过克隆选择、变异和免疫记忆等步骤,有效解决了TSP问题。程序使用MATLAB 2022a版本运行,展示了良好的优化效果。