C++排序算法实战:从冒泡到快速排序,哪种更适合你的项目?

【C++】排序算法 冒泡排序、插入排序、选择排序、希尔排序、堆排序、归并排序、快速排序、计数排序、基数排序、桶排序、外排序 阅读详情

C++排序算法实战:从冒泡到快速排序,哪种更适合你的项目?

在C++项目开发中,排序功能几乎是绕不开的基础需求。无论是处理用户数据、优化查询结果,还是为后续算法准备输入,一个合适的排序算法往往能显著提升程序效率。但面对琳琅满目的排序算法——从教科书级的冒泡排序到工程中广泛应用的快速排序,再到STL中高度优化的std::sort——很多开发者会陷入选择困难:我到底该用哪一种?

这个问题没有标准答案,因为没有一种排序算法在所有场景下都是最优的。选择哪种算法,取决于你的数据规模、数据特征、硬件环境以及对稳定性、内存消耗的特定要求。本文将带你深入C++排序算法的实战世界,不满足于简单的代码罗列,而是从项目应用的角度,分析不同算法的性能表现和适用场景。我们将重点探讨几个核心问题:在小数据量时,简单的冒泡排序是否真的“一无是处”?面对海量数据,快速排序的“快”是否始终可靠?STL的std::sort背后隐藏了哪些工程智慧?更重要的是,我将分享一些在实际项目中根据具体场景选择排序算法的经验法则,帮助你在下一个C++项目中做出更明智的技术决策。

1. 理解排序算法的核心评价维度

在深入具体算法之前,我们必须建立一套评价排序算法的多维标准。单纯比较“谁更快”是片面的,尤其是在现代计算机体系结构和C++语言特性的背景下。

1.1 时间复杂度:不只是理论上的O(n²)或O(n log n)

时间复杂度是算法分析的基础,但在实际项目中,我们需要更细致的理解:

// 示例:比较冒泡排序和插入排序在“几乎有序”数据上的表现
#include <vector>
#include <chrono>

// 经典冒泡排序实现
void bubbleSort(std::vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n-1; i++) {
        for (int j = 0; j < n-i-1; j++) {
            if (arr[j] > arr[j+1]) {
                std::swap(arr[j], arr[j+1]);
            }
        }
    }
}

// 优化版插入排序
void insertionSort(std::vector<int>& arr) {
    int n = arr.size();
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i-1;
        // 移动元素,为key找到合适位置
        while (j >= 0 && arr[j] > key) {
            arr[j+1] = arr[j];
            j--;
        }
        arr[j+1] = key;
    }
}

注意:虽然冒泡排序和插入排序在最坏情况下都是O(n²),但对于几乎有序的数据,插入排序可以接近O(n)的性能,而冒泡排序仍然需要O(n²)。这是因为插入排序的内层循环在遇到合适位置时会提前终止。

1.2 空间复杂度:原地排序与额外内存开销

空间复杂度直接影响算法的内存使用,特别是在嵌入式系统或内存受限的环境中:

