【数据结构】论如何拿捏快速排序?(含非递归)

简介: 【数据结构】论如何拿捏快速排序?(含非递归)

一,快速排序(递归)

1,快排思想

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

基本代码思想如下:

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
 if(right < left)
 return;
 // 按照基准值对array数组的 [left, right)区间中的元素进行划分
 int div = partion(array, left, right);
 // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
 // 递归排[left, div)
 QuickSort(array, left, div);
 // 递归排[div+1, right)
 QuickSort(array, div+1, right);
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可;

2,霍尔排序

根据快排思想,我们需要实现的就是 partion 函数了----将区间按照基准值划分为左右两半部分;

常见的方式有很多,我们先来了解最初的版本 【霍尔排序】

思想图解:

对就是这样的,右边的小人先出发向左移动,找到比 key 小的数,然后左边的小人向右移动找到比  key 大的数,然后交换两个小人的值,直至他们相遇然后再交换 key 与任意一个小人的值

这样一趟下来,他们相遇后,左边的数都比 key 小,右边的数都比 key 大

思路实现:

//霍尔排序
int PartSort1(int* arr, int left, int right)
{
  int keyi = left;
  while (left < right)
  {
    //右边先走
    while (left<right && arr[right]>=arr[keyi])
    {
      right--;
    }
    //左边后走
    while (left < right && arr[left] <= arr[keyi])
    {
      left++;
    }
    //交换
    Swap(&arr[left], &arr[right]);
  }
  Swap(&arr[left], &arr[keyi]);
  return left;
}

然后我们运行一下:

//快速排序
void QuickSort(int* arr, int begin, int end)
{
  if (begin >= end)
  {
    return NULL;
  }
    //霍尔法
  int keyi = PartSort1(arr, begin, end);
  //排序[begin,keyi) & [keyi+1,end]
  QuickSort(arr, begin, keyi);
  QuickSort(arr, keyi + 1, end);
}
int main()
{
  int arr[] = { 9,1,2,5,7,4,8,6,3,5 };
  //快速排序
  QuickSort(arr, 0,sizeof(arr) / sizeof(arr[0])-1);
  PrintSort(arr, sizeof(arr) / sizeof(arr[0]));
  return 0;
}

可以看到是有序的,选择排序就 OK 了;

3,挖坑法

然后我们再来认识另一种方式 【挖坑法】,其实跟【霍尔排序】思路差不多,不过更容易理解一点;

思想图解:

对还是一样的思路,让第一个元素为坑位,然后右边的小人先出发向左走找比 key 小的数,然后填充坑位并且右边小人的位置变为新坑位,然后左边的小人向右走找比 key 大的数,然后填充坑位并且左边小人的位置变为新坑位,直至两个小人相遇于坑位然后再给坑位赋值 key

这样一趟下来,坑位左边的数都比 key 小,右边的数都比 key 大

思路实现:

//挖坑法
int PartSort2(int* arr, int left, int right)
{
  int key = arr[left];
    //坑位
  int hole = left;
  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;
}

然后我们运行一下:

//快速排序
void QuickSort(int* arr, int begin, int end)
{
  if (begin >= end)
  {
    return NULL;
  }
    //挖坑法
  int keyi = PartSort2(arr, begin, end);
  //排序[begin,keyi) & [keyi+1,end]
  QuickSort(arr, begin, keyi);
  QuickSort(arr, keyi + 1, end);
}

可以看到是有序的,选择排序就 OK 了;

4,前后指针法

然后呢,在介绍最后一种排序方式了 【前后指针】法;

这个呢就比较新颖了,跟之前的都不一样;

请看图解:

这个呢就是,定义两个指针,从首元素开始走,快指针(cur)刚开始领先慢指针(prev)一个身位,然后 cur 先走找比 key 小的数,然后与 prev 的下一个数交换,直至 cur 越界,然后再让 prev 与 key 交换;

这样一趟下来,prev左边的数都比 key 小,右边的数都比 key 大

思路实现:

//前后指针法
int PartSort3(int* arr, int left, int right)
{
  int keyi = left;
  int slow = left, fast = left+1;
  while (fast<=right)
  {
    if (arr[fast] < arr[keyi] && ++slow!=fast)
    {
      //交换
      Swap(&arr[fast], &arr[slow]);
    }
    fast++;
  }
  Swap(&arr[slow], &arr[keyi]);
  return slow;
}

然后我们运行一下:

//快速排序
void QuickSort(int* arr, int begin, int end)
{
  if (begin >= end)
  {
    return NULL;
  }
  int keyi = PartSort3(arr, begin, end);
  //排序[begin,keyi) & [keyi+1,end]
  QuickSort(arr, begin, keyi);
  QuickSort(arr, keyi + 1, end);
}

