【探索排序算法的魅力:优化、性能与实用技巧】(上)

简介: 【探索排序算法的魅力:优化、性能与实用技巧】

本章重点


  1. 排序的概念及其运用
  2. 常见排序算法的实现
  3. 排序算法复杂度及稳定性分析


1.排序的概念及其运用


1.1排序的概念


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


稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。


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


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


1.2排序运用



1.3 常见的排序算法



2.常见排序算法的实现


2.1 插入排序


2.1.1基本思想:


直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。


实际中我们玩扑克牌时,就用了插入排序的思想


2.1.2直接插入排序:


       当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。


插入过程:如果end+1位置的值小于end位置,就将end位置的值挪到end+1位置处,否则,直接插入到end+1位置。注意,这里需要保存end+1位置的值,因为end位置的值挪到end+1位置时,会覆盖end+1位置的值。


结束条件:当end+1位置的值小于当前有序序列所有的值时,此时end的值为-1,也就是排序的终止条件


单趟插入排序代码:单趟[0,end]有序,把end+1位置的值插入到前面序列,控制[0,end+1]序列有序。

// 插入排序
void InsertSort(int* a, int n)
{
  int end = ;
  //保存要排序的值,防止数据被覆盖找不到数据
  int temp = a[end + 1];
  while (end >= 0)
  {
    if (temp < a[end])
    {
      a[end + 1] = a[end];
    }
    else
    {
      break;
    }
  }
  //将temp的值赋给end+1位置,这样就插入有序了
  a[end + 1] = temp;
}


整趟插入排序:我们默认第一个数有序,第二个数插入到有序序列中,调整有序,那么end就是0,如下图,当最后一个元素插入有序系列中,此时end的值时n-2。

#include <stdio.h>
// 插入排序
void InsertSort(int* a, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
    int end = i;
    //保存要排序的值,防止数据被覆盖找不到数据
    int temp = a[end + 1];
    while (end >= 0)
    {
      if (temp < a[end])
      {
        a[end + 1] = a[end];
      }
      else
      {
        break;
      }
      end--;
    }
    //将temp的值赋给end+1位置,这样就插入有序了
    a[end + 1] = temp;
  }
}
int main()
{
  int a[] = { 1,4,2,7,6,9 };
  InsertSort(a, 6);
  for (int i = 0; i < 6; i++)
  {
    printf("%d ", a[i]);
  }
  return 0;
}



直接插入排序的特性总结:


1. 元素集合越接近有序,直接插入排序算法的时间效率越高

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1),它是一种稳定的排序算法

4. 稳定性:稳定


2.1.3 希尔排序( 缩小增量排序 )


希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。


思想:

  1. 预排序:间接为gap为一组进行排序
  2. 直接插入排序


单趟排序:将gap为一组的数据进行排序,希尔排序和上面的直接插入排序方法相同,只是希尔排序是将gap间距的数据进行直接插入排序,若gap==1时,希尔排序和直接插入排序相同。

// 希尔排序
void ShellSort(int* a, int n)
{
  int gap = 5;
  for (int i = 0; i < n - gap; i+=gap)
  {
    int end = i;
    int temp = a[end + gap];
    while (end >= 0)
    {
      if (a[end] > temp)
      {
        a[end + gap] = a[end];
      }
      else
      {
        break;
      }
      end -= gap;
    }
    a[end + gap] = temp;
  }
}


多趟排序:通过gap将一组数据分割,依次将分割的数据直接插入排序,每组的数据都将有序。

// 希尔排序
void ShellSort(int* a, int n)
{
  int gap = 5;
  for (int j = 0; j < gap; j++)
  {
    for (int i = j; i < n - gap; i += gap)
    {
      int end = i;
      int temp = a[end + gap];
      while (end >= 0)
      {
        if (a[end] > temp)
        {
          a[end + gap] = a[end];
        }
        else
        {
          break;
        }
        end -= gap;
      }
      a[end + gap] = temp;
    }
  }
}


