c++算法之基本算法篇 - 排序与排列

1. sort() - 快速排序/内省排序

  • 快速排序(Quick Sort):这是sort()函数底层使用的主要算法
  • 内省排序(Introsort):实际上是C++中sort()的完整实现,结合了快速排序、堆排序和插入排序的优点

2. stable_sort() - 归并排序

  • 归并排序(Merge Sort):stable_sort()通常基于归并排序实现,保证相等元素的相对顺序不变

3. partial_sort() - 堆排序

  • 堆排序(Heap Sort):partial_sort()函数底层使用堆排序算法,只对前m个元素进行排序

4. nth_element() - 快速选择算法

  • 快速选择算法(Quickselect):这是nth_element()函数使用的算法,用于找到第n小的元素

文章中还提到的其他相关算法:

5. 插入排序(Insertion Sort)

  • 在小规模数据时,内省排序会切换到插入排序

6. 冒泡排序(Bubble Sort)

  • 作为基础排序算法在文章中提及

7. 选择排序(Selection Sort)

  • 作为基础排序算法在文章中提及

8. 计数排序(Counting Sort)

  • 在进阶学习方向中提到的特定数据高效排序算法

9. 桶排序(Bucket Sort)

  • 在进阶学习方向中提到的特定数据高效排序算法

10. 堆排序(Heap Sort)

  • 除了partial_sort()使用外,还在进阶学习方向中作为独立算法提及

11. 归并排序(Merge Sort)

  • 除了stable_sort()使用外,还在进阶学习方向中作为独立算法提及

一、引言:为什么排序和排列如此重要?

想象一下你正在整理书架:

  • 排序就像按字母顺序排列书籍,让查找变得轻松
  • 排列则像尝试所有可能的书籍摆放方式,找到最合适的布局

在编程世界中,排序和排列是两大基础操作:

  • 排序:将数据按特定规则(升序、降序等)重新排列
  • 排列:生成数据所有可能的排列组合

C++标准库提供了强大的排序和排列工具,让我们能高效处理这些操作。本文将带你从零开始,彻底掌握C++中的排序与排列技术。

二、排序函数:C++中的排序神器

2.1 排序函数全家福

C++标准库提供了多种排序函数,各有特色:

函数名用途时间复杂度稳定性适用场景
sort()完全排序O(n log n)不稳定通用排序
stable_sort()稳定排序O(n log n)稳定需要保持相等元素顺序
partial_sort()部分排序O(n log m)不稳定只需前m个有序
nth_element()第n小元素O(n)不稳定找中位数或分位数

2.2 sort():最常用的排序函数

基本用法
#include <algorithm>
#include <vector>

int main() {
    std::vector<int> nums = {5, 2, 9, 1, 5, 6};
    
    // 默认升序排序
    std::sort(nums.begin(), nums.end());
    
    // 排序后:1, 2, 5, 5, 6, 9
}
工作原理

sort()使用**内省排序(Introsort)**算法:

  1. 开始使用快速排序(平均O(n log n))
  2. 当递归深度过大时切换为堆排序(避免最坏情况O(n²))
  3. 对小规模数据使用插入排序(优化小数组性能)
降序排序
// 方法1:使用greater
std::sort(nums.begin(), nums.end(), std::greater<int>());

// 方法2:使用lambda表达式
std::sort(nums.begin(), nums.end(), [](int a, int b) {
    return a > b;
});

2.3 stable_sort():保持相对顺序的稳定排序

为什么需要稳定排序?

想象你有一个学生名单,先按成绩排序,再按姓名排序:

  • 使用sort():成绩相同的学生顺序可能被打乱
  • 使用stable_sort():成绩相同的学生保持原有顺序
示例
struct Student {
    std::string name;
    int score;
};

std::vector<Student> students = {
    {"Alice", 90},
    {"Bob", 85},
    {"Charlie", 90}
};

// 先按成绩排序(稳定)
std::stable_sort(students.begin(), students.end(), 
    [](const Student& a, const Student& b) {
        return a.score > b.score;
    });