可以看到是有序的,选择排序就 OK 了;

5,快速排序优化

1,三数取中法选key

这第一个呢,就是对 key 的取值进行优化,当 key 的值太过于小或者大时,遍历数组的时间会增加,所以我们尽量让  key 的取值随机;

我们可以取首元素,尾元素,中间元素的值进行比较选 key

思路实现:

//三数取中
int middle(int* arr, int left, int right)
{
  //int mid = (left +right)/ 2;
  if (arr[left] < arr[mid])
  {
    if (arr[mid] < arr[right])
    {
      return mid;
    }
    if (arr[left] < arr[right])
    {
      return right;
    }
    else
    {
      return left;
    }
  }
  //arr[mid]<=arr[left]
  else
  {
    if (arr[mid] > arr[right])
    {
      return mid;
    }
    if (arr[left] > arr[right])
    {
      return right;
    }
    else
    {
      return left;
    }
  }
}

这样我们选择的 key 就不会受 首元素的束缚了;

我们还可不可以在这个基础上再优化一下呢?

答案是肯定的!

我们可以用随机数来取代中间数

//三数取中
int middle(int* arr, int left, int right)
{
  //随机数取中
  int mid = left + (rand() % (right - left));
  if (arr[left] < arr[mid])
  {
    if (arr[mid] < arr[right])
    {
      return mid;
    }
    if (arr[left] < arr[right])
    {
      return right;
    }
    else
    {
      return left;
    }
  }
  //arr[mid]<=arr[left]
  else
  {
    if (arr[mid] > arr[right])
    {
      return mid;
    }
    if (arr[left] > arr[right])
    {
      return right;
    }
    else
    {
      return left;
    }
  }
}

这样子才是真正意义上的随机值,这样 key 就不受束缚了,再任何场景下都可以排序自如;

我们选完 key 的下标后,要让数组首元素的值与之交换,这样后面不动就 OK 了;

【前后指针法】为例:

//前后指针法
int PartSort3(int* arr, int left, int right)
{
  //三数取中
  int ret = middle(arr, left, right);
  Swap(&arr[left], &arr[ret]);
  int keyi = left;
  int slow = left, fast = left+1;
  while (fast<=right)
  {
    if (arr[fast] < arr[keyi] && ++slow!=fast)
    {
      //交换
      Swap(&arr[fast], &arr[slow]);
    }
    fast++;
  }
  Swap(&arr[slow], &arr[keyi]);
  return slow;
}

主函数需要写 srand 函数来引用随机值;

//快速排序
void QuickSort(int* arr, int begin, int end)
{
  srand(time(0));
  if (begin >= end)
  {
    return NULL;
  }
  int keyi = PartSort3(arr, begin, end);
  //排序[begin,keyi) & [keyi+1,end]
  QuickSort(arr, begin, keyi);
  QuickSort(arr, keyi + 1, end);
}

现在我们运行测试一下:

其实速度是更快的,大家可以在【力扣】上测试一下;

2,小区间优化

还有一种优化方式是当递归到小的子区间时,可以考虑使用插入排序;

当数组的区间不大时,使用【插入排序】是会更快的,同时也可以减少压栈的次数,也就是降低【空间复杂度】

//快速排序
void QuickSort(int* arr, int begin, int end)
{
  srand(time(0));
  if (begin >= end)
  {
    return NULL;
  }
  if (end - begin <10)
  {
    InsertSort1(arr,begin,end);
  }
  else
  {
    int keyi = PartSort3(arr, begin, end);
    //排序[begin,keyi) & [keyi+1,end]
    QuickSort(arr, begin, keyi);
    QuickSort(arr, keyi + 1, end);
  }
}

然后我们还需要改变一下插入排序,之前都是传数组元素个数的,现在我们要传区间需要改造一下;

//插入排序(改造版)
void InsertSort1(int* arr, int left, int right)
{
  int i = 0;
  for (i = left; i < right; i++)
  {
    int end = i;
    int tmp = arr[end + 1];
    while (end >= 0)
    {
      if (arr[end] >= tmp)
      {
        //交换
        Swap(&arr[end], &arr[end + 1]);
        end--;
      }
      else
      {
        break;
      }
    }
    arr[end + 1] = tmp;
  }
}

这样就可以了,现在我们运行测试一下:

可以看到是有序的,选择排序就 OK 了;

二,快速排序(非递归)

之前咱们拿捏了递归版的快速排序,现在咱们来秒杀非递归版的快速排序

我们之前了解到,快速排序与二叉树的前序遍历相似,所以我们非递归也要用这种手法来表示

所以我们需要借助【栈】来帮助我们来实现;