上面的代码将gap组数据进行直接插入排序,是一组数据排完后再排下一组数据,下面我们来看一个更厉害的代码,它在上面的基础上省略了一个for循环。

// 希尔排序
void ShellSort(int* a, int n)
{
  int gap = 5;
  for (int i = 0; i < n - gap; i++)
  {
    int end = i;
    int temp = a[end + gap];
    while (end >= 0)
    {
      if (a[end] > temp)
      {
        a[end + gap] = a[end];
      }
      else
      {
        break;
      }
      end -= gap;
    }
    a[end + gap] = temp;
  }
}


上面的代码不是一组数据排完后再排下一组数据,它不用跳到下一组数据排序,而是不跨组排序,多组并排,比刚刚的代码更简洁。经过上面的排序,我们只是使得每组数据有序,并没有使得全部的数据有序。上面的排序使得大的数据更快的移动到后面,小的数据更快的移动到前面,gap越大跳的越快,越不接近有序,gap越小跳的越慢,越接近有序。gap为1,该数据就有序了。所有要想数据有序,就要让gap为1。gap>1时就是预排序,gap==1时就是有序。

// 希尔排序
void ShellSort(int* a, int n)
{
  int gap = n;
  while (gap > 1)
  {
    gap /= 2;//gap等于1,数据有序
    for (int i = 0; i < n - gap; i++)
    {
      int end = i;
      int temp = a[end + gap];
      while (end >= 0)
      {
        if (a[end] > temp)
        {
          a[end + gap] = a[end];
        }
        else
        {
          break;
        }
        end -= gap;
      }
      a[end + gap] = temp;
    }
  }
}
int main()
{
  int a[] = { 1,4,2,7,6,9 };
  ShellSort(a, 6);
  for (int i = 0; i < 6; i++)
  {
    printf("%d ", a[i]);
  }
  return 0;
}

我们上面定义的gap是每次除2,这样排序的次数还是有点多,所以我们定义的gap是每次除3,但是除3不一定能保证最后一次是1,比如gap为8,除3是2,再除3就是0,所以我们只需要除3后加1就可以满足最后一次gap为0。

// 希尔排序
void ShellSort(int* a, int n)
{
  int gap = n;
  while (gap > 1)
  {
    gap = gap / 3 + 1;//gap等于1,数据有序
    for (int i = 0; i < n - gap; i++)
    {
      int end = i;
      int temp = a[end + gap];
      while (end >= 0)
      {
        if (a[end] > temp)
        {
          a[end + gap] = a[end];
        }
        else
        {
          break;
        }
        end -= gap;
      }
      a[end + gap] = temp;
    }
  }
}



希尔排序的特性总结:


  1. 希尔排序是对直接插入排序优化
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:因为咋们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:


2.2 选择排序


2.2.1基本思想


每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。


2.2.2 直接选择排序


  • 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素



// 选择排序
void SelectSort(int* a, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
    //假设下标为0的数据最小
        int mini = i;
    for (int j = mini + 1; j < n; j++)
    {
      if (a[mini] > a[j])
      {
        mini = j;
      }
    }
    //此时a[mini]是数组中最小的值
    Swap(&a[i], &a[mini]);
  }
}


但是上面的每次都是找最小值,然后再放到正确的位置,效率太低,我们是不是可以每次找出一个最大值,再找出一个最小值,然后各自放到正确的位置上,这样效率就提高了很多。

// 选择排序
void SelectSort(int* a, int n)
{
  int begin = 0;
  int end = n - 1;
  while(begin < end)
  {
    int mini = begin;
    int maxi = begin;
    for (int i = begin + 1; i <= end; i++)
    {
      if (a[mini] > a[i])
      {
        mini = i;
      }
      if (a[maxi] < a[i])
      {
        maxi = i;
      }
    }
    //此时a[mini]是数组中最小的值
    //此时a[maxi]是数组中最大的值
    Swap(&a[begin], &a[mini]);
    Swap(&a[end], &a[maxi]);
    begin++;
    end--;
  }
}



