当前位置: 首页 > news >正文

数据结构——排序

排序

1. 排序相关概念

将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序(递增/递减)的序列称为排序
设n个记录的序列为{R1,R2,…,Rn},其相应关键字序列为{K1,K2,…,Kn},这些关键字相互之间可以进行比较,即在它们之间存在着一种排序P1,P2,…,Pn,使其相应的关键字满足递增(升序),或递减(降序)的关系。

内排序

  • 在排序过程中,所有排序记录调到内存中进行的排序,整个排序过程不需要访问外存
  • 内排序是排序的基础
  • 内存速度快,比较过程主导性能,内排序效率主要用比较次数和移动次数来衡量

外排序

  • 排序过程中需对外存进行访问的排序
  • 参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成
  • 由于读写延迟是外存性能相关的主要问题,所以,外排序主要用读/写外存的次数作为衡量效率的指标

排序方法的稳定性

  • 稳定的排序方法是若待排序的序列中,存在多个具有相同关键字的记录,经过排序,这些记录的相对次序保持不变,则称该算法是稳定的

    原始序列:( 5, 3, 7, 2, 8, 10, 4, 7 )
    排序结果:( 2, 3 ,4, 6, 7, 7, 8, 10 ) 是稳定的
    排序结果:( 2, 3 ,4, 6, 7, 7, 8, 10 ) 是不稳定的

2. 插入排序

2.1 直接插入排序

插入排序类似于整理扑克牌。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412092034467.png

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412092035917.png

以下图为例:

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412092024798.gif

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412092041518.png

void InsertSort(int *a, int n)
{
    for (int i = 1; i < n; i++)
    {
        int end = i - 1;
        int temp = a[i];
        while (end >= 0)
        {
            if (temp < a[end])
            {
                a[end + 1] = a[end];
                end--;
            }
            else
            {
                break;
            }
        }
        a[end + 1] = temp;
    }
}

性能分析如下:

空间复杂度: O ( 1 ) O(1) O(1)

时间复杂度: O ( n 2 ) O(n^2) O(n2)

稳定性:因为每次插入元素时总是从后往前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的排序算法。

2.2 折半插入排序

折半插入排序就是使用前面提到的折半查找法,来找到需要插入元素的位置,原理很简单。确定插入位置后,再统一向后挪动元素。

void BInsertSort(SqList &L)
{
    // 对顺序表L做折半插入排序
    int i, j, low, high, m;
    for (i = 2; i <= L.length; ++i)
    {
        L.r[0] = L.r[i]; // 将待插入的记录暂存到监视哨中
        low = 1;
        high = i - 1; // 置查找区间初值
        while (low <= high)  //折半查找法
        {                         // 在r[low..high]中折半查找插入的位置
            m = (low + high) / 2; // 折半
            if (L.r[0].key < L.r[m].key)
                high = m - 1; // 插入点在前一子表
            else
                low = m + 1; // 插入点在后一子表
        } // while
        for (j = i - 1; j >= high + 1; --j)
            L.r[j + 1] = L.r[j]; // 记录后移
        L.r[high + 1] = L.r[0];  // 将r[0]即原r[i],插入到正确位置
    } // for
}

从上述算法中,不难看出折半插入排序仅减少了比较元素的次数,时间复杂度约为 O ( n l o g n ) O(nlogn) O(nlogn)该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数 n n n;而元素的移动次数并未改变它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为 O ( n 2 ) O(n^2) O(n2),但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能。折半插入排序是一种稳定的排序算法。折半插入排序仅适用于顺序存储的线性表,比如数组等

2.3 希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序

如下图所示,第一趟增量取5,第二趟取增量取3,第三趟增量为1。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101100755.png

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101054695.gif

代码如下:

// 教材写法
void ShellInsert(SqList &L, int dk)
{
    // 对顺序表L做一趟增量是dk的希尔插入排序
    int i, j;
    for (i = dk + 1; i <= L.length; ++i)
        if (L.r[i].key < L.r[i - dk].key)
        {                    // 需将L.r[i]插入有序增量子表
            L.r[0] = L.r[i]; // 暂存在L.r[0]
            for (j = i - dk; j > 0 && L.r[0].key < L.r[j].key; j -= dk)
                L.r[j + dk] = L.r[j]; // 记录后移,直到找到插入位置
            L.r[j + dk] = L.r[0];     // 将r[0]即原r[i],插入到正确位置
        } // for
}
// ShellInsert
void ShellSort(SqList &L, int dt[], int t)
{
    // 按增量序列dt[0..t-1]对顺序表L作t趟希尔排序
    int k;
    for (k = 0; k < t; ++k)
        ShellInsert(L, dt[k]); // 一趟增量为dt[t]的希尔插入排序
} // ShellSor

我更推荐下面这种写法,直观,便于理解。

