C语言 快速排序详细解读

前言

本文主要介绍快速排序算法的三大实现方式,时间复杂度的计算,以及简单优化一下快速排序算法的小技巧。


一、快速排序的介绍与基本思想

  • 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法.
  • 基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
    *大致呈现为以下形式:
    在这里插入图片描述

二、实现快速排序的三大方式(递归版本)

(一).挖洞法

  • 第一趟操作:定义洞为最开始的位置以及以第一个元素为基准,然后从最右边开始找比基准小的数放到左边,从最左边开始找比基准大的数放到右边。具体实现上面的操作为不断利用洞的位置转换,一步步将小的数放到左区间,大的数放到右区间,最后将一开始定的基准元素转到其真实的大小位置上。
  • 分治递归:第一趟后形成了基准元素左面全都是比其小的数,基准元素右边全都是比其大的数,下一步就是实现左序列和右序列全部变为有序序列。将左右序列分别递归,重复以上操作,最终分成最小单位一个元素即完成有序排列
  • 图形解释:
  • 第一趟排序:

在这里插入图片描述

  • 多趟排序:
    在这里插入图片描述
    点这里
    该方式算法代码:(此处给出算法函数的代码,后面会给出汇总代码)
int Part1Sort(int* a, int left, int right)
{
    //第一趟排序:
    int begin = left, end = right;
    int pivot = begin;//让洞的位置为最开始位置
    int key = a[begin];
    while (begin < end)
    {
        //从最右面开始找比key小的数,放到左面
        while (begin < end && a[end] >= key)//注意begin<end要加上防止比的超出界限,a[end]>=key要加上等于防止出现死循环BUG
        {
            --end;
        }
        //小的数据放在左边的坑位,自己的位置形成新的坑位
        a[pivot] = a[end];
        pivot = end;
        //从最左面开始找比key大的数,放在右面
        while (begin < end && a[begin] <= key)
        {
            ++begin;
        }
        //大的数据放在右边的坑位,自己的位置形成新的坑位
        a[pivot] = a[begin];
        pivot = begin;
    }
    //最后将begin或者end(此两者相等)赋给洞,此时洞的位置即key值自己真实的大小位置
    pivot = begin;
    a[pivot] = key;
    //此时再让左子区间和右子区间分别有序,那么最终就有序了
    //实现方法:分治递归:}
    return pivot;
}
void QuickSort(int* a, int left, int right)//实现分治递归
{
    if (left >= right)
    {
        return;
    }
    //将左子区间和右子区间变为有序,实现方法:分治递归;
    int keyIndex = Part1Sort(a, left, right);
    QuickSort(a, left, keyIndex - 1);
    QuickSort(a, keyIndex + 1, right);
}

(二).左右指针法

  • 第一趟操作:以第一个元素为基准,**从右边开始找比基准小的数据,从右边开始找比基准大的数据,然后将从两边分别找的比基准小的数据和比基准大的数据进行交换,**最后将基准元素转到其真实大小的位置
  • 分治递归:第一趟后形成了基准元素左面全都是比其小的数,基准元素右边全都是比其大的数,下一步就是实现左序列和右序列全部变为有序序列。将左右序列分别递归,重复以上操作,最终分成最小单位一个元素即完成有序排列
  • 图形解释:
  • 第一趟排序:
    在这里插入图片描述
  • 多趟排序
    在这里插入图片描述
    在这里插入图片描述
    该方式算法代码:(此处给出算法函数的代码,后面会给出汇总代码)
int Part2Sort(int* a, int left, int right)//左右指针法来进行快速排序
{
    int begin = left, end = right;
    int keyi = begin;
    while (begin < end)
    {
        //从右边开始找比a[keyi]小的数据
        while (begin < end && a[end] >= a[keyi])
        {
            --end;
        }
        //从左边开始找比a[keyi]大的数据
        while (begin < end && a[begin] <= a[keyi])
        {
            ++begin;
        }
        //从两边分别找到比a[keyi]小的数据和比a[keyi]大的数据,将两数据进行交换
        Swap(&a[begin], &a[end]);
    }
    //将a[keyi]于a[begin]交换或者于a[end]交换,将a[keyi]放在真实大小的位置
    Swap(&a[begin], &a[keyi]);
    return begin;
}
void QuickSort(int* a, int left, int right)
{
    if (left >= right)
    {
        return;
    }
    //将左子区间和右子区间变为有序,实现方法:分治递归;
    int keyIndex = Part2Sort(a, left, right);
    QuickSort(a, left, keyIndex - 1);
    QuickSort(a, keyIndex + 1, right);
}

