数据结构与算法⑰(第五章_八大排序)(完整代码+动图+详解+对比)(中)

简介: 数据结构与算法⑰(第五章_八大排序)(完整代码+动图+详解+对比)

数据结构与算法⑰(第五章_八大排序)(完整代码+动图+详解+对比)(上):https://developer.aliyun.com/article/1513566


3.交换排序

交换排序分为冒泡排序和快速排序,冒泡排序我们写过很多次了这里放个动图就不讲了。

3.1.1冒泡排序代码:

 
void BubbleSort(int* arr, int sz)
{
    int i = 0;
    int j = 0;
    for (i = 0;i < sz - 1;i++)
    {
        int flag = 1;
        for (j = 0;j < sz - 1 - i;j++)
        {
            if (arr[j] > arr[j + 1])
            {
                Swap(&arr[j], &arr[j + 1]);
            }
            flag = 0;
        }
        if (flag)
        {
            break;
        }
    }
}

3.1.2 快速排序

3.1.2.1 Hoare法快速排序

Hoare法快速排序的思想

快速排序的单趟:key放到它正确的位置上(整体排完序后最终放的位置),

key的左边的值比key小,key右边值比它大

单趟排完,再递归让key左边区间有序,key的右边区间有序,整体就有序了

我们先写出单趟排序PartSort(int* arr,int left,int right);因为是递归式的二叉树结构。

1.先选出一个key,一般是最左边或者最右边的。(后面用三数取中选,现在先选最左)

2.定义left和right,left向右遍历序列,right向左遍历序列(如果定义最左边的那个元素为key,那么right先走,若定义最右边的那个元素为key,则left先走)。


3.在走的过程中,若R遇到小于key的数则停下,到L开始走,直到L遇到比key大的数时,L也停下,将下标为left,right的数据交换,直到L与R相撞,最后把它们相遇的位置和a[keyi]交换,返回L与R相遇的位置。


4.经过一次单趟排序,最终使得key左边的数据都小于key,右边的数据都大于key。


5.然后我们再将key的左序列和右序列再次进行这种单趟排序,每进行一次单趟排序就可以确定一个数据的排序位置,如此递归操作下去,直到最后左右序列只有一个数据或没有数据,则层层返回,此时序列就排好啦。

 
int PartSort1(int* arr, int left, int right)//Hoare法快速排序
{
    int keyi = left;//左边做key
    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 left, int right)
{
    if (left >= right)
    {
        return;
    }
    int keyi = PartSort1(arr, left, right);
    QuickSort(arr, left, keyi - 1);
    QuickSort(arr, keyi + 1, right);
}

为什么左边做key,要让右边先走?

结论:因为要保证相遇位置的值,比key小或者就是key的位置

①right先走,right停下来,left去遇到right

则相遇位置就是right停下里的位置,right停的位置就是比key要小的位置

②right先走,right没有找到比key要小的值,right去遇到了left

相遇位置要么是left上一轮停下里的位置(本来是大的,但交换变成小的了),

要么就是key的位置(第一次往左找就找不到)

3.1.2.2 挖坑法快速排序

挖坑法 只是比Hoare法 好理解一点,并没有实质的改变

1.利用优化后的选key逻辑:三数取中,(后面讲,现在先选最左)


选出key变量作为坑位,保存作为坑位的值(保存起来就可以覆盖了,就相当于坑位)。


2.定义一个L,一个R,L从左向右走,R从右向左走


(若在最左边挖坑,则R先走;若在最右边挖坑,则L先走)。


3.假设我们定义最左边为坑位,在R走的过程中,若R遇到小于key的数,则将这个数抛入坑位,


然后这个数原来的位置变成坑位,然后L向右走走,若遇到大于key的数则停下,将这个数抛入坑位,


这个数的位置又形成新的坑位,如此循环下去,直到最终L与R相遇,


最后再将key抛入最后形成的坑位。


4.经过一次单趟排序,也能使得key左边的数据都小于key,右边的数据都大于key。


5.然后也是跟Hoare法一样,key的左右子序列不断的进行单趟排序(递归),直到左右序列只有一个数据或者没有数据,便停止递归,层层返回。

227a001cf0c33bc259437f5d9352464d.gif

 
/*3.假设我们定义最左边为坑位,在R走的过程中,若R遇到小于key的数,则将这个数抛入坑位,
    然后这个数原来的位置变成坑位,然后L向右走走,若遇到大于key的数则停下,将这个数抛入坑位,
    这个数的位置又形成新的坑位,如此循环下去,直到最终L与R相遇,
    最后再将key抛入最后形成的坑位。*/
