C++:七大排序算法全面解析

C++:七大排序算法全面解析

目录

一、选择排序

1. 思路

2. 平均时间复杂度:O(n^2)

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

4. 稳定性:不稳定

二、冒泡排序

1. 思路

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

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

4. 稳定性:稳定

三、计数排序(桶排下标)

1. 思路

2. 时间复杂度:O(n)

3. 空间复杂度:O(m)

4. 稳定性:不稳定

四、插入排序(两个下标)

1. 思路

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

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

4. 稳定性:稳定

五、堆排序

1. 思路

2. 时间复杂度:O(n \log n)

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

4. 稳定性:不稳定

六、快速排序

三色旗分区版本

完整递归版本(三色旗+两次递归)

七、归并排序

二分法基础

归并排序算法

八、整体比较与总结


一、选择排序

1. 思路
  • 双层循环,遍历数组找到每次的最大值,记录最大值下标 index,将最大值与最后一个值交换:swap(nums[index], nums[n-1])
  • 在剩下元素中,重复上述操作,直至数组排序完成:swap(nums[index], nums[n-2]),一般形式为 swap(nums[index], nums[nums.size() - i])
2. 平均时间复杂度:O(n^2)
  • 过程
  • 总结
    • 元素交换次数为 k(k < n-1 次)。
    • 时间复杂度 = 比较次数 + 交换次数。
    • 所以复杂度为: O(1+2+3+...+n-1+k)=O(n*(n-1)/2+k)=O(n²)
迭代次数元素个数还需比较次数
第一次nn-1
第二次n-1n-2
第n-1次21
第n次10
3. 空间复杂度:O(1)
  • 原地排序,不需要额外空间。
4. 稳定性:不稳定
  • 例子:数组 3 2 3 1 从小到大排序(小的放前面),排序后第一个 3 在第二个 3 之前。

代码示例

#include<iostream> #include<vector> using namespace std; void selectSort(vector<int> &nums) { //引用 for (int i = 1; i < nums.size(); i++) { //遍历n-1轮 int index = 0; for (int j = 1; j <= nums.size() - i; j++) { //找最大值下标,找n-1轮 if (nums[index] < nums[j]) index = j; } //交换最大值和最后一个位置的值 swap(nums[index], nums[nums.size() - i]); } } int main() { vector<int> vec = { 2,1,7,4,-1,3,5 }; selectSort(vec); for (auto it : vec) { cout << it << " "; } return 0; } 


二、冒泡排序

1. 思路
  • 冒泡排序就是通过不断比较相邻元素:如果它们顺序有错误,就交换它们的位置,让较大的元素(或较小的元素)逐渐“冒泡”移动到列表的一端,经过多次循环这样的过程,最终使得整个列表变得有序。
2. 时间复杂度:O(n) ~ O(n^2)
  • 最好情况(已排序)为 O(n),最坏情况(逆序)为 O(n^2)。
3. 空间复杂度:O(1)
  • 原地排序。
4. 稳定性:稳定
  • 相同元素不会交换顺序。

代码示例

#include<iostream> #include<vector> #include<algorithm> using namespace std; void Bubble_Sort(vector<int>& vec) { for (int i = 0; i < vec.size() - 1; i++) { bool flag = 1; for (int j = 0; j < vec.size() - 1 - i; j++) { swap(vec[j], vec[j + 1]); flag = 0; } if (flag) return; } } int main() { vector<int> vec = { 4,3,1,5,9,2 }; Bubble_Sort(vec); for (auto it : vec) { cout << it << " "; } return 0; } 


三、计数排序(桶排下标)

1. 思路
  • 将数值作为桶号,遍历整个数组,对相应的桶进行计数。
    • 遍历原数组,找到最大值 max,申请 max+1 个空间的 bucket 数组(桶),初始化为 0,下标:0-max(vectorbucket(max+1,0))。
    • 遍历原数组,找到每个数值对应的桶号,并对桶计数 ++,即 bucket[vec[i]]++
    • 遍历桶数组,根据桶内计数取出下标值,放入原数组中。
2. 时间复杂度:O(n)
  • 线性时间,依赖于输入范围。
3. 空间复杂度:O(m)
  • m 为最大值 max,需要额外空间。
4. 稳定性:不稳定
  • 相同元素可能改变顺序。

代码示例

