【数据结构】八大排序之快速排序算法

举报
修修修也 发表于 2024/09/30 16:49:27 2024/09/30
【摘要】 ​ 🦄个人主页:修修修也 🎏所属专栏:数据 结 构 ⚙️操作环境:Visual Studio 2022​编辑目录一.快速排序 简 介及思路 二.快速排序代 码实现 的三种方式 📌 左右交 换 法 📌 挖坑填坑法 📌 前后指 针 法 三.快速排序的 时间 复 杂 度分析 四.快速排序的 优 化 🎏 优 化 选 key方式 📌 随机 选 key法 📌 三数取中法 🎏 小区 间优...

 

🦄个人主:修修修也

🎏所属专栏:数据

⚙️操作:Visual Studio 2022

​编辑


一.快速排序 介及思路

二.快速排序代 码实现 的三种方式

📌 左右交

📌 挖坑填坑法

📌 前后指

三.快速排序的 时间 度分析

四.快速排序的

🎏 key方式

📌 随机 key法

📌 三数取中法

🎏 小区 间优

📌 小区 间优 化的原理

📌 小区 间优 化的代 码实现

五.借助 栈实现 递归 快速排序

📌 什么要将 递归 的快速排序算法改 递归 ?

📌 递归 函数改非 递归 的思路

📌 快速排序改非 递归 的思路

📌 快速排序改非 递归 的代 码实现

六.快速排序的三路划分

结语


一.快速排序介及思想

快速排序(Quick Sort)是一种效率较高的排序算法.

它的基本思想是:

一趟排序将待排数据分割成独立的两部分

其中一部分数据的关字均比另一部分数据的关字小

可分别对这两部分数据继续进行排序,以达到整个序列有序的目的.

算法动图演示:

​编辑


二.快速排序代码实现的三种方式

我们了解了快速排序的基本思想一趟排序将待排数据分割成独立的两部分之后,在代码的实现上,其实就有很多可以自由发挥的空间,如下较为主流的快速排序有三种实现思路,接下来我将一一带大家理解这三个思路并使用它们实现快排算法:

注:本文的快排实现思路均以升序为例!

📌左右交

左右交法的思路是:

1. 定当前待排序列的首元素位置的值为基准(key).

2. 然后置一个右指,使其从后向前遍,找到比基准(key)小的元素就停下来.

3. 置一个左指,使其从前向后遍,找到比基准(key)大的元素就停下来.

4. 当左右指都找到相元素,交找到的元素.

5. 重复步2~4,直到左右指相遇

6. 左右指相遇后,将基准(key)与相遇位置做交,此被重新一分二成两个新的待排子序列.

7. 别继续对新的待排子序列继续执行步1~6排序,直到所有元素都排列在相位置上.

左右交法算法演示:​编辑

 清楚了左右交换法实现快排的思路后,我们编写实现代码就比较容易了,代码如下:

//交换函数

void Swap(int* a, int* b)

{

    int tmp = *a;

    *a = *b;

    *b = tmp;

}




//快速排序(左右交换法

void QuickSort_swap(int* a, int left, int right)

{

    if (left >= right)

        return;

    int begin = left, end = right;




    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]);

    }

    //相遇时交换key和相遇位置的值

    Swap(&a[keyi], &a[left]);




    keyi = left;//用keyi记录下本轮确定的基准值位置




    QuickSort_swap(a, begin, keyi - 1);//递归排序左区间[begin , keyi-1]

    QuickSort_swap(a, keyi + 1, end);//递归排序右区间[keyi+1 , end]

}


📌挖坑填坑法

挖坑填坑法是基于快排的基础思想提出的一种创新实现的思路,它的思路是这样的:

1. 记录当前待排序列的首元素基准(key).

2. 时认为首元素的位置是空缺的,即位置成了一个坑.

3. 置一个右指,使其从后向前遍,找到比基准(key)小的元素停下来将其填入才的坑位中,此时认为右指找到的个元素位置又形成了一个坑.

