【初阶数据结构篇】冒泡排序和快速排序(中篇)

简介: 与直接插入排序法相比,比较次数一致,但冒泡排序的交换需要执行三次,而直接插入排序因为使用了tmp临时变量存储要插入的数据,只用执行一次,所以直接插入排序法效率明显更高。

【初阶数据结构篇】插入、希尔、选择、堆排序介绍(上篇):https://developer.aliyun.com/article/1590232?spm=a2c6h.13148508.setting.15.57374f0eRFNrzS



冒泡排序和快速排序


前言

本篇以排升序为例


代码位置

gitee


冒泡排序


动图理解



  • 作为第一个接触的排序算法,冒泡排序想必大家已经很熟悉了
  • 总共n个数据,要排n-1趟
  • 第i(i从0开始取)趟要比较n-1-i次
  • 等差数列求和,最坏时间复杂度为O(n2)



  • 定义exchange变量,当数组已经有序时不进入交换,直接跳出循环
  • 最好时间复杂度为O(n)



  • 空间复杂度O(1)


void BubbleSort(int* arr, int n)
{
  for (int i = 0; i < n-1; i++)
  {
    int exchange = 0;
    for (int j = 0; j < n - i - 1; j++)
    {
      //升序
      if (arr[j] < arr[j + 1])
      {
        exchange = 1;
        Swap(&arr[j], &arr[j + 1]);
      }
    }
    if (exchange == 0)
    {
      break;
    }
  }
}
  • 与直接插入排序法相比,比较次数一致,但冒泡排序的交换需要执行三次,而直接插入排序因为使用了tmp临时变量存储要插入的数据,只用执行一次,所以直接插入排序法效率明显更高
  • 与直接选择排序法相比,直接选择排序法无论数组是否有序都要执行到结束条件,不存在最好最坏时间复杂度。而冒泡排序因为使用了exchange变量进行优化,可以在最好时间复杂度上达到线性的结果。所以冒泡排序更胜一筹
  • 虽然但是,实际中还是不会使用冒泡排序,但它的教学意义是我们不能忽视的😂


快速排序


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


递归法实现

 快速排序实现主框架:


void QuickSort(int* arr, int left, int right)
{
  if (left >= right)
  {
    return;
  }
  //[left,right]--->找基准值mid
  int keyi = _QuickSort(arr, left, right);
  //左子序列:[left,keyi-1]
  QuickSort(arr, left, keyi - 1);
  //右子序列:[keyi+1,right]
  QuickSort(arr, keyi + 1, right);
}
  • 快速排序最重要的就是找基准值:
  • 基准值左边元素都小于它,右边都大于,显然的基准值所在的位置就是所有数据排序好后它应该在的位置上
  • 每次将这个数据(即基准值)放在正确的位置上,然后对其左右序列递归,最后所有数据都被放在了正确的位置上,排序就完成了





将区间中的元素进⾏划分的 _QuickSort ⽅法主要有以下⼏种实现⽅式:

hoare版本

算法思路


  • 假设将序列第一个数作为基准值
  • 定义左右指针
  • left:从左找比基准值大的 ,right:从右找比基准值小的
  • 找到后交换,left++,right–,进入下次循环
  • 跳出循环后交换基准值到正确位置


可以大致写出代码:

int _QuickSort1(int* arr, int left, int right)
{
  int keyi = left;
  ++left;

  while (left <right)
  {
    while (left < right && arr[right] > arr[keyi])
    {
      right--;
    }
    while (left < right && arr[left] < arr[keyi])
    {
      left++;
    }
    //right left
    if (left < right)
    {
      Swap(&arr[left++], &arr[right--]);
    }
  }
  
  Swap(&arr[keyi], &arr[right]);

  return right;
}

于是这里就抛出几个问题:


  1. 外层循环结束条件是否应该取=?
  2. 内层循环当right或left处数据和基准值相等时是否应该跳出循环?
  3. 最后跳出外层循环我们将基准值交换到正确位置时应该与right还是left处数据交换?



问题1:



  • 二者相遇时在9的位置,如果不取等,第一次交换完后就跳出循环,此时9和6交换,显然不行


