【排序算法】——交换排序

发布于:2024-12-19 ⋅ 阅读:(9) ⋅ 点赞:(0)

 

前言

        排序(Sorting) 是计算机程序设计中的一种重要操作,它的功能是将一个数据元素(或记录)的任意序列,重新排列成一个关键字有序的序列。所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作

        排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。

简介

        所谓排序算法,即通过特定的算法因式将一组或多组数据按照既定模式进行重新排序。这种新序列遵循着一定的规则,体现出一定的规律,因此,经处理后的数据便于筛选和计算,大大提高了计算效率。对于一个排序算法的优劣,我们需要从它的时间复杂度、空间复杂度和稳定性三个方面来考虑。什么叫稳定性呢?即当两个相同的元素同时出现于某个序列之中,则经过一定的排序算法之后,两者在排序前后的相对位置不发生变化。换言之,即便是两个完全相同的元素,它们在排序过程中也是各有区别的。

        本篇文章讲述的是排序算法中的交换排序,其中包含了两种排序算法,分别是冒泡排序快速排序,下面将会一一为大家详细介绍。(用升序进行讲解)

基本思想

        所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

1.冒泡排序

        冒泡排序的基本思想是把较小的元素往前调或者把较大的元素往后调。这种方法主要是通过对相邻两个元素进行大小的比较,根据比较结果和对该两元素的位置进行交换,这样逐个依次进行比较和交换,就能达到排序目的。        

        冒泡排序是一种最基础的交换排序。之所以叫做冒泡排序,是因为每⼀个元素都可以像小气泡⼀样,根据自身大小一点一点向数组的一侧移动。冒泡排序基本上可以说是我们计算机人所接触的第一个排序算法了。我们先来通过一个动图来体会一下冒泡排序:

        第一轮,我们对第1个元素进行排序,先将它和第2个元素比较大小,如果是大于第二个元素,就将这两个元素进行交换,再对第2个和第3个元素进行比较,依次类推,重复进行上述计算,直至完成第(n一1)个和第n个元素之间的比较,此后,再按照上述过程进行第2次、第3次排序,直至整个序列有序为止。相信大家已经对冒泡排序有了深刻的认识了。

2.快速排序 

        快速排序也就是我们平常所说的快排,是Hoare于1962年提出的一种二叉树结构的交换排序方法,快速排序的基本思想是 : 任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复该过程,直到所有元素都排列在相应位置上为止,最终能够实现将所需排序的无序序列元素变为一个有序的序列。

        快速排序有有很多版本,大致有 hoare版本,挖坑法,lomuto版本等等,在这三种中前两种都是用左右指针来实现快排的过程,而第三种则是使用前后指针来实现。在这里只为大家讲解lomuto版本,若是对其他方法感兴趣的可以去搜索了解一下。

 lomuto版本 :

        lomuto版本的思路是创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。

        我们先创建两个指针 prevcur ,具体过程大家跟着动图可以深刻理解:

         快速排序的时间效率与我们所选的 Key 值有很大的关系,若是我们每次所选的 Key 值都有许多与其重复的元素,那么快速排序的时间效率就会急速下降。例如,当我们用快速排序对一个所有元素都相同的数组进行排序时,因为每一趟都找不到比 Key 小的值,所以每一趟都是 Keyprev 原地交换,这样下来,时间复杂度就来到了O(N ^ 2),那有没有什么方法能够解决这种方法呢?当然是有的,那就是使用三路划分。

三路划分 :   

        什么是三路划分呢?顾名思义,就是把一个数组分为三个部分,我们上面讲解的 lomuto版本以及其他版本都没有考虑与 Key 值相同的元素该放在哪里,既可以放在左边,也可以放在右边。那么我们的三路划分就是把与 Key 值相同的元素放在中间,我们可以把三路划分看作lumotoHoare 两种方法的结合。可以通过下图具体了解:

         这便是进行一趟排序后的结果,整个数组分为三个部分,left 左边的都小于 Key 值,right 的右边都大于 Key 值,而 leftright 中间的值即为与 Key 相等的值。这便是三路划分,具体的方法如下:

首先我们需要两个三个指针 left、 right、 cur。

        1. Key 默认取 left 的值。

        2. left 指向区间最左边,right 指向区间最右边,cur 指向 left + 1 位置。

        3. 开始循环比较

        4. cur 遇到比 Key 小的值后跟 left 位置交换,换到左边,然后 left++,cur++。

        5. cur 遇到比 Key 大的值后跟 right 位置交换,换到右边,然后 right--。

        6. cur 遇到跟 Key 相等的值后, cur++。

        7. cur > right 时循环结束。

        这就是三路划分的具体思路。在解决重复元素较多的排序时三路划分的效率非常高,但有得必有失,当解决重复元素很少的排序时三路划分的时间效率远不及lomuto等版本的时间效率。因此,我们在不同场景下,要学会选择合适的方法。