为什么结果不对呢?我们上面的代码出现了什么问题?


很明显,按照上图的情况,maxi和begin下标相同,当begin和minx下标的数据交换之后,maxi的下标的值就发生变化了,此时end和maxi交换就不正确了,所以上面的程序是有问题的。我们只需要更新一下maxi的位置就行啦。

void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
// 选择排序
void SelectSort(int* a, int n)
{
  int begin = 0;
  int end = n - 1;
  while(begin < end)
  {
    int mini = begin;
    int maxi = begin;
    for (int i = begin + 1; i <= end; i++)
    {
      if (a[mini] > a[i])
      {
        mini = i;
      }
      if (a[maxi] < a[i])
      {
        maxi = i;
      }
    }
    //此时a[mini]是数组中最小的值
    //此时a[maxi]是数组中最大的值
    Swap(&a[begin], &a[mini]);
    //maxi如果被换走就要修正一下
    if (maxi == begin)
      maxi = mini;
    Swap(&a[end], &a[maxi]);
    begin++;
    end--;
  }
}
int main()
{
  int a[] = { 9,1,2,5,7,4,8,6,3,5,1,2,3,5,1,8,3 };
  SelectSort(a, 17);
  for (int i = 0; i < 17; i++)
  {
    printf("%d ", a[i]);
  }
}



2.2.3 堆排序


       堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是 通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。


所以我们可以建大堆,将堆顶的数据和最后一个叶子结点交换,由于此时的堆结构没有破坏,左子树和右子树仍然是堆,使用堆的向下调整去调整堆,然后在缩小下次向下调整的范围,也就是把最大的那个数不算做堆的范围了,这样最大的数据就保存在了下标最大的位置处,满足了升序的要求。

void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
// 堆排序
void AdjustDown(int* a, int n, int parent)
{
  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 = 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--;
  }
}



堆排序的特性总结:

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定


【探索排序算法的魅力:优化、性能与实用技巧】(中):https://developer.aliyun.com/article/1425258

相关文章
|
10天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
10天前
|
机器学习/深度学习 算法 5G
基于MIMO系统的SDR-AltMin混合预编码算法matlab性能仿真
基于MIMO系统的SDR-AltMin混合预编码算法通过结合半定松弛和交替最小化技术,优化大规模MIMO系统的预编码矩阵,提高信号质量。Matlab 2022a仿真结果显示,该算法能有效提升系统性能并降低计算复杂度。核心程序包括预编码和接收矩阵的设计,以及不同信噪比下的性能评估。
27 3
|
11天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
13天前
|
机器学习/深度学习 算法 数据挖掘
提高时钟置换算法的性能
【10月更文挑战第25天】通过上述一种或多种方法的综合应用,可以在不同程度上提高时钟置换算法的性能,使其更好地适应各种复杂的系统环境和应用场景,提高虚拟内存管理的效率和系统的整体性能。
33 5
|
21天前
|
人工智能 算法 数据安全/隐私保护
基于遗传优化的SVD水印嵌入提取算法matlab仿真
该算法基于遗传优化的SVD水印嵌入与提取技术,通过遗传算法优化水印嵌入参数,提高水印的鲁棒性和隐蔽性。在MATLAB2022a环境下测试,展示了优化前后的性能对比及不同干扰下的水印提取效果。核心程序实现了SVD分解、遗传算法流程及其参数优化,有效提升了水印技术的应用价值。
|
20天前
|
存储 缓存 算法
优化轮询算法以提高资源分配的效率
【10月更文挑战第13天】通过以上这些优化措施,可以在一定程度上提高轮询算法的资源分配效率,使其更好地适应不同的应用场景和需求。但需要注意的是,优化策略的选择和实施需要根据具体情况进行详细的分析和评估,以确保优化效果的最大化。
|
21天前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
22天前
|
存储 缓存 算法
前端算法:优化与实战技巧的深度探索
【10月更文挑战第21天】前端算法:优化与实战技巧的深度探索
19 1
|
24天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
9天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。