如何优化快速排序?

简介: 如何优化快速排序?

前言:

还记得上次的快速排序吗?还记得 key 是怎么取的吗?当时我直接把数组的起始元素作为了 key 值,其实这样做是有弊端的,试想:一个降序的数列,要排列成升序的序列,最大的数字作为 key 值,那岂不是效率较低?所以,这就意味着仍有优化的空间,那么这期就来优化下快速排序。

注:

这篇博客属于数据结构内容,建议学习完C语言的小伙伴食用~


目录

🍕Part1:取随机数法

1.思想

2.实现

🍔Part2:三数取中法

1.思想

2.实现

🍟Part3:小区间调整

1.思路

2.实现

🌭Part4:三种方法对比

1.测试性能的试金石

2.开始测试



Part1:取随机数法


1.思想


这个思想就是字面意思啦 ~ 就是取一个数组中极大值与极小值之间的一个随机数

为什么这样做会优化一点时间呢?

我们回忆一下快速排序,它的思想就是找一个 key 值,一趟下来,数组 左部分 的数字都 小于 key 右部分 的数字都 大于 key

我们进行调整,就是在 key的选取上下功夫,

试想:你选择的 key 值是数组中最大的,一趟下来,只有 key 值的位置变了,那岂不是挫到家了?

88374438178085ad0ba2ae95f53f497f_c0eaabbf87be4f17925c4549b08f1e89.png

我们要尽量避免这种情况发生,就可以随机一些,数字的量越大,取到最大值的概率越小,近似认为避免了这种情况。


2.实现


实现很简单,加一个生成随机数的函数,由于取的是 keyi,即 key 的下标,所以随机数模上数组的长度,再加上 left 即可(一趟过后开始递归, 左部分下标不是从0开始 ):

//快排(前后指针法)(取随机数优化)
void QuickSort3(int* a, int left, int right)
{
  if (left >= right)
    return;
  // 取随机数
  //int randi = left + (rand() % (right - left));
  //Swap(&a[randi], &a[left]); // 交换randi与left对应的数字
  int keyi = left;
  int key = a[left];
  int prev = left;
  int cur = left + 1;
  while (cur <= right)
  {
    if (a[cur] < key && ++prev != cur)
      Swap(&a[cur], &a[prev]);
    cur++;
  }
  Swap(&a[keyi], &a[prev]);//与下标为keyi的交换
  keyi = prev;
  //递归
  QuickSort3(a, left, keyi - 1);
  QuickSort3(a, keyi + 1, right);
}


Part2:三数取中法


1.思想


在看随机数排列的时候,我相信你已经开始思考这个问题了:

与其说是避免取到不合适的值,不如说是直接取最合适的值

是的,三数取中法就是取最合适的值,那么什么样的值是最合适的呢?

中值最合适;

如何找到中值呢?

最简单的就是取三个值:最左值,最右值,中间值挨个比较;

下面看看是如何实现的:


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 // a[left] > a[mid]
  {
    if (a[left] < a[right])
      return left;
    else if (a[mid] > a[right])
      return mid;
    else
      return right;
  }
}


解释:

其实就是分两种大情况:

8d55cbbe0d366c8d19f3024d242c08f4_cd56f7abcd5d48ae83588620661b6c81.png

在这两种大情况下取中。

接入排序中,就是这样的:

  int midi = GetMidi(a, left, right);
  if (midi != left)
    Swap(&a[midi], &a[left]);


Part3:小区间调整


1.思路


有这种调整方法是因为当数据数量较小时,利用快速排序未免有杀鸡焉用牛刀的感觉,

想想,五个数字排有序,需要调用七次快排函数,是不是大题小作?

所以在这种数据量小的情况下,不妨利用其他的排序,

那么选择哪个排序呢?

选择最灵活的, 稳定的,那就是插入排序。


2.实现


可以在数字长度小于10的情况下进行插入排序,大于10就进行快速排序:

void QuickSort(int* a, int left, int right)
{
  if (left >= right)
    return;
  int keyi = PartSort3(a, left, right);
  // 小区间调整
  if ((right - left + 1) > 10)
  {
    QuickSort(a, left, keyi - 1);
    QuickSort(a, keyi + 1, right);
  }
  InsertSort(a + left, right - left + 1);// 插入排序不再赘叙
}