template <class T>
void ShellSort(T *a, int n, int gap)
{
    while (gap > 1)
    {
        gap = gap / 3 + 1;
        for (int i = 0; i < n - gap; i++)
        {
            int end = i;
            T temp = a[end + gap];
            while (end >= 0)
            {
                if (temp < a[end])
                {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
            }
            a[end + gap] = temp;
        }
    }
}

性能分析如下:

空间复杂度: O ( 1 ) O(1) O(1)

时间复杂度:希尔排序的时间复杂度是尚未解决的难题,没有具体的值,以教材说法为主,主要与gap的值有关。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101108733.png

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101109004.png

稳定性:不稳定,划分不同区间会改变相对次序。

3. 交换排序

3.1 冒泡排序

冒泡排序就不用多说了,典中典,刚开始学习C语言就接触到的排序算法。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101111894.gif

template <class T>
void BubbleSort(T *a, int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        bool flag = false;
        for (int j = 0; j < n - i - 1; j++)//为什么是n-i-1呢,因为第n-i-1个元素是已经排好的
        {
            if (a[j] > a[j + 1])
            {
                std::swap(a[j], a[j + 1]);
                flag = true;
            }
        }
        if (!flag)
            break;
    }
}

第i趟第i躺排好第几大的值第i趟排好的的第几大的值所在下标
11n-1
22n-2
33n-3
n-1n-11
nn0

相当于就是每次从前往后挪动,都把最大的挪到后面了。以下图为例。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101131565.png

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101131906.png

性能分析如下:

空间复杂度: O ( 1 ) O(1) O(1)

时间复杂度: O ( n 2 ) O(n^2) O(n2)

稳定性:稳定

3.2 快速排序

快速排序算是排序算法中的重点和难点了,希尔排序是插入排序的优化,那么快速排序就是冒泡排序的优化。事实上,不论是C++STL、Java SDK、.NETFrameWorkSDK等开发工具包中的源代码中都能找到它的某种实现版本。

快速排序是一种分治算法,

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

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101334369.png

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101252890.gif

int PartSort(int *a,int left,int right)
{
    // 以左边为基准,要先走右找小,再走左找大
    int key=left;
    while(left<right)
    {
        
        while(left<right&&a[right]>=a[key])
        {
            right--;
        }
        while(left<right&&a[left]<=a[key])
        {
            left++;
        }
        std::swap(a[left],a[right]);
    }
    std::swap(a[left],a[key]);
    return left;
}
// 排序范围[left,right)
void QuickSort(int *a,int left,int right)
{
    if(left>=right)
        return ;
    int key=PartSort(a,left,right);
    QuickSort(a,left,key-1);
    QuickSort(a,key+1,right);
}
int PartSort(int *a,int left,int right)
{
     // 以左边为基准,要先走左找大,再走右找小
    int key=right;
    while(left<right)
    {
        while(left<right&&a[left]<=a[key])
        {
            left++;
        }
        while(left<right&&a[right]>=a[key])
        {
            right--;
        }
        std::swap(a[left],a[right]);
    }
    std::swap(a[left],a[key]);
    return left;
}
// 排序范围[left,right)
void QuickSort(int *a,int left,int right)
{
    if(left>=right)
        return ;
    int key=PartSort(a,left,right);
    QuickSort(a,left,key-1);
    QuickSort(a,key+1,right);
}

性能分析如下:

空间复杂度:由于利用了递归,需要使用栈,最好情况下为 O ( l o g 2 n ) O(log_2n) O(log2n),最坏情况下为 O ( n ) O(n) O(n),平均情况下为

时间复杂度:快速排序是一种高效的排序算法,其核心思想基于分治法。在最理想的情况下,即每次划分都能将数组分为两个几乎相等的子序列时,快速排序的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101623758.png

然而,在最坏的情况下,对逆序和有序的序列使用快速排序,即每次选择的pivot(基准值)都是最大或最小元素,导致每次只能排除一个元素,剩下的元素还需要继续排序,这样会形成类似冒泡排序的情况,需要进行大约n次的划分,每次划分需要遍历几乎所有元素,因此时间复杂度退化为 O ( n 2 ) O(n^2) O(n2)

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101629267.png

稳定性:在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,它们的相对位置会发生变化,即快速排序是一种不稳定的排序算法。例如,表L={3,2,2},经过一趟排序后L={2,2,3},最终排序序列也是L={2,2.3},显然,2与2的相对次序已发生了变化。

适用性:仅使用与顺序存储的线性表

快速排序可以进行优化,如三数取中。

template <class T>
int minIndex(T *a, int left, int right) // 快速排序三数取中优化法
{
    int mid = (left + right) / 2;
    if (a[left] < a[right])
    {
        if (a[mid] < a[left])
        {
            return left;
        }
        else if (a[mid] > a[right])
        {
            return right;
        }
        else
        {
            return mid;
        }
    }
    else
    {
        if (a[mid] > a[left])
        {
            return left;
        }
        else if (a[mid] < a[right])
        {
            return right;
        }
        else
        {
            return mid;
        }
    }
}