int PartSort2(int* arr, int left, int right)//挖坑法快速排序
{
    int pit = left;
    int keyi = pit;
    while (left < right)
    {
        while (left < right && arr[right] >= arr[keyi])
        {
            right--;
        }
        arr[pit] = arr[right];
        pit = right;
        while (left < right && arr[left] <= arr[keyi])
        {
            left++;
        }
        arr[pit] = arr[left];
        pit = left;
    }
    arr[pit] = arr[keyi];
    return pit;
}
void QuickSort(int* arr, int left, int right)
{
    if (left >= right)
    {
        return;
    }
    //int keyi = PartSort1(arr, left, right);
    int keyi = PartSort2(arr, left, right);
    QuickSort(arr, left, keyi - 1);
    QuickSort(arr, keyi + 1, right);
}

3.1.2.3 前后指针法快速排序

前后指针法比前面两种方法更简洁,理解后不易错

1.用三数取中法,选出一个key(后面讲,这里先选最左边的)。

2.刚开始prev指向序列开头,cur指向prev+1的位置。


3.若cur指向的值比key小,则cur停下来,++prev,交换prev和cur位置上的数据,然后cur继续往后++,prev紧跟cur,但若cur指向的数据大于等于key,则cur继续++,但prev不动,如此规律进行下去,直到cur越界,跳出循环,最后将prev指向的数据和keyi指向的数据交换即可。


4.经过一次单趟排序,也能使得key左边的数据都小于key,右边的数据都大于key。


5.然后也是跟Hoare法一样,key的左右子序列不断的进行单趟排序(递归),直到左右序列只有一个数据或者没有数据,便停止递归,层层返回。


66ec14eea28485950ace3d921c3303e4.gif

 
int PartSort3(int* arr, int left, int right)//前后指针法快速排序
{
    int prev = left;
    int cur = left + 1;
    int keyi = left;
    while (cur <= right)
    {
        /*while (cur <= right && arr[cur] >= arr[keyi])
        {
            cur++;
        }
        if (cur <= right)
        {
            prev++;
            Swap(&arr[cur], &arr[prev]);
            cur++;
        }*/
        if (arr[cur] < arr[keyi] && ++prev != cur)//反正都要cur++,注意这里prev已经++了
        {
            Swap(&arr[cur], &arr[prev]);
        }
        cur++;//前面注释的优化
    }
    Swap(&arr[prev], &arr[keyi]);
    return prev;
}
void QuickSort(int* arr, int left, int right)
{
    if (left >= right)
    {
        return;
    }
    //int keyi = PartSort1(arr, left, right);
    //int keyi = PartSort2(arr, left, right);
    int keyi = PartSort3(arr, left, right);
    QuickSort(arr, left, keyi - 1);
    QuickSort(arr, keyi + 1, right);
}

3.1.3 快速排序的两个优化

3.1.3.1 三数取中

快速排序的时间复杂度是O(NlogN),是我们在理想情况下计算的结果。在理想情况下,

我们每次进行完单趟排序后,key的左序列与右序列的长度都相同:


若每趟排序所选的key都正好是该序列的中间值,即单趟排序结束后key位于序列正中间,

那么快速排序的时间复杂度就是O(NlogN)。

 可是谁能保证你每次选取的key都是正中间的那个数呢?当待排序列本就是一个

有序(或者接近有序)的序列时,我们若是依然每次都选取最左边或是最右边的数作为key,

那么快速排序的效率将达到最低


可以看到,这种情况下,快速排序的时间复杂度退化为O(N2)。其实,

对快速排序效率影响最大的就是选取的key,若选取的key越接近中间位置,则效率越高。

为了少出现上面的情况,有人提出随机选key,但还是会出现极端情况,

为了避免这种极端情况的发生,于是出现了三数取中:

三数取中,当中的三数指的是:最左边的数、最右边的数以及中间位置的数。

三数取中就是取这三个数当中,值的大小居中的那个数作为该趟排序的key。

这就确保了我们所选取的数不会是序列中的最大或是最小值了。

所以说加入三数取中后才认为快速排序的时间复杂度是O(NlogN)。

 
int GetMidIndex(int* arr, int left, int right)
{
    int mid = left + (right - left) / 2;
    if (arr[mid] > arr[left])
    {
        if (arr[mid] < arr[right])
            return mid;
        else if (arr[left] > arr[right])
            return left;
        else
            return right;
    }
    else
    {
        if (arr[mid] > arr[right])
            return mid;
        else if (arr[left] > arr[right])
            return right;
        else
            return left;
    }
}

3.1.3.2 小区间优化

随着递归深度的增加,递归次数以每层2倍的速度增加,这对效率有着很大的影响。

