【算法】排序——选择排序和交换排序(快速排序)

简介: 上篇文章讲述了插入排序及插入排序的优化希尔排序,今天我们继续给大家带来排序中的选择排序和交换排序,选择排序包括直接选择排序、 其中还包括堆排序,因为之前讲过堆排序,这篇文章就不多讲解,点击直达堆排序。交换排序包括冒泡排序、快速排序。让我们开始今天的选择排序之旅吧!!!

选择排序

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


直接选择排序

在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素

若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换

在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素


实现代码

//交换函数
void swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
//直接选择排序
void SelectSort(int* a, int n)
{
  for (int i = 0; i < n; i++)
  {
    int mini = i;
    for (int j = i + 1; j < n; j++)
    {
      if (a[mini] > a[j])
      {
        mini = j;
      }
    }
    swap(&a[i], &a[mini]);
  }
}

直接选择排序的特性总结:

1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用

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

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

4. 稳定性:不稳定


交换排序

基本思想:

所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。

交换排序的特点是:

将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。


冒泡排序

实现代码

void BubbleSort(int* a, int n)
{
  for (int j = 0; j < n; j++)
  {
    for (int i = 0; i < n - 1-j; i++)
    {
      if (a[i + 1] < a[i])
      {
        Swap(&a[i + 1], &a[i]);
      }
    }
  }
}

如果我们给一个有序数组的话,冒泡排序还是会两两相比较,会很麻烦我们不妨给一个变量,如果发生交换的话就改变这个变量的值每一趟冒泡排序后我们判断这个值是否改变,如果没改变则这个数组是有序的。


冒泡排序优化

void BubbleSort(int* a, int n)
{
  for (int j = 0; j < n; j++)
  {
    int tmp = 1;
    for (int i = 0; i < n - 1-j; i++)
    {
      if (a[i + 1] < a[i])
      {
        Swap(&a[i + 1], &a[i]);
                tmp=0;
      }
    }
    if (tmp == 1)
    {
      return;
    }
  }
}

快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。



这里我们能发现每一趟排序后的基准值都会被排序到正确的位置,因此我们以排好序的基准值为分界线,将数组分成左右两部分,左右两部分再根据排序的方法进行排序。每次排序都会产生一个排好序的基准值,每次都要根据这个基准值将数组分成两部分。因此我们考虑使用递归。


hoare法


实现代码

//hoare法
int PartSort1(int* a, int left, int right)
{
  int keyi = left;
  while (left < right)
  {
    //右边找小
    //防止越界, 防止死循环
    while (left < right && a[right] >= a[keyi])
    {
      right--;
    }
    //左边找大
    while (left < right && a[left] <= a[keyi])
    {
      left++;
    }
    Swap(&a[left], &a[right]);
  }
  Swap(&a[keyi], &a[left]);
  return left;
}
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  int keyi = PartSort1(a, begin, end);
  //[0 , keyi-1] keyi [keyi+1 , end]
  QuickSort(a, keyi + 1, end);
  QuickSort(a, begin, keyi - 1);
}


挖坑法


实现代码


//挖坑法
int PartSort2(int* a, int left, int right)
{
  int key = a[left];
  int keyi = left;
  while (left < right)
  {
    while (left < right && a[right] >= a[keyi])
    {
      right--;
    }
    a[keyi] = a[right];
    keyi = right;
    while (left < right && a[left] <= a[keyi])
    {
      left++;
    }
    a[keyi] = a[left];
    keyi = left;
  }
  a[keyi] = key;
  return keyi;
}
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  int keyi = PartSort2(a, begin, end);
  //[0 , keyi-1] keyi [keyi+1 , end]
  QuickSort(a, keyi + 1, end);
  QuickSort(a, begin, keyi - 1);
}

双指针法

实现代码

//双指针法
int PartSort3(int* a, int left, int right)
{
  int prev = left;
  int cur = left + 1;
  int keyi = left;
  while (cur <= right)
  {
    if (a[cur] < a[keyi])
    {
      Swap(&a[++prev], &a[cur]);
    }
    cur++;
  }
  Swap(&a[prev], &a[keyi]);
  return  prev;
}
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  int keyi = PartSort3(a, begin, end);
  //[0 , keyi-1] keyi [keyi+1 , end]
  QuickSort(a, keyi + 1, end);
  QuickSort(a, begin, keyi - 1);
}

