- 分享
# 15种排序算法简介(已完结)
- 2024-8-26 10:42:05 @
各种排序算法简介:
1.选择排序
选择排序是一种简单直观的排序算法,无论什么 数据进去都是 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
算法步骤 1.首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
2.再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
代码实现:
void selection_sort(int arr[]) {
for (int i = 0; i < arr.size() - 1; i++) {
int min = i;
for (int j = i + 1; j < arr.size(); j++){
if (arr[j] < arr[min]){
min = j;
}
swap(arr[i], arr[min]);
}
}
}
2.冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来
说并没有什么太大作用。 算法步骤:
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
代码实现:
void bubble_sort(int arr[], int len) {
int i, j;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1])
swap(arr[j], arr[j + 1]);
}
}
}
}
3.插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
算法步骤 1.将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2.从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
代码实现:
void insertion_sort(int arr[],int len){
for(int i=1;i<len;i++){
int key=arr[i];
int j=i-1;
while((j>=0) && (key<arr[j])){
arr[j+1]=arr[j];
j--;
}
arr[j+1]=key;
}
}
4.归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法): 自下而上的迭代
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
算法步骤 1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
2.设定两个指针,最初位置分别为两个已经排序序列的起始位置;
3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
重复步骤 3 直到某一指针达到序列尾;
将另一序列剩下的所有元素直接复制到合并序列尾。
代码实现:
迭代版:
void merge_sort(int arr[], int len) {
int *a = arr;
int *b = new int[len];
for (int seg = 1; seg < len; seg += seg) {
for (int start = 0; start < len; start += seg + seg) {
int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1)
b[k++] = a[start1++];
while (start2 < end2)
b[k++] = a[start2++];
}
int *temp = a;
a = b;
b = temp;
}
if (a != arr) {
for (int i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
delete[] b;
}
5.快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
算法步骤
1.从数列中挑出一个元素,称为 "基准"(pivot)
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
这里就不放代码实现了
懂得都懂
6.计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
- 计数排序的特征 当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。
算法的步骤如下:
1.找出待排序的数组中最大和最小的元素 2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项 3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加) 4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
代码实现:
void countingSort(int arr[], int size) {
const int maxVal = 100;
const int minVal = 0;
int count[maxVal + 1] = {0};
for (int i = 0; i < size; ++i) {
count[arr[i]]++;
}
int index = 0;
for (int i = minVal; i <= maxVal; ++i) {
while (count[i] > 0) {
arr[index++] = i;
count[i]--;
}
}
}
7.希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
算法步骤
1.选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
2.按增量序列个数 k,对序列进行 k 趟排序;
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
代码实现:
void shell_sort(int array[], int length) {
int h = 1;
while (h < length / 3) {
h = 3 * h + 1;
}
while (h >= 1) {
for (int i = h; i < length; i++) {
for (int j = i; j >= h && array[j] < array[j - h]; j -= h) {
swap(array[j], array[j - h]);
}
}
h = h / 3;
}
}
8.猴子排序
猴子排序作者的灵感来源于一个叫无限猴子定理的悖论
猴子排序是一种基于随机性的简单排序算法。它的基本思想是通过不断地随机排列元素,直到这些元素恰好按照顺序排列。具体步骤如下:
1.随机排列数组中的元素。
2.检查数组是否已经排序。
如果数组尚未排序,则重复步骤1和2,直到数组排序为止。 猴子排序的时间复杂度非常高,在最坏的情况下是O(n×n!),在最好的情况下是O(n),平均情况下可能是无限时间,因此它不适用于实际应用,主要用作教育目的和理论探讨
9.堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列; 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列; 堆排序的平均时间复杂度为 Ο(nlogn)。
算法步骤
1.创建一个堆
2.把堆首(最大值)和堆尾互换;
把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
重复步骤 2,直到堆的尺寸为 1
代码实现:
void max_heapify(int arr[], int start, int end) {
int dad = start;
int son = dad * 2 + 1;
while (son <= end) {
if (son + 1 <= end && arr[son] < arr[son + 1])
son++;
if (arr[dad] > arr[son])
return;
else {
swap(arr[dad], arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(int arr[], int len) {
for (int i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
for (int i = len - 1; i > 0; i--) {
swap(arr[0], arr[i]);
max_heapify(arr, 0, i - 1);
}
}
10.桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
1.在额外空间充足的情况下,尽量增大桶的数量 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
2.同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
代码实现:
void BucketSort(vector<int> &a){
int n = a.size();
vector<vector<int>> bucket(N);
for(int i = 0; i < n; i++){
bucket[a[i] / 10].push_back(a[i]);
}
int k = 0;
for(int i = 0; i < N; i++){
sort(bucket[i].begin(), bucket[i].end());
int size = bucket[i].size();
for(int j = 0; j < size; j++){
a[k++] = bucket[i].at(j);
}
}
}
11.基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数
代码实现:
int maxbit(int data[], int n){
int maxData = data[0];
for (int i = 1; i < n; ++i){
if (maxData < data[i])
maxData = data[i];
}
int d = 1;
int p = 10;
while (maxData >= p){
maxData /= 10;
++d;
}
return d;
}
void radixsort(int data[], int n){
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10];
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++){
for(j = 0; j < 10; j++){
count[j] = 0;
}
for(j = 0; j < n; j++){
k = (data[j] / radix) % 10;
count[k]++;
}
for(j = 1; j < 10; j++){
count[j] = count[j - 1] + count[j];
}
for(j = n - 1; j >= 0; j--){
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++){
data[j] = tmp[j];
}
radix = radix * 10;
}
delete []tmp;
delete []count;
}
12.双调排序
所谓双调序列(Bitonic Sequence)是指由一个非严格增序列X和非严格减序列Y构成的序列,比如序列(23,10,8,3,5,7,11,78)。 定义:一个序列a1,a2,…,an是双调序列(Bitonic Sequence),如果:
1.存在一个ak(1≤k≤n), 使得a1≥…≥ak≤…≤an成立;或者
2.序列能够循环移位满足条件(1)
双调归并网络是基于Batcher定理而构建的。Batcher定理是说将任意一个长为2n的双调序列A分为等长的两半X和Y,将X中的元素与Y中的元素一一按原序比较,即a[i]
与a[i+n](i<n)
比较,将较大者放入MAX序列,较小者放入MIN序列。则得到的MAX和MIN序列仍然是双调序列,并且MAX序列中的任意一个元素不小于MIN序列中的任意一个元素。
根据这个原理,我们可以将一个输入的n元素双调序列首先通过洗牌比较操作得到一个MAX序列和一个MIN序列,然后通过两个n/2阶双调归并器处理就可以得到一个有序序列
void bitonicSort(int arr[], int len) {
int k;
for (k = 1; k < n; k = 2 * k) {
for (int i = 0; i < len - k; i++) {
int j = i ^ k;
if (i < j && (i & k) == 0) {
if (arr[i] > arr[j]) {
swap(arr[i], arr[j]);
}
}
}
}
for (int i = 1; i < len; i++) {
if (arr[i - 1] > arr[i]) {
swap(arr[i - 1], arr[i]);
}
}
}
13.梳排序
梳排序(Comb Sort)是一种由Wlodzimierz Dobosiewicz于1980年所发明的不稳定排序算法,并由Stephen Lacey和Richard Box于1991年四月号的Byte杂志中推广。梳排序是改良自冒泡排序和快速排序,其要旨在于消除乌龟,亦即在阵列尾部的小数值,这些数值是造成冒泡排序缓慢的主因。相对地,兔子,亦即在阵列前端的大数值,不影响冒泡排序的效能。
原理 梳排序的基本思想是:最开始的时候先定义一个步长,该步长为数组的长度除以1.3(注意:直接梳排序的原理:通过比较元素彼此之间的步长位置这种方式对数据进行预处理。)。在正式排序之前,把较大的一些数据移到数组的底部。在每次移动中,步长越来越小,直到步长等于1为止。然后使用冒泡排序进行最后的整理。
void Comb_sort(int arr[], int n) {
int gap = n;
while (gap != 1) {
gap = max(1, static_cast<int>(gap / 1.3));
for (int i = 0; i < n - gap; i++) {
if (arr[i] > arr[i + gap]) {
swap(arr[i], arr[i + gap]);
while (gap >= 1 && arr[i] > arr[i + gap]) {
swap(arr[i], arr[i + gap]);
gap = max(1, static_cast<int>(gap / 1.3));
}
}
}
}
}
14.鸡尾酒排序
对于无序数列,先找到最小的数字放到第一位,然后找到最大的数字放到最后一位。然后再找到第二小的数字放到第二位,再找到第二大的数字放到倒数第二位。以此类推,直到完成排序。
排序时是以双向在序列中进行排序,先对数组从左到右进行冒泡排序(升序),再对数组从右到左进行冒泡排序(降序),依次类推,不断缩小未排序元素的范围,直到最后一个元素结束。
比冒泡排序的效率稍微好一点,原因是冒泡排序只从一个方向进行比对(由低到高),每次循环只移动一个项目。而鸡尾酒排序是双向的元素比较和交换。
最糟或是平均所花费的次数都是 O(n²),但如果序列在一开始已经大部分排序过的话,会接近 O(n)。
代码实现:
void cooktail_sort(int arr[],int len){
for(int i=1;i<=len;i++){
if(i%2!=0){
for(int j=1;j<len;j++)
{
if(arr[j]>arr[j+1])
{
swap(arr[j],arr[j+1]);
}
}
}else if(i%2==0){
for(int j=len;j>1;j--)
{
if(arr[j]<arr[j-1])
{
swap(arr[j],arr[j-1]);
}
}
}
}
}
15.地精排序
地精排序只有一层循环,通过不断的比较和交换相邻的元素来完成排序。具体来说,它会查找最开始逆序的一对相邻数,并交换它们的位置。然后,它会基于交换后的新状态再次查找逆序的相邻数,并依此进行下去,直到整个序列有序。
地精排序与冒泡排序类似,都是通过不断的交换相邻元素来实现排序。但地精排序在发现逆序对时,不是将其一直“冒泡”到末尾,而是将其“按回去”直到排好序,然后再继续向前移动。因此,地精排序可以看作是冒泡排序的一种简化版。
代码实现:
void dickgrune(int arr[] , int len) {
for(int index=0;index<=len;){
if(arr[index]>arr[index+1]){
swap(arr[index],arr[index+1]);
index--;
}else{
index++;
}
}
}
注:地精排序的数组只能从1开始(否则会RE)
以上排序算法可视化
注:
1.建议把声音开打一点,尤其是快排
4 条评论
-
-
以下是对这些算法的简要概述:
-
选择排序:通过选择未排序部分的最小(或最大)元素,放到已排序部分的末尾。时间复杂度为O(n²)。
-
冒泡排序:重复遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就交换它们。时间复杂度为O(n²)。
-
插入排序:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。时间复杂度为O(n²),但在接近排序好的数据上表现较好。
-
归并排序:采用分治法,将数组分为两半,分别排序,然后合并。时间复杂度为O(nlogn)。
-
快速排序:通过选取一个基准值将数组分为两部分,然后递归地对这两部分进行排序。平均时间复杂度为O(nlogn),但在最坏情况下为O(n²)。
-
计数排序:将输入的数据值转化为键存储在额外开辟的数组空间中。时间复杂度为O(n+k),适用于数据范围不大的整数排序。
-
希尔排序:是插入排序的改进版本,通过比较间隔较大的元素来减少交换次数。时间复杂度取决于增量序列。
-
猴子排序:基于随机性的排序算法,时间复杂度不确定,适用于理论探讨。
-
堆排序:利用堆这种数据结构进行排序,时间复杂度为O(nlogn)。
-
桶排序:将数据分到有限数量的桶里,每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。时间复杂度为O(n+k)。
-
基数排序:非比较型整数排序算法,根据整数位数来分配到不同的桶中,按桶的顺序来排序。时间复杂度为O(nk)。
-
梳排序:通过比较元素彼此之间的步长位置对数据进行预处理,然后使用冒泡排序进行最后的整理。时间复杂度平均为O(n^2/2.71^p),其中p为操作次数。
-
-
2024-8-28 16:08:29@
猴子排序是理论上最快的排序算法
-
2024-8-28 10:00:23@
蛙趣,这年代还有人用指针啊
- 1