4. 置一个左指,使其从前向后遍,找到比基准(key)大的元素停下来将其填入才的坑位中,此时认为左指找到的个元素位置又形成了一个坑.

5. 左右指不断向中不断填坑又形成新坑,直到两指相遇

6. 最后将基准(key)填入左右指相遇位置的坑中,此被重新一分二成两个新的待排子序列.

7. 别继续对新的待排子序列继续执行步1~6排序,直到所有元素都排列在相位置上.

挖坑填坑法算法演示: ​编辑

挖坑填坑法实现快排代码如下:

//快速排序(挖坑填坑法

void QuickSort_hole(int* a, int left, int right)

{

    if (left >= right)

        return;

    int begin = left, end = right;




    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;

    }




    //相遇时把key的值填进相遇的坑里

    a[hole] = key;




    QuickSort_hole(a, begin, hole - 1);//递归遍历左区间[begin , hole-1]

    QuickSort_hole(a, hole + 1, end);//递归遍历右区间[hole+1 , end]

}


📌前后指

前后指针法思路为:

1. 定当前待排序列的首元素位置的值为基准(key).

2. 立前指prev,使其指向序列开,即基准位置

3. 立后指cur,使其指向prev指的后一个位置

4. 判断cur指向的数据是否小于key:如果小于,prev后移一位,然后将cur指向的内容与prev指向的内容交, 然后cur指++;如果不小于,cur++.

5. 4,直到cur移到超出序列范围时,交prev位置和基准位置的,此被重新一分二成两个新的待排子序列.

6. 别继续对新的待排子序列继续执行步1~5排序,直到所有元素都排列在相位置上.

前后指法算法演示:

编辑

前后指针法实现快排代码如下:

//交换

void Swap(int* a, int* b)

{

    int tmp = *a;

    *a = *b;

    *b = tmp;

}




//快速排序(前后指针法

void QuickSort_follow(int* a, int left, int right)

{

    if (left >= right)

        return;

    int begin = left, end = right;




    int keyi = left;//选定序列首元素为基准值,记录下基准值位置




    int prev = left;

    int cur = prev + 1;




    while (cur <= right) //cur没有越界就继续

    {

        if (a[cur] < a[keyi] && ++prev != cur) //如果cur指向的值小于key,并且++prev与cur不重叠

        {

            Swap(&a[cur], &a[prev]); //就交换它们

        }

        cur++;

    }




    Swap(&a[prev], &a[keyi]);//交换prev和key的值

    keyi = prev;




    QuickSort_follow(a, begin, keyi - 1);//递归遍历左区间[begin , keyi-1]

    QuickSort_follow(a, keyi + 1, end);//递归遍历右区间[keyi+1 , end]

}

三.快速排序的时间度分析

"快速排序的平均时间为 ,其中n待排序序列中数据的个数,k某个常数,经验证,在所有同数量的此(先)排序算法中,快速排序的常数因子k最小.因此,就平均时间而言,快速排序是目前被认为最好的一种内部排序方法.

通常,快速排序被认为,在所有同数量(O(nlogn))的排序算法中,其平均性能最好.但是,若初始数据序列按关字有序或基本有序,快速排序将冒泡排序,其时间O(n^2)."

                                ——《数据构》蔚敏

也就是说,快排的时间我们可以认为是O(nlogn),但当遇到原数本身有序的情况下,其时间度就会退化至O(n^2),这个其实很好理解,举个例子就明白了:

情况下,即每趟选择key都恰好选择到数的中间值时(第n层可以确定个数字位置),快排的时间复杂度如下图完全(满)二叉树:​编辑

该树需要遍一遍数,时间n,而,因此最优状态下快排的时间仅为O(nlogn).

最坏情况下,即每趟选择key都恰好选择到数最大或最小的时(即每一层都只能确定一个数字位置),快排的时间复杂度如下单支树:

​编辑

一遍数,时间n,而高也n,因此最坏状快排的时间O(n^2).

综上,对快排时间复杂度的分析,我们不光理解了什么快排排先天有序的数组时反而效率最差,同样也为我们后续对快排算法的提供了思路.


四.快速排序的

🎏key方式

