【C++】排序算法

一、排序算法概述

在C语言中,通常需要手写排序算法实现对数组或链表的排序,但是在C++中,标准库中的<algorithm>头文件中已经实现了基于快排的不稳定排序算法 std::sort() ,以及稳定的排序算法 std::stable_sort()

排序算法通常会伴随着容器使用,当然有些时候,我们也可以直接通过容器的特性来实现对元素的去重和排序,比如 map 和 unordered_map 底层的红黑树,就可以在插入数据的时候实现排序。

虽然C++标准库中已经封装有了现成排序算法了,那么我们为何还要学习排序算法呢?

对于应聘者而言,排序算法无论是笔试还是面试,都是常考点,所以我们需要掌握排序算法的算法原理与代码实现,通过学习排序算法,能够加深我们对于代码的复杂度分析,同时也能加深我们对于数据的稳定性分析

复杂度分为时间复杂度和空间复杂度,在早期的程序中,内存往往是限制程序运行的关键因素,因此在算法设计的时候,都会尽可能在考虑代码效率的同时去考虑节约内存,从而做到时间复杂度和空间复杂度的相对平衡,但是随着硬件技术的发展,计算机的内存越来越大,已经不再是紧缺资源了,所以如今的算法设计,会尽可能地优先考虑时间复杂度,在最大化地降低时间复杂度之后,才会考虑去节约内存,甚至有些算法会考虑牺牲空间复杂度来提高效率,比如归并排序。

稳定性在排序算法中也是需要重点考虑的一个因素,稳定性是指在排序过程中,两个等值的元素它们的相对位置是否会在排序过程中改变,如果它们的位置不变,即这是稳定的排序算法,否则就是不稳定的排序算法。

排序算法经历了长远的发展历程,到如今最广为人知的排序算法有:冒泡排序、插入排序、选择排序、希尔排序、桶排序、归并排序、快速排序、计数排序、基数排序、桶排序、外排序……

算法思维进行分类的话,可以分为以下几类:

  1. 暴力求解思想:冒泡排序
  2. 基于插入思想:插入排序、希尔排序
  3. 基于选择思想:选择排序、堆排序
  4. 基于分治思想:归并排序、快速排序
  5. 基于哈希思想:计数排序、基数排序
  6. 基于分割思想:桶排序、外排序

算法稳定性进行分类的话,可以进行如下分类:

  • 稳定:冒泡排序、插入排序、归并排序、计数排序、基数排序、桶排序
  • 不稳定:选择排序、希尔排序、堆排序、快速排序
  • 不确定:外排序

堆排序和外排序通常应用于理大量数据的排序,因为数据量太大而内存存不下,所以需要先将数据分割再进行排序,桶排序利用分布式思想,先将数据分割在多个桶中,每个桶中的数据量差不多,然后对每个桶中的数据进行排序最后再将数据合并,外排序是通过将数据存储在磁盘上来缓解内存不足的问题,通过内存与磁盘间进行多次数据的交换实现对全部数据的排序。

所以通常情况下,桶排序是稳定的,它在每个桶中的排序算法同行会保证元素间的相对顺序,最后的合并过程也会保持稳定性,而外排序稳定性不确定,因为外排序通常会采用归并排序或者快速排序来进行小批量数据的排序。

二、初级排序算法

初级排序算法是指冒泡排序插入排序选择排序

初级排序算法时间复杂度高,很少会在项目中直接使用,但是初级排序算法的算法思想是其他排序算法的基础,我们可以做了解,以此来对比出其他排序算法的高效性。