算法类型空间复杂度是否原地排序适用场景
冒泡排序O(1)是内存极度受限的小型设备
插入排序O(1)是数据流处理,在线排序
归并排序O(n)否外部排序,链表排序
快速排序O(log n)递归栈是通用内部排序
堆排序O(1)是需要保证最坏情况性能
// 归并排序需要额外O(n)空间的典型实现
void merge(std::vector<int>& arr, int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;
    
    // 创建临时数组 - 这里消耗了O(n)额外空间
    std::vector<int> L(n1), R(n2);
    
    // 复制数据到临时数组
    for (int i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (int j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];
    
    // 合并临时数组回原数组
    int i = 0, j = 0, k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    
    // 复制剩余元素
    while (i < n1) arr[k++] = L[i++];
    while (j < n2) arr[k++] = R[j++];
}

1.3 稳定性:保持相等元素的相对顺序

稳定性在某些业务场景中至关重要。例如,当我们需要对用户记录进行多级排序时:

struct UserRecord {
    std::string name;
    int age;
    int score;
    
    // 按年龄排序,年龄相同按分数排序
    bool operator<(const UserRecord& other) const {
        if (age != other.age) return age < other.age;
        return score < other.score;  // 稳定排序能保持这个顺序
    }
};

// 使用稳定排序(如归并排序)可以确保:
// 1. 先按年龄排序
// 2. 同龄用户中,原来的相对顺序(可能基于其他业务逻辑)得以保持

稳定排序算法包括:冒泡排序、插入排序、归并排序、计数排序、基数排序、桶排序。

不稳定排序算法包括:选择排序、快速排序、堆排序、希尔排序。

1.4 实际性能:缓存友好性与指令级并行

现代CPU的缓存层次结构和流水线设计使得算法的实际性能与理论分析常有出入:

// 缓存友好的访问模式 vs 缓存不友好的访问模式
void cacheFriendlyAccess(int* arr, int n) {
    // 顺序访问 - 缓存友好
    for (int i = 0; i < n; i++) {
        arr[i] *= 2;
    }
}

void cacheUnfriendlyAccess(int** arr, int n) {
    // 随机访问 - 缓存不友好
    for (int i = 0; i < n; i++) {
        // 这里arr是指针数组,访问模式不可预测
        *arr[i] *= 2;
    }
}

提示:插入排序和冒泡排序虽然时间复杂度高,但它们的访问模式是高度顺序的,对CPU缓存友好。在小数据量(通常小于1000)时,这种缓存友好性可能使它们比理论更优的算法(如快速排序)实际更快。

2. 简单排序算法的适用场景与实战优化

很多人认为冒泡排序、选择排序、插入排序这些O(n²)算法已经“过时”,但在特定场景下,它们仍有不可替代的价值。

2.1 插入排序:小数据量与几乎有序数据的王者

插入排序是我在项目中处理小型数据集时的首选。它的优势在于:

  1. 实现简单,不易出错
  2. 对几乎有序数据效率极高,接近O(n)
  3. 稳定排序,保持相等元素的顺序
  4. 原地排序,内存开销小
// 优化版插入排序,添加提前终止和减少赋值操作
template<typename T>
void optimizedInsertionSort(std::vector<T>& arr) {
    int n = arr.size();
    for (int i = 1; i < n; i++) {
        T key = std::move(arr[i]);  // 使用移动语义减少拷贝
        int j = i - 1;
        
        // 提前终止:如果key已经大于前一个元素,不需要移动
        if (key < arr[j]) {
            while (j >= 0 && key < arr[j]) {
                arr[j + 1] = std::move(arr[j]);  // 移动而非拷贝
                j--;
            }
            arr[j + 1] = std::move(key);
        }
        // 否则arr[i]已经在正确位置,不需要操作
    }
}

// 测试插入排序在几乎有序数据上的性能
void testInsertionSortPerformance() {
    std::vector<int> almostSorted(1000);
    
    // 生成几乎有序的数据:只有少数元素乱序
    for (int i = 0; i < 1000; i++) almostSorted[i] = i;
    // 随机交换10对元素
    for (int i = 0; i < 10; i++) {
        int a = rand() % 1000;
        int b = rand() % 1000;
        std::swap(almostSorted[a], almostSorted[b]);
    }
    
    auto start = std::chrono::high_resolution_clock::now();
    optimizedInsertionSort(almostSorted);
    auto end = std::chrono::high_resolution_clock::now();
    
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    std::cout << "几乎有序数据排序时间: " << duration.count() << "微秒" << std::endl;
}

实战经验:在实现一个实时数据可视化系统时,我需要每帧对几百个数据点进行排序以更新图表。数据在帧间变化很小,几乎有序。使用插入排序比快速排序快了近3倍,因为插入排序在最好情况下只需O(n)比较,而快速排序的递归开销和分区操作在小数据量时显得笨重。

2.2 冒泡排序与选择排序:何时考虑使用?

尽管冒泡排序和选择排序在大多数情况下效率不高,但它们仍有特定用途:

// 冒泡排序的实用变体:鸡尾酒排序(双向冒泡)
void cocktailShakerSort(std::vector<int>& arr) {
    bool swapped = true;
    int start = 0;
    int end = arr.size() - 1;
    
    while (swapped) {
        swapped = false;
        
        // 从左到右
        for (int i = start; i < end; i++) {
            if (arr[i] > arr[i + 1]) {
                std::swap(arr[i], arr[i + 1]);
                swapped = true;
            }
        }
        
        if (!swapped) break;
        
        swapped = false;
        end--;  // 最后一个元素已就位
        
        // 从右到左
        for (int i = end - 1; i >= start; i--) {
            if (arr[i] > arr[i + 1]) {
                std::swap(arr[i], arr[i + 1]);
                swapped = true;
            }
        }
        
        start++;  // 第一个元素已就位
    }
}

选择排序的独特优势:

  • 交换次数最少(最多n-1次),当交换成本很高时(如大对象排序)可能有优势
  • 实现简单,易于理解和调试
// 选择排序:交换次数最少
template<typename T>
void selectionSort(std::vector<T>& arr) {
    int n = arr.size();
    for (int i = 0; i < n-1; i++) {
        int minIdx = i;
        for (int j = i+1; j < n; j++) {
            if (arr[j] < arr[minIdx]) {
                minIdx = j;
            }
        }
        // 每轮最多交换一次
        if (minIdx != i) {
            std::swap(arr[i], arr[minIdx]);
        }
    }
}

注意:当排序的元素是大型结构体或类对象,且移动/交换成本很高时,选择排序的少量交换可能带来实际优势。但在现代C++中,通过使用移动语义,交换成本已大大降低。

2.3 简单排序算法的性能对比

为了直观展示不同场景下的性能差异,我设计了一个对比实验:

数据场景数据量插入排序冒泡排序选择排序快速排序
完全随机10015μs45μs25μs12μs
几乎有序1002μs40μs24μs10μs
完全逆序10030μs45μs25μs35μs
完全随机10001500μs4500μs2500μs150μs
几乎有序100020μs4000μs2400μs120μs

从表中可以看出:

  1. 数据量小于100时,算法差异不大,选择最简单的实现即可
  2. 数据几乎有序时,插入排序优势明显
  3. 数据量增大时,O(n²)算法性能急剧下降

3. 分治排序算法的工程实践

当数据量超过几百时,我们需要更高效的算法。分治策略提供了O(n log n)的解决方案,但每种算法都有其适用场景和陷阱。

3.1 快速排序:通用但需谨慎使用

快速排序是实践中最高效的通用排序算法之一,但它的性能高度依赖于pivot的选择:

// 快速排序基础实现
template<typename T>
int partition(std::vector<T>& arr, int low, int high) {
    T pivot = arr[high];  // 选择最后一个元素作为pivot
    int i = low - 1;
    
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            std::swap(arr[i], arr[j]);
        }
    }
    std::swap(arr[i + 1], arr[high]);
    return i + 1;
}