外层循环需要取等,同时在内层循环时相应left和right判断处也要取等,不然left和right相等就死循环了


问题3:


  • 既然跳出循环时是left>right,right处在left扫描过的区域,都是不大于基准值的数据,而left处在right扫描过的区域,都是不小于基准值的数据
  • 显然我们将right处数据和基准值交换,基准值就来到了正确的位置


跳出外层循环应该与right处数据交换,right处数据就是基准值的位置


经过上面两层分析:


改进如下:

int _QuickSort1(int* arr, int left, int right)
{
  int keyi = left;
  ++left;

  while (left <= right)//left和right相遇的位置的值比基准值要大
  {
    while (left <= right && arr[right] > arr[keyi])
      right--;
    }
    //right找到比基准值小/  等于?
    while (left <= right && arr[left] < arr[keyi])
    {
      left++;
    }
    //right left
    if (left <= right)
    {
      Swap(&arr[left++], &arr[right--]);
    }
  }
  //right keyi交换
  Swap(&arr[keyi], &arr[right]);

  return right;
}

问题2:

  • 假设数组全是相同的数据



  • 取等于,第一次循环right就和left都在下标为1的位置,此时返回去的基准值就是下标1,左序列只有一个数据,右边序列还有n-2个数据
  • 同样的下次循环的左序列也只有一个数据
  • 像这样一次排一个数据时间复杂度很高


所以不应该取等于,尽量让左右子序列的数据个数平均一些

所以上述改进版本就是最终的hoare排序法


挖坑法


基本思路


创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)


动图解析




  • 动图演示的很清楚
  • 这里同样有两个问题
  • left和right是否取等?
  • 当right或left处数据与基准值key相等时是否继续循环




问题1:

  • 以上面动图为例,如果取等最后当left和right相遇时left还要++一次,导致hole所在位置偏移,发生错误,所以不取等


问题2:

  • 同hoare版本,如果全是相等数据时每次只会排序一个数据,时间复杂度太高,所以不取等


//挖坑法
int _QuickSort2(int* arr, int left, int right)
{
  int hole = left;
  int key = arr[hole];

  while (left < right)
  {
    while (left < right && arr[right] > key)
    {
      --right;
    }
    arr[hole] = arr[right];
    hole = right;
    while (left < right && arr[left] < key)
    {
      ++left;
    }
    arr[hole] = arr[left];
    hole = left;
  }
  arr[hole] = key;
  return hole;
}

lomuto前后指针

基本思想


创建前后指针,从左往右找⽐基准值⼩的进⾏交换,使得⼩的都排在基准值的左边。


动图解析:



  • 这种方法比较好理解代码也简单,就不赘述了(就是想不到一点🤣)
//lomuto前后指针法
int _QuickSort(int* arr, int left, int right)
{
  int prev = left, cur = left + 1;
  int keyi = left;
 
  while (cur <= right)
  {
    if (arr[cur] < arr[keyi] && ++prev != cur)
    {
      Swap(&arr[cur], &arr[prev]);
    }
    cur++;
  }
  Swap(&arr[keyi], &arr[prev]);
  return prev;
}
  • 这里当cur和keyi数据相同时是否交换?
  • 假设仍然全是重复数据,代入后会发现二者都是一样的,如果不加等号最后prev下标在0;反之prev下标在end。可见其对重复数据无法通过此来进行优化



递归法复杂度分析


  • 时间复杂度:每一层的总时间复杂度都是O(n),因为需要对每一个元素遍历一次。而且在最好的情况下,同样也是有logn层,所以快速排序最好的时间复杂度为O(nlogn)。
  • 空间复杂度:二叉树递归最大深度为logn,即O(nlogn)
  • 以上是最好情况,最坏情况则是上面说的一次排序一个数据,时间复杂度O(n2),空间复杂度O(n)。不过现实中基本不会出现这种情况。


注意:在以上找基准值方法中,我们默认都是把基准值定为left所在位置,这种方法当数组接近升序时会导致分割的序列也出现“一边倒”的情况,在高阶数据结构中会讲到如何优化,敬请期待😘


非递归法实现


借助栈这样一种数据结构