// 冒泡排序
// 每次遍历都将最值移动到数组末端
// 时间复杂度:O(N^2)
void BubbleSort(std::vector<int>& arr) {int n = arr.size();for (int i = 0; i < n-1; i++) {bool swapped = false;for (int j = 0; j < n-i-1; j++) {if (arr[j] > arr[j+1]) {std::swap(arr[j], arr[j+1]);swapped = true;}}if (!swapped) {break;}}
}
// 插入排序
// 每次遍历都将当前元素往前进行比较,找到它适合插入的位置
// 时间复杂度和数组的原始有序度有关,为 O(N) ~ O(N^2)
void InsertSort(std::vector<int>& arr) {int n = arr.size();for (int i = 1; i < n; i++) {int key = arr[i];int j = i - 1;while (j >= 0 && arr[j] > key) {arr[j+1] = arr[j];j = j - 1;}arr[j+1] = key;}
}
// 选择排序
// 每次遍历会找到最小值和最大值将其放入数组的最前端和最后端
// 只是简单实现的话,可以每次遍历只选择一个最值进行交换
// 同时选择最小值和最大值进行交换,提高了效率
// 时间复杂度:O(N^2)
void SelectSort(std::vector<int>& arr) {int n = arr.size();for (int i = 0; i < n / 2; i++) {int minIndex = i;int maxIndex = i;for (int j = i + 1; j < n - i; j++) {if (arr[j] < arr[minIndex]) {minIndex = j;}if (arr[j] > arr[maxIndex]) {maxIndex = j;}}if (minIndex != i) {std::swap(arr[i], arr[minIndex]);}if (maxIndex == i) {maxIndex = minIndex;}if (maxIndex != n - i - 1) {std::swap(arr[n - i - 1], arr[maxIndex]);}}
}// void SelectionSort(std::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;
//             }
//         }
//         std::swap(arr[i], arr[minIndex]);
//     }
// }

三、进阶排序算法

进阶排序算法是指在初级排序算法上进行了升级,从而实现的算法,如:希尔排序堆排序

希尔排序是插入排序的升级,堆排序是选择排序的升级

插入排序的时间复杂度受原始数组有序度的影响,所以希尔排序的核心思想就是先提高原始数组的有序度,然后再采用插入排序,所以对于希尔排序的具体实现就是先将原始数组的元素按照一个间隔进行分组,先确保每个分组的有序,最后不断缩小这个间隔,最后以此间隔为1,那么就是在进行插入排序了,这既是预排序机制。

堆排序利用二叉树的思想来实现对于数组最值的寻找,在进行升序排序的时候建大根堆,在进行降序排序的时候建小根堆,在建堆的时候,从后往前的父节点进行向下调整,在进行排序的时候,将最值范围内最后的元素交换,再不断降低排序范围从根节点往下进行调整。

堆排序常常用于解决Top-K的问题,若要从海量的数据中寻找K个最大值,我们可以建立一个空间复杂度为O(1)的小根堆,然后遍历海量数据,只要出现比堆顶元素大的数据,则删去堆顶元素,该数据入堆,时间复杂度是O(N)。

// 希尔排序
// 时间复杂度无法准确计算,在O(N)~O(N^2)之间,但远低于O(N^2)
void ShellSort(std::vector<int>& arr) {int n = arr.size();for (int gap = n / 2; gap > 0; gap /= 2) {for (int i = gap; i < n; i++) {int temp = arr[i];int j;for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {arr[j] = arr[j - gap];}arr[j] = temp;}}
}
// 堆排序
// 向下调整可以采用循环或者递归都可以
// 时间复杂度:O(N * log N)
void DownAdjust(int* arr, int parent, int n) {int child = parent * 2 + 1;while (child < n) {if (child + 1 < n && arr[child] < arr[child + 1])child++;if (arr[parent] < arr[child]) {Swap(&arr[parent], &arr[child]);parent = child;child = child * 2 + 1;}else {break;}}
}void HeapSort(int* arr, int n) {int parent = (n - 2) / 2;	// 最后一个节点的父节点while (parent >= 0) {DownAdjust(arr, parent, n);--parent;}while (--n) {Swap(&arr[0], &arr[n]);DownAdjust(arr, 0, n);}
}