因为【栈】的特性(后进先出)很符合二叉树的前序遍历的思想,这样我们可以先排序最左边的序列,在排序右边的,前面的都放在【栈】】里等后面排序

所以我们需要一个【栈】:

Stack.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int STDataType;
typedef struct StackTop
{
  STDataType* a;
  int top;
  int capacity;
}ST;
//初始化
void STInit(ST* ps);
//销毁
void STDestroy(ST* ps);
//插入
void STPush(ST* ps, STDataType x);
//删除
void STPop(ST* ps);
//返回栈顶
STDataType STInsert(ST* ps);
//数量
int STSize(ST* ps);
//判断是否为空
int STEmpty(ST* ps);

Stack.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
//初始化
void STInit(ST* ps)
{
  assert(ps);
  ps->a = NULL;
  ps->top = ps->capacity = 0;
}
//销毁
void STDestroy(ST* ps)
{
  assert(ps);
  free(ps->a);
  ps->a = NULL;
  ps->top = ps->capacity = 0;
}
//插入
void STPush(ST* ps, STDataType x)
{
  assert(ps);
  if (ps->top == ps->capacity)
  {
    ps->capacity = ps->top == 0 ? 4 : ps->capacity*2;
    ps->a = (STDataType*)realloc(ps->a,sizeof(STDataType)*ps->capacity);
  }
  ps->a[ps->top] = x;
  ps->top++;
}
//删除
void STPop(ST* ps)
{
  assert(ps);
  assert(ps->top > 0);
  ps->top--;
}
//返回栈顶
STDataType STInsert(ST* ps)
{
  assert(ps);
  assert(ps->top > 0);
  return ps->a[ps->top-1];
}
//数量
int STSize(ST* ps)
{
  assert(ps);
  return ps->top;
}
//判断是否为空
int STEmpty(ST* ps)
{
  assert(ps);
  if (ps->top == 0)
  {
    return 1;
  }
  else
  {
    return 0;
  }
}

然后我们就可以实现代码了;

我们的思路是

将两边的下标存进【栈】,然后再取栈顶元素进行排序(霍尔或者其他)每取一个栈顶元素之后要把栈顶元素删除,然后再存放  keyi 两边的区间,再重复上面的过程,直至【栈】为空排序结束

//快速排序(非递归)
void QuickNon(int* arr, int begin, int end)
{
  srand(time(0));
  ST ps;
  //初始化
  STInit(&ps);
  if (begin >= end)
  {
    return;
  }
  //插入
  STPush(&ps, end);
  STPush(&ps, begin);
  //栈不为空就进去
  while (!STEmpty(&ps))
  {
    int left = STInsert(&ps);//栈顶元素
    STPop(&ps);//删除
    int right = STInsert(&ps);
    STPop(&ps);
    int keyi = PartSort1(arr, left, right);
    //排序[left,keyi-1] & [keyi+1,right]
    if (keyi + 1 < right)
    {
      //插入
      STPush(&ps, right);
      STPush(&ps, keyi + 1);
    }
    if (left < keyi - 1)
    {
      //插入
      STPush(&ps, keyi - 1);
      STPush(&ps, left);
    }
  }
  //销毁
  STDestroy(&ps);
}

我们运行测试一下:

可以看到也是完全 OK 的;

这就是快速排序的非递归实现!

三,快速排序源代码

以上的快速排序的全部代码如下(不包括【栈】):