既然在快排在面对原本就接近有序的数组时排序会因为key值的选取导致效率降低,那么我们不妨化一下我快排时选key的方式,下面为大家介绍两种常用的key的方式:

📌随机key法

随机key的思路:

1. 先使用rand()函数随机出一个在[left,right]范内的下标值randi

2. 将randi下的数据和keyi下的数据互


随机key函数的实现:

//随机选key法

void SwapRand_key(int* a,int left, int right)

{

int randi = left + (rand() % (right - left));

     Swap(&a[left], &a[randi]);

}


合随机key法实现快排

我们写好随机选key函数后只需要在正常快排函数中选定keyi后(如下函数的第15行后)调用一下随机选keyi函数就可以将随机选出的key值和原本的key值做交换了.

实现代码如下:

//随机选key法

void SwapRand_key(int* a,int left, int right)

{

int randi = left + (rand() % (right - left));

     Swap(&a[left], &a[randi]);

}




//快速排序(前后指针法

void QuickSort_follow(int* a, int left, int right)

{

    if (left >= right)

        return;

    int begin = left, end = right;




    int keyi = left; //选定序列首元素为基准值,记录下基准值




    SwapRand_key(a,left, right); //随机选出一个数据交换基准值key

    

    int prev = left;

    int cur = prev + 1;




    while (cur <= right)

    {

        if (a[cur] < a[keyi] && ++prev != cur)

        {

            Swap(&a[cur], &a[prev]);

        }

        cur++;

    }

    Swap(&a[prev], &a[keyi]);

    keyi = prev;




    QuickSort_follow(a, begin, keyi - 1);//递归遍历左区间[begin , keyi-1]

    QuickSort_follow(a, keyi + 1, end);//递归遍历右区间[keyi+1 , end]

}


📌三数取中法

三数取中法的思路是:

1. 序列首元素,尾元素,中元素,取三者中的中间值midi

2. 将midi下的数据和keyi下的数据互


三数取中函数的实现:

//三数取中法

void SwapMid_key(int* a, int left, int right)

{

    int midi = (left + right) / 2;

    if (a[left] < a[midi])

    {

        if (a[midi] < a[right])

        {

            midi = midi;

        }

        else if (a[left] > a[right])

        {

            midi = left;

        }

        else

        {

            midi = right;

        }

    }

    else // a[left] > a[mid]

    {

        if (a[midi] > a[right])

        {

            midi = midi;

        }

        else if (a[left] < a[right])

        {

            midi = left;

        }

        else

        {

            midi = right;

        }

    }




    if (midi != left)

        Swap(&a[midi], &a[left]);

}


合三数取中法实现快排

我们写好三数取中函数后只需要在正常快排函数中选定keyi后(如下函数的第45行后)调用一下三数取中函数就可以将三数取中选出的key值和原本的key值做交换了.

实现代码如下:

//三数取中函数

void SwapMid_key(int* a, int left, int right)

{

    int midi = (left + right) / 2;

    if (a[left] < a[midi])

    {

        if (a[midi] < a[right])

        {

            midi = midi;

        }

        else if (a[left] > a[right])

        {

            midi = left;

        }

        else

        {

            midi = right;

        }

    }

    else // a[left] > a[mid]

    {

        if (a[midi] > a[right])

        {

            midi = midi;

        }

        else if (a[left] < a[right])

        {

            midi = left;

        }

        else

        {

            midi = right;

        }

    }

    if (midi != left)

        Swap(&a[midi], &a[left]);

}




//快速排序(前后指针法

void QuickSort_follow(int* a, int left, int right)

{

    if (left >= right)

        return;

    int begin = left, end = right;




    int keyi = left; //选定序列首元素为基准值,记录下基准值




    SwapMid_key(a, left, right); //三数取中法选出基准值后交换基准值




    int prev = left;

    int cur = prev + 1;




    while (cur <= right)

    {

        if (a[cur] < a[keyi] && ++prev != cur)

        {

            Swap(&a[cur], &a[prev]);

        }

        cur++;

    }

    Swap(&a[prev], &a[keyi]);

    keyi = prev;




    QuickSort_follow(a, begin, keyi - 1);//递归遍历左区间[begin , keyi-1]

    QuickSort_follow(a, keyi + 1, end);//递归遍历右区间[keyi+1 , end]

}