代码实现

1. 冒泡排序 :

         冒泡排序是最基础的排序算法,不仅易理解而且代码也非常简洁,下面是代码展示:

void Bubble_Sort(vector<int>& a)
{
    int n = a.size();
    //第一层for循环控制循环的趟数
    for (int i = 0; i < n - 1; i++)
    {
        //定义一个标志,一趟中没有发生交换时,说明数组已经有序,后面不必进行,直接结束即可
        int flag = 0;
        //第二层for循环进行比较
        for (int j = 0; j < n - i - 1; j++)
        {
            if (a[j] > a[j + 1])
            {
                swap(a[j], a[j + 1]);
                flag = 1;
            }
        }
        if (flag == 0)
        {
            break;
        }
    }
}

2. 快速排序 :

        对于快速排序来说,我们既可以用递归的方式去实现它的代码,当然也可以用非递归的方式,用非递归的方式来实现快速排序时需要借助数据结构--栈来实现,感兴趣的可以自己去试一试,这里只用递归的方式为大家展示。 

 lomuto

        首先是适用于重复元素较少的lomuto版本,代码如下:

void Quick_Sort(vector<int>& a, int left, int right)
{
    //函数出口 : 当left >= right时,结束递归
    if (left >= right)
    {
        return;
    }

    //用随机数来选取key值
    int randi = left + (rand() % (left - right));
    swap(a[left], a[randi]);
    
    int prev = left, cur = prev + 1, keyi = left;
    while (cur <= right)
    {
        //当a[cur] < a[keyi]并且prev加一后不等于cur时,交换两者的位置,这是因为当prev加一后如果等于cur,就相当于自己和自己交换
        if (a[cur] < a[keyi] && ++prev != cur)
        {
            swap(a[cur], a[prev]);
        }
        cur++;
    }
    //循环结束后,prev位置上的值一定是小于key值的,因此将key值也就是keyi位置上的值与prev位置上的值进行交换
    swap(a[keyi], a[prev]);

    //对左边以及右边继续进行快排
    Quick_Sort(a, left, prev - 1);
    Quick_Sort(a, prev + 1, right);
}

 三路划分:

        接下来是适用于重复元素较多时的三路划分方法,代码如下:

void Quick_Sort(vector<int>& a, int l, int r)
{
    //函数出口 : 当left >= right时,结束递归
    if (l >= r)
    {
        return;
    }
    
    int left = l, right = r;
    int cur = left + 1, key = a[left];
    while (cur <= r)
    {
        if (a[cur] < key)
        {
            swap(a[left], a[cur]);
            left++;
            cur++;
        }
        else if (a[cur] < key)
        {
            swap(a[cur], a[right]);
            right--;
        }
        else
        {
            cur++;
        }
    }

    //对左边以及右边继续进行快排
    Quick_Sort(a, l, left - 1);
    Quick_Sort(a, right + 1, r);
}

        若是对中间循环部分不理解可以自己画一个图,再结合代码相信能够很好的理解。 

总结

1.时空复杂度

        对于冒泡排序,两层for循环,不用说的O(N ^ 2)以及常数级的变量,因此

冒泡排序:

        时间复杂度:O(N ^ 2)

        空间复杂度:O(1)

        对于快速排序,因为是基于二叉树所创造的排序方法,因此其时间复杂度为O(NlogN),至于具体的计算方法感兴趣的可以百度搜索一下。因为我们递归需要开辟函数栈帧,所以空间上会有所消耗,因此

 快速排序:

        时间复杂度:O(NlogN)

        空间复杂度:O(logN ~ N)

2.稳定性

        在排序算法中,我们不光要关注算法的时空复杂度,还在看看算法的稳定性,什么是稳定性呢?

稳定性是假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

        冒泡排序过程中要特别注意的是,当相邻两个元素大小一致时,这一步操作就不需要交换位置,因此也说明冒泡排序是一种严格的稳定排序算法,它不改变序列中相同元素之间的相对位置关系。  而对于快速排序来说,元素并不能保证相同元素的相对位置,因此快速排序是不稳定。

冒泡排序 稳定

快速排序 不稳定

尾声

        若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!