上面的三中方法我们使用函数递归来完成的,如果给予一个很大的数组需要排序,递归的深度可能会很深,会导致栈溢出。


快速排序优化

我们能否发现快速排序的递归很像二叉树的遍历,但是还有一个问题我们每次选的基准值排好序后都不一定都处于数组中大致的中间位置,这是我们理想的情况,如果不理想呢,每次选好的基准值排好序后都位于开头,这样只能将一个数组元素分割出去,而不是将近分开一半一半。

最好情况:每次都将近分开一半。


时间复杂度:O(N*logN)

最坏的情况:每次都分开一小部分

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


优化一、三数取中

我们在传参数时会传数组的长度,我们不妨在在数组头,数组尾和数组中间这三个值之间挑出中间值放在数组的头部,作为基准值,在进行排序。

实现代码

int GetMidi(int* a, int left, int right)
{
  int mid= (left + right) / 2;
  if (a[left] < a[mid])
  {
    if (a[mid] < a[right])
    {
      return mid;
    }
    else if (a[left] > a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
  else //left>mid
  {
    if (a[mid] > a[right])
    {
      return mid;
    }
    else if (a[left] < a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
}

这样我们实现了三数取中,再将下面的代码放到三种方法排序之前就可以了

int mid = GetMidi(a, left, right);
  Swap(&a[left], &a[mid]);

这样我们初步的优化就完成了。


优化二、小区间优化

我们一上面的10个元素的数组为例,递归深度只有三四层没必要使用递归向下深入,继续浪费栈空间,我们不妨当数组元素大于等于10的时候递归,当数组元素小于10时使用插入排序。


实现代码

void QuickSort(int* a, int begin,int end)
{
  if (begin >= end)
    return;
  //小区间优化,小区间不在递归分割排序,减少递归次数
  if((end-begin+1)>10)
  { 
    int keyi = PartSort3(a, begin, end);
    //[begin,keyi-1] keyi [keyi+1 ,end]zd
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
  }
  else
  {
    InsertSort(a + begin, end - begin + 1);
  }
}

快速排序非递归

这里我们使用栈数据结构配合数组下标来完成非递归的实现。

实现代码

void QuickSortNonR(int* a, int left, int right)
{
  ST st;
  STInit(&st);
  STPush(&st, right);
  STPush(&st, left);
  while (!STEmpty(&st))
  {
    int begin = STTop(&st);
    STPop(&st);
    int end = STTop(&st);
    STPop(&st);
    int keyi = PartSort1(a, begin, end);
    if (keyi+1<end)
    {
      STPush(&st, right);
      STPush(&st, keyi + 1);
    }
    if (keyi - 1 > begin)
    {
      STPush(&st, keyi - 1);
      STPush(&st, left);
    }
  }
  STDer(&st);
}


快速排序的特性总结:

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

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



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

4. 稳定性:不稳定


选择排序和快速排序的基本内容及优化到这就讲完了,其中快速排序使用非递归来实现确实优点难理解,大家可以一边分析代码,一边画图来理解。希望能积极指出文章中的错误,下期带来排序的最后一篇文章归并排序,敬请期待!!!  


相关文章
|
8天前
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
31 4
|
10天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
51 8
|
10天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
37 7
|
1月前
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
20 1
|
1月前
|
算法 搜索推荐 Java
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
基数排序是一种稳定的排序算法,通过将数字按位数切割并分配到不同的桶中,以空间换时间的方式实现快速排序,但占用内存较大,不适合含有负数的数组。
23 0
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
|
1月前
|
算法 搜索推荐
数据结构与算法学习十一:冒泡排序、选择排序、插入排序
本文介绍了冒泡排序、选择排序和插入排序三种基础排序算法的原理、实现代码和测试结果。
17 0
数据结构与算法学习十一:冒泡排序、选择排序、插入排序
|
23天前
|
算法 安全 Java
介绍一下比较与交换算法
【10月更文挑战第20天】介绍一下比较与交换算法
14 0
|
1月前
|
算法
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
31 0
|
25天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
10天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。