C语言实现快速排序(hoare法、挖坑法、前后指针法与非递归实现)

简介: C语言实现快速排序(hoare法、挖坑法、前后指针法与非递归实现)

image.png

目录


1. hoare法

方法与步骤

代码实现

2. 挖坑法

方法与步骤

代码实现

3. 前后指针法

方法与步骤

代码实现

4. 快速排序的缺点与优化

1.快速排序的缺点

2.快速排序的优化

① 三数取中法选 key

代码实现

② 小区间优化

代码实现

5. 快速排序的非递归实现

附录·完整源码

快速排序递归实现

快速排序非递归实现


前言


快速排序是霍尔大佬在1962年提出的排序方法,因其出色的排序效率使得它成为使用最广泛的排序算法。快速排序之所以敢叫做快速排序,自然是有一定的道理,今天我们就来看看快速排序是如何凌驾于其它算法之上的。


快速排序的基本思想是:任取待排序数列中的一个数作为 key 值,通过某种方法使得 key 的左边所有的数都比它小,右边的数都比它大;以 key 为中心,将 key 左边的数列与右边的数列取出,做同样的操作(取 key 值,分割左右区间),直至所有的数都到了正确的位置。


上述所提到的某种方法可以有很多种,例如:hoare法、挖坑法、前后指针法。它们虽然做法不相同,但做的都是同一件事——分割出 key 的左右区间(左边的数比 key 小,右边的数比 key 大)。


我们首先来看看霍尔大佬所用的方法——hoare法。


正文


1. hoare法


方法与步骤


以数列 6,1,2,7,9,3,4,5,8,10 为例:

31.png1.取最左边为 key ,分别有 left 和 right 指向数列的最左端与最右端;

32.png

2. right 先走,找到比 key 小的数就停下来;33.png

3. left 开始走,找到比 key 大的数就停下来;34.png

4. 交换 left 与 right 所在位置的数;35.png

5.重复上述操作,right 找小,left 找大,进行交换;36.png

6. right 继续找小;37.png

7. left 继续找大,若与 right 就停下来;

8.交换二者相遇位置与 key 处的值;38.png

此时一趟排序就完成了,此时的数列有两个特点:

1. key 所指向的值(6)已经到了正确的位置;

2. key 左边的数字都比 key 要小,右边的都比 key 要大;

接下来就是递归的过程了,分别对左右区间进行同样的操作:

41.png


代码实现


知道了详解步骤,用代码来实现并不困难,但是有很多很多的细节需要注意。(这里的代码未经优化,当前的代码有几种极端的情况不能适应)

void Swap(int* p, int* q)
{
  int tmp = *p;
  *p = *q;
  *q = tmp;
}
void QuickSort(int* a, int begin, int end)
{
    //数列只有一个数,或无数列则返回
  if (begin >= end)
  {
    return;
  }
  int left = begin;
  int right = end;
  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]);
  QuickSort(a, begin, left - 1);
  QuickSort(a, left + 1, end);
}


2. 挖坑法


挖坑法相比于hoare法,思路上更为简单易懂。


方法与步骤


还是以同样的数列 6,1,2,7,9,3,4,5,8,10 为例:

31.png

1. 先将第一个数存放到 key 中,形成一个坑位:分别有 left 和 right 指向数列的最左端与最右端;

42.png

2.  right 先走,找到比 key 小的数,将该数丢到坑里;同时又形成了一个新的坑;43.png

3. left 开始走,找到比 key 大的数,将该数丢到坑里;同时形成一个新的坑;44.png

4. right继续找小,进行重复的操作;45.png

5. left 找大;46.png

6. right 找小;47.png

7. left 找大;

8.若二者相遇就停下来;将 key 值放入坑;48.png

至此,一趟排序已经完成,我们发现此时的数列与hoare具有相同的特点:


1. key 所指向的值(6)已经到了正确的位置;


2. key 左边的数字都比 key 要小,右边的都比 key 要大;


挖坑法、hoare、前后指针法完成一趟排序后都具有相同的特点,所以不同版本的快速排序不一样的只有单趟排序的实现,总体思路都是相同的。


代码实现


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


3. 前后指针法


方法与步骤


以同样的数列为例:

1.png

1. 取第一个值为 key ;有 prev 和 cur 分别指向数列开头和 prev 的下一个数;

2.png

2. cur 先走,找到比 key 小的数就停下来;