// 再按姓名排序(稳定)
std::stable_sort(students.begin(), students.end(), 
    [](const Student& a, const Student& b) {
        return a.name < b.name;
    });

// 结果:Alice(90), Charlie(90), Bob(85)
// 注意:Alice和Charlie的顺序保持不变

2.4 partial_sort():部分排序专家

应用场景

当你只需要前几名有序时,partial_sort()是最佳选择:

  • 找出成绩前10名的学生
  • 获取销售额最高的5个产品
示例
std::vector<int> nums = {9, 6, 1, 3, 8, 2, 5, 7, 4};

// 只需要前3个元素有序
std::partial_sort(nums.begin(), nums.begin() + 3, nums.end());

// 结果:1, 2, 3, 9, 8, 6, 5, 7, 4
// 前三个是最小的三个,且有序,后面顺序不定

2.5 nth_element():找第n小元素的神器

应用场景
  • 找中位数
  • 找第90百分位数
  • 快速选择算法
示例
std::vector<int> nums = {5, 2, 9, 1, 5, 6};

// 找第4小的元素(索引3)
std::nth_element(nums.begin(), nums.begin() + 3, nums.end());

// 结果:1, 2, 5, 5, 9, 6
// 第4个位置(5)是正确的,前面都<=5,后面都>=5

三、排列算法:生成所有可能组合

3.1 next_permutation():生成下一个排列

基本概念

排列是指对一组元素进行重新排列的所有可能方式。例如:

  • {1, 2, 3}的排列有:
    1, 2, 3
    1, 3, 2
    2, 1, 3
    2, 3, 1
    3, 1, 2
    3, 2, 1
基本用法
#include <algorithm>
#include <vector>

int main() {
    std::vector<int> nums = {1, 2, 3};
    
    do {
        // 处理当前排列
        for (int num : nums) std::cout << num << " ";
        std::cout << std::endl;
    } while (std::next_permutation(nums.begin(), nums.end()));
}
工作原理

next_permutation()按字典序生成下一个排列:

  1. 从后向前找第一个相邻升序对(i, j),满足a[i] < a[j]
  2. 从后向前找第一个大于a[i]的元素a[k]
  3. 交换a[i]和a[k]
  4. 反转i之后的所有元素
示例解析

以{1, 2, 3}为例:

  1. 初始:1, 2, 3
  2. next_permutation() → 1, 3, 2
  3. next_permutation() → 2, 1, 3
  4. next_permutation() → 2, 3, 1
  5. next_permutation() → 3, 1, 2
  6. next_permutation() → 3, 2, 1
  7. next_permutation() → false(回到1,2,3)

3.2 prev_permutation():生成前一个排列

与next_permutation()相反,prev_permutation()生成字典序的前一个排列:

std::vector<int> nums = {3, 2, 1};

do {
    // 处理当前排列
    for (int num : nums) std::cout << num << " ";
    std::cout << std::endl;
} while (std::prev_permutation(nums.begin(), nums.end()));

3.3 排列生成注意事项

  1. 初始顺序很重要:要生成所有排列,必须从升序开始
   std::vector<int> nums = {3, 1, 2};
   std::sort(nums.begin(), nums.end()); // 先排序
   do { ... } while (next_permutation(...));
  1. 处理重复元素:当有重复元素时,会生成重复排列
   std::vector<int> nums = {1, 1, 2};
   // 会生成:1,1,2; 1,2,1; 2,1,1
  1. 避免无限循环:当已经是最后一个排列时,next_permutation返回false

四、自定义比较:打造专属排序规则

4.1 比较函数的三种形式

1. 函数指针
bool compareDesc(int a, int b) {
    return a > b;
}

std::sort(nums.begin(), nums.end(), compareDesc);
2. 函数对象(仿函数)
struct Compare {
    bool operator()(int a, int b) const {
        return a > b;
    }
};

std::sort(nums.begin(), nums.end(), Compare());
3. Lambda表达式(C++11)
std::sort(nums.begin(), nums.end(), [](int a, int b) {
    return a > b;
});

4.2 复杂对象排序

按多个字段排序
struct Person {
    std::string name;
    int age;
    double height;
};

