排序

冒泡排序

基础版

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数
  3. 针对所有的元素重复以上的步骤,除了最后一个。
void bubbleSort(vector<int>& v)
{
    // i < v.size()-1 最后一个元素不用排
    for (int i = 0; i < v.size()-1; i++)
    {
        // 最后的 i 个元素已排好
        for (int j = 0; j < v.size()-i-1; j++)
        {
            // 不取等确保稳定性
            if (v[j] > v[j+1])
            {
                swap(v[j], v[j+1]);
            }
        }
    }
}
  • 时间复杂度:无论最好最坏\(O(n^2)\)
  • 空间复杂度:\(O(1)\)
  • 稳定性:稳定

原理简单,手搓容易

改良版

考虑如果给定一个有序数组,基础版依然要进行 \(n(n-1)/2\) 次比较,因此考虑在每一轮比较中,记录最后一次交换的位置,该位置之后的元素已经有序,下一轮比较只需要比较到该位置即可。

void bubbleSort(vector<int>& v)
{
    auto n = v.size();
    auto lastSwapIdx = 0;
    // n 可以理解为未排序元素个数
    while (n > 0)
    {
        for (int j = 0; j < n-1; j++)
        {
            if (v[j] > v[j+1])
            {
                swap(v[j], v[j+1]);
                lastSwapIdx = j;
            }
        }
        // lastSwapIdx+1 及之后的元素有序
        n = lastSwapIdx;
    }
}
  • 时间复杂度在最好的情况下可以优化到 \(O(n)\),但最坏情况下依然是 \(O(n^2)\)
  • 空间复杂度:\(O(1)\)
  • 稳定性:稳定

选择排序

  1. 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 从剩余未排序元素中继续寻找最小(大)元素,然后通过交换放到已排序序列的末尾
  3. 重复第二步,直到所有元素均排序完毕
void selectionSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n - 1; i++) {
        int minIndex = i;
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex != i) {
            // 交换时可能破坏稳定性
            swap(arr[i], arr[minIndex]);
        }
    }
}
  • 时间复杂度:铁打的\(O(n^2)\)
  • 空间复杂度:\(O(1)\)
  • 稳定性:不稳定

插入排序

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
void insertionSort(vector<int>& v)
{
    for (int i = 0; i < v.size(); i++)
    {
        // 待排元素
        int key = v[i];
        // 前 i-1 个元素已经排好
        int j = i - 1;
        // j>=0 确保第一个元素直接放
        // v[j] > key 不取等确保稳定性
        while (j >= 0 && v[j] > key)
        {
            // 如果当前元素比待排元素大,后移
            v[j+1] = v[j];
            j--;
        }
        // 代派元素放入正确位置
        v[j+1] = key;
    }
}
  • 时间复杂度:
    • 最好:\(O(n)\)
    • 最坏:\(O(n^2)\)
    • 平均:\(O(n^2)\)
  • 空间复杂度:\(O(1)\)

插入排序常用于更快速的排序算法中小规模数据排序的优化

改良版是希尔排序但因为懒而没去学…

归并排序

分治算法,递归将数组不断均分为两个子数组,再对有序的两个子数组进行合并

void mergeSort(vector<int>& v,int l,int r)
{
    if(l >= r)
    {
        return;
    }
    // 这种中值求法可一定程度上避免溢出
    int mid = l + (r - l) / 2;
    mergeSort(v,l,mid);
    mergeSort(v,mid+1,r);

    vector<int> left(v.begin()+l,v.begin()+mid+1);
    vector<int> right(v.begin()+mid+1,v.begin()+r+1);
    int i = 0,j = 0,k = l;
    while (i < left.size() && j < right.size())
    {
        // 优先放左子数组,确保稳定性
        if(left[i] <= right[j])
        {
            v[k++] = left[i++];
        }
        else
        {
            v[k++] = right[j++];
        }
    }
    while (i < left.size())
    {
        v[k++] = left[i++];
    }
    while (j < right.size())
    {
        v[k++] = right[j++];
    }
}
  • 时间复杂度:\(O(nlogn)\)
  • 空间复杂度:\(O(n)\)
  • 稳定性:稳定