3. ++prev ,交换 prev 与 cur 位置的数;(前两次无需交换,因为自己与自己换没有意义)

3.png

4. 重复此步骤;

4.png

5. 直到 cur 走完整个数列,交换 prev 与 key 处的值;


5.png


至此,第一趟排序就结束了,又是与前两种方法相同的结果;



代码实现


void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  int prev = begin;
  int cur = prev + 1;
  int keyi = begin;
  while (cur <= end)
  {
    if (a[cur] < a[keyi] && ++prev != cur)
    {
      Swap(&a[prev], &a[cur]);
    }
    cur++;
  }
  Swap(&a[keyi], &a[prev]);
  keyi = prev;
  QuickSort(a, begin, keyi - 1);
  QuickSort(a, keyi + 1, end);
}


4. 快速排序的缺点与优化


1.快速排序的缺点


我们用三种方式实现了快速排序,其实这三种方式并无明显的优劣之分。但是我们前面设计的快速排序其实是有两个缺点的:


1.在最坏情况下它的的效率极慢;


2.在数据量太大时会造成栈溢出。


那么什么情况是最坏情况呢?答案是,当数据本身就是有序的时候(无论是逆序还是顺序)。在最坏情况下,每次我们的 key 值都是最大或者最小,这样就会使 key 与数列的每个数都比较一次,它的时间复杂度为 O(n^2);


为什么会发生栈溢出呢?因为我们的快速排序是利用递归实现的,有递归调用,就要建立函数栈帧,并且随着递归的深度越深所要建立的函数栈帧的消耗就越大 。如这幅图所示:50.png


2.快速排序的优化


① 三数取中法选 key


为了应对最坏情况会出现时间复杂度为 O(N^2) 的情况,有人提出了三数取中的方法。


旧方法中,我们每次选 key 都是数列的第一个元素。三数取中的做法是,分别取数列的第一个元素、最后一个元素和最中间的元素,选出三个数中不是最大也不是最小的那个数当作 key 值。


有了三数取中,之前的最坏情况立马变成了最好情况。


代码实现


由于hoare法、挖坑法、前后指针法最终的效果都相同且效率差异很小,所以就任意选取一个为例,其余两者都类似。

//三数取中的函数
int GetMidIndex(int* a, int begin, int end)
{
  int mid = (begin + end) / 2;
  if (a[begin] < a[mid])
  {
    if (a[mid] < a[end])
    {
      return mid;
    }
    else if (a[begin] > a[end])
    {
      return begin;
    }
    else
    {
      return end;
    }
  }
  else // a[begin] > a[mid]
  {
    if (a[mid] > a[end])
    {
      return mid;
    }
    else if (a[begin] < a[end])
    {
      return begin;
    }
    else
    {
      return end;
    }
  }
}
//hoare法
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  int mid = GetMidIndex(a, begin, end);
  Swap(&a[mid], &a[begin]);
  int left = begin;
  int right = end;
  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]);
  QuickSort(a, begin, left - 1);
  QuickSort(a, left + 1, end);
}


② 小区间优化


随着递归的调用越深入,此时有个很大的缺点就是函数栈帧的消耗很大。但是同时又有一个好处,就是越往下,数列就越接近有序,且此时每个小区间的数据个数特别少。


那么有什么办法可以取其长处避其短处呢?不知道你是否还记得插入排序的特点——数据越接近有序,效率就越高。并且,在数据量极少的情况下,时间复杂度为 O(N^2) 的插入排序与时间复杂度为 O(N*log N) 的快速排序基本没有什么区别。所以,我们干脆就在排序数据量少的数列时,采用插入排序代替。


代码实现


//三数取中的函数
int GetMidIndex(int* a, int begin, int end)
{
  int mid = (begin + end) / 2;
  if (a[begin] < a[mid])
  {
    if (a[mid] < a[end])
    {
      return mid;
    }
    else if (a[begin] > a[end])
    {
      return begin;
    }
    else
    {
      return end;
    }
  }
  else // a[begin] > a[mid]
  {
    if (a[mid] > a[end])
    {
      return mid;
    }
    else if (a[begin] < a[end])
    {
      return begin;
    }
    else
    {
      return end;
    }
  }
}
//插入排序
void InsertSort(int* a, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
    int end = i;
    int tmp = a[end + 1];
    while (end >= 0)
    {
      if (a[end] > tmp)         //大于tmp,往后挪一个
      {
        a[end + 1] = a[end];
        end--;
      }
      else
      {
        break;
      }
    }
    a[end + 1] = tmp;          //把tmp插入空隙
  }
}
//hoare法
void QuickSort(int* a, int begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  if ((end - begin + 1) < 15)
      {
        // 小区间用直接插入替代,减少递归调用次数
        InsertSort(a+begin, end - begin + 1);
      }
  else
  {
    int mid = GetMidIndex(a, begin, end);
    Swap(&a[mid], &a[begin]);
    int left = begin;
    int right = end;
    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]);
    QuickSort(a, begin, left - 1);
    QuickSort(a, left + 1, end);
  }
}