四、分治思想排序

分治思想的排序是指归并排序快速排序,它们的算法逻辑受二分法影响。

归并排序和快速排序算法各有优缺点,分不清孰优孰劣,只是应用场景不同。

在排序算法中,通常将额外消耗掉内存空间视为空间复杂度,大多数排序算法不会消耗额外的内存空间,即空间复杂度为O(1),但是归并排序的空间复杂度为O(N),从内存损耗的角度来看,快速排序算法更优,但是归并排序是稳定的排序算法,从稳定性来说,归并排序更优。

归并排序是先将数组无限分割,然后再将一块块分割后的数据进行填入额外开辟的空间中,再用额外空间中已经合并好的有序数据对原数组进行copy覆盖,先无限分割、再合并为一,最后实现了数组的排序。

void __MergeSort(int* arr, int begin, int end, int* tmp) {if (begin >= end)return;// 递归,先将数组无限分割,直到每个子数列的长度小于等于2int mid = (begin + end) / 2;_MergeSort(arr, begin, mid, tmp);_MergeSort(arr, mid + 1, end, tmp);int leftBegin = begin;int leftEnd = mid;int rightBegin = mid + 1;int rightEnd = end;int pos = begin;while (leftBegin <= leftEnd && rightBegin <= rightEnd) {// 从前往后同步遍历两个子数列,将相对较小的值先写入tmp中if (arr[leftBegin] < arr[rightBegin])tmp[pos++] = arr[leftBegin++];elsetmp[pos++] = arr[rightBegin++];}// 将还没遍历完的子数列的值一次性写入tmp中while (leftBegin <= leftEnd) {tmp[pos++] = arr[leftBegin++];}while (rightBegin <= rightEnd) {tmp[pos++] = arr[rightBegin++];}// 内存数据copy,将临时存储有序子数列的tmp数组的值写入arr对应位置memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}void MergeSort(int* arr, int n) {int* tmp = (int*)malloc(sizeof(int) * n);__MergeSort(arr, 0, n - 1, tmp);free(tmp);tmp = NULL;
}

快速排序的思想是找到一个关键Key值,每一次遍历都将比Key值小的数据放在Key左边,比Key值大的数据放在Key值右边,然后通过递归对Key值左右两边的子数组用同样的方法进行排序,最终实现了数组的排序。但是当子数组范围已经很小的时候,此时再通过递归调用的效率损耗是很大的,而且此时的数组有序度已经相当高了,所以归并排序可以配合着插入排序使用,即将子数组分割到一定的范围后,就不再分治,而是直接调用插入排序算法。

快速排序算法有多种实现方式,如Hoare法挖坑法前后指针法非递归法。虽然快排有多种实现方式,但是他们的算法原理是一样的。

// Hoare法
// 选左边为key值,则右边先走,最后左右相遇时比key值小
// 选右边为key值,则左边先走,最后左右相遇时比key值大
int __Hoare(std::vector<int>& arr, int begin, int end) {int ikey = begin;while (begin < end) {while (arr[end] >= arr[ikey] && end > begin) {--end;}while (arr[begin] <= arr[ikey] && begin < end) {++begin;}std::swap(arr[begin], arr[end]);}swap(arr[ikey], arr[end]);return end;
}void QuickSort(std::vector<int>& arr, int begin, int end) {if (begin >= end)return;int ikey = __Hoare(arr, begin, end);QuickSort(arr, begin, ikey - 1);QuickSort(arr, ikey + 1, end);
}

// 挖坑法
int __DigHole(std::vector<int>& arr, int begin, int end) {int key = arr[begin];while (begin < end) {while (arr[end] >= key && end > begin) {--end;}arr[begin] = arr[end];while (arr[begin] <= key && begin < end) {++begin;}arr[end] = arr[begin];}arr[end] = key;return end;
}void QuickSort(std::vector<int>& arr, int begin, int end) {if (begin >= end)return;int ikey = __DigHole(arr, begin, end);QuickSort(arr, begin, ikey - 1);QuickSort(arr, ikey + 1, end);
}