std::vector<Person> people = {{"Alice", 25, 1.65}, {"Bob", 30, 1.80}, {"Charlie", 25, 1.75}};

// 先按年龄升序,年龄相同按身高降序
std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
    if (a.age != b.age) {
        return a.age < b.age;
    }
    return a.height > b.height;
});
自定义排列规则
// 自定义比较函数,生成特定顺序的排列
bool customCompare(int a, int b) {
    // 偶数排在奇数前面
    if (a % 2 == 0 && b % 2 != 0) return true;
    if (a % 2 != 0 && b % 2 == 0) return false;
    // 同奇偶则按升序
    return a < b;
}

std::vector<int> nums = {1, 2, 3, 4};
std::sort(nums.begin(), nums.end(), customCompare);
// 结果:2, 4, 1, 3

五、实例演示:排序与排列的实际应用

5.1 实例1:学生成绩管理系统

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

struct Student {
    std::string name;
    int id;
    int score;
};

int main() {
    std::vector<Student> students = {
        {"Alice", 101, 85},
        {"Bob", 102, 92},
        {"Charlie", 103, 78},
        {"David", 104, 92},
        {"Eve", 105, 88}
    };

    // 1. 按成绩降序排序
    std::sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
        return a.score > b.score;
    });

    std::cout << "按成绩排名:\n";
    for (const auto& s : students) {
        std::cout << s.name << ": " << s.score << std::endl;
    }

    // 2. 找出成绩前3名的学生
    std::partial_sort(students.begin(), students.begin() + 3, students.end(), 
        [](const Student& a, const Student& b) {
            return a.score > b.score;
        });

    std::cout << "\n前三名:\n";
    for (int i = 0; i < 3; ++i) {
        std::cout << students[i].name << ": " << students[i].score << std::endl;
    }

    // 3. 找出成绩中位数
    std::vector<int> scores;
    for (const auto& s : students) {
        scores.push_back(s.score);
    }
    std::nth_element(scores.begin(), scores.begin() + scores.size()/2, scores.end());
    std::cout << "\n成绩中位数: " << scores[scores.size()/2] << std::endl;

    return 0;
}

5.2 实例2:旅行路线规划

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

int main() {
    std::vector<std::string> cities = {"北京", "上海", "广州", "深圳"};
    
    std::cout << "所有可能的旅行路线:\n";
    int count = 0;
    
    // 生成所有可能的旅行路线
    do {
        std::cout << ++count << ": ";
        for (const auto& city : cities) {
            std::cout << city << " -> ";
        }
        std::cout << "起点\n";
    } while (std::next_permutation(cities.begin(), cities.end()));
    
    std::cout << "\n共有 " << count << " 种不同的旅行路线\n";
    return 0;
}

5.3 实例3:密码破解模拟

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

int main() {
    std::string password = "1234";
    std::string guess = "1234";
    std::vector<char> chars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    
    std::cout << "尝试破解4位数字密码...\n";
    int attempts = 0;
    
    // 生成所有可能的4位数字组合
    do {
        attempts++;
        std::string current(chars.begin(), chars.begin() + 4);
        
        if (current == password) {
            std::cout << "破解成功!密码是: " << current << std::endl;
            std::cout << "尝试次数: " << attempts << std::endl;
            break;
        }
    } while (std::next_permutation(chars.begin(), chars.end()));
    
    return 0;
}

六、性能考虑:选择合适的算法

6.1 排序算法选择指南

场景推荐算法原因
通用排序sort()平均性能最好,O(n log n)
需要保持相等元素顺序stable_sort()稳定排序,但稍慢
只需前m个有序partial_sort()O(n log m)比完全排序快
找第n小元素nth_element()O(n)线性时间
小规模数据(<10)插入排序虽然sort()内部已优化,但手动插入排序可能更快
链表排序list::sort()专门为链表优化

6.2 排列生成效率

  • 时间复杂度:生成n个元素的所有排列需要O(n!)时间
  • 空间复杂度:O(1)(原地操作)
  • 实际考虑
    • 当n>10时,排列数量巨大(10! = 3,628,800)
    • 对于大n,考虑使用回溯算法+剪枝
    • 如果只需要部分排列,考虑使用组合数学