//三数取中
int middle(int* arr, int left, int right)
{
  //int mid = (left +right)/ 2;
  //随机数取中
  int mid = left + (rand() % (right - left));
  if (arr[left] < arr[mid])
  {
    if (arr[mid] < arr[right])
    {
      return mid;
    }
    if (arr[left] < arr[right])
    {
      return right;
    }
    else
    {
      return left;
    }
  }
  //arr[mid]<=arr[left]
  else
  {
    if (arr[mid] > arr[right])
    {
      return mid;
    }
    if (arr[left] > arr[right])
    {
      return right;
    }
    else
    {
      return left;
    }
  }
}
//霍尔排序
int PartSort1(int* arr, int left, int right)
{
  //三数取中
  int ret = middle(arr, left, right);
  Swap(&arr[left], &arr[ret]);
  int keyi = left;
  while (left < right)
  {
    //右边先走
    while (left<right && arr[right]>=arr[keyi])
    {
      right--;
    }
    //左边后走
    while (left < right && arr[left] <= arr[keyi])
    {
      left++;
    }
    //交换
    Swap(&arr[left], &arr[right]);
  }
  Swap(&arr[left], &arr[keyi]);
  return left;
}
//挖坑法
int PartSort2(int* arr, int left, int right)
{
  //三数取中
  int ret = middle(arr, left, right);
  Swap(&arr[left], &arr[ret]);
  int key = arr[left];
  int hole = left;
  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;
}
//前后指针法
int PartSort3(int* arr, int left, int right)
{
  //三数取中
  int ret = middle(arr, left, right);
  Swap(&arr[left], &arr[ret]);
  int keyi = left;
  int slow = left, fast = left+1;
  while (fast<=right)
  {
    if (arr[fast] < arr[keyi] && ++slow!=fast)
    {
      //交换
      Swap(&arr[fast], &arr[slow]);
    }
    fast++;
  }
  Swap(&arr[slow], &arr[keyi]);
  return slow;
}
//插入排序(改造版)
void InsertSort1(int* arr, int left, int right)
{
  int i = 0;
  for (i = left; i < right; i++)
  {
    int end = i;
    int tmp = arr[end + 1];
    while (end >= 0)
    {
      if (arr[end] >= tmp)
      {
        //交换
        Swap(&arr[end], &arr[end + 1]);
        end--;
      }
      else
      {
        break;
      }
    }
    arr[end + 1] = tmp;
  }
}
//快速排序
void QuickSort(int* arr, int begin, int end)
{
  srand(time(0));
  if (begin >= end)
  {
    return NULL;
  }
  if (end - begin <10)
  {
    InsertSort1(arr,begin,end);
  }
  else
  {
    int keyi = PartSort1(arr, begin, end);
    //排序[begin,keyi) & [keyi+1,end]
    QuickSort(arr, begin, keyi);
    QuickSort(arr, keyi + 1, end);
  }
}
//快速排序(非递归)
void QuickNon(int* arr, int begin, int end)
{
  srand(time(0));
  ST ps;
  //初始化
  STInit(&ps);
  if (begin >= end)
  {
    return;
  }
  //插入
  STPush(&ps, end);
  STPush(&ps, begin);
  //栈不为空就进去
  while (!STEmpty(&ps))
  {
    int left = STInsert(&ps);//栈顶元素
    STPop(&ps);//删除
    int right = STInsert(&ps);
    STPop(&ps);
    int keyi = PartSort1(arr, left, right);
    //排序[left,keyi-1] & [keyi+1,right]
    if (keyi + 1 < right)
    {
      //插入
      STPush(&ps, right);
      STPush(&ps, keyi + 1);
    }
    if (left < keyi - 1)
    {
      //插入
      STPush(&ps, keyi - 1);
      STPush(&ps, left);
    }
  }
  //销毁
  STDestroy(&ps);
}
特性总结:

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

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

3.,空间复杂

度:O(logN)

4, 稳定性:不稳定

第三阶段就到这里了,带大家啃块硬骨头磨磨牙!

后面博主会陆续更新;

如有不足之处欢迎来补充交流!

完结。。

目录
相关文章
|
6天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
15 1
|
1月前
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
20 1
|
1月前
|
存储
【数据结构】二叉树链式结构——感受递归的暴力美学
【数据结构】二叉树链式结构——感受递归的暴力美学
|
1月前
【初阶数据结构】打破递归束缚:掌握非递归版快速排序与归并排序
【初阶数据结构】打破递归束缚:掌握非递归版快速排序与归并排序
|
1月前
|
算法
蓝桥杯宝藏排序 | 数据结构 | 快速排序 归并排序
蓝桥杯宝藏排序 | 数据结构 | 快速排序 归并排序
|
5月前
|
存储 算法 程序员
数据结构与算法===递归
数据结构与算法===递归
|
1月前
|
算法 定位技术
数据结构与算法学习九:学习递归。递归的经典实例:打印问题、阶乘问题、递归-迷宫问题、八皇后问题
本文详细介绍了递归的概念、重要规则、形式,并展示了递归在解决打印问题、阶乘问题、迷宫问题和八皇后问题等经典实例中的应用。
40 0
|
3月前
|
存储 算法 搜索推荐
【初阶数据结构篇】冒泡排序和快速排序(中篇)
与直接插入排序法相比,比较次数一致,但冒泡排序的交换需要执行三次,而直接插入排序因为使用了tmp临时变量存储要插入的数据,只用执行一次,所以直接插入排序法效率明显更高。
31 1
|
3月前
【数据结构】遍历二叉树(递归思想)-->赋源码
【数据结构】遍历二叉树(递归思想)-->赋源码
53 4
|
4月前
|
搜索推荐
【数据结构常见七大排序(三)上】—交换排序篇【冒泡排序】And【快速排序】
【数据结构常见七大排序(三)上】—交换排序篇【冒泡排序】And【快速排序】
【数据结构常见七大排序(三)上】—交换排序篇【冒泡排序】And【快速排序】