堆排序

区别于内存空间的堆,这里的堆是一种数据结构

堆是一种完全二叉树,分为大顶堆和小顶堆,大顶堆的每个节点都大于其子节点,小顶堆的每个节点都小于其子节点

堆排序只需先构建堆,然后依次取堆顶即可

粗陋版

class heap
{
    vector<int> v;

    public:
    int top()
    {
        return v[0];
    }

    void push(int x)
    {
        v.push_back(x);
        int i = v.size() - 1;
        while (i > 0)
        {
            int parent = (i - 1) / 2;
            if (v[i] < v[parent])
            {
                swap(v[i], v[parent]);
                i = parent;
            }
            else
            {
                break;
            }
        }
    }

    void pop()
    {
        swap(v[0], v[v.size() - 1]);
        v.pop_back();
        int i = 0;
        int n = v.size();
        while (true)
        {
            int left = 2 * i + 1;
            int right = left + 1;
            int smallest = i;
            if (left < n && v[left] < v[smallest])
            {
                smallest = left;
            }
            if (right < n && v[right] < v[smallest])
            {
                smallest = right;
            }

            if (smallest != i)
            {
                swap(v[i], v[smallest]);
                i = smallest;
            }
            else
            {
                break;
            }
        }
    }
};
void heapSort(vector<int>& v) {
    heap h;
    for (int i : v)
    {
        h.push(i);
    }
    for (int & i : v)
    {
        i = h.top();
        h.pop();
    }
}
  • 时间复杂度:\(O(nlogn)\)
  • 空间复杂度:\(O(n)\)
  • 稳定性:不稳定

手搓堆时涉及到0-based1-based的转换,容易出错。但好在STL中有priority_queue,以pair形式存储元素,并使用懒删除可以通过很多算法题

标准版

void heapify(vector<int>& v, int i, int size)
{
    // 往子节点递归
    int largest = i;
    int left = 2 * i + 1;
    int right = left + 1;
    if (left < size && v[left] > v[largest])
    {
        largest = left;
    }
    if (right < size && v[right] > v[largest])
    {
        largest = right;
    }
    if (largest != i)
    {
        swap(v[i], v[largest]);
        heapify(v, largest, size);
    }
}

void heapSort(vector<int>& v) {
    int n = v.size();
    // 从最下面的非叶子节点开始往上构建堆
    for (int i = n / 2 - 1; i >= 0; i--)
    {
        heapify(v, i, n);
    }
    for (int i = n - 1; i > 0; i--)
    {
        // 模拟 pop,数组从后往前存放堆顶
        swap(v[0], v[i]);
        heapify(v, 0, i);
    }
}
  • 时间复杂度:\(O(nlogn)\)
  • 空间复杂度:优化至 \(O(1)\)
  • 稳定性:不稳定

快速排序

快速排序的核心是划分,即找到一个基准值,将数组划分为两部分,左边的元素都小于等于基准值,右边的元素都大于等于基准值,然后递归对左右两部分进行划分

基础版

int partition(vector<int> &v, int l, int r)
{
    // i 比基准小的索引
    int i = l;
    for (int j = l; j < r; j++)
    {
        // 取最后一位为基准
        if (v[j] <= v[r])
        {
            swap(v[j], v[i++]); // 往前放
        }
    }
    // v[r] 的位置
    swap(v[i], v[r]);
    return i;
}

void quickSort(vector<int>& v,int l,int r)
{
    if (l < r)
    {
        int pi = partition(v, l, r);
        // pi 的位置坐定了
        quickSort(v, l, pi - 1);
        quickSort(v, pi + 1, r);
    }
}
  • 时间复杂度:
    • 最好/平均: \(O(nlogn)\)
    • 最坏: \(O(n^2)\)
  • 空间复杂度:快速排序的空间消耗主要来自递归调用时的栈空间
    • 最好/平均: \(O(logn)\)
    • 最坏: \(O(n)\)
  • 稳定性:不稳定

改良版

方向一: 减少递归开销