有关栈的相关知识,不了解的小伙伴可以看看这篇:

栈的实现方法


  • 栈是先进后出,所以插入先插入right后插入left
  • 找基准值方法使用双指针法最简单
  • 根据基准值划分左右区间
  • 左区间:[begin,keyi-1]
  • 右区间:[keyi+1,end]
  • 循环直到栈为空






//非递归版本快排
//--借助数据结构--栈
void QuickSortNonR(int* arr, int left, int right)
{
  ST st;
  STInit(&st);
  StackPush(&st, right);
  StackPush(&st, left);

  while (!StackEmpty(&st))
  {
    //取栈顶元素---取两次
    int begin = StackTop(&st);
    StackPop(&st);

    int end = StackTop(&st);
    StackPop(&st);
    //[begin,end]---找基准值

    int prev = begin;
    int cur = begin + 1;
    int keyi = begin;

    while (cur <= end)
    {
      if (arr[cur] < arr[keyi] && ++prev != cur)
      {
        Swap(&arr[cur], &arr[prev]);
      }
      cur++;
    }
    Swap(&arr[keyi], &arr[prev]);

    keyi = prev;
    //根据基准值划分左右区间
    //左区间:[begin,keyi-1]
    //右区间:[keyi+1,end]
    
    if (keyi + 1 < end)
    {
      StackPush(&st, end);
      StackPush(&st, keyi + 1);
    }
    if (keyi - 1 > begin)
    {
      StackPush(&st, keyi - 1);
      StackPush(&st, begin);
    }
  }

  STDestroy(&st);
}


【初阶数据结构篇】归并排序和计数排序(总结篇):https://developer.aliyun.com/article/1590282?spm=a2c6h.13148508.setting.15.d66e4f0esdhd1l

目录
相关文章
|
11天前
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
13 1
|
11天前
|
算法 搜索推荐
数据结构与算法学习十一:冒泡排序、选择排序、插入排序
本文介绍了冒泡排序、选择排序和插入排序三种基础排序算法的原理、实现代码和测试结果。
12 0
数据结构与算法学习十一:冒泡排序、选择排序、插入排序
|
19天前
【初阶数据结构】打破递归束缚:掌握非递归版快速排序与归并排序
【初阶数据结构】打破递归束缚:掌握非递归版快速排序与归并排序
|
17天前
|
算法
蓝桥杯宝藏排序 | 数据结构 | 快速排序 归并排序
蓝桥杯宝藏排序 | 数据结构 | 快速排序 归并排序
|
3月前
|
搜索推荐
【数据结构常见七大排序(三)上】—交换排序篇【冒泡排序】And【快速排序】
【数据结构常见七大排序(三)上】—交换排序篇【冒泡排序】And【快速排序】
【数据结构常见七大排序(三)上】—交换排序篇【冒泡排序】And【快速排序】
|
4月前
|
机器学习/深度学习 算法 搜索推荐
数据结构算法--2 冒泡排序,选择排序,插入排序
**基础排序算法包括冒泡排序、选择排序和插入排序。冒泡排序通过相邻元素比较交换,逐步将最大值“冒”到末尾,平均时间复杂度为O(n^2)。选择排序每次找到剩余部分的最小值与未排序部分的第一个元素交换,同样具有O(n^2)的时间复杂度。插入排序则类似玩牌,将新元素插入到已排序部分的正确位置,也是O(n^2)复杂度。这些算法适用于小规模或部分有序的数据。**
|
4月前
|
算法 搜索推荐
数据结构与算法-冒泡排序
数据结构与算法-冒泡排序
25 2
|
4月前
|
算法
数据结构与算法-快速排序
数据结构与算法-快速排序
23 1
|
4月前
|
搜索推荐 算法 大数据
​【数据结构与算法】冒泡排序:简单易懂的排序算法解析
​【数据结构与算法】冒泡排序:简单易懂的排序算法解析
|
4月前
|
算法 搜索推荐
数据结构和算法——快速排序(算法概述、选主元、子集划分、小规模数据的处理、算法实现)
数据结构和算法——快速排序(算法概述、选主元、子集划分、小规模数据的处理、算法实现)
35 0