#include<iostream> #include<vector> #include<algorithm> using namespace std; void Bucket_Sort(vector<int>& vec) { //找出vec最大值 int vec_max = vec[0]; for (auto it : vec) { vec_max = vec_max > it ? vec_max : it; } //创建bucket数组:大小为:max+1 vector<int>bucket(vec_max + 1, 0); //代表bucket数组中有vec_max+1个0 // 使用bucket记录vec中的元素个数 for (auto it : vec) { bucket[it]++; //统计个数 } //遍历bucket输出 for (int i = 0; i < bucket.size(); i++) { while (bucket[i] > 0) { cout << i << " "; bucket[i]--; } } } int main() { vector<int> vec = { 4,3,1,5,9,2,1,2,1,7,1 }; Bucket_Sort(vec); return 0; } 


四、插入排序(两个下标)

1. 思路
  • 将数组分为有序与无序两部分,每次从无序表取出一个元素,插入到有序表的适当位置。开始时有序表只有一个元素,无序表有 n-1 个数。
  • 每遍历一次,有序表元素增加 1,无序表减少 1,重复 n-1 次。
2. 时间复杂度:O(n) ~ O(n^2)
  • 最好情况(已排序)为 O(n),最坏情况(逆序)为 O(n^2)。
3. 空间复杂度:O(1)
  • 原地排序。
4. 稳定性:稳定
  • 相同元素不会改变相对顺序。

代码示例

#include<iostream> #include<vector> using namespace std; void insert_Sort(vector<int>& vec) { for (int i = 1; i < vec.size(); i++) { int temp = vec[i]; //要插入的元素 //j-有序部分最后一个元素位置 int j = i - 1; for (; j >= 0; j--) { if (temp < vec[j]) { vec[j + 1] = vec[j]; } else { break; } } vec[j + 1] = temp; } } int main() { vector<int> vec = { 7,1,3,4,0,6 }; insert_Sort(vec); for (auto it : vec) { cout << it << " "; } return 0; } 


五、堆排序

1. 思路
  • 首先,从最后一个非叶子节点开始,自底向上调整数组使其成为最大堆,确保每个父节点都大于其子节点
  • 然后,重复将堆顶元素(最大值)与当前末尾元素交换,并调整剩余部分使其重新成为最大堆,最终实现排序。这一过程的时间复杂度为O(n \log n),空间复杂度为O(1)(原地排序),但稳定性较差,因为相同元素的相对顺序可能改变。
2. 时间复杂度:O(n \log n)
  • 建堆和调整堆的过程。
3. 空间复杂度:O(1)
  • 原地排序。
4. 稳定性:不稳定
  • 相同元素可能改变顺序。

代码示例

#include<iostream> #include<vector> using namespace std; void adjustHeap(vector<int>& vec, int strat, int end) { int father = strat; //根节点 int child = 2 * father + 1; //左子树 //while 循环为了调整最大堆的过程中破坏子树结构,继续向下调整 while (child <= end) { //因为child是左子树,根节点要大于左右子树,所以在子树中找最大的与根节点比较 //防止右子树越界在数组中下标为child+1元素,child就是子树中最大的元素 if (child + 1 <= end && vec[child + 1] > vec[child]) { child++; } //如果根节点小于child就交换 if (vec[child] > vec[father]) { swap(vec[child], vec[father]); //如果发生交换就继续向下调整,因为可能破坏子树最大堆结构 father = child; child = 2 * father + 1; } //如果没交换就退出该函数 else return; } } void HeapSort(vector<int>& vec) { //从最后一个有子节点的节点开始调整 for (int i = vec.size() / 2 - 1; i >= 0; i--) //vec.size()/2-1是完全二叉树中最后一个有子节点子树的根 { adjustHeap(vec, i, vec.size() - 1); } for (int i = vec.size() - 1; i >= 1; i--) { swap(vec[0], vec[i]); //只有下标为0的元素被打乱,从根节点开始向下调整 adjustHeap(vec, 0, i - 1); } } int main() { vector<int>vec = { 53,17,22,89,33,45,12 }; HeapSort(vec); for (auto it : vec) { cout << it << " "; //12 17 22 33 45 53 89 } return 0; } 


六、快速排序