(三).前后指针法

  • 第一趟排序:以第一个元素为基准,定义prev 与cur 两个变量,让prev一开始指向第一个元素,cur指向第二个元素,cur从当前位置开始向后遍历到末尾,期间
    cur找比基准小的数,每次遇到比基准小的值就停下来,++prev,交换prev和cur位置的值
    ,最后将基准元素与prev位置的值进行交换,让基准的数转到其真实大小的位置。
  • 分治递归:第一趟后形成了基准元素左面全都是比其小的数,基准元素右边全都是比其大的数,下一步就是实现左序列和右序列全部变为有序序列。将左右序列分别递归,重复以上操作,最终分成最小单位一个元素即完成有序排列
  • 图形解释:
  • 第一趟排序
    在这里插入图片描述
    *多趟排序:
    在这里插入图片描述
    在这里插入图片描述
    该方式算法代码:(此处给出算法函数的代码,后面会给出汇总代码)
int Part3Sort(int* a, int left, int right)//前后指针法来进行快速排序
{
    int keyi = left;
    int prev = left, cur = left + 1;
    //cur找比a[keyi]小的数,每次遇到比key小的值就停下来,++prev,交换prev和cur位置的值
    while (cur<=right)
    {
        if (a[cur] < a[keyi]&&++prev !=cur)//这里不需要等于,不会出现死循环的BUG
        {
            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 keyIndex = Part3Sort(a, left, right);
    QuickSort(a, left, keyIndex - 1);
    QuickSort(a, keyIndex + 1, right);
}

三、快速排序的简单优化

(一).三数取中法

  • 在实现了快速排序之后,我们发现,keyi的位置,是影响快速排序效率的重大因素。因此有人采用了三数取中的方法解决选keyi不合适的问题。
  • 三数取中:即知道这组无序数列的首和尾后,我们只需要在首,中,尾这三个数据中,选择一个排在中间的数据作为基准值(keyi),进行快速排序,即可进一步提高快速排序的效率。
  • 算法代码实现:
int GetMidIndex(int* a, int left, int right)
{
    int mid = (left + right) >> 1;//等效于整除于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[mid] > a[right])
        {
            return mid;
        }
        else if (a[left] < a[right])
        {
            return left;
        }
        else
        {
            return right;
        }
    }
}

(二).小区间改造法

  • 由于快速排序是递归进行的,当递归到最后几层时,此时数组中的值其实已经接近有序,而且这段区间再递归会极大占用栈(函数栈帧开辟的地方)的空间,
    在这里插入图片描述
  • 接下来,我们对其进行优化,如果区间数据量小于10,我们就不进行递归快速排序了,转而使用插入排序。(插入排序会在最后总的代码中体现)
    代码实现:
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyIndex = PartSort3(a, left, right);

	// 小区间
	if (keyIndex - 1 - left > 10)
	{
		QuickSort(a, left, keyIndex - 1);
	}
	else
	{
		InsertSort(a + left, keyIndex - 1 - left + 1);//插入排序
	}

	if (right - (keyIndex + 1) > 10)
	{
		QuickSort(a, keyIndex + 1, right);
	}
	else
	{
		InsertSort(a + keyIndex + 1, right - (keyIndex + 1) + 1);插入排序
	}
}

四、快速排序的时间复杂度计算

  • 每一趟的时间复杂度为O(N)
  • 递归的时间复杂度为O(logN)
  • 故而总的时间复杂度为O(N*logN)
    在这里插入图片描述

五、快速排序的完整代

#include<stdio.h>
//打印数组函数:
void Print(int* a, int n) {
    for (int i = 0; i < n; i++)
    {
        printf("%d ", a[i]);
    }
    printf("\n");
}

//插入排序:
void InsertSort(int* a, int n) {
    for (int i = 0;i < n - 1;i++) {
        int end=i;
        //[0,end]有序,插入后,[0,end+1]有序
        int tmp = a[end + 1];
        while (end >= 0)
        {
            if (a[end] > tmp)
            {
                a[end + 1] = a[end];//数据往后挪动
                end--;
            }
            else {
                break;
            }
        }
        a[end + 1] = tmp;//比较到末尾的一刻,或者照顾到在中间的情况:当end比tmp小的时候
    }


//交换元素的函数:
    void Swap(int* p1, int* p2)
{
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}


//三数取中函数:
  int GetMidIndex(int* a, int left, int right)
{
    int mid = (left + right) >> 1;//等效于整除于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[mid] > a[right])
        {
            return mid;
        }
        else if (a[left] < a[right])
        {
            return left;
        }
        else
        {
            return right;
        }
    }
}