4. 选择排序

4.1 简单选择排序

简单选择排序就是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。原理简单,不过多说了。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101632244.gif

void SelectSort(int *a, int n)
{
    for(int i=0;i<n;i++)
    {
        int minIndex=i;
        for(int j=i;j<n;j++)
        {
            if(a[minIndex]>a[j])
            {
                minIndex=j;
            }
        }
        if(minIndex!=i)
            std::swap(a[minIndex],a[i]);
    }
}

性能分析如下:

空间复杂度: O ( 1 ) O(1) O(1)

时间复杂度: O ( n 2 ) O(n^2) O(n2)

稳定性:在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与含有相同关键字的元素的相对位置发生改变。例如,表L={2,2,1},经过一趟排序后L={1,2,2},最终排序序列也是L={1,2.2},显然,2与2的相对次序已发生变化。因此,简单选择排序是一种不稳定的排序算法。

4.2 堆排序

堆排序就是使用堆这种数据结构来进行排序,注意一点,排升序建大堆,排降序建小堆。

每次建完堆后,将堆顶元素与最后一个元素交换,堆顶元素是最大值,然后在此重新向下调整建堆,要注意建堆的范围。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101654073.png

template <class T>
void AdjustDown(T *a, int parent, int n)
{
    int child = 2 * parent + 1;
    while (child < n)
    {
        if (child + 1 < n && a[child + 1] < a[child])
            child++;
        if (a[child] < a[parent]) // 小则交换,构建小根堆
        {
            std::swap(a[child], a[parent]);
            parent = child;
            child = 2 * parent + 1;
        }
        else
        { // 没有交换则不需要交换
            break;
        }
    }
}

template <class T>
void HeapSort(T *a, int n)
{
    for (int i = n - 1 - 1 / 2; i >= 0; i--)
        AdjustDown(a, i, n);
    for (int i = n - 1; i > 0; i--)
    {
        std::swap(a[0], a[i]);
        AdjustDown(a, 0, i);
    }
}

性能分析如下:

空间复杂度: O ( 1 ) O(1) O(1)

时间复杂度:向下调整建堆的时间复杂度为 O ( n ) O(n) O(n),证明过程见树那一章节,每次调整的时间复杂度为 O ( h ) O(h) O(h),故综合时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

稳定性:堆排序可能把有相同关键字元素排到前面,故堆排序不稳定

5. 归并排序、基数排序、计数排序

5.1 归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

  1. 把原始需要排序的序列分成两个或多个子序列
  2. 把每个子序列排好序
  3. 把多个排好序的子序列合并称一个序列,即为结果

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101749130.png

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101737437.gif

void _Merge(int *a, int left, int right, int *tmp)
{
    if (left >= right)
        return;
    int mid = (left) + (right - left) / 2;
    _Merge(a, left, mid, tmp);
    _Merge(a, mid + 1, right, tmp);
    int begin1 = left, end1 = mid;
    int begin2 = mid + 1, end2 = right;
    int i = left;
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (a[begin1] < a[begin2])
            tmp[i++] = a[begin1++];
        else
            tmp[i++] = a[begin2++];
    }
    //当遍历完其中一个区间,将另一个区间剩余的数据直接放到tmp的后面
    while (begin1 <= end1)
        tmp[i++] = a[begin1++];
    while (begin2 <= end2)
        tmp[i++] = a[begin2++];
    // int j=0;
    for (int j = left; j <= right; j++)
        a[j] = tmp[j];
}
void MergeSort(int *a, int n)
{
    int *tmp = (int *)malloc(sizeof(int) * n);
    _Merge(a, 0, n - 1, tmp);
    free(tmp);
}

5.2 基数排序

对于数字型或字符型的单关键字,可以看成是由多个数位或多个字符构成的多关键字,采用多关键字排序,称作基数排序。

为实现多关键字排序,通常有两种方法:第一种是最高位优先(MSD)法,按关键字位权重递减依次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列;第二种是最低位优先(LSD)法,按关键字位权重递增依次进行排序,最后形成一个有序序列。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412101900254.gif

我们以研究生成绩排序为例,按照分数高低排序,分数相同按专业课排序。

#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <algorithm>
#include <iomanip>
#include <ctime>
#include "LinkQueue.hpp"  //自己在实验2写的链栈

// 基数排序
#define K 3 // 关键字个数 百位、十位、个位
#define Radix 10