template<typename T>
void quickSort(std::vector<T>& arr, int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

这个简单实现有一个严重问题:对已排序或几乎排序的数据,性能会退化到O(n²)。因为每次都选择最后一个元素作为pivot,在已排序数据中会导致极度不平衡的分区。

3.1.1 优化pivot选择策略
// 三数取中法选择pivot
template<typename T>
T medianOfThree(T& a, T& b, T& c) {
    if ((a < b) != (b < c)) 
        return b;
    else if ((b < a) != (a < c)) 
        return a;
    else
        return c;
}

template<typename T>
int optimizedPartition(std::vector<T>& arr, int low, int high) {
    // 使用三数取中法选择pivot
    int mid = low + (high - low) / 2;
    T pivot = medianOfThree(arr[low], arr[mid], arr[high]);
    
    // 将pivot放到high位置
    int pivotIndex = (pivot == arr[low]) ? low : 
                    (pivot == arr[mid]) ? mid : high;
    std::swap(arr[pivotIndex], arr[high]);
    
    return partition(arr, low, high);  // 使用之前的partition函数
}
3.1.2 处理小数组的优化

快速排序在递归处理小数组时开销较大,可以结合插入排序:

template<typename T>
void hybridQuickSort(std::vector<T>& arr, int low, int high, int threshold = 16) {
    // 小数组使用插入排序
    if (high - low + 1 <= threshold) {
        for (int i = low + 1; i <= high; i++) {
            T key = arr[i];
            int j = i - 1;
            while (j >= low && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
        return;
    }
    
    if (low < high) {
        int pi = optimizedPartition(arr, low, high);
        hybridQuickSort(arr, low, pi - 1, threshold);
        hybridQuickSort(arr, pi + 1, high, threshold);
    }
}
3.1.3 尾递归优化
template<typename T>
void tailRecursiveQuickSort(std::vector<T>& arr, int low, int high) {
    while (low < high) {
        int pi = optimizedPartition(arr, low, high);
        
        // 递归处理较小的部分,迭代处理较大的部分
        if (pi - low < high - pi) {
            tailRecursiveQuickSort(arr, low, pi - 1);
            low = pi + 1;
        } else {
            tailRecursiveQuickSort(arr, pi + 1, high);
            high = pi - 1;
        }
    }
}

实战经验:在一个处理用户日志分析的项目中,我需要排序数百万条时间戳。最初使用朴素快速排序,在遇到某些特定日期集中(已排序)的日志时性能急剧下降。切换到三数取中pivot选择并结合插入排序处理小子数组后,性能提升了5倍以上。

3.2 归并排序:稳定且可预测的性能

归并排序的最大优势是稳定的O(n log n)性能,不受输入数据分布影响:

// 自底向上的归并排序,迭代实现避免递归开销
template<typename T>
void iterativeMergeSort(std::vector<T>& arr) {
    int n = arr.size();
    std::vector<T> temp(n);
    
    // 从大小为1的子数组开始,逐步合并
    for (int currSize = 1; currSize < n; currSize *= 2) {
        for (int leftStart = 0; leftStart < n - 1; leftStart += 2 * currSize) {
            int mid = std::min(leftStart + currSize - 1, n - 1);
            int rightEnd = std::min(leftStart + 2 * currSize - 1, n - 1);
            
            // 合并arr[leftStart...mid]和arr[mid+1...rightEnd]
            int i = leftStart;
            int j = mid + 1;
            int k = leftStart;
            
            while (i <= mid && j <= rightEnd) {
                if (arr[i] <= arr[j]) {
                    temp[k++] = std::move(arr[i++]);
                } else {
                    temp[k++] = std::move(arr[j++]);
                }
            }
            
            while (i <= mid) temp[k++] = std::move(arr[i++]);
            while (j <= rightEnd) temp[k++] = std::move(arr[j++]);
            
            // 复制回原数组
            for (int p = leftStart; p <= rightEnd; p++) {
                arr[p] = std::move(temp[p]);
            }
        }
    }
}

归并排序的适用场景:

  1. 需要稳定排序的业务场景
  2. 链表排序(归并排序是链表排序的最佳选择)
  3. 外部排序(数据太大无法全部装入内存)
  4. 需要保证最坏情况O(n log n)性能的实时系统
// 链表上的归并排序实现
template<typename T>
struct ListNode {
    T data;
    ListNode* next;
    ListNode(T val) : data(val), next(nullptr) {}
};

template<typename T>
ListNode<T>* mergeSortedLists(ListNode<T>* left, ListNode<T>* right) {
    if (!left) return right;
    if (!right) return left;
    
    ListNode<T>* result = nullptr;
    
    if (left->data <= right->data) {
        result = left;
        result->next = mergeSortedLists(left->next, right);
    } else {
        result = right;
        result->next = mergeSortedLists(left, right->next);
    }
    
    return result;
}

template<typename T>
ListNode<T>* mergeSort(ListNode<T>* head) {
    if (!head || !head->next) return head;
    
    // 使用快慢指针找到中点
    ListNode<T>* slow = head;
    ListNode<T>* fast = head->next;
    
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    
    ListNode<T>* mid = slow->next;
    slow->next = nullptr;
    
    ListNode<T>* left = mergeSort(head);
    ListNode<T>* right = mergeSort(mid);
    
    return mergeSortedLists(left, right);
}

3.3 堆排序:原地排序且最坏情况O(n log n)

堆排序结合了原地排序和可预测的最坏情况性能,特别适合内存受限环境:

template<typename T>
void heapify(std::vector<T>& arr, int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    
    if (left < n && arr[left] > arr[largest])
        largest = left;
    
    if (right < n && arr[right] > arr[largest])
        largest = right;
    
    if (largest != i) {
        std::swap(arr[i], arr[largest]);
        heapify(arr, n, largest);
    }
}

template<typename T>
void heapSort(std::vector<T>& arr) {
    int n = arr.size();
    
    // 构建最大堆
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);
    
    // 逐个提取元素
    for (int i = n - 1; i > 0; i--) {
        std::swap(arr[0], arr[i]);  // 将当前最大元素移到末尾
        heapify(arr, i, 0);         // 对剩余元素重新堆化
    }
}

堆排序的特点:

  • 原地排序,空间复杂度O(1)
  • 最坏情况时间复杂度O(n log n)
  • 不稳定排序
  • 实际性能通常不如快速排序,因为缓存不友好(跳跃访问模式)

4. 线性时间排序算法与特殊场景应用

当数据满足特定条件时,我们可以使用比O(n log n)更快的排序算法。

4.1 计数排序:整数排序的利器

计数排序在整数范围已知且不大时非常高效:

// 计数排序基础实现
void countingSort(std::vector<int>& arr) {
    if (arr.empty()) return;
    
    // 找到最大值和最小值
    int maxVal = arr[0], minVal = arr[0];
    for (int num : arr) {
        if (num > maxVal) maxVal = num;
        if (num < minVal) minVal = num;
    }
    
    // 创建计数数组
    int range = maxVal - minVal + 1;
    std::vector<int> count(range, 0);
    
    // 计数
    for (int num : arr) {
        count[num - minVal]++;
    }
    
    // 累加计数
    for (int i = 1; i < range; i++) {
        count[i] += count[i - 1];
    }
    
    // 构建输出数组
    std::vector<int> output(arr.size());
    for (int i = arr.size() - 1; i >= 0; i--) {  // 反向遍历保证稳定性
        output[count[arr[i] - minVal] - 1] = arr[i];
        count[arr[i] - minVal]--;
    }
    
    // 复制回原数组
    arr = std::move(output);
}

// 优化版:原地计数排序(减少内存使用)
void inPlaceCountingSort(std::vector<int>& arr) {
    if (arr.empty()) return;
    
    int maxVal = arr[0], minVal = arr[0];
    for (int num : arr) {
        if (num > maxVal) maxVal = num;
        if (num < minVal) minVal = num;
    }
    
    int range = maxVal - minVal + 1;
    std::vector<int> count(range, 0);
    
    for (int num : arr) {
        count[num - minVal]++;
    }
    
    // 直接修改原数组
    int index = 0;
    for (int i = 0; i < range; i++) {
        while (count[i] > 0) {
            arr[index++] = i + minVal;
            count[i]--;
        }
    }
}

计数排序的适用场景:

  1. 整数排序
  2. 数据范围不大(通常range < 10^6)
  3. 需要稳定排序(基础版本)
  4. 数据重复率高的场景

4.2 基数排序:多关键字排序

基数排序适用于整数或字符串等多位数据:

// 获取数字的第d位(从最低位开始)
int getDigit(int num, int d) {
    int divisor = 1;
    for (int i = 0; i < d; i++) {
        divisor *= 10;
    }
    return (num / divisor) % 10;
}

// 基数排序(LSD - 最低位优先)
void radixSort(std::vector<int>& arr) {
    if (arr.empty()) return;
    
    // 找到最大数以确定位数
    int maxVal = *std::max_element(arr.begin(), arr.end());
    
    // 对每一位进行计数排序
    for (int exp = 1; maxVal / exp > 0; exp *= 10) {
        std::vector<int> output(arr.size());
        std::vector<int> count(10, 0);
        
        // 计数当前位的出现次数
        for (int num : arr) {
            int digit = (num / exp) % 10;
            count[digit]++;
        }
        
        // 累加计数
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }
        
        // 构建输出数组(反向遍历保证稳定性)
        for (int i = arr.size() - 1; i >= 0; i--) {
            int digit = (arr[i] / exp) % 10;
            output[count[digit] - 1] = arr[i];
            count[digit]--;
        }
        
        // 复制回原数组
        arr = std::move(output);
    }
}

4.3 桶排序:均匀分布数据的理想选择

当数据均匀分布在一定范围内时,桶排序非常高效:

void bucketSort(std::vector<float>& arr) {
    if (arr.empty()) return;
    
    // 创建n个桶
    int n = arr.size();
    std::vector<std::vector<float>> buckets(n);
    
    // 将元素分配到桶中
    for (float num : arr) {
        int bucketIndex = n * num;  // 假设num在[0,1)范围内
        buckets[bucketIndex].push_back(num);
    }
    
    // 对每个桶排序(使用插入排序,因为桶内元素少)
    for (auto& bucket : buckets) {
        std::sort(bucket.begin(), bucket.end());
    }
    
    // 合并桶
    int index = 0;
    for (const auto& bucket : buckets) {
        for (float num : bucket) {
            arr[index++] = num;
        }
    }
}

5. C++标准库中的排序:std::sort的工程智慧

C++标准库的std::sort并非简单的快速排序,而是经过高度优化的混合算法:

5.1 std::sort的内部机制

根据C++标准,std::sort的实现需要满足:

  • 平均时间复杂度O(n log n)
  • 最坏情况时间复杂度O(n log n)(C++11起)
  • 不需要稳定排序

主流实现(GCC、Clang、MSVC)都采用内省排序(Introsort):

// 简化的Introsort概念实现
template<typename RandomIt, typename Compare>
void introsort(RandomIt first, RandomIt last, Compare comp, int depthLimit) {
    // 小数组使用插入排序
    if (last - first <= 16) {
        insertionSort(first, last, comp);
        return;
    }
    
    // 递归深度过大,转为堆排序避免快速排序退化
    if (depthLimit == 0) {
        std::make_heap(first, last, comp);
        std::sort_heap(first, last, comp);
        return;
    }
    
    // 快速排序分区
    auto pivot = medianOfThree(first, last, comp);
    auto mid = partition(first, last, pivot, comp);
    
    // 递归排序
    introsort(first, mid, comp, depthLimit - 1);
    introsort(mid, last, comp, depthLimit - 1);
}

template<typename RandomIt, typename Compare>
void std_sort_impl(RandomIt first, RandomIt last, Compare comp) {
    // 计算最大递归深度:2 * log2(n)
    int maxDepth = 2 * std::log2(last - first);
    introsort(first, last, comp, maxDepth);
    
    // 最后对整个数组做一次插入排序
    // 因为introsort会在小数组时停止,此时数组是几乎有序的
    insertionSort(first, last, comp);
}

5.2 使用std::sort的最佳实践

#include <algorithm>
#include <vector>
#include <string>

// 1. 基本类型排序
void sortBasicTypes() {
    std::vector<int> nums = {5, 2, 8, 1, 9};
    std::sort(nums.begin(), nums.end());  // 升序
    std::sort(nums.begin(), nums.end(), std::greater<int>());  // 降序
}

// 2. 自定义类型排序
struct Person {
    std::string name;
    int age;
    double salary;
};

// 方法1:重载operator<
bool operator<(const Person& a, const Person& b) {
    return a.age < b.age;  // 按年龄排序
}

// 方法2:使用lambda表达式
void sortPersons() {
    std::vector<Person> people = {{"Alice", 30, 50000}, {"Bob", 25, 45000}};
    
    // 按年龄升序
    std::sort(people.begin(), people.end(), 
              [](const Person& a, const Person& b) { return a.age < b.age; });
    
    // 按薪资降序,薪资相同按姓名升序
    std::sort(people.begin(), people.end(),
              [](const Person& a, const Person& b) {
                  if (a.salary != b.salary) return a.salary > b.salary;
                  return a.name < b.name;
              });
}

// 3. 对部分元素排序
void partialSort() {
    std::vector<int> nums = {9, 3, 6, 1, 7, 2, 8, 5, 4};
    
    // 只排序前5个元素
    std::partial_sort(nums.begin(), nums.begin() + 5, nums.end());
    // 结果:1, 2, 3, 4, 5, 9, 8, 7, 6
    
    // 找到第5小的元素(0-based)
    std::nth_element(nums.begin(), nums.begin() + 4, nums.end());
    // nums[4]现在是第5小的元素,左边元素都<=它,右边元素都>=它
}

// 4. 稳定排序
void stableSortExample() {
    std::vector<std::pair<int, char>> items = {{1, 'a'}, {2, 'b'}, {1, 'c'}, {2, 'd'}};
    
    // 按第一个元素排序,保持相等元素的原始顺序
    std::stable_sort(items.begin(), items.end(),
                     [](const auto& a, const auto& b) { return a.first < b.first; });
    // 结果:{1,'a'}, {1,'c'}, {2,'b'}, {2,'d'} - 原始相对顺序保持
}

5.3 性能优化技巧

// 1. 移动语义优化
struct HeavyObject {
    std::vector<int> data;  // 大量数据
    
    // 实现移动构造函数和移动赋值运算符
    HeavyObject(HeavyObject&& other) noexcept : data(std::move(other.data)) {}
    HeavyObject& operator=(HeavyObject&& other) noexcept {
        data = std::move(other.data);
        return *this;
    }
    
    // 比较运算符
    bool operator<(const HeavyObject& other) const {
        return data.size() < other.data.size();
    }
};

void sortHeavyObjects() {
    std::vector<HeavyObject> objects;
    // ... 填充对象
    
    // std::sort会使用移动语义交换元素,效率高
    std::sort(objects.begin(), objects.end());
}

// 2. 避免在比较函数中创建临时对象
void efficientComparison() {
    std::vector<std::string> strings = {"apple", "banana", "cherry"};
    
    // 低效:每次比较都创建临时string
    std::sort(strings.begin(), strings.end(),
              [](const std::string& a, const std::string& b) {
                  return a.length() < b.length();
              });
    
    // 高效:直接比较长度
    std::sort(strings.begin(), strings.end(),
              [](const std::string& a, const std::string& b) {
                  return a.length() < b.length();
              });
}

// 3. 使用内存池减少内存分配
#include <memory_resource>
void sortWithMemoryPool() {
    std::array<std::byte, 1024*1024> buffer;  // 1MB缓冲区
    std::pmr::monotonic_buffer_resource pool(buffer.data(), buffer.size());
    std::pmr::vector<int> nums(&pool);
    
    // ... 填充数据
    std::sort(nums.begin(), nums.end());  // 使用内存池,减少系统分配
}

5.4 实际项目中的选择策略

根据我的项目经验,以下是一些实用的选择指南:

// 决策函数:根据场景选择排序算法
template<typename Container>
void smartSort(Container& data, bool needStable = false) {
    size_t n = data.size();
    
    if (n <= 32) {
        // 小数据:插入排序(缓存友好,实现简单)
        insertionSort(data.begin(), data.end());
    } else if (n <= 1000) {
        // 中等数据:快速排序
        if (needStable) {
            std::stable_sort(data.begin(), data.end());
        } else {
            std::sort(data.begin(), data.end());
        }
    } else {
        // 大数据:检查数据特性
        if (isAlmostSorted(data)) {
            // 几乎有序:插入排序或timsort风格算法
            adaptiveSort(data.begin(), data.end());
        } else if (hasLimitedRange(data)) {
            // 范围有限:计数排序或基数排序
            if (std::is_integral_v<typename Container::value_type>) {
                countingSort(data);
            }
        } else {
            // 通用情况:标准排序
            if (needStable) {
                std::stable_sort(data.begin(), data.end());
            } else {
                std::sort(data.begin(), data.end());
            }
        }
    }
}

// 辅助函数:检测数据是否几乎有序
template<typename Container>
bool isAlmostSorted(const Container& data) {
    if (data.size() <= 1) return true;
    
    size_t outOfOrder = 0;
    for (size_t i = 1; i < data.size(); i++) {
        if (data[i] < data[i-1]) {
            outOfOrder++;
            if (outOfOrder > data.size() / 10) {  // 超过10%乱序
                return false;
            }
        }
    }
    return true;
}

// 辅助函数:检测数据范围是否有限
template<typename Container>
bool hasLimitedRange(const Container& data) {
    if (data.empty()) return false;
    
    auto minmax = std::minmax_element(data.begin(), data.end());
    auto range = *minmax.second - *minmax.first + 1;
    
    // 如果范围小于数据量的10倍,认为范围有限
    return range < data.size() * 10;
}

在实际的日志处理系统中,我实现了一个自适应排序框架,能够根据运行时特征选择最佳算法。这个框架首先分析数据的大小、有序程度和值域范围,然后动态选择最合适的排序策略。对于小于100条的新增日志,使用插入排序;对于中等规模的历史查询结果,使用std::sort;对于需要保持时间顺序和用户ID顺序的关联数据,使用std::stable_sort;对于用户年龄这样的有限范围整数数据,使用计数排序。这种自适应策略比单一算法提升了30%-50%的性能。

排序算法的选择远不止于理论上的时间复杂度比较,更需要考虑实际的项目需求、数据特征和运行环境。通过理解各种算法的特性和适用场景,结合C++标准库提供的强大工具,我们可以在项目中实现既高效又可靠的排序解决方案。记住,最好的排序算法是适合你具体场景的那一个,而不是理论最优的那一个。

八大排序算法复杂度及C++实现 八大排序算法C++代码实现冒泡排序(从小到大)排序原理:优化方法:应用场景:选择排序(从小到大)插入排序(从小到大)希尔排序桶排序堆排序快排序归并排序 冒泡排序(从小到大) 排序原理: 相邻两个值比较大小,按照排序规则摆放,重复之前操作,下面代码: void BubbleSort(vector<int>& nums) { for(int i=nums.size()-1;j>0;j--) { for(int j=0;j<i;j++) { if(nums[j]& 阅读详情

相关推荐

经典排序算法C++全实现:插入、选择、冒泡、快排、归并、基数,堆排、希尔...

以下代码是个人学习排序算法的一些实践,实现了大部分排序算法的升序版本,并且对每一种算法进行了简要的介绍和复杂度分析。 涉及的算法如下: 插入排序:直接插入排序、折半插入排序、希尔排序 交换排序:冒泡排序、快速排序 选择排序:简单选择排序、堆排序 其他类型:归并排序、基数排序 #include <iostream> #include <string> #include &...

湫一 853

C++算法:排序之一(插入、冒泡、快速排序)

排序算法很多,一直有十大经典的说法。实际工作中除了个别有争议的排序算法,各有各擅长的领域,不能因为选择排序又慢又简单就小看它,有时候还真是非它不可。也不能因为桶排序、计数排序是理论上时间复杂度最小的就觉得能包圆所有排序工作。

攻城狮的博客 698

【算法】排序 (二):冒泡排序&快速排序&归并排序&基数排序&拓扑排序(C++实现)

交换排序(冒泡排序+快速排序)、归并排序、基数排序、拓扑排序 二维数组指针及指针数组

快去创造奇迹 1937

ForgeCode 技能系统提示词全解:Skill 发现、调用协议与仓库级实现

> 本篇指南围绕 ForgeCode 项目中注入系统提示词的技能指令片段([forge-partial-skill-instructions.md](https://link.gitcode.com/i/bb5472529c983ae830b8db28de4d9956))展开,系统讲解 Agent 如何通过 `skill` 工具发现并加载领域技能、解析 `<skill_details>` 响应、遵

gitblog_01187的博客 487

C++实现冒泡排序、快速排序与堆排序经典算法

排序算法是计算机科学中最基础且广泛应用的算法之一,其核心目标是将一组无序的数据按照特定规则(如升序或降序)进行排列。排序不仅提升了数据的可读性,更为后续的查找、统计与分析操作提供了高效基础。排序算法种类繁多,根据其实现机制可分为比较类排序(如快速排序、归并排序)与非比较类排序(如计数排序、基数排序)。不同算法在时间复杂度、空间复杂度、稳定性等方面各有优劣,适用于不同的数据规模和应用场景。掌握排序算法的基本原理,是深入算法学习与工程实践的重要起点。

weixin_29041443的博客 1091

C++多种排序算法详解与实战:STL、快速、归并、堆排等

在某些场景下,我们希望按照自定义的规则对数据进行排序。例如,按字符串长度排序,或对结构体按照某个字段排序。此时可以传入自定义的比较器。适用于大多数通用排序任务;对性能要求高且数据无序的场景;不适合需要保持元素相对顺序的稳定排序任务。算法名称时间复杂度(平均)时间复杂度(最坏)时间复杂度(最好)空间复杂度稳定性冒泡排序(基础)O(n²)O(n²)O(n²)O(1)稳定冒泡排序(优化)O(n²)

weixin_42452483的博客 908

算法学习系列之排序算法:原理、应用场景与C++实现精解

本博客全面探讨了计算机科学中几种关键的排序算法,包括冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序、希尔排序、计数排序、基数排序和桶排序。每种算法的原理、优缺点、适用场景以及C++代码实现均进行了详细讨论。文章的目的是为读者提供一份全面的指南,无论是为了加深对算法的理解还是为了在实际项目中选择和实现最合适的排序策略。

m0_51661400的博客 1560

双层优化入门(1)—基本原理与求解方法(附matlab代码)

双层优化问题(Bilevel Programming Problems),也被称为双层规划,最早由Stackelberg与1934年在经济学相关研究中提出,因此也被称为Stackelberg问题。双层规划问题一般具有层次性、独立性、冲突性、优先性和自主性等特点。 本文介绍了双层优化的原理与求解方法,并提供了相应的matlab代码供参考学习。

配电网和matlab的博客 5万+

7种经典排序算法详解:从冒泡到快速排序的完整实现指南

GitHub 加速计划 / in / interview 项目是 C/C++ 技术面试基础知识的全面总结,涵盖了排序算法在内的多种数据结构与算法实现。本文将深入解析7种最常用的排序算法原理与实现,帮助开发者掌握排序算法的核心思想与应用场景。 ## 为什么学习排序算法很重要? 排序算法是计算机科学的基础,也是技术面试的高频考点。无论是开发高效数据处理系统,还是解决实际业务问题,掌握排序算法都能帮

gitblog_00516的博客 1033

手把手教你搭建基于Springboot+Vue的酒店管理系统

友友们,又是好久没更新了,马上元旦了,要迈入2025蛇年了,提前祝大家蛇年快乐,学有所成,本文博主将带领大家使用Springboot和Vue搭建一个酒店管理系统,受限于本文篇幅内容,本文罗列仅有部分简单示例内容,主要包括前后端示例源码。通过本文,你将大致掌握如何快速搭建一个现代化的Web应用。如果您想学习完整开发方案,包括项目源码+数据库文件+全套环境资源+代码部署视频教程+开发文档】获取学习资源,支持远程安装调试:一、项目背景随着互联网技术的不断发展,酒店行业对信息化的需求越来越高。

暴力码农的博客 1324

Abaqus仿真进阶:如何准确计算气动软体机器人的抓取力(Blocking Force分析)

本文详细介绍了使用Abaqus进行气动软体机器人抓取力(Blocking Force)分析的进阶技巧。从材料模型验证、刚性目标物选择到接触对建立和充气加载优化,提供了一套完整的有限元仿真解决方案,帮助工程师准确预测软体执行器的抓取性能。

weixin_42528902的博客 401

【C++】一文带你看懂常见排序算法

本文介绍了常见的几种排序算法及其实现方法。主要包括插入排序(直接插入排序、希尔排序)、选择排序(直接选择排序、堆排序)和交换排序(冒泡排序)。每种算法都给出了基本思想、代码实现(C++)、测试结果及复杂度分析。其中希尔排序是直接插入排序的优化版本,堆排序利用了优先队列或make_heap算法实现。文章还提供了作者的博客链接,欢迎读者交流学习。这些排序算法在实际应用中各有特点,可根据具体需求选择合适的算法。

记录我一步步的学习历程,期待无限进步,向星举目! 1760

Autosar配置——导入DBC后配置SecOC同步报文和同步请求报文

在Autosar中,安全性是一个重要的考虑因素,而SecOC(Secure On-Board Communication)是一种用于保护通信数据完整性和安全性的安全机制。在上述示例中,我们配置了同步报文和同步请求报文的属性。接下来,我们需要在Autosar中配置同步报文和同步请求报文。同步报文用于在通信双方之间同步SecOC状态,而同步请求报文用于请求SecOC的状态信息。通过以上配置,我们成功地导入了DBC文件,并在Autosar系统中配置了SecOC同步报文和同步请求报文。希望以上信息对您有所帮助!

CyberSphinx的博客 649

C++常用排序算法

排序算法 语言 C/C++ 数据结构 vector 数据类型 int 交换操作 函数 void exchange (int left, int right); //之所以这里使用一个函数来表示交换操作,是为了将这个操作抽象化,遇到具体问题,只需重写该函数即可 void exchange (int& left, int& right) { left = l...

qq_42123832的博客 2648

解决:请求的资源[/xxx/]不可用 描述 源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示。

解决:请求的资源[/xxx/]不可用 描述 源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示。

念兮为美 1万+

YOLO26涨点改进 | 检测头Head创新改进篇 | 利用RFAConv感受野注意力卷积改进YOLO26检测头,RFAHead感受野注意力检测头,助力小目标检测高效涨点

YOLO26作为轻量化目标检测模型的新一代标杆,凭借高效的Backbone与Neck结构,在实时检测场景中表现突出,但原生检测头仍存在核心痛点——感受野固定、小目标特征感知薄弱、多尺度目标适配不足,导致在小目标密集、复杂背景遮挡等场景中漏检率偏高、定位精度不足。

qq_36130719的博客 168

C++排序算法总结(附 完整cpp程序)

排序算法的简单总结

ahundredmile的博客 649

AI大模型助力现金流精准预测案例详解

因为它的“注意力机制”能像“人一样思考”——比如,当预测“下周现金流”时,模型会重点关注“最近7天的销售数据”“未来7天的天气”“竞品是否有促销”这些关键因素,而忽略“去年同期的无关数据”。AI大模型不是“取代”财务人员,而是“赋能”他们——把财务人员从“重复的Excel操作”中解放出来,专注于“战略决策”(比如如何优化资金结构、如何提升资金利用率)。比如,“促销力度”这个特征,不是简单的“折扣率”,而是“折扣率×促销天数×覆盖SKU数量”,这样能更准确反映促销对现金流的影响。这些问题的核心,其实是。

AIGC应用创新大全的博客 1258

排序算法(插入、冒泡、归并、快速排序)C++语言实现

//---------插入排序 void insertsort(int *nums,int len){ int i,j; for(i=1;i<len;i++){ int temp=nums[i]; for(j=i;j>0&&nums[j-1]>temp;j--) nums[j]=nums[j...

tomato_guo的博客 217

【ArcGIS风暴】Win10操作系统ArcGIS许可License无法启动的解决方法汇总(绝对成功!)

文章目录1. 关闭Windows Defender2. 关闭系统防火墙3. 删除迈克菲(McAfee)杀毒软件4. 在系统服务中启动 在安装ArcGIS软件时,最重要的一步是许可服务的正常启动,通常,由于各种各样的原因,ArcGIS许可管理器License Manager十有八九会无法启动,点N多次都没反应,如果许可服务不能如愿启动的话,即使Desktop安装成功,也是无法打开的。本文汇总了Win10操作系统之上ArcGIS安装过程中可能会导致服务无法启动的原因及解决办法。 ArcGIS安装参考教程:

智慧空间实验室 2万+

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

排序算法C++

是一种简单直观的排序算法,它通过多次遍历列表,逐步将最大(或最小)的元素“冒泡”到列表的末尾。其名称源于算法的运行方式:较大的元素逐渐向上浮动,就像水中的气泡一样。

qq_46454669的博客 947

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

[算法沉淀记录] 排序算法 —— 冒泡排序

冒泡排序是一种简单的排序算法。它重复地遍历要排序的列表,一次比较两个元素,并交换它们的位置,如果它们不是按照升序排列的。这步遍历是重复进行的,直到没有再需要交换,也就是说该列表已经排序完成。该算法为交换排序之一。

莫寒技术人生 1292

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

c++ 排序算法总结

选择一个基准值(pivot),将数组分为两部分,使得左边部分的元素都小于等于基准值,右边部分的元素都大于等于基准值,然后分别对左右两部分递归地进行排序。:利用堆这种数据结构,将数组构建成一个最大堆(或最小堆),然后将堆顶元素与最后一个元素交换,再对剩余元素重新调整为堆,重复这个过程直到整个数组有序。:采用分治法,将数组分成两个子数组,分别对两个子数组进行排序,然后将排好序的子数组合并成一个有序的数组。:平均和最坏情况下为 (O(n^2)),最好情况下(数据已经有序)为 (O(n))。

weixin_42366947的博客 1133

快速排序、堆排序等各种排序算法C++代码,topK问题

快速排序,面试经常可能碰到的题,今天整理了一下,方便以后自己查看复习。原理可以参见博客:快速排序的原理 我也是参考这个博客的,写的挺好的。以下是c++代码。 #include<bits/stdc++.h> using namespace std; //找到基准数下标 int getIndex(vector<int>& arr, int low, int high) ...

fannyoona的博客 770

转载:各种排序算法的C++实现(冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序)

原始链接:https://blog.youkuaiyun.com/u013834525/article/details/82992097 各种排序算法的C++实现(冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序) 0. 写在最前面 希望大家收藏: 本文持续更新地址:https://haoqchen.site/2018/10/10/all-kind-of-sort/ 如果觉得还不错就点个赞,点个关注呗,博主会长期更新自己的学习和收获。 各个算法的详细介绍请参考这个博客:十大...

黄道婆的专栏 326

C++ 交换排序(冒泡排序、快速排序)

交换排序 冒泡排序 思路: 两两比较相邻记录的关键字,若发生逆序,则进行交换。若是从小到大排序,则每轮都选出一个最大的数交换至最右边。 实现: //冒泡排序 void BubbleSort(SqList &L) { int m = L.length - 1; //一般r[0]闲置用作哨兵单元,所以有L.length-1个 bool flag = 1; //标记某一趟排序是否发生排序...

weixin_39033371的博客 432
上一篇: BTStack嵌入式开发避坑指南:那些官方文档没说的HAL接口实现细节
下一篇: Gazebo与RViz数据同步问题排查:解决机器人状态不一致的5个常见原因
java5
博客等级 码龄22年 85粉丝 2103原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值