为了减少最后几层递归,我们可以设置一个判断语句,

当序列的长度小于某个数的时候就不再进行快速排序,转而使用其他种类的排序。

小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,

而且待排序列的长度越长,该效果越明显。

这时我们前面讲的对于小区间快速且简单的几个排序中的选择排序就派上用场(呼应铺垫)

虽然减少了80%以上的递归,但是现在编译器对递归的优化已经很大了,

所以小区间优化并没有三数取中对快排的优化大。

 
void QuickSort(int* arr, int left, int right)
{
    if (left >= right)
    {
        return;
    }
    if (right - left + 1 < 19)//可以自己取,官方是十几
    {
        InsertSort(arr + left, right - left + 1);
    }
    else
    {
        //int keyi = PartSort1(arr, left, right);
        //int keyi = PartSort2(arr, left, right);
        int keyi = PartSort3(arr, left, right);
        QuickSort(arr, left, keyi - 1);
        QuickSort(arr, keyi + 1, right);
    }
}

3.1.4 快速排序非递归版

递归版的致命问题就是递归层次太深可能会导致栈溢出,因为栈是不大的

所以对应大多数递归我们都要掌握改非递归的能力。树的递归改非递归有点难,后面再学。

递归改非递归有两种方法


直接改循环。比如斐波那契数列,还有后面的归并排序


用数据结构的栈模拟递归,因为数据结构的栈是在堆申请空间的(以前写过),堆很大


递归的算法主要是在划分子区间,如果要非递归实现快排,只要使用一个栈来保存区间就可以了。


一般将递归程序改成非递归首先想到的就是使用栈,因为递归本身就是一个压栈的过程。


非递归快排的基本思想:

1. 申请一个栈,存放排序数组的起始位置和终点位置。

2. 将整个数组的起始位置和终点位置入栈。

3. 由于栈的特性是:后进先出,right后进栈,所以right先出栈。

定义一个end接收栈顶元素,出栈操作、定义一个begin接收栈顶元素,出栈操作。

4. 对数组进行一次单趟排序,返回key关键值的下标。

5. 这时候需要排基准值key左边的序列。

如果只将基准值key左边序列的起始位置和终点位置存入栈中,等左边排序完将找不到后边的区间。

所以先将右边序列的起始位置和终点位置存入栈中,再将左边的起始位置和终点位置后存入栈中。

6.判断栈是否为空,若不为空 重复4、5步、若为空则排序完成。

这时我们把以前写的栈拷贝来:(后面会放完整八大排序和栈的代码)

数据结构与算法⑧(第三章_上)栈的概念和实现(力扣:20. 有效的括号)_GR C的博客-CSDN博客

 
void QuickSortNonR(int* arr, int left, int right)
{
    Stack st;
    StackInit(&st);
    StackPush(&st, right);//为了更类似上面的递归就先入右
    StackPush(&st, left);
 
    while (!StackEmpty(&st))
    {
        int begin = StackTop(&st);
        StackPop(&st);
 
        int end = StackTop(&st);
        StackPop(&st);
 
        int keyi = PartSort3(arr, begin, end);
        
        //区间被成两部分了 [begin,keyi-1]    [keyi+1,end]
        if (keyi + 1 < end)//先处理右
        {
            StackPush(&st, end);
            StackPush(&st, keyi + 1);
        }
        if(begin < keyi - 1)
        {
            StackPush(&st, keyi - 1);
            StackPush(&st, begin);
        }
    }
    StackDestroy(&st);
}

数据结构与算法⑰(第五章_八大排序)(完整代码+动图+详解+对比)(下):https://developer.aliyun.com/article/1513609

目录
相关文章
|
28天前
|
存储 算法 程序员
C 语言递归算法:以简洁代码驾驭复杂逻辑
C语言递归算法简介:通过简洁的代码实现复杂的逻辑处理,递归函数自我调用解决分层问题,高效而优雅。适用于树形结构遍历、数学计算等领域。
|
29天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
62 1
|
1月前
|
存储 缓存 算法
通过优化算法和代码结构来提升易语言程序的执行效率
通过优化算法和代码结构来提升易语言程序的执行效率
|
1月前
|
算法
分享一些提高二叉树遍历算法效率的代码示例
这只是简单的示例代码,实际应用中可能还需要根据具体需求进行更多的优化和处理。你可以根据自己的需求对代码进行修改和扩展。
|
1月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
135 7
|
1月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
115 8
|
1月前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
49 3
|
1月前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
2月前
|
缓存 分布式计算 监控
优化算法和代码需要注意什么
【10月更文挑战第20天】优化算法和代码需要注意什么
26 0
|
2月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
96 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。