[数据结构 -- 手撕排序算法第六篇] 递归实现快速排序(集霍尔版本,挖坑法,前后指针法为一篇的实现方法,很能打)2

简介: [数据结构 -- 手撕排序算法第六篇] 递归实现快速排序(集霍尔版本,挖坑法,前后指针法为一篇的实现方法,很能打)2

5、前后指针版本

5.1 实现思路

我们规定排升序,排序数组名称为a,基准值 key。
1.选出一个key,key可以是需要排序的数组中任意一个元素,我们依然选key为a[left];


2.定义一个prev指针,和一个cur指针,初始化 prev 指向数组首部位置,cur 指向 prev 的下一个位置。cur先走,cur 找小于 key 的元素,找到之后停下来,让 prev++,然后交换 (a[cur], a[prev])。交换完继续往后走,cur 找的值不小于 key,cur继续往后走,找到后让 prev++,交换 (a[cur], a[prev]),不断重复此步骤;


3.当cur走完整个数组的时候,交换(a[left], a[prev]),这时key的最终位置就确定下来了。key 将数组分为左右两个子区间,左子区间小于 key,右子区间大于 key;


4.左右子区间继续重复前 3 步骤,递归下去就实现了数组的排序。

5.2 思路图解



这里不断交换其实是将小于 key 的值一直在往前抛,把大于 key 的值往后抛,cur与prev 之间的值其实就是大于 key 的所有值,不断交换就实现了最终以 key 划分的左右区间,左区间小于 key,右区间大于 key。

5.3 前后指针法代码实现

// 快速排序前后指针法
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] && ++prev != cur)
    {
      Swap(&a[prev], &a[cur]);
    }
    cur++;
  }
  Swap(&a[keyi], &a[prev]);
  return prev;
}
void QuickSort(int* a, int left, int right)
{
  if (left >= right)
    return;
  int keyi = PartSort3(a, left, right);
  QuickSort(a, left, keyi - 1);
  QuickSort(a, keyi + 1, right);
}

5.4 前后指针法代码测试

// 快速排序前后指针法
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] && ++prev != cur)
    {
      Swap(&a[prev], &a[cur]);
    }
    cur++;
  }
  Swap(&a[keyi], &a[prev]);
  return prev;
}
void QuickSort(int* a, int left, int right)
{
  if (left >= right)
    return;
  int keyi = PartSort3(a, left, right);
  QuickSort(a, left, keyi - 1);
  QuickSort(a, keyi + 1, right);
}
void test()
{
  int a[] = { 6,3,2,1,5,7,9 };
  QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
  Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
  test();
  return 0;
}



6、时间复杂度分析

6.1 最好情况

上面的三种情况下,最好情况时间复杂度是O(N* logN)。

每次 key 被排到区间的中间位置,像二叉树一样要递归 logN 次,每一次的子区间排序的时间复杂度是O(N),所以最好的情况就是O(N * logN)。


6.2 最坏情况

当数组有序的时候排序,无论 key 选最左边还是最右边,时间复杂度都是O(N^2)。



7、优化快速排序

快速排序的优化有两种思想:

1.我们对选key法可以进行优化;

2.递归到小的子区间,我们可以考虑使用插入排序,也称小区间优化。

7.1 选 key 优化

选 key 优化主要是针对数组有序,或者是接近有序。

对选 key 的优化我们有两种思路:

1.随机选 key;

2.三数取中选 key。(拿出left, mid, right,在下标为这三个位置的数中选出一个中间值作为 key)。

第一种思路是不可控的,所以第二种选 key 的思路才是最合适的。

下面是三数取中的优化代码:

int GetMidIndex(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 right;
    else
      return left;
  }
  else //a[left] > a[mid]
  {
    if (a[mid] > a[right])
      return mid;
    else if (a[left] > a[right])
      return right;
    else
      return left;
  }
}
int PartSort3(int* a, int left, int right)
{
  int midi = GetMidIndex(a, left, right);
  Swap(&a[left], &a[midi]);
  int prev = left;
  int cur = left + 1;
  int keyi = left;
  while (cur <= right)
  {
    if (a[cur] < a[keyi] && ++prev != cur)
    {
      Swap(&a[prev], &a[cur]);
    }
    cur++;
  }
  Swap(&a[prev], &a[keyi]);
  keyi = prev;
  return keyi;
}