设定阈值,当数组长度小于阈值时,使用插入排序

void quickSort(vector<int>& v,int l,int r)
{
    if (l < r)
    {
        if (r - l + 1 <= 10)
        {
            insertionSort(v, l, r);
        }
        else
        {
            // 其他代码
        }
    }
}

方向二:基准的选择

如果基准选择不当,最坏情况会退化到 \(O(n^2)\),因此需要选择合适的基准

  • 随机选择
int partition(vector<int> &v, int l, int r)
{
    int random = rand() % (r-l+1);
    swap(v[l+random],v[r]);
    // 其他代码
}

期望时间复杂度为 \(O(nlogn)\),极难退化,接近理论上的平均情况

鲁棒性极好,无法被输入数据针对

但调用随机数生成函数,计算开销略高

  • 三数取中
int partition(vector<int> &v, int l, int r)
{
    int mid = l + (r - l) / 2;
    if (v[l] > v[mid]) swap(v[l], v[mid]);
    if (v[l] > v[r]) swap(v[l], v[r]);
    if (v[mid] > v[r]) swap(v[mid], v[r]);
    // 此时 v[l] <= v[mid] <= v[r]
    swap(v[mid], v[r]);
    // 其他代码
}

非常适用于通用场景,通常能提供比随机选择略好的平均性能

  • 当然,还可以将随机取数与三数取中结合,这里不再赘述

方向三:区域划分

对于大规模数据输入时,比二分区域更高效的是三分区域

  • 单基准三分

注意到每次选定的基准在排序后位置固定了,故可以尝试扩充此单个位置为一个区间,当数据中存在大量相等数据时可显著提升效率

pair<int,int> partition(vector<int> &v, int l, int r)
{
    // 基准选择代码
    int i = l; // 左指针
    int j = r - 1; // 右指针
    int current = l;
    while (current <= j)
    {
        if (v[current] < v[r])
        {
            // 之前的数据必然 <= pivot,故 current++
            swap(v[current++], v[i++]);
        }
        else if (v[current] > v[r])
        {
            // 交换来的数据未知,current 不动
            swap(v[current], v[j--]);
        }
        else
        {
            current++;
        }
    }
    swap(v[current], v[r]);
    return {i,current};
}
            

quickSort中更新递归区间

auto[equalLeft, equalRight] = partition(v, l, r);
quickSort(v, l, equalLeft - 1);
quickSort(v, equalRight + 1, r);
  • 双基准三分

选择两个基准 p1 <= p2,将区域划分为 < p1 p1 <= ... <= p2 > p2三个区域,再递归排序这三个区域

实现略复杂,因为懒,所以…

如果有兴趣可以参考 Java7Arrays.sort 实现

C++ STL 排序简介

sort

高度优化的工业级实现

内省排序 (Introsort):

  • 开始时使用快速排序
  • 当递归深度超过 2*log(n) 时切换到堆排序,避免最坏情况
  • 对小数组使用插入排序
  • 不稳定

stable_sort

  • 主体采用归并排序
  • 对小数组使用插入排序
  • 若内存不足,采用原地归并排序(效率略低)

partial_sort

对指定区间 \([first, last)\) 进行处理,使前 k 个元素 \([first, middle)\) 为整个区间中最小的 k 个元素,且这 k 个元素已排序

  • 基于堆排序的变种:

    • 对前 k 个元素构建最大堆
    • 遍历剩余元素 \([k, last)\):若元素小于堆顶,则替换堆顶并重新调整堆
    • 遍历结束后,前 k 个元素构成的最大堆包含了整个区间中最小的 k 个元素,最后对这 k 个元素执行堆排序转为升序
  • 不稳定


除此外还能有更快的算法吗?

以下介绍在特定场景突破 \(O(nlogn)\) 的排序算法 —— 非比较型排序

计数排序

对于一个值域在 \([0, k)\) 之间的整数序列,统计各个值的出现次数,然后按顺序输出

计数排序也是一种桶排序,但每个桶只存储一个值,桶排序在此不展开叙述