struct Student
{
    int stuid;        // 考生号
    std::string name; // 姓名
    int totalScore;   // 总成绩
    int majorScore;   // 专业课
    Student() {}
    // 比较思路如下:
    // 进行两次分发回收,第一次以专业课,这样的话总分排序时,专业课高的一定在前面
    Student(int id, const std::string n, int t, int m) : stuid(id), name(n), totalScore(t), majorScore(m)
    {
    }
};

LinkQueue<Student> Q[Radix];
// 基数排序的实现
// 即使用队列将每一次分发的进行存储,每一次分发按照高位到低位进行
int GetKey(int value, int k)
{
    int key = 0;
    while (k >= 0)
    {
        key = value % 10;
        value /= 10;
        k--;
    }
    return key;
}
void DistributeMajor(std::vector<Student> &a, int left, int right, int k)
{
    for (int i = left; i < right; i++)
    {
        int key = GetKey(a[i].majorScore, k);
        Q[key].push(a[i]);
    }
}
void DistributeTotal(std::vector<Student> &a, int left, int right, int k)
{
    for (int i = left; i < right; i++)
    {
        int key = GetKey(a[i].totalScore, k);
        Q[key].push(a[i]);
    }
}
void Collect(std::vector<Student> &a)
{
    int k = 0;
    for (int i = 0; i < Radix; i++)
    {
        while (!Q[i].empty())
        {
            a[k++] = Q[i].front();
            Q[i].pop();
        }
    }
}

void RadixSort(std::vector<Student> &a, int left, int right)
{
    for (int i = 0; i < K; i++)
    {
        DistributeMajor(a, left, right, i);
        Collect(a);
    }
    for (int i = 0; i < K; i++)
    {
        DistributeTotal(a, left, right, i);
        Collect(a);
    }
}

int main()
{
    std::vector<Student> students = {
        {20240001, "Brian", 338, 82},
        {20240002, "Queen", 384, 82},
        {20240003, "Xiomara", 330, 94},
        {20240004, "Gwen", 347, 74},
        {20240005, "Ryan", 347, 75},
        {20240006, "Holly", 325, 92},
        {20240007, "Damon", 399, 88},
        {20240008, "Quinn", 440, 120},
        {20240009, "Yvette", 306, 94},
        {20240010, "Steve", 306, 79},
        {20240011, "Mona", 380, 73}};

    RadixSort(students, 0, students.size());

    std::cout
        << "考生号      "
        << "姓名      "
        << "总分   "
        << "专业课分数" << std::endl;

    // 打印学生数据
    for (int i = students.size() - 1; i >= 0; i--)
    {
        std::cout << std::left
                  << std::setw(12) << students[i].stuid
                  << std::setw(10) << students[i].name
                  << std::setw(10) << students[i].totalScore
                  << std::setw(12) << students[i].majorScore << std::endl;
    }

    return 0;
}
算法平均时间复杂度最坏时间复杂度额外空间复杂度稳定性说明
简单选择排序O(N2)O(N2)O(1)不稳定
冒泡排序O(N2)O(N2)O(1)稳定
直接插入排序O(N2)O(N2)O(1)稳定
快速排序O(NlogN)O(N2)O(logN)不稳定
归并排序O(NlogN)O(NlogN)O(N)稳定
堆排序O(NlogN)O(NlogN)O(1)不稳定
希尔排序O(Nd)O(N2)O(1)不稳定依赖增量序列d
基数排序O(d(N+rd))O(d(N+rd))O(N+rd)稳定d是趟数;rd是基数

相关文章:

  • 【探寻C++之旅】第九章:二叉搜索树
  • GetX 中GetView、GetXController 和 Bindings的联合使用
  • minikube部署Go应用
  • 蓝桥杯备考-----》差分数组+二分答案 借教室
  • deepseek连续对话与API调用机制
  • axios防止重复请求
  • DJ串烧集 2.4.5 | 海量大型DJ串烧歌曲,无广告,无需登录,高清在线播放
  • Apache Shiro 使用教程
  • Redis,从数据结构到集群的知识总结
  • OpenGL ES 入门指南:从基础到实战
  • 【JavaEE】Spring Boot 日志
  • 基于VMware的虚拟机集群搭建
  • 机器学习之浅层神经网络
  • Matlab 舰载机自动着舰控制系统研究
  • 咪咕MG101_晨星MSO9380芯片_安卓5.1.1_免拆卡刷固件包
  • Markdown 模板变量的使用
  • 科研入门--SCI及分区
  • Linux:UDP和TCP报头管理
  • C++ STL map
  • 模板字面量标签函数
  • 一个网站空间可以做多少个网站/做企业推广的公司
  • 青岛网站设计流程/百度推广靠谱吗
  • 河南建设网证书查询平台/seo工作流程图
  • 绍兴专业做网站的公司/独立网站怎么做
  • 分类目录网站有哪些/最新热点新闻
  • 电子商务网站建设(论文/网站搜索引擎优化的基本内容