快速排序是一种分治算法,通过选择基准元素将序列分区,然后递归排序子序列。以下是两种版本:一个是三色旗分区(用于特定问题),另一个是完整的递归实现

    • 问题背景:此版本针对类似“荷兰国旗问题”的场景,序列只包含三种值(例如0、1、2)。算法将序列分为三个区域:小于基准、等于基准、大于基准。
    • 时间复杂度:O(n),因为它只遍历序列一次进行分区。
    • 算法步骤
      • 初始化指针:i(左边界)、j(右边界)、index(当前索引)。
      • 遍历序列:比较当前元素与基准temp(通常设为中间值,如1)。
        • 如果vec[index] == temp,则index++
        • 如果vec[index] < temp,则交换到左侧区域(i++后交换)。
        • 如果vec[index] > temp,则交换到右侧区域(j--后交换)。
      • 分区完成后,序列被划分为三个部分。
    • 算法描述:在分区的基础上,递归排序左右子序列。基准值选择序列首元素vec[L]
    • 时间复杂度分析
      • 最好情况:每次分区基准值接近中位数,序列均匀划分。此时:
        • 时间复杂度:O(n \log n)
        • 空间复杂度:O(\log n)(递归栈深度)
      • 最坏情况:每次基准值是最小或最大值,序列极度不平衡。此时:
        • 时间复杂度:O(n^2)
        • 空间复杂度:O(n)(递归栈深度)
    • 稳定性:不稳定,因为分区过程涉及交换,可能改变相同元素的相对顺序。
    • 算法步骤
      • 分区:使用三色旗方法将序列分为三部分(小于、等于、大于基准)。
      • 递归:对左右子序列(LijR)递归调用快速排序。

代码示例