注意,这个 \(k\) 不能太大,否则空间复杂度会很高

简陋版

只能排纯粹自然数数组,且不稳定

void countingSort(vector<int>& v, int k)
{
    vector<int> count(k, 0);
    for (int i = 0; i < v.size(); i++)
    {
        count[v[i]]++;
    }
    int index = 0;
    for (int i = 0; i < k; i++)
    {
        while (count[i]--)
        {
            v[index++] = i;
        }
    }
}

标准版

即使出现排序对象是非自然数,也可在一定程度上建立起非自然数与自然数之间的映射关系

且通过从后往前遍历,可以确保稳定性

void countingSort(vector<obj>& v, int k)
{
    int n = v.size();
    vector<int> count(k, 0);
    for (int i = 0; i < n; i++)
    {
        count[objToNum(v[i])]++;
    }
    // 计算前缀和
    for (int i = 1; i < k; i++)
    {
        count[i] += count[i-1];
    }
    vector<obj> tmp(v.size());
    for (int i = n - 1; i >= 0; i--)
    {
        // 0-based 索引需先自减
        tmp[--count[objToNum(v[i])]] = v[i];
    }
    v = tmp;
}
  • 时间复杂度:\(O(n+k)\)
  • 空间复杂度:\(O(n+k)\)
  • 稳定性:稳定

基数排序

是计数排序的延伸,将待排对象按位拆分,再从低位到高位依次进行计数排序

void radixSort(vector<obj>& v, int k)
{
    int n = v.size();
    // 临时数组声明在循环体外可减小开销
    vector<obj> tmp(n);
    // 从后往前排及计数排序的稳定性确保了最终排序的正确性和稳定性
    for (int pos = v[0].size() - 1; pos >= 0; pos--)
    {
        // 按位进行计数排序
        vector<int> count(k, 0);
        for (int i = 0; i < n; i++)
        {
            count[objToNum(v[i][pos])]++;
        }
        for (int i = 1; i < k; i++)
        {
            count[i] += count[i-1];
        }
        for (int i = n - 1; i >= 0; i--)
        {
            tmp[--count[objToNum(v[i][pos])]] = v[i];
        }
        v = tmp;
    }
}
  • 时间复杂度:\(O(nk)\)
  • 空间复杂度:\(O(n+k)\)
  • 稳定性:稳定

给定一个数组,查找其中第 k 大的元素。

当然可以排序后直接返回下标,时间复杂度是 O(nlogn),有无更优的算法?

基于快速排序的选择算法

注意到快速排序的划分操作,每次划分后,都能确定基准的最终位置。

如果基准的位置正好是 k,那么基准就是第 k 大的元素;如果基准的位置大于 k,那么第 k 大的元素在基准的左边;如果基准的位置小于 k,那么第 k 大的元素在基准的右边。

因此,我们可以使用快速排序的划分操作来选择第 k 大的元素。

随机化选择基准

int partition(vector<int> &v, int l, int r)
{
    int random = rand() % (r - l + 1) + l;
    swap(v[r], v[random]);
    int i = l;
    for (int j = l; j < r; j++)
    {
        if (v[j] <= v[r])
        {
            swap(v[j], v[i++]);
        }
    }
    swap(v[i], v[r]);
    return i;
}

int randSelect(vector<int> &v, int l, int r,int q)
{
    if (l == r)
    {
        return v[l];
    }

    int i = partition(v, l, r);
    if (i == q)
    {
        return v[q];
    }
    else if (i > q)
    {
        return randSelect(v, l, i - 1, q);
    }
    else
    {
        return randSelect(v, i + 1, r, q);
    }
}

中位数

// todo?

基于堆的选择算法

// todo?

C++ STL 中的快速选择

int main()
{
    // ···
    nth_element(v.begin(),v.begin()+q-1,v.end());
    cout<<v[q-1];
}

标题:排序

作者:Zwing

创建于:2026-01-16 18:57:08

更新于:2026-08-07 16:17:52

链接:https://zanytriumph.github.io/posts/排序.html

版权声明:本文章采用 CC BY-NC-SA 4.0 进行许可