🎏小区间优

📌小区间优化的原理

快排的递归展开思路似于二叉,因此它们拥有同样的弊病,就是越靠近的底部,递归的情况就越多,并且递归模量非常大,拿下面这颗树来举例:

我们递归遍历该树,发现递归(紫色)访问次数竟然和有效访问次数(绿)是相同的.而对于快排来说,这样的空递归不仅浪费时间,而且是没有任何实际意义的.

​编辑

因此我们可以考虑采用一种,将快排的递归加以限制,比如当我们不断分割快排子区,当子区元素小于10个数时,我们就不再行快排递归排序,而使用直接插入排序来对该小区间进行排序,这样就可以有效的消一半的递归,从而提升快排的效率.


📌小区间优化的代码实现

清楚了上面的原理之后,我们实现小区间优化的思路为:

1. 判断小区是否小于10个数.

2. 如果区不小于10,则执行快排逻辑.

3. 如果区小于等于10,则执行直接插入排序逻辑.

综上所述,小区间代码实现如下:

//交换函数

void Swap(int* a, int* b)

{

    int tmp = *a;

    *a = *b;

    *b = tmp;

}




//三数取中法取key函数

void SwapMid_key(int* a, int left, int right)

{

    int midi = (left + right) / 2;

    if (a[left] < a[midi])

    {

        if (a[midi] < a[right])

        {

            midi = midi;

        }

        else if (a[left] > a[right])

        {

            midi = left;

        }

        else

        {

            midi = right;

        }

    }

    else // a[left] > a[mid]

    {

        if (a[midi] > a[right])

        {

            midi = midi;

        }

        else if (a[left] < a[right])

        {

            midi = left;

        }

        else

        {

            midi = right;

        }

    }

    if (midi != left)

        Swap(&a[midi], &a[left]);

}




//直接插入排序函数

void InsertSort(int* a, int n)

{

    for (int i = 1; i < n; i++)

    {

        int end = i - 1;

        int tmp = a[i];

        //将tmp插入到[0,end]这个区间里

        while (end >= 0)

        {

            if (tmp < a[end])

            {

                a[end + 1] = a[end];

                end--;

            }

            else

            {

                break;

            }

        }

        a[end + 1] = tmp;

    }

}




//小区间优化版快排

void QuickSort(int* a, int left, int right)

{

    if (left >= right)

        return;

    if ((right - left + 1 ) > 10)

    {

        int keyi = left;

        SwapMid_key(a, left, right);




        //双指针前后移动法快排

        int prev = left;

        int cur = left + 1;

        while (cur <= right)

        {

            if (a[cur] < a[keyi] && ++prev != cur)

                Swap(&a[cur], &a[prev]);




            ++cur;

        }

        Swap(&a[prev], &a[keyi]);

        keyi = prev;




        QuickSort(a, left, keyi - 1);

        QuickSort(a, keyi + 1, right);

    }

    else

    {

        InsertSort( a + left , right - left + 1 );

    }

}


五.借助栈实现递归快速排序

📌什么要将递归的快速排序算法改递归?

递归函数有以下几个缺点:

内存消耗大:递归调用会占用大量的内存空间,因为每次调用都需要在内存中保存当前的状态和参数。

性能低下:递归调用会增加函数调用的开销,因此在一些情况下会导致程序的性能下降。

性差:递归函数通常比较复杂,难以理解和调试,降低了代码的可读性。

可能溢出:如果递归调,会溢出的问题,使程序崩

化:一些编译器和优化工具难以对递归函数进行有效的优化,导致性能不佳。


📌递归函数改非递归的思路

1. 直接改.(如:斐波那契数列)

2. 利用栈辅助改.(如:二叉的前序遍)


📌快速排序改非递归的思路

1. 将初始数间压

2. 里取一段区,趟排序