Part4:三种方法对比


1.测试性能的试金石


生成N个随机数,测试每种排序的性能,输出的是排序所用的时间(单位毫秒

// 测试排序的性能对比
void TestOP()
{
  srand(time(0));
  const int N = 1000000;// 数据的数量
  int* a1 = (int*)malloc(sizeof(int) * N);
  int* a2 = (int*)malloc(sizeof(int) * N);
  int* a3 = (int*)malloc(sizeof(int) * N);
  int* a4 = (int*)malloc(sizeof(int) * N);
  int* a5 = (int*)malloc(sizeof(int) * N);
  int* a6 = (int*)malloc(sizeof(int) * N);
  for (int i = 0; i < N; ++i)
  {
    a1[i] = rand();
    a2[i] = a1[i];
    a3[i] = a1[i];
    a4[i] = a1[i];
    a5[i] = a1[i];
    a6[i] = a1[i];
  }
  int begin1 = clock();
  //InsertSort(a1, N);
  int end1 = clock();
  int begin2 = clock();
  ShellSort(a2, N);
  int end2 = clock();
  int begin3 = clock();
  //SelectSort(a3, N);
  int end3 = clock();
  int begin4 = clock();
  HeapSort(a4, N);
  int end4 = clock();
  int begin5 = clock();
  QuickSort2(a5, 0, N - 1);
  int end5 = clock();
  int begin6 = clock();
  //MergeSort(a6, N);
  int end6 = clock();
  printf("InsertSort:%d\n", end1 - begin1);
  printf("ShellSort:%d\n", end2 - begin2);
  printf("SelectSort:%d\n", end3 - begin3);
  printf("HeapSort:%d\n", end4 - begin4);
  printf("QuickSort:%d\n", end5 - begin5);
  printf("MergeSort:%d\n", end6 - begin6);
  free(a1);
  free(a2);
  free(a3);
  free(a4);
  free(a5);
  free(a6);
}


2.开始测试


这里只测试快速排序:

image.png

注意:只是简单的样本采集(甚至不算样本),不具有统计学意义,仅供参考

似乎单独三数取中法是最好的... ...

我做完之后对这个数据挺诧异的:

为什么三数取中法与小区间调整共用之后更慢了

有研究过的大佬可以教教我🤣


总结:

(总结)

目录
相关文章
|
18天前
|
存储 人工智能 运维
企业Agent解决方案全解析:从技术原理到落地实践,一文扫清
企业Agent正重塑数字化转型:作为具备自主决策能力的“数字员工”,它打通系统孤岛、实现流程闭环,提升效率72%。从金融风控到智能制造,覆盖多行业场景,2025年全球市场规模超1200亿美元。选型需聚焦场景、集成、安全与服务,未来将迈向多Agent协同与行业深度定制。
|
Android开发
|
Linux Python Windows
Centos7 下安装python3及卸载
Centos7 下安装python3及卸载
1587 0
Centos7 下安装python3及卸载
|
机器学习/深度学习 PyTorch TensorFlow
|
负载均衡 网络协议 安全
H12-831-ARS-EXA
H12-831-ARS-EXA
991 0
H12-831-ARS-EXA
|
存储 JavaScript 前端开发
块级作用域和函数作用域的区别在哪些方面会对性能产生影响?
【10月更文挑战第29天】块级作用域和函数作用域在变量查找效率、内存管理、闭包、代码执行顺序以及作用域链维护等方面的区别,都会在不同程度上对性能产生影响。在实际开发中,需要根据具体的代码逻辑、应用场景和性能需求,合理地选择和运用这两种作用域,以达到最佳的性能和代码质量平衡。
|
监控 Linux 数据处理
|
SQL Cloud Native 数据挖掘
Hologres:高性能实时数据分析引擎
Hologres:高性能实时数据分析引擎
|
存储 算法 C++
虚拟存储管理(OPT,FIFO,LRU,LFU,NUR算法的C++实现)
虚拟存储管理(OPT,FIFO,LRU,LFU,NUR算法的C++实现)
645 1
Flutter 进度条
Flutter 进度条,详细介绍 Flutter 提供了多种类型的进度条,可以用于展示应用程序中的长时间运行任务的进度。进度条可以是线性或圆形,可以显示确定进度或不确定进度,可以按照应用程序的主题进行自定义。
345 0