// 前后指针法
// 三数取中是快速排序算法的优化方案,目的是为了避免最坏情况的发送
// 除了三数取中,还可以在将子数组分到足够的小的范围的时候采用插入排序进行优化
// 时间复杂度:O(N * log N)
int __MidIndex(vector<int>& arr, int r, int m, int l) {if ((arr[r] - arr[m]) * (arr[m] - arr[l]) >= 0)return m;else if ((arr[m] - arr[r]) * (arr[r] - arr[l]) >= 0)return r;elsereturn l;
}int __Ptrptr(std::vector<int>& arr, int begin, int end) {int key = arr[begin];int prev = begin;int cur = begin + 1;while (cur <= end) {if (arr[cur] < key && ++prev != cur) {std::swap(arr[prev], arr[cur]);}++cur;}std::swap(arr[begin], arr[prev]);return prev;
}void QuickSort(std::vector<int>& arr, int begin, int end) {if (begin >= end)return;int ikey = (arr, 0, (begin + end) / 2, end);std::swap(arr[begin], arr[ikey]);ikey = __Ptrptr(arr, begin, end);QuickSort(arr, begin, ikey - 1);QuickSort(arr, ikey + 1, end);
}
// 非递归快排
// 在某些极端的情况下,大量的递归会导致堆栈移除,所以需要使用非递归快排算法
// 非递归快排的本质是通过栈或队列来实现对分治的下标控制
// 无论是Hoare法、挖坑法、前后指针法都可以改写为非递归形式
void QuickSort(std::vector<int>& arr, int begin, int end) {if (begin >= end)return;int ikey = (arr, 0, (begin + end) / 2, end);std::swap(arr[begin], arr[ikey]);std::stack<std::pair<int, int>> stk;stk.push(std::make_pair(begin, end));while (!stk.empty()) {int b = stk.top().first;int e = stk.top().second;stk.pop();//ikey = __Hoare(arr, b, e);//ikey = __DigHole(arr, b, e);ikey = __Ptrptr(arr, b, e);if (ikey - 1 > b)stk.push(std::make_pair(b, ikey - 1));if (ikey + 1 < e)stk.push(std::make_pair(ikey + 1, e));}
}

五、哈希思想排序

哈希思想的排序是指计数排序和基数排序,哈希思想的排序算法时间复杂度都非常低,但是他们都有一定的局限性,使用计数排序需要数组中的最大值和最小值差值不能太大,否则空间损耗很大,使用基数排序需要明确知道最大值的位数。

// 计数排序
// 时间复杂度取决于最值之差
void CountSort(std::vector<int>& arr) {int max = *std::max_element(arr.begin(), arr.end());int min = *std::min_element(arr.begin(), arr.end());int range = max - min + 1;std::vector<int> count(range);for (int i = 0; i < arr.size(); ++i) {count[arr[i] - min]++;}int j = 0;for (int i = 0; i < range; ++i) {while (count[i] != 0) {arr[j++] = i + min;count[i] -= 1;}}
}
// 基数排序
// 计数排序可以是做一种特殊的桶排序
// 时间复杂度为O(K * N)
// 设数据最大位数
#define K 3		// 取模的类别数(桶数、基数)
#define RADIX 10// 桶
queue<int> _q[RADIX];// 获取val值对应桶的位置,即哈希映射
int GetPos(int a, int k)
{int pos = a % RADIX;while (k--){a /= RADIX;pos = a % RADIX;}return pos;
} // 分发数据,将数组数据按模分发到哈希桶上
void Distribute(int* arr, int left, int right, int k)
{for (int i = left; i < right; ++i){int pos = GetPos(arr[i], k);_q[pos].push(arr[i]);}
} // 收集数据,根据队列的特性从哈希桶上收集数据,存入数组
void Collect(int* arr)
{int pos = 0;for (int i = 0; i < RADIX; ++i){while (!_q[i].empty()){arr[pos++] = _q[i].front();_q[i].pop();}}
} void RadixSort(int* arr, int n)
{int k = 0;while (k < K){Distribute(arr, 0, n, k++);Collect(arr);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/273802.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vscode中解决驱动编写的时候static int __init chrdev_init()报错的问题

目录 错误出错原因解决方法 错误 在入口函数上&#xff0c;出现 expected a ; 这样的提示 出错原因 缺少了 __KERNEL __ 宏定义 解决方法 补上__KERNEL__宏定义 具体做法&#xff1a;在vscode中按下ctrlshiftp &#xff0c;输入&#xff1a;C/C:Edit Configurations&#xff0…

首发:鸿蒙面试真题分享【独此一份】

最早在23年华为秋季发布会中&#xff0c;就已经宣布了“纯血鸿蒙”。而目前鸿蒙处于星河版中&#xff0c;加速了各大互联网厂商的合作。目前已经有200参与鸿蒙的原生应用开发当中。对此各大招聘网站上的鸿蒙开发需求&#xff0c;每日都在增长中。 2024大厂面试真题 目前的鸿蒙…

并发通信(网络进程线程)

如果为每个客户端创建一个进程&#xff08;或线程&#xff09;&#xff0c;因为linux系统文件标识符最多1024位&#xff0c;是有限的。 所以使用IO复用技术&#xff0c;提高并发程度。 阻塞与非阻塞 阻塞式复用 非阻塞复用 信号驱动IO 在属主进程&#xff08;线程中声明&…

java网络编程 01 IP,端口,域名,TCP/UDP, InetAddress

01.IP 要想让网络中的计算机能够互相通信&#xff0c;必须为计算机指定一个标识号&#xff0c;通过这个标识号来指定要接受数据的计算机和识别发送的计算机&#xff0c;而IP地址就是这个标识号&#xff0c;也就是设备的标识。 ip地址组成&#xff1a; ip地址分类&#xff1a;…

王道机试C++第 3 章 排序与查找:排序问题 Day28(含二分查找)

查找 查找是另一类必须掌握的基础算法&#xff0c;它不仅会在机试中直接考查&#xff0c;而且是其他某些算法的基础。之所以将查找和排序放在一起讲&#xff0c;是因为二者有较强的联系。排序的重要意义之一便是帮助人们更加方便地进行查找。如果不对数据进行排序&#xff0c;…

git 命令怎么回退到某个特定的 commit 并将其推送到远程仓库?

问题 不小心把提交的名称写错提交上远程仓库了&#xff0c;这里应该是 【029】的&#xff0c;这个时候我们想回到【028】这一个提交记录&#xff0c;然后再重新提交【029】到远程仓库&#xff0c;该怎么处理。 解决 1、首先我们找到【028】这条记录的提交 hash&#xff0c;右…

js【详解】DOM

文档对象模型&#xff08;Document Object Model&#xff0c;简称DOM&#xff09; DOM 是哪种数据结构 &#xff1f; DOM 的本质是浏览器通过HTML代码解析出来的一棵 树。 操作 DOM 常用的 API 有哪些 &#xff1f; 获取 DOM 节点 //方式 1&#xff1a;通过【id】获取&#xf…

掼蛋的牌型与规律(下篇)

一、三不带 一般出三不带有几种情况&#xff1a;没有对子配、对子和三张数量不匹配、对子成了三连对、对子太大。作为发牌方&#xff0c;首发三不带可以迷惑对手。三不带打出来很难处理&#xff0c;如果接了三不带可能就会将小对子留下&#xff0c;不接又不甘心让对方继续有出牌…

ceph跨集群迁移ceph pool rgw

1、跨集群迁移ceph pool rgw 我这里是迁移rgw的pool l老环境 [rootceph-1 data]# yum install s3cmd -y [rootceph-1 ~]# ceph config dump WHO MASK LEVEL OPTION VALUE RO mon advanced au…

CKB转型为BTC Layer2后月涨超 300%,还有哪些转型热门赛道的老项目?

虽然说牛市下&#xff0c;炒新不炒旧。但一些渡过漫长熊市的老牌项目方&#xff0c;重新回到牌桌前开始新叙事后&#xff0c;市场依然有人买单。 部分项目方已经初步尝到了甜头&#xff0c;Arweave&#xff08;AR&#xff09;宣布从去中心化数据存储转换到「以太坊杀手」后&am…

HTML静态网页成品作业(HTML+CSS)——花主题介绍网页设计制作(1个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有1个页面。 二、作品演示 三、代…

深入理解React中的useState:函数组件状态管理的利器

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

洛谷 素数环 Prime Ring Problem

题目描述 PDF 输入格式 输出格式 题意翻译 输入正整数 nn&#xff0c;把整数 1,2,\dots ,n1,2,…,n 组成一个环&#xff0c;使得相邻两个整数之和均为素数。输出时&#xff0c;从整数 11 开始逆时针排列。同一个环恰好输出一次。n\leq 16n≤16&#xff0c;保证一定有解。 多…

day04-Maven-SpringBootWeb入门

文章目录 01. Maven1.1 课程安排1.2 什么是Maven1.3 Maven的作用1.4 Maven模型1.5 Maven仓库1.6 Maven安装1.6.1 下载1.6.2 安装步骤 2 IDEA集成Maven2.1 配置Maven环境2.1.1 当前工程设置2.1.2 全局设置 2.2 创建Maven项目2.3 POM配置详解2.4 Maven坐标详解2.5 导入Maven项目 …

【鸿蒙 HarmonyOS 4.0】弹性布局(Flex)

一、介绍 弹性布局&#xff08;Flex&#xff09;提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。容器默认存在主轴与交叉轴&#xff0c;子元素默认沿主轴排列&#xff0c;子元素在主轴方向的尺寸称为主轴尺寸&#xff0c;在交叉轴方向的尺寸称为交叉轴尺寸…

设计模式:软件开发的秘密武器

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

20240310-1-Java后端开发知识体系

Java 基础 知识体系 Questions 1. HashMap 1.8与1.7的区别 1.71.8底层结构数组链表数组链表/红黑树插入方式头插法尾插法计算hash值4次位运算5次异或运算1次位运算1次异或运算扩容、插入先扩容再插入先插入再扩容扩容后位置计算重新hash原位置或原位置旧容量 (1) 扩容因子…

【吊打面试官系列】Java虚拟机JVM篇 - 关于JVM启动参数

大家好&#xff0c;我是锋哥。今天分享关于JVM启动参数的JVM面试题&#xff0c;希望对大家有帮助&#xff1b; 常用的JVM启动参数有哪些? JVM可配置参数已经达到1000多个&#xff0c;其中GC和内存配置相关的JVM参数就有600多个。 但在绝大部分业务场景下&#xff0c;常用的JV…

【C++】STL(二) string容器

一、string基本概念 1、本质 string是C风格的字符串&#xff0c;而string本质上是一个类 string和char * 区别&#xff1a; char * 是一个指针 string是一个类&#xff0c;类内部封装了char*&#xff0c;管理这个字符串&#xff0c;是一个char*型的容器。 2、特点 1、stri…

Dockerfile的使用,怎样制作镜像

Docker 提供了一种更便捷的方式&#xff0c;叫作 Dockerfile docker build命令用于根据给定的Dockerfile构建Docker镜像。 docker build命令参数&#xff1a; --build-arg&#xff0c;设置构建时的变量 --no-cache&#xff0c;默认false。设置该选项&#xff0c;将不使用Build …