void quik(vector<int>& vec, int L, int R) { if (L > R) return; int temp = vec[L]; // 基准值为首元素 int i = L - 1; int j = R + 1; int index = L; while (index < j) { if (temp == vec[index]) index++; else if (temp > vec[index]) swap(vec[++i], vec[index++]); else swap(vec[--j], vec[index]); } quik(vec, L, i); quik(vec, j, R); } 

以下是完整代码:

#include<iostream> #include<vector> using namespace std; void quik(vector<int>& vec, int L, int R) { if (L > R) return; int temp = vec[L]; int i = L - 1; int j = R + 1; int index = L; while (index < j) { if (temp == vec[index]) index++; //和左侧交换 直接index++,因为左侧比较完,交换过来的值一定是temp else if (temp > vec[index]) swap(vec[++i], vec[index++]);//i右移之后和index交换,再index++ else//temp < vec[index] swap(vec[--j], vec[index]);//从右侧交换过来后,index要重新比较,因为不能确定交换过来的值与temp关系 } quik(vec, L, i); quik(vec, j, R); } int main() { vector<int> vec = { 7,3,0,9,4,2,2,6,5 }; quik(vec, 0, vec.size() - 1); for (auto it : vec) cout << it << " "; return 0; }
完整递归版本(三色旗+两次递归)

代码示例

void quik(vector<int>& vec, int L, int R) { int temp = 1; // 基准值设为1 int i = L - 1; int j = R + 1; int index = L; while (index < j) { if (temp == vec[index]) index++; else if (temp > vec[index]) swap(vec[++i], vec[index++]); else swap(vec[--j], vec[index]); } } 

以下是完整代码:

#include<iostream> #include<vector> using namespace std; void quik(vector<int>& vec, int L, int R) { int temp = 1; int i = L - 1; int j = R + 1; int index = L; while (index < j) { if (temp == vec[index]) index++; //和左侧交换 直接index++,因为左侧比较完,交换过来的值一定是temp else if (temp > vec[index]) swap(vec[++i], vec[index++]);//i右移之后和index交换,再index++ else//temp < vec[index] swap(vec[--j], vec[index]);//从右侧交换过来后,index要重新比较,因为不能确定交换过来的值与temp关系 } } int main() { vector<int> vec = { 1,0,2,0,1,1,2,0 }; quik(vec, 0, vec.size() - 1); for (auto it : vec) cout << it << " "; return 0; }
三色旗分区版本

七、归并排序

归并排序是一种分治算法,基于二分法将序列递归分割,然后合并有序子序列。您的代码包括二分查找基础示例和归并排序实现。

    • 问题背景:二分查找是归并排序的灵感来源,用于在有序序列中高效定位元素。
    • 时间复杂度:O(\log n),因为每次比较将搜索范围减半。
    • 算法步骤
      • 初始化指针:L(左边界)、R(右边界)。
      • 循环:计算中点mid,比较目标值与vec[mid]
        • 如果target < vec[mid],则R = mid - 1
        • 如果target > vec[mid],则L = mid + 1
        • 如果相等,则返回元素。
    • 算法原理:递归将序列分为两半,排序子序列后合并。核心:合并过程,确保有序性。
    • 时间复杂度:O(n \log n),因为分割和合并各需O(\log n)层,每层O(n)工作。
    • 空间复杂度:O(n),由于需要辅助数组存储合并结果。
    • 稳定性:稳定,因为合并时相同元素保持相对顺序(代码中使用<=比较)。
    • 算法步骤
      • 分割:递归调用MergeSort将序列分为左右子序列。
      • 合并:使用辅助数组比较左右子序列元素,按序合并。
        • 初始化指针:i(左子序列起始)、j(右子序列起始)。
        • 循环比较元素,放入辅助数组。
        • 处理剩余元素。
        • 将辅助数组复制回原序列。

代码示例

void Merge(vector<int>& vec, int L, int mid, int R) { vector<int> temp(R - L + 1); int i = L, j = mid + 1, index = 0; while (i <= mid && j <= R) { if (vec[i] <= vec[j]) temp[index++] = vec[i++]; else temp[index++] = vec[j++]; } while (i <= mid) temp[index++] = vec[i++]; while (j <= R) temp[index++] = vec[j++]; for (int k = 0; k < temp.size(); k++) vec[L + k] = temp[k]; } void MergeSort(vector<int>& vec, int L, int R) { if (L >= R) return; int mid = (R + L) / 2; MergeSort(vec, L, mid); MergeSort(vec, mid + 1, R); Merge(vec, L, mid, R); } 

以下是完整代码:

#include<iostream> #include<vector> using namespace std; void Merge(vector<int>& vec, int L,int mid,int R) { //申请一个辅助数组大小:R-L+1 vector<int>temp(R - L + 1); int i = L; int j = mid + 1; int index = 0; //利用while循环比较两个有序数组L到mid和mid+1到R while (i <= mid && j <= R) { if (vec[i] <= vec[j]) { temp[index++] = vec[i++]; } else { temp[index++] = vec[j++]; } } //经历上一个步骤一定会有剩余元素,利用两个while循环判断 while (i <= mid)//左半边有剩余 { temp[index++] = vec[i++]; } while (j <= R)//右半边有剩余 { temp[index++] = vec[j++]; } //将辅助数组中元素还原到原数组,对应L到R的位置 index = L; for (int i = 0; i < temp.size(); i++) { vec[index++] = temp[i]; } } void MergeSort(vector<int>& vec, int L, int R) { if (L >= R) return; int mid = (R + L) / 2; MergeSort(vec, L, mid); MergeSort(vec, mid + 1, R); Merge(vec, L, mid, R); } int main() { vector<int> vec = { 1,4,2,5,8,3,7,7,2,9 }; MergeSort(vec, 0, vec.size() - 1); for (auto it : vec) { cout << it << " ";//1 2 2 3 4 5 7 7 8 9 } return 0; }
归并排序算法

示例代码:

int HalfSearch(vector<int>& vec, int target) { int L = 0; int R = vec.size() - 1; while (L <= R) { int mid = (R - L) / 2 + L; if (target < vec[mid]) R = mid - 1; else if (target > vec[mid]) L = mid + 1; else return vec[mid]; } return -1; } 

以下是完整代码:

#include<iostream> #include<vector> using namespace std; int HalfSearch(vector<int>& vec, int target) { int L = 0; int R = vec.size() - 1; while (L <= R) { int mid = (R - L) / 2 + L; if (target < vec[mid]) { R = mid - 1; } else if (target > vec[mid]) { L = mid + 1; } else { return vec[mid]; } } return -1; } int main() { vector<int> vec = { 1,4,2,5,8,3,7,7,2,9 }; int n = HalfSearch(vec, 8); cout << n; return 0; }
二分法基础

八、整体比较与总结

时间复杂度对比
  • O(n²)级别:选择排序、冒泡排序、插入排序。适用于小规模数据或部分有序数据,其中插入排序在接近有序时效率最高(接近O(n))。
  • O(n log n)级别:堆排序、快速排序、归并排序。适合大规模数据,快速排序平均性能最优,但最坏情况退化为O(n²);堆排序稳定在O(n log n);归并排序稳定但需额外空间。
  • O(n)级别:计数排序(桶排)。仅适用于整数且范围较小的数据,空间消耗与数据范围相关。
空间复杂度对比
  • 原地排序(O(1)):选择排序、冒泡排序、插入排序、堆排序、快速排序(递归栈空间不计入,但最坏情况下递归深度为O(n))。
  • 非原地排序:归并排序(O(n))、计数排序(O(m),m为数据范围)。
稳定性对比
  • 稳定算法:冒泡排序、插入排序、归并排序。保持相同元素的相对顺序。
  • 不稳定算法:选择排序、堆排序、快速排序、计数排序(依赖具体实现)。
排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性适用场景
选择排序O(n²)O(n²)O(1)不稳定小规模数据
冒泡排序O(n²)O(n²)O(1)稳定教学或简单场景
插入排序O(n²)O(n²)O(1)稳定接近有序的小数据
快速排序O(n log n)O(n²)O(log n)不稳定大规模随机数据
归并排序O(n log n)O(n log n)O(n)稳定需要稳定性的外部排序
堆排序O(n log n)O(n log n)O(1)不稳定实时数据或Top K问题
计数排序O(n)O(n)O(m)不稳定范围小的整数数据

通过对比可见,不同排序算法在时间、空间、稳定性上各有优劣,实际选择需结合数据规模、分布特征及稳定性需求。

Read more

免费部署openClaw龙虾机器人(经典)

免费部署openClaw龙虾机器人(经典)

前几天出了个免费玩龙虾的详细教程,很多小伙伴觉得不错,但是还有一些新手留言反馈内容不够详细,这次我将重新梳理一遍,做一期更细致的攻略,同时扩展补充配置好之后的推荐(我认为是必要)操作,争取一篇文章让大家可以收藏起来,随时全套参照复用。 先看效果测试 部署完成基础运行效果测试,你可以直接问clawdbot当前的模型: 1.Token平台准备 首先,还是准备好我们可以免费撸的API平台 这里我找到了两个可以免费使用的API,测试之后执行效率还可以,下面将分别进行细致流程拆解。 1.1 硅基流动获取ApiKey (相对免费方案 推荐) 硅基流动地址:https://cloud.siliconflow.cn/i/6T57VxS2 如果有账号的直接登录,没有的注册一个账号,这个认证就送16元,可以直接玩收费模型,真香。认证完成后在API秘钥地方新建秘钥。 硅基流动里面很多模型原来是免费的,有了16元注册礼,很多收费的模型也相当于免费用了,我体验一下了原来配置免费模型还能用,也是值得推荐的。建议使用截图的第一个模型体验一下,我一直用它。 1.2 推理时代

By Ne0inhk
宇树机器人SDK2开发指南:从环境搭建到Demo测试

宇树机器人SDK2开发指南:从环境搭建到Demo测试

本文以宇树 G1 人形机器人为主线,系统介绍 unitree_sdk2(C++)与 unitree_sdk2_python(Python)的完整开发流程,涵盖通信架构原理、环境搭建、依赖安装、Demo 编译运行、网络配置以及常见问题处理,适合具身智能领域的初中级开发者快速上手。 目录 1. SDK2 概述与架构原理 2. 开发环境要求 3. 获取官方 SDK 包 4. 安装依赖与编译 5. 机器人与开发机网络配置 6. 调试并运行 Demo 7. Python SDK Demo 测试 8. 常见问题与解决方案 9. 总结 1. SDK2 概述与架构原理 1.

By Ne0inhk
从零开始使用ISSACLAB训练自己的机器人行走

从零开始使用ISSACLAB训练自己的机器人行走

ISAACLAB入门教程 作者:陈维耀 1. 环境配置 1.1 推荐配置 * 操作系统: Ubuntu 22.04 LTS * 显卡: NVIDIA RTX 4080或以上 1.2 ubuntu 22.04 LTS安装 参考ZEEKLOG的Ubuntu 16.04 LTS安装教程,将其中的ubuntu 16.04镜像文件替换为ubuntu 22.04镜像文件,其他步骤保持不变,建议/home与/usr的硬盘容量均不少于200G。 1.3 安装NVIDIA驱动 根据自身显卡型号与操作系统,选择对应的显卡驱动,建议选择550.xxx.xxx版本的显卡驱动,按照教程进行安装即可,安装完成后在终端输入nvidia-smi,若出现以下信息则表示驱动安装成功: Thu Jun 5

By Ne0inhk
最新 neo4j 5.26版本下载安装配置步骤(新手必备)

最新 neo4j 5.26版本下载安装配置步骤(新手必备)

目录 初识:neo4j 安装环境要求 一、下载Neo4j 二、配置环境变量 三、启动测试 四、常用命令及配置 创作不易,禁止转载抄袭!!!违者必究!!! 创作不易,禁止转载抄袭!!!违者必究!!! 创作不易,禁止转载抄袭!!!违者必究!!! 初识:neo4j Neo4j是一个高性能的NoSQL图形数据库,它将结构化数据存储在网络(从数学角度称为图)上而不是传统的表中。‌ Neo4j是一个嵌入式的、基于磁盘的、具备完全事务特性的Java持久化引擎,特别适合处理具有复杂关系的数据‌。 安装环境要求 * 操作系统:Windows 10/8/7、macOS 10.13或更高版本、Linux(Ubuntu、CentOS、Red Hat 等) * JDK 17 或更高版本(Neo4j

By Ne0inhk