//利用挖洞法来进行快速排序:
int Part1Sort(int* a, int left, int right)//实现分治递归//时间复杂度:O(N*log(N))
{
    //三数取中法,如果一开始为有序时候,为了降低时间复杂度(将begin赋予整体数据较为靠中数据的下标值)
    int index = GetMidIndex(a, left, right);
    Swap(&a[left], &a[index]);
    //第一趟排序:
    int begin = left, end = right;
    int pivot = begin;//让洞的位置为最开始位置
    int key = a[begin];
    while (begin < end)
    {
        //从最右面开始找比key小的数,放到左面
        while (begin < end && a[end] >= key)//注意begin<end要加上防止比的超出界限,a[end]>=key要加上等于防止出现死循环BUG
        {
            --end;
        }
        //小的数据放在左边的坑位,自己的位置形成新的坑位
        a[pivot] = a[end];
        pivot = end;
        //从最左面开始找比key大的数,放在右面
        while (begin < end && a[begin] <= key)
        {
            ++begin;
        }
        //大的数据放在右边的坑位,自己的位置形成新的坑位
        a[pivot] = a[begin];
        pivot = begin;
    }
    //最后将begin或者end(此两者相等)赋给洞,此时洞的位置即key值自己真实的大小位置
    pivot = begin;
    a[pivot] = key;
    //此时再让左子区间和右子区间分别有序,那么最终就有序了
    //实现方法:分治递归:}
    return pivot;
}


//左右指针法来实现快速排序:
int Part2Sort(int* a, int left, int right)//左右指针法来进行快速排序
{
    int index = GetMidIndex(a, left, right);
    Swap(&a[left], &a[index]);
    int begin = left, end = right;
    int keyi = begin;
    while (begin < end)
    {
        //从右边开始找比a[keyi]小的数据
        while (begin < end && a[end] >= a[keyi])
        {
            --end;
        }
        //从左边开始找比a[keyi]大的数据
        while (begin < end && a[begin] <= a[keyi])
        {
            ++begin;
        }
        //从两边分别找到比a[keyi]小的数据和比a[keyi]大的数据,将两数据进行交换
        Swap(&a[begin], &a[end]);
    }
    //将a[keyi]于a[begin]交换或者于a[end]交换,将a[keyi]放在真实大小的位置
    Swap(&a[begin], &a[keyi]);
    return begin;

}


//前后指针法来进行快速排序:
int Part3Sort(int* a, int left, int right)
{
    int index = GetMidIndex(a, left, right);
    Swap(&a[left], &a[index]);
    int keyi = left;
    int prev = left, cur = left + 1;
    //cur找比a[keyi]小的数,每次遇到比key小的值就停下来,++prev,交换prev和cur位置的值
    while (cur<=right)
    {
        if (a[cur] < a[keyi]&&++prev !=cur)//这里不需要等于,不会出现死循环的BUG
        {
            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;
//再次可改变Part3Sort的函数名来实现上述三种快排方式的转变
	int keyIndex = Part3Sort(a, left, right);
	
	// 小区间优化
	if (keyIndex - 1 - left > 10)
	{
		QuickSort(a, left, keyIndex - 1);
	}
	else
	{
		InsertSort(a + left, keyIndex - 1 - left + 1);
	}

	if (right - (keyIndex + 1) > 10)
	{
		QuickSort(a, keyIndex + 1, right);
	}
	else
	{
		InsertSort(a + keyIndex + 1, right - (keyIndex + 1) + 1);
	}
}
int main()
{
    int a[] = { 3, 5, 2, 7, 8, 6, 1, 9, 4, 0 };
    QuickSort(a, 0, sizeof(a) / sizeof(int)-1);
    Print(a, sizeof(a) / sizeof(int));
    return 0;
}
  • 注意可以通过改变Part1Sort,Part2Sort,Part3Sort,的函数名来实现对快排三种方式的转变

总结

  • 本文主要呈现了实现快排的三种方式–挖洞法、左右指针法、前后指针法,此过程大家可依据调试过程加深理解,以及优化快排的 两种小技巧–三数取中以及设置小区间的 方法。
  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值