我们取到中后,将该数字与 a[left]交换,依旧用之前的前后指针法的思路是没有问题的。霍尔版本与挖坑法是一样的优化方法。


如果我们不做三数取中的优化,当数组是有序或者接近有序的时候,时间复杂度会是最坏情况,O(N^2)。经过三数取中后,如果数组是有序的,时间复杂度仍是O(N * logN)。

7.2 小区间优化

在递归的时候,我们之前画的图中不难看到,在不断的划分的时候,到后面划分的越来越多了,当数据量特别大的时候,对栈的消耗会很大,会造成栈溢出的风险。因此,当划分到一定的程度,我们不再划分,直接选择插入排序。一般的情况下,当我们的子区间数据个数为10的时候,我们就不再递归了,直接就用插入排序。


实现代码:

// 插入排序
//时间复杂度(最坏):O(N^2) -- 逆序
//时间复杂度(最好):O(N) -- 顺序
void InsertSort(int* a, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
    int end = i;
    int tmp = a[i + 1];
    while (end >= 0)
    {
      if (a[end] > tmp)
      {
        a[end + 1] = a[end];
        end--;
      }
      else
      {
        break;
      }
    }
    a[end + 1] = tmp;
  }
}
int GetMidIndex(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 right;
    else
      return left;
  }
  else //a[left] > a[mid]
  {
    if (a[mid] > a[right])
      return mid;
    else if (a[left] > a[right])
      return right;
    else
      return left;
  }
}
// 快速排序前后指针法
//[left, right]
int PartSort3(int* a, int left, int right)
{
  int midi = GetMidIndex(a, left, right);
  Swap(&a[left], &a[midi]);
  int prev = left;
  int cur = left + 1;
  int keyi = left;
  while (cur <= right)
  {
    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 left, int right)
{
  //子区间只有一个值,或者子区间不存在的时候递归结束
  if (left >= right)
    return;
  //小区间优化
  if (right - left + 1 < 10)
  {
    InsertSort(a + left, right - left + 1);
  }
  int keyi = PartSort3(a, left, right);
  QuickSort(a, left, keyi - 1);
  QuickSort(a, keyi + 1, right);
}

这两种优化的方式在时间与空间两个方面都有一定程度的提升,但快速排序的本质没有改变,优化只是在原有的思想上锦上添花。

相关文章
|
2月前
|
机器学习/深度学习 算法 数据挖掘
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构。本文介绍了K-means算法的基本原理,包括初始化、数据点分配与簇中心更新等步骤,以及如何在Python中实现该算法,最后讨论了其优缺点及应用场景。
147 4
|
2天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
15 2
|
18天前
|
存储 运维 监控
探索局域网电脑监控软件:Python算法与数据结构的巧妙结合
在数字化时代,局域网电脑监控软件成为企业管理和IT运维的重要工具,确保数据安全和网络稳定。本文探讨其背后的关键技术——Python中的算法与数据结构,如字典用于高效存储设备信息,以及数据收集、异常检测和聚合算法提升监控效率。通过Python代码示例,展示了如何实现基本监控功能,帮助读者理解其工作原理并激发技术兴趣。
53 20
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
123 23
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
71 1
|
2月前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
62 4
|
2月前
|
算法 vr&ar 计算机视觉
数据结构之洪水填充算法(DFS)
洪水填充算法是一种基于深度优先搜索(DFS)的图像处理技术,主要用于区域填充和图像分割。通过递归或栈的方式探索图像中的连通区域并进行颜色替换。本文介绍了算法的基本原理、数据结构设计(如链表和栈)、核心代码实现及应用实例,展示了算法在图像编辑等领域的高效性和灵活性。同时,文中也讨论了算法的优缺点,如实现简单但可能存在堆栈溢出的风险等。
70 0
|
2月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
186 13
|
3月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
45 0