6.3 优化技巧

  1. 预分配内存:对于vector,提前reserve()减少重新分配
   std::vector<int> largeData;
   largeData.reserve(1000000); // 预分配空间
  1. 移动语义:对于大对象,使用移动语义减少拷贝
   std::vector<std::string> strings;
   std::string temp = "hello";
   strings.push_back(std::move(temp)); // 移动而非拷贝
  1. 并行排序:C++17支持并行排序
   std::sort(std::execution::par, vec.begin(), vec.end());
  1. 避免不必要的排序:如果只需要最小/最大值,使用min_element/max_element
   auto minIt = std::min_element(vec.begin(), vec.end());

七、常见陷阱与解决方案

7.1 排序常见问题

问题1:排序后元素"消失"
std::vector<int> v = {5, 3, 1, 2, 4};
std::sort(v.begin(), v.end() - 1); // 错误:排除了最后一个元素

解决方案:确保范围正确

std::sort(v.begin(), v.end()); // 正确
问题2:自定义比较函数不一致
// 错误的比较函数
bool badCompare(int a, int b) {
    return a <= b; // 应该是严格小于
}

解决方案:比较函数必须满足严格弱序

bool goodCompare(int a, int b) {
    return a < b; // 正确
}
问题3:排序后迭代器失效
std::vector<int> v = {3, 1, 2};
auto it = v.begin() + 1; // 指向1
std::sort(v.begin(), v.end()); // 排序后it可能不再指向1

解决方案:排序后不要依赖旧的迭代器

7.2 排列常见问题

问题1:没有生成所有排列
std::vector<int> v = {3, 1, 2};
do { ... } while (std::next_permutation(v.begin(), v.end())); // 可能遗漏排列

解决方案:先排序

std::sort(v.begin(), v.end());
do { ... } while (std::next_permutation(v.begin(), v.end()));
问题2:重复元素导致重复排列
std::vector<int> v = {1, 1, 2};
// 会生成重复排列:1,1,2; 1,2,1; 2,1,1

解决方案:使用set去重

std::set<std::vector<int>> uniquePerms;
do {
    uniquePerms.insert(v);
} while (std::next_permutation(v.begin(), v.end()));
问题3:排列顺序不符合预期
std::vector<int> v = {1, 2, 3};
std::next_permutation(v.begin(), v.end());
// 期望得到1,3,2但实际可能不同

解决方案:理解字典序,确保初始状态正确

八、总结与进阶学习

8.1 核心要点回顾

  1. 排序函数

    • sort():通用排序,O(n log n)
    • stable_sort():稳定排序
    • partial_sort():部分排序
    • nth_element():找第n小元素
  2. 排列算法

    • next_permutation():生成下一个排列
    • prev_permutation():生成前一个排列
    • 需要从升序开始才能生成所有排列
  3. 自定义比较

    • 函数指针、函数对象、lambda表达式
    • 必须满足严格弱序
  4. 性能考虑

    • 根据场景选择合适算法
    • 注意排列生成的指数级复杂度

8.2 进阶学习方向

  1. 其他排序算法

    • 归并排序(merge)
    • 堆排序(make_heap, push_heap, pop_heap)
    • 计数排序、桶排序(特定数据高效)
  2. 高级排列组合

    • 组合生成(next_combination)
    • 子集生成
    • 可重复排列
  3. 并行算法

    • C++17并行排序
    • 多线程排列生成
  4. 实际应用

    • 数据库索引
    • 搜索算法
    • 密码学
    • 统计分析

8.3 实践建议

  1. 动手实践

    • 实现自己的排序算法(快速排序、归并排序等)
    • 手动模拟排列生成过程
    • 解决LeetCode相关题目(排序、排列组合类)
  2. 性能测试

    • 比较不同排序算法的性能
    • 测试不同数据规模下的表现
    • 分析缓存、分支预测等影响
  3. 项目应用

    • 在实际项目中使用排序和排列
    • 优化现有代码中的排序逻辑
    • 设计需要排列组合的功能
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值