• 【数据结构】快速排序算法你会写几种?


    在这里插入图片描述

    👦个人主页:Weraphael
    ✍🏻作者简介:目前正在学习c++和算法
    ✈️专栏:数据结构
    🐋 希望大家多多支持,咱一起进步!😁
    如果文章有啥瑕疵
    希望大佬指点一二
    如果文章对你有帮助的话
    欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


    一、hoare版本(左右指针法)

    1.1 算法思想

    【思想】

    1. 任取待排序元素序列中的某元素作为 基准值key。一般是第一个元素和最后一个元素。

    2. 【重点】 将待排序集合 分割成两子序列使得左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值。

    做法:定义两个变量ij分别指向开头和最后一个元素。请务必记住次结论:如果选取的基准值是第一个元素,要让j先动,反之让i先动。假设选取的基准值为第一个元素并且要求序列为升序。

    j遇到小于等于 key的数,则停下,然后i开始走,直到i遇到一个大于key的数时,将ij的内容交换,如果区间内还有元素,则重复以上操作。最后你会发现:ij最后一定会相遇(可以参考下面动图),此时将相遇点的内容与key交换即可

    1. 最后左右子序列重复该过程,直到所有元素都排列在相应位置上为止。(递归)

    以下是一趟排序的动图演示

    在这里插入图片描述

    在我的往期博客中,我写了一篇快排算法模板,有兴趣的可以来看看:点击跳转

    1.2 hoare版本代码实现

    #include 
    
    void Swap(int* x, int* y)
    {
        int t = *x;
        *x = *y;
        *y = t;
    }
    
    // hoare版本
    void quick_sort(int a[], int l, int r)
    {
        // 如果区间只有一个元素或者没有元素,就没必要排序了
        if (l >= r)
            return;
        // 1. 选取一个基准值
        // 以选取第一个元素为例
        int key = a[l];
        // 2. 定义i和j,i从左向右走,j从右向左走。
        int i = l, j = r;
        while (i < j)
        {
            // 注意:若选择第一个元素作为基准值,则需要j先走;反之让i先走
            while (i < j && a[j] >= key) // 找小
            {
                j--;
            }
            while (i < j && a[i] <= key) // 找大 
            {
                i++;
            }
            // 交换
            if (i < j)
            	Swap(&a[i], &a[j]);
        }
        // 循环结束后,i和j一定会相遇,和基准值交换
        Swap(&a[l], &a[i]);
    
        // 递归
        quick_sort(a, l, i - 1);
        quick_sort(a, i + 1, r);
    }  
    
    int main()
    {
        int a[] = { 6, 1, 2, 7, 9, 3, 4, 5, 10, 8 };
        int n = sizeof(a) / sizeof(a[0]);
        quick_sort(a, 0, n - 1);
    
        for (int i = 0; i < n; i++)
            printf("%d ", a[i]);
    
        printf("\n");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    【程序结果】

    在这里插入图片描述

    注意:这里会有一个越界 + 死循环问题

    在这里插入图片描述

    • while (i < j && a[j] >= key)循环中,如果不加i < j,那么假设序列已经是升序了,那么就会越界;while (i < j && a[i] <= key)也同理

    在这里插入图片描述

    • 并且如果只写a[i] < key,将序列中出现数据冗余,就会陷入死循环
    • 最后还有一个问题,就是本人初学时犯的(忽略了一个小的知识点qwq)。

    与基准值交换Swap(&a[l], &a[i]),不能写成Swap(&key, &a[i])。因为key是一个局部变量,只是存储序列key,虽然交换了,但是序列第一个元素并没有改变。我也是通过调试发现的hh

    1.3 hoare版本性能分析

    首先可以和堆排序以及希尔排序来比较一下

    在这里插入图片描述

    我们发现:当数据个数为一百万的时候,快速排序还是非常快的

    接下来再来分析时间复杂度

    快速排序其实是二叉树结构的交换排序方法

    在这里插入图片描述

    递归的高度是logN,而单趟排序基本都要遍历一遍序列,大约有N个数,因此时间复杂度是NlogN

    那么快排最坏的情况是什么?

    最坏的情况即包括逆序,也包括有序。其时间复杂度是O(N2)

    在这里插入图片描述

    如果数据量大的话,那么栈一定会爆。那如果是这样,快排还叫快排吗?

    先说结论:快排的时间复杂度是:O(NlogN)

    那么如何解决这个问题呢?我们发现,有序和无序就是因为基准值选取的不好。

    因此,有人提出了优化基准值可以选取随机值或者三数取中

    1.4 基准值选取随机值(优化)

    做法:使用rand函数随机选取区间中的下标rand() % (r - l),但是这样远远不够,因为递归的时候,左区间会随之改变。因此正确下标取法rand() % (r - l) + l

    void quick_sort(int a[], int l, int r)
    {
        if (l >= r)
            return;
        // 随机选key
        // 区间下标范围
        int randIdx = (rand() % (r - l)) + l; 
        Swap(&a[l], &a[randIdx]);
        
        // 以下都和hoare版本一样
        int key = a[l];
        int i = l, j = r;
        while (i < j)
        {
            while (i < j && a[j] >= key) 
            {
                j--;
            }
            while (i < j && a[i] <= key)
            {
                i++;
            }
            Swap(&a[i], &a[j]);
        }
        Swap(&a[l], &a[i]);
    
        quick_sort(a, l, i - 1);
        quick_sort(a, i + 1, r);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    【程序结果】

    在这里插入图片描述

    1.5 三数取中(优化)

    • 三数取中是指:第一个元素、最后一个元素和中间元素,选出不是最小也不是最大的那一个(找的是下标)
    int GetMinNum(int a[], int l, int r)
    {
        int mid = (l + r) >> 1;
        // 选出不是最大也不是最小的
        // 两两比较
        if (a[l] < a[mid])
        {
            if (a[mid] < a[r])
            {
                return mid;
            }
            else if (a[r] < a[l])
            {
                return l;
            }
            else
            {
                return r;
            }
        }
        else // a[l] >= a[mid]
        {
            if (a[l] < a[r])
            {
                return l;
            }
            else if (a[r] < a[mid])
            {
                return mid;
            }
            else
            {
                return r;
            }
        }
    }
    
    void quick_sort(int a[], int l, int r)
    {
        if (l >= r)
            return;
        // 三数取中
    
        int mid = GetMinNum(a, l, r);
        Swap(&a[mid], &a[l]);
        int key = a[l];
    
        int i = l, j = r;
    
        while (i < j)
        {
            while (i < j && a[j] >= key) 
            {
                j--;
            }
            while (i < j && a[i] <= key)
            {
                i++;
            }
            Swap(&a[i], &a[j]);
        }
        Swap(&a[l], &a[i]);
    
        quick_sort(a, l, i - 1);
        quick_sort(a, i + 1, r);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    【程序结果】

    在这里插入图片描述

    1.6 三路划分

    这个优化是为了解决大量元素重复的问题,这个博主还未学到。暂且先放放hh

    二、挖坑法

    2.1 算法思路

    在这里插入图片描述

    【算法思路】

    1. 选取一个基准值并用一个变量保存(这个基值一般是第一个元素或者是最后一个元素),然后序列中基准值这个位置相当于是一个坑等待下一个元素填入
    2. 如果选取的基准值是第一个元素,老样子j要从右边向左开始找小,如果找到小,就将j指向的元素填入到坑中,而此时 j这个位置是一个坑等待填入;接下来就是i从左向右找大,如果找到了大,就将i指向的元素填入到坑中,同理的,i这个位置是一个坑等待填入
    3. 最后ij相遇,并且一起站着一个坑位hole,然后就把基准值key填入即可
    4. 递归重复区间[l, hole - 1]和区间[hole + 1, r]

    本质上来说,填坑法和hoare版本类似,相比其更加容易理解

    2.2 代码实现

    void QuickSort5(int a[], int l, int r)
    {
    	if (l >= r) return;
    	int x = a[l];
    	// 如果选择左端点为基准值
    	// 那么坑位一开始是以基准值为下标
    	int hole = l;
    
    	int i = l, j = r;
    	while (i < j)
    	{
    		while (i < j && a[j] >= x) // 找小
    		{
    			j--;
    		}
    		// 循环结束后,来到此处说明找到小了
    		// 将小的填入上一个坑位
    		// 再更新坑位
    		a[hole] = a[j];
    		hole = j;
    
    		while (i < j && a[i] <= x)
    		{
    			i++;
    		}
    		// 和上同理
    		a[hole] = a[i];
    		hole = i;
    	}
    	// 最后i和j相遇一定会同站一个坑位
    	// 将基准值填入坑位即可
    	a[hole] = x;
    	// 递归
    	QuickSort5(a, l, hole - 1);
    	QuickSort5(a, hole + 1, r);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    三、前后指针法(最优 + 最好写的方法)

    3.1 算法思路

    在这里插入图片描述

    1. 选出一个基准值key,一般是最左边或是最右边的。
    2. 起始时,prev指针指向序列开头,cur指针指向prev的下一个位置。
    3. cur指向的内容小于keyprev先向后移动一位,然后交换prevcur指针指向的内容,然后cur指针继续向后遍历
    4. cur指向的内容大于key,则cur指针直接向后遍历。因此可以得出结论,cur本质就是在找小,然后让小的往前靠
    5. cur超出序列,此时将keyprev指针指向的内容交换即可。

    经过一次单趟排序,最终也能使得key左边的数据全部都小于keykey右边的数据全部都大于key

    最后再重复以上操作,直至序列只有一个数据或者序列没有数据时。(递归区间[l, prev - 1][prev + 1, r]

    3.2 代码实现

    void quick_sort(int a[], int l, int r)
    {
        if (l >= r)
            return;
            
        // 1. 选出一个key
        int key = a[l];
        // 2. 起始时,prev指针指向序列开头,cur指针指向prev+1。
        int prev = l, cur = prev + 1;
        
        while (cur <= r)
        {
            // 3. 若cur指向的内容小于key
            // 则prev先向后移动一位,
            // 然后交换prev和cur指针指向的内容,
            // 然后cur指针++(可以归到第四点)
            if (a[cur] < key)
            {
                ++prev;
                Swap(&a[prev], &a[cur]);
            }
            // 4. 若cur指向的内容大于key,则cur指针直接++
            ++cur;
        }
        // 若cur到达r + 1位置,此时将key和prev指针指向的内容交换即可。
        Swap(&a[l], &a[prev]);
        // 递归
        quick_sort(a, l, prev - 1);
        quick_sort(a, prev + 1, r);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
  • 相关阅读:
    Java入门-07-Java学习-JDK下载和安装
    IOS Swift 从入门到精通: 结构体的访问控制、静态属性和惰性
    1. 梯度下降法
    《最重要的事,只有一件》集中精力在最重要的事情上,可以更高效地实现个人和职业上的成功 - 三余书屋 3ysw.net
    Mendelay-文献管理软件使用教程
    旷世轻量化网络ShuffulNetV2学习笔记
    测试老鸟总结,Web/APP与接口测试测试流程总结,避背黑锅...
    STM32F1网络编程-HTTP服务器(基于W5500网卡)
    CCF CSP认证 历年题目自练Day37
    STM32F103标准库开发---SPI实验---底层驱动程序
  • 原文地址:https://blog.csdn.net/Weraphael/article/details/134431772