3. 趟分割子区

4. 子区只有一个或着不存在就不入

5. 重复步2-4,直到栈为,排序完成.


📌快速排序改非递归的代码实现

因为快排改非递归时要借助栈结,因此我先相关定文件贴在这里,具体C言完整实现可以移步我的另一篇博客,在文末有数据结构栈实现的完整代码,大家可以直接粘贴过来使用:

【数据 构】 C 实现顺 (附完整运行代 ) http://t.csdnimg.cn/FL0V3 (注:如果本身没有自己实现数据的工程文件的,一定要将博客末尾的Stack.h文件和Stack.c文件粘在排序目文件里才可以正常使用的相关功能,否C言是不支持直接使用的!)

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>

#include<stdlib.h>

#include<stdbool.h>

#include<assert.h>




typedef int STDataType;




typedef struct Stack

{

    STDataType*arr;

    int top;

    int capacity;

}ST;




void STInit(ST* ps);

void STDestroy(ST* ps);

void STPush(ST* ps, STDataType x);

void STPop(ST* ps);

int STSize(ST* ps);

bool STEmpty(ST* ps);

STDataType STTop(ST* ps);

void STMenu();

综上,快排的非递归代码实现如下:

//交换函数

void Swap(int* a, int* b)

{

    int tmp = *a;

    *a = *b;

    *b = tmp;

}




//三数取中法取key函数

void SwapMid_key(int* a, int left, int right)

{

    int midi = (left + right) / 2;

    if (a[left] < a[midi])

    {

        if (a[midi] < a[right])

        {

            midi = midi;

        }

        else if (a[left] > a[right])

        {

            midi = left;

        }

        else

        {

            midi = right;

        }

    }

    else // a[left] > a[mid]

    {

        if (a[midi] > a[right])

        {

            midi = midi;

        }

        else if (a[left] < a[right])

        {

            midi = left;

        }

        else

        {

            midi = right;

        }

    }

    if (midi != left)

        Swap(&a[midi], &a[left]);

}




//快速排序主函数(非递归版

void QuickSortNonR(int* a, int left, int right)

{

    ST st;

    STInit(&st);




    STPush(&st, right);//先入后出

    STPush(&st, left);




    while (!STEmpty(&st))

    {

        int begin = STTop(&st);

        STPop(&st);

        int end = STTop(&st);

        STPop(&st);




        int keyi = begin;

        SwapMid_key(a, begin, end);




        //双指针前后移动法快排

        int prev = begin;

        int cur = begin + 1;




        while (cur <= end)

        {

            if (a[cur] < a[keyi] && ++prev != cur)

                Swap(&a[cur], &a[prev]);

            ++cur;

        }




        Swap(&a[prev], &a[keyi]);

        keyi = prev;




        if (keyi + 1 < end)

        {

            STPush(&st, end);

            STPush(&st, keyi + 1);

        }




        if (begin < keyi - 1)

        {

            STPush(&st, keyi - 1);

            STPush(&st, begin);

        }

    }




    STDestroy(&st);

}

六.快速排序的三路划分

//主要解决快速排序面对大量重复数据时效率低下的问题

//该部分内容待补


结语

希望快速排序算法解能大家有所帮助,迎大佬留言或私信与我交流.

有关更多排序相关知可以移步:

【数据 构】八大排序算法 ​​ https://blog.csdn.net/weixin_72357342/article/details/135038495?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22135038495%22%2C%22source%22%3A%22weixin_72357342%22%7D&fromshare=blogdetail

学海漫浩浩,我亦苦作舟!关注我,大家一起学,一起!

 相关文章推荐

【数据 构】八大排序之冒泡排序算法

【数据 构】八大排序之希 排序算法

【数据 构】八大排序之直接插入排序算法

【数据 构】八大排序之 简单选择 排序

【数据 构】八大排序之堆排序算法

【数据 构】八大排序之快速排序算法

【数据 构】八大排序算法之 并排序算法

【数据 构】八大排序之 数排序算法


 数据构排序篇思维导图:

​编辑


​编辑

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。