两外两种方法的代码实现已打包完成,可在文末直接取用。


5. 快速排序的非递归实现


快速排序的非递归思路与递归相差无几,唯一不同的是,非递归用栈或队列模拟函数递归建立栈帧的过程。

void QuickSortNonR(int* a, int begin, int end)
{
  Stack st;
  StackInit(&st);
  StackPush(&st, begin);
  StackPush(&st, end);
  while (!StackEmpty(&st))
  {
    int right = StackTop(&st);
    StackPop(&st);
    int left = StackTop(&st);
    StackPop(&st);
    int keyi = PartSort1(a, left, right);//三种方法任选其一
    //int keyi = PartSort2(a, left, right);
    //int keyi = PartSort3(a, left, right);
    if (keyi + 1 < right)
    {
      StackPush(&st, keyi + 1);
      StackPush(&st, right);
    }
    if (left < keyi - 1)
    {
      StackPush(&st, left);
      StackPush(&st, keyi - 1);
    }
  }
  StackDestroy(&st);
}


附录·完整源码


快速排序递归实现


//交换函数
void Swap(int* p, int* q)
{
  int tmp = *p;
  *p = *q;
  *q = tmp;
}
//三数取中
int GetMidIndex(int* a, int begin, int end)
{
  int mid = (begin + end) / 2;
  if (a[begin] < a[mid])
  {
    if (a[mid] < a[end])
    {
      return mid;
    }
    else if (a[begin] > a[end])
    {
      return begin;
    }
    else
    {
      return end;
    }
  }
  else // a[begin] > a[mid]
  {
    if (a[mid] > a[end])
    {
      return mid;
    }
    else if (a[begin] < a[end])
    {
      return begin;
    }
    else
    {
      return end;
    }
  }
}
//插入排序
void InsertSort(int* a, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
    int end = i;
    int tmp = a[end + 1];
    while (end >= 0)
    {
      if (a[end] > tmp)         //大于tmp,往后挪一个
      {
        a[end + 1] = a[end];
        end--;
      }
      else
      {
        break;
      }
    }
    a[end + 1] = tmp;          //把tmp插入空隙
  }
}
// Hoare法
int PartSort1(int* a, int begin, int end)
{
  int mid = GetMidIndex(a, begin, end);
  Swap(&a[begin], &a[mid]);
  int left = begin, right = end;
  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[left], &a[keyi]);
  keyi = left;
  return keyi;
}
// 挖坑法
int PartSort2(int* a, int begin, int end)
{
  int mid = GetMidIndex(a, begin, end);
  Swap(&a[begin], &a[mid]);
  int left = begin, right = end;
  int key = a[left];
  int hole = left;
  while (left < right)
  {
    while (left < right && a[right] >= key)
    {
      --right;
    }
    a[hole] = a[right];
    hole = right;
    while (left < right && a[left] <= key)
    {
      ++left;
    }
    a[hole] = a[left];
    hole = left;
  }
  a[hole] = key;
  return hole;
}
//前后指针法
int PartSort3(int* a, int begin, int end)
{
  int mid = GetMidIndex(a, begin, end);
  Swap(&a[begin], &a[mid]);
  int keyi = begin;
  int prev = begin, cur = begin + 1;
  while (cur <= end)
  {
    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 begin, int end)
{
  if (begin >= end)
  {
    return;
  }
  if ((end - begin + 1) < 15)
  {
    // 小区间用直接插入替代,减少递归调用次数
    InsertSort(a + begin, end - begin + 1);
  }
  else
  {
    int keyi = PartSort1(a, begin, end);
    //int keyi = PartSort2(a, begin, end);
    //int keyi = PartSort3(a, begin, end);
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
  }
}


快速排序非递归实现


#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int STDataType;
typedef struct Stack
{
  STDataType* a;  //动态开辟数组
  int capacity; //记录栈的容量大小
  int top; //记录栈顶的位置
}Stack;
//栈的初始化
void StackInit(Stack* ps);
//释放动态开辟的内存
void StackDestroy(Stack* ps);
//压栈
void StackPush(Stack* ps, STDataType data);
//出栈
void StackPop(Stack* ps);
//读取栈顶的元素
STDataType StackTop(Stack* ps);
//判断栈是否为空
bool StackEmpty(Stack* ps);
// Hoare法
int PartSort1(int* a, int begin, int end)
{
  int mid = GetMidIndex(a, begin, end);
  Swap(&a[begin], &a[mid]);
  int left = begin, right = end;
  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[left], &a[keyi]);
  keyi = left;
  return keyi;
}
// 挖坑法
int PartSort2(int* a, int begin, int end)
{
  int mid = GetMidIndex(a, begin, end);
  Swap(&a[begin], &a[mid]);
  int left = begin, right = end;
  int key = a[left];
  int hole = left;
  while (left < right)
  {
    while (left < right && a[right] >= key)
    {
      --right;
    }
    a[hole] = a[right];
    hole = right;
    while (left < right && a[left] <= key)
    {
      ++left;
    }
    a[hole] = a[left];
    hole = left;
  }
  a[hole] = key;
  return hole;
}
int PartSort3(int* a, int begin, int end)
{
  int mid = GetMidIndex(a, begin, end);
  Swap(&a[begin], &a[mid]);
  int keyi = begin;
  int prev = begin, cur = begin + 1;
  while (cur <= end)
  {
    if (a[cur] < a[keyi] && ++prev != cur)
      Swap(&a[prev], &a[cur]);
    ++cur;
  }
  Swap(&a[prev], &a[keyi]);
  keyi = prev;
  return keyi;
}
void QuickSortNonR(int* a, int begin, int end)
{
  Stack st;
  StackInit(&st);
  StackPush(&st, begin);
  StackPush(&st, end);
  while (!StackEmpty(&st))
  {
    int right = StackTop(&st);
    StackPop(&st);
    int left = StackTop(&st);
    StackPop(&st);
    int keyi = PartSort1(a, left, right);//三种方法任选其一
    //int keyi = PartSort2(a, left, right);
    //int keyi = PartSort3(a, left, right);
    if (keyi + 1 < right)
    {
      StackPush(&st, keyi + 1);
      StackPush(&st, right);
    }
    if (left < keyi - 1)
    {
      StackPush(&st, left);
      StackPush(&st, keyi - 1);
    }
  }
  StackDestroy(&st);
}
//栈的实现_函数定义
void StackInit(Stack* ps)
{
  assert(ps);
  //初始化时,可附初值,也可置空
  ps->a = NULL;
  ps->capacity = 0;
  ps->top = 0;
}
void StackDestroy(Stack* ps)
{
  assert(ps);
  //若并未对ps->a申请内存,则无需释放
  if (ps->capacity == 0)
    return;
  //释放
  free(ps->a);
  ps->a = NULL;
  ps->capacity = ps->top = 0;
}
void StackPush(Stack* ps,STDataType data)
{
  assert(ps);
  //若容量大小等于数据个数,则说明栈已满,需扩容
  if (ps->capacity == ps->top)
  {
    //若为第一次扩容,则大小为4,否则每次扩大2倍
    int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
    STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * newCapacity);
    if (tmp == NULL)
    {
      perror("realloc fail");
      exit(-1);
    }
    ps->a = tmp;
    ps->capacity = newCapacity;
  }
  //压栈
  ps->a[ps->top] = data;
  ps->top++;
}
void StackPop(Stack* ps)
{
  assert(ps);
  assert(!StackEmpty(ps));
  //出栈
  ps->top--;
}
STDataType StackTop(Stack* ps)
{
  assert(ps);
  assert(!StackEmpty(ps));
  //返回栈顶的数据
  return ps->a[ps->top - 1];
}
bool StackEmpty(Stack* ps)
{
  assert(ps);
  //返回top
  return ps->top == 0;
}


目录
相关文章
|
15天前
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
41 4
|
1月前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
49 0
|
1月前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
61 7
|
1月前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
18 2
|
1月前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
1月前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
1月前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
1月前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
1月前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
1月前
|
C语言
C语言指针(3)
C语言指针(3)
14 1
下一篇
无影云桌面