选择排序、插入排序、冒泡排序、希尔排序算法的总结 - 复杂度、实现和稳定性

原文地址:https://www.jianshu.com/p/916b15eae350

常见排序算法的总结 - 复杂度、实现和稳定性

2018.08.29 16:20*

最基础的算法问题,温故知新。排序算法的几个主要指标是,时间复杂度(最好,最差和平均),空间复杂度(额外空间)和稳定性。本文主要描述八种常见算法:简单选择排序、冒泡排序、简单插入排序、希尔排序、归并排序、快速排序、堆排序和基数排序,关于它们的指标统计可以直接看最后。本文均为基本C++实现,不使用STL库。

值得一提的是排序算法的稳定性,之前关注较少。稳定性的意思是对于序列中键值(Key value)相同的元素,它们在排序前后的相对关系保持不变。对于int这样的基本数据类型,稳定性基本上是没有意义的,因为它的键值就是元素本身,两个元素的键值相同他们就可以被认为是相同的。但对于复杂的数据类型,数据的键值相同,数据不一定相同,比如一个Student类,包括Name和Score两个属性,以Score为键值排序,这时候键值相同元素间的相对关系就有意义了。

简单选择排序

应该是最自然的思路。选择排序的思想是,从全部序列中选取最小的,与第0个元素交换,然后从第1个元素往后找出最小的,与第一个元素交换,再从第2个元素往后选取最小的,与第2个元素交换,直到选取最后一个元素。

void selectionSort(int a[], int n) {for (int i = 0; i < n - 1; ++i) {int minIdx = i;for (int j = i + 1; j < n; ++j) {if (a[j] < a[minIdx]) {minIdx = j;}}int tmp = a[i];a[i] = a[minIdx];a[minIdx] = tmp;}
}

无论如何都要完整地执行内外两重循环,故最好、最差和平均时间复杂度都是O(n2),不需要额外空间。选择排序是不稳定的。

冒泡排序

冒泡排序的思想是,从第0个元素到第n-1个元素遍历,若前面一个元素大于后面一个元素,则交换两个元素,这样可将整个序列中最大的元素冒泡到最后,然后再从第0个到第n-2遍历,如此往复,直到只剩一个元素。

void bubbleSort(int a[], int n) {for (int i = 0; i < n - 1; ++i) {for (int j = 0; j < n - i - 1; ++j) {if (a[j] > a[j + 1]) {int tmp = a[j];a[j] = a[j + 1];a[j + 1] = tmp;}}}
}

冒泡排序与简单选择排序类似,无论如何都要执行完两重循环,故最好、最坏和平均时间复杂度均为O(n2),不需要额外空间。冒泡排序是稳定的。
冒泡排序的一个改进是,在内层循环之前设置一个标记变量,用于标记循环是否进行了交换,在内层循环结束时,若判断没有进行交换,则说明剩下的序列中,每个元素都小于等于后面一个元素,即已经有序,可终止循环。这样,冒泡排序的最好时间复杂度可以提升到O(n)。

简单插入排序(Insertion Sort)

思路是类似扑克牌的排序,每次从未排序序列的第一个元素,插入到已排序序列中的合适位置。假设初始的有序序列为第0个元素(本文描述的序号都从0开始),只有一个元素的序列肯定是有序的,然后从原先序列的第1个元素开始到第n-1个元素遍历,每次将当前元素插入到它之前序列中的合适位置。

void insertionSortBSearch(int a[], n) {for (int i = 1; i < n; ++i) { int j, val = a[i]; for (j = i - 1; j >= 0 && a[j] > val; --j) {a[j + 1] = a[j];}a[j + 1] = val;}
}

两重循环,最差和平均时间复杂度为O(n2),最好情况是原序列已有序,则忽略内层循环,时间复杂度O(n)。插入排序是稳定的。
这里,内层循环我们用的是从后向前遍历,来找到合适的插入位置,而内层循环所遍历的,是已排序的数组,所以我们可以使用二分查找来寻找插入位置,从而使时间复杂度提高到O(n*log n)。代码如下。

// 二分查找改进的插入排序
void insertionSortBSearch(int a[], n) {for (int i = 1; i < n; ++i) { int j, val = a[i]; int begin = 0, end = i - 1;while (begin < end) {int mid = begin + (end - begin) / 2;if (a[mid] > val) {end = mid - 1;}else {begin = mid;}}for (j = i - 1; j >= begin; --j) {a[j + 1] = a[j];}a[begin] = val;}
}

希尔排序

希尔排序可以被认为是简单插入排序的一种改进。插入排序一个比较耗时的地方在于需要将元素反复后移,因为它是以1为增量进行比较的元素的后移可能会进行多次。一个长度为n的序列,以1为增量就是一个序列,以2为增量就形成两个序列,以i为增量就形成i个序列。希尔排序的思想是,先以一个较大的增量,将序列分成几个子序列,将这几个子序列分别排序后,合并,在缩小增量进行同样的操作,知道增量为1时,序列已经基本有序,这是进行简单插入排序的效率就会较高。希尔排序的维基词条上有一个比较好的解释例子如下:

// 原始序列
13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10
// 以5为增量划分,5列,每列即为一个子序列
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
// 对每一个子序列进行插入排序得到以下结果
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
// 恢复一行显示为
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45
// 再以3为增量划分,3列,每列即为一个子序列
10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
// 对每一个子序列进行插入排序得到如下结果
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
// 恢复一行为
10 14 13 25 23 33 27 25 59 39 65 73 45 94 82 94
// 然后再以1为增量进行插入排序,即简单插入排序
// 此时序列已经基本有序,分布均匀,需要反复后移的情况较少,效率较高

上面的例子中,我们依次选取了5、3和1为增量,实际中增量的选取并没有统一的规则,唯一的要求就是最后一次迭代的增量需为1。最初的增量选取规则为,从n/2折半递减直到1。还有一些关于希尔排序增量选取的研究,针对不同数据有不同的表现,在此不做展开。下面是增量从n/2折半递减到1的代码示例。

void shellSort(int a[], int n) {for (int step = n / 2; step > 0; step /= 2) {for (int i = step; i < n; ++i) {int j, val = a[i];for (j = n - step; j >= 0 && a[j] > val; j -= step) {a[j + step] = a[j];}a[j + 1] = val;}}
}

希尔排序在简单插入排序的基础上做了些改进,它的最好及最差时间复杂度和简单插入排序一样,分别是是O(n)和O(n2),平均时间复杂度试增量选取规则而定,一般认为介于O(n)和O(n2)之间。它不需要额外空间。它是不稳定的。

归并排序

归并排序的思想是,利用二分的特性,将序列分成两个子序列进行排序,将排序后的两个子序列归并(合并),当序列的长度为2时,它的两个子序列长度为1,即视为有序,可直接合并,即达到归并排序的最小子状态。基于递归的实现如下:

void mergeSortRecursive(int a[], int b[], int start, int end) {if (start >= end) {return;}int mid = start + (end - start) / 2,start1 = start, end1 = mid,start2 = mid + 1, end2 = end;mergeSortRecursive(a, b, start1, end1);mergeSortRecursive(a, b, start2, end2);int i = 0;while (start1 <= end1 && start2 <= end2) {b[i++] = a[start1] < a[start2] ? a[start1++] : a[start2++];}while (start1 <= end1) {b[i++] = a[start1++];}while (start2 <= end2) {b[i++] = a[start2++];}for (i = start; i < end; ++i) {a[i] = b[i];}
}

void mergeSort(int a[], int n) {
int *b = new int[n];
mergeSortRecursive(a, b, 0, n - 1);
delete[] b;
}

归并排序的最好,最坏和平均时间复杂度都是O(n*logn)。但需要O(n)的辅助空间。归并排序是稳定的。

快速排序

快速排序可能是最常被提到的排序算法了,快排的思想是,选取第一个数为基准,通过一次遍历将小于它的元素放到它的左侧,将大于它的元素放到它的右侧,然后对它的左右两个子序列分别递归地执行同样的操作。

void quickSortRecursive(int a[], int start, int end) {if (start >= end)return;int mid = a[start];int left = start + 1, right = end;while (left < right) {while (a[left] <= mid && left < right)++left;while (a[right] > mid && left < right)--right;swap(a[left], a[right]);}if (a[left] <= a[start])swap(a[left], a[start]);else--left;if (left)quickSortRecursive(a, start, left - 1);quickSortRecursive(a, left + 1, end);
}

void quickSort(int a[], int n) {
quickSortRecursive(a, 0, n - 1);
}

快速排序利用分而治之的思想,它的最好和平均实际复杂度为O(nlogn),但是,如果选取基准的规则正好与实际数值分布相反,例如我们选取第一个数为基准,而原始序列是倒序的,那么每一轮循环,快排都只能把基准放到最右侧,故快排的最差时间复杂度为O(n2)。快排算法本身没有用到额外的空间,可以说需要的空间为O(1);对于递归实现,也可以说需要的空间是O(n),因为在递归调用时有栈的开销,当然最坏情况是O(n),平均情况是O(logn)。快速排序是不稳定的。

堆排序

堆排序利用的是二叉树的思想,所谓堆就是一个完全二叉树,完全二叉树的意思就是,除了叶子节点,其它所有节点都有两个子节点,这样子的话,完全二叉树就可以用一个一块连续的内存空间(数组)来存储,而不需要指针操作了。堆排序分两个流程,首先是构建大顶堆,然后是从大顶堆中获取按逆序提取元素。
首先是大顶堆,大顶堆即一个完全二叉树,的每一个节点都大于它的所有子节点。大顶堆可以按照从上到下从左到右的顺序,用数组来存储,第i个节点的父节点序号为(i-1)/2,左子节点序号为2i+1,右子节点序号为2(i+1)。构建大顶堆的过程即从后向前遍历所有非叶子节点,若它小于左右子节点,则与左右子节点中最大的交换,然后递归地对原最大节点做同样的操作。下面是一个较好的示意图来自bubkoo:

构建大顶堆示意图

构建完大顶堆后,我们需要按逆序提取元素,从而获得一个递增的序列。首先将根节点和最后一个节点交换,这样最大的元素就放到最后了,然后我们更新大顶堆,再次将新的大顶堆根节点和倒数第二个节点交换,如此循环直到只剩一个节点,此时整个序列有序。下面是一个较好的示意图来自 bubkoo:
从大顶堆逆序提取元素使其有序示意图

void updateHeap(int a[], int i, int n) {int iMax = i,iLeft = 2 * i + 1,iRight = 2 * (i + 1);if (iLeft < n && a[iMax] < a[iLeft]) {iMax = iLeft;}if (iRight < n && a[iMax] < a[iRight]) {iMax = iRight;}if (iMax != i) {int tmp = a[iMax];a[iMax] = a[i];a[i] = tmp;updateHeap(a, iMax, n);}
}

void heapSort(int a[], int n) {
for (int i = (n - 1) / 2; i >= 0; i–) {
updateHeap(a, i, n);
}
for (int i = n - 1; i > 0; --i) {
int tmp = a[i];
a[i] = a[0];
a[0] = tmp;
updateHeap(a, i, n);
}
}

堆排序的整个过程中充分利用的二分思想,它的最好、最坏和平均时间复杂度都是O(nlogn)。堆排序不需要额外的空间。堆排序的交换过程不连续,显然是不稳定的。

基数排序

基数排序是一种典型的空间换时间的排序方法。以正整数为例,将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位(个位)排序一直到最高位排序完成以后,数列就变成一个有序序列。
对正整数我们常以10为基数,每一位可以为0到9,对于其它数据类型如字符串,我们可以进一步拓展基数,基数越大越占空间,但时间更快,如果有一段足够长的内存空间,也就是说基数为无穷大,那就足够表示所有出现的数值,我们就可以通过一次遍历就实现排序,当然实现上这是不可能的(对已知输入范围的数据是可能的,而且非常有用的,可以用这种思想来模拟一个简单的hash函数)。

int maxBit(int a[], int n)
{int maxData = a[0];     for (int i = 1; i < n; ++i){if (maxData < a[i]) {maxData = a[i];}   }int d = 1;int p = 10;while (maxData >= p){maxData /= 10;++d;}return d;
}
void radixsort(int a[], int n)
{int d = maxBit(a, n);int *tmp = new int[n];int *count = new int[10];int i, j, k;int radix = 1;for (i = 1; i <= d; i++, radix *= 10){for (j = 0; j < 10; j++) {count[j] = 0;}   for (j = 0; j < n; j++){k = (a[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 = (a[j] / radix) % 10;tmp[count[k] - 1] = a[j];count[k]--;}for (j = 0; j < n; j++) {a[j] = tmp[j];}}delete[]tmp;delete[]count;
}

基数排序的最好,最好、最坏和平均时间复杂度都是O(n*k),其中n是数据大小,k是所选基数。它需要O(n+k)的额外空间。它是稳定的。

八种排序算法总结

上面介绍了最常提到的八种排序算法,最基础的是选择和插入,基于选择和插入分别改进出了冒泡和希尔。基于二分思想又提出了归并、快排和堆排序。最后基于数据的分布特征,提出了基数排序。这些排序算法的主要指标总结如下。

算法最好时间最坏时间平均时间额外空间稳定性
选择n2 n2 n2 1不稳定
冒泡nn2 n2 1稳定
插入nn2 n2 1稳定
希尔nn2 n1.3(不确定) 1不稳定
归并nlog2nnlog2nnlog2nn稳定
快排nlog2nn2 nlog2nlog2n至n不稳定
nlog2nnlog2nnlog2n1不稳定
基数n*kn*kn*kn+k稳定

参考

排序算法时间复杂度:https://www.geeksforgeeks.org/time-complexities-of-all-sorting-algorithms/
排序算法稳定性:https://www.geeksforgeeks.org/stability-in-sorting-algorithms/

      </div></div>
</div>

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

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

相关文章

当知识图谱遇上推荐系统之PippleNet模型(论文笔记二)

RippleNet | Propagating User Preferences on the Knowledge 类别&#xff1a;联合学习 将知识图谱特征学习和推荐算法的目标函数结合&#xff0c;使用端到端&#xff08;end-to-end&#xff09;的方法进行联合学习。 [论文下载链接]https://arxiv.org/abs/1803.03467 1、…

POJ 3690 找星座(2D匹配)(未解答)

文章目录1. 题目信息1.1 题目链接1.2 题目大意1.3 解题思路2. 代码2.1 Time Limit Exceeded 代码2.2 Time Limit Exceeded 代码2.3 Time Limit Exceeded 代码1. 题目信息 1.1 题目链接 http://poj.org/problem?id3690 1.2 题目大意 给定大的矩阵&#xff08;天空的样子&am…

综述 | 事件抽取及推理 (上)

本文转载自公众号&#xff1a;知识工场。 事件概要事件是一种重要的知识&#xff0c;近年来&#xff0c;越来越多的工作关注于从开放域或领域文本中抽取结构化事件知识。同时&#xff0c;除了本身就很困难的…

下载 | 李宏毅:1 天搞懂深度学习,我总结了 300 页 PPT

《1 天搞懂深度学习》&#xff0c;300 多页的 ppt&#xff0c;台湾李宏毅教授写的&#xff0c;非常棒。不夸张地说&#xff0c;是我看过最系统&#xff0c;也最通俗易懂的&#xff0c;关于深度学习的文章。这份 300 页的 PPT&#xff0c;被搬运到了 SlideShare 上&#xff0c;下…

史上最全Redis面试49题(含答案):哨兵+复制+事务+集群+持久化等

最全面试题答案系列 史上最强多线程面试44题和答案&#xff1a;线程锁线程池线程同步等 最全MySQL面试60题和答案 史上最全memcached面试26题和答案 史上最全Spring面试71题与答案 今天主要分享redis最全答案系列 Redis主要有哪些功能&#xff1f; 1.哨兵&#xff08;Sen…

SinglepassTextCluster项目:基于single-pass算法思想的自动文本聚类组件

项目的背景 SinglepassTextCluster, an TextCluster tool based on Singlepass cluster algorithm that use tfidf vector and doc2vec&#xff0c;which can be used for individual real-time corpus cluster task。基于single-pass算法思想的自动文本聚类小组件&#xff0c…

DTW动态时间规整算法

原文地址&#xff1a;https://blog.csdn.net/qcyfred/article/details/53824507 https://zhuanlan.zhihu.com/p/43247215 动态时间规整&#xff08;DTW&#xff09;算法简介相忘天涯&#xff0c;深藏于心19 人赞同了该文章DTW最初用于识别语音的相似性。我们用数字表示音调高低…

POJ 3461 字符串匹配(KMP / 哈希(有推导))

文章目录1. 题目1.1 题目链接1.2 题目大意2. Accepted代码2.1 KMP解法2.2 哈希法&#xff08;有推导过程&#xff09;1. 题目 1.1 题目链接 http://poj.org/problem?id3461 类似题目&#xff1a;LeetCode 30. 串联所有单词的子串&#xff08;字符串哈希&#xff09; 1.2 题…

莫比乌斯:百度凤巢下一代广告召回系统

星标/置顶小屋&#xff0c;带你解锁最萌最前沿的NLP、搜索与推荐技术文 | 江城编 | 夕小瑶今天聊聊百度在最顶级的数据挖掘会议KDD2019的计算广告track上提出的query-ad匹配模型——莫比乌斯&#xff08;MOBIUS&#xff09;。这也是百度凤巢下一代广告召回系统的内部代号&#…

当知识图谱遇上推荐系统之MKR模型(论文笔记三)

Multi-Task Feature Learning for Knowledge Graph Enhanced Recommendation 类别&#xff1a;交替学习 将知识图谱特征学习和推荐算法视为两个分离但又相关的任务&#xff0c;使用多任务学习的框架进行交替学习。 1、背景 MKR是一个通用的、端对端的深度推荐框架&#xf…

关于话题演化关系网络生成的路线思考:从话题聚类到话题网络展示

话题演化关系网络生成&#xff0c;是实现事件演化追踪的一个重要方法。通过对文本话题进行聚类、内容处理、话题演化关联、话题演化网络的展示&#xff0c;能够在一定程度上为用户揭示出一个事件发展的情况。本文就笔者对该方向的实现路线思考进行总结&#xff0c;分享给大家。…

综述 | 事件抽取及推理 (下)

本文转载在公众号&#xff1a;知识工场 。 上篇事件抽取及推理的推文已经介绍了事件抽取的基本方法&#xff0c;本篇主要介绍事件推理的相关工作。就目前来看&#xff0c;事件方向相关的研究还是以事件抽取为主流任务&#xff0c;当前大多都是在模型的框架和优化方面进行研究。…

Redis系列教程(三):如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题

Java相关的面试都会问到缓存的问题&#xff1a;史上最全Redis面试49题&#xff08;含答案&#xff09;:哨兵复制事务集群持久化等&#xff0c;除此之外还会问到缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等不常见的问题&#xff0c;但却是非常重要的问题&#xff0c;今…

随机森林:提供银行精准营销解决方案

原文地址&#xff1a;https://blog.csdn.net/weixin_34233679/article/details/88480912 本例是根据科赛网练习赛进行练手&#xff0c;学习巩固一下随机森林建模以及应用。 赛题描述本练习赛的数据&#xff0c;选自UCI机器学习库中的「银行营销数据集(Bank Marketing Data Set)…

谁说2021届秋招算法岗一定要灰飞烟灭啦?

没错&#xff0c;这是一碗鸡汤&#xff0c;希望肝完这碗鸡汤的师弟师妹们就不要过度焦虑啦&#xff5e;理性上车&#xff0c;理性下车&#xff0c;希望萌新们都能遇到最适合自己的坑位2014年末入坑AI&#xff0c;一路见证了AI行业的快速起飞、爆炸、焦虑和冷却。小夕前几天在知…

论文浅尝 | 基于深度强化学习的远程监督数据集的降噪

论文链接&#xff1a;https://arxiv.org/pdf/1805.09927.pdf来源&#xff1a;ACL2018Motivation&#xff1a;远程监督是以一种生成关系抽取训练样本的方法&#xff0c;无需人工标注数据。但是远程监督引入了噪音&#xff0c;即存在很多的假正例。本文的出发点非常简单&#xff…

字符串匹配算法(AC自动机 Aho-Corasick)

文章目录1. 多模式串匹配2. 经典多模式串匹配--AC自动机2.1 AC自动机构建2.2 在AC自动机上匹配主串2.3 复杂度分析3. python包1. 多模式串匹配 前面学的BF、RK、BM、KMP都是单模式串匹配算法&#xff08;一个模式串&#xff0c;一个主串&#xff09;多模式串匹配&#xff0c;即…

机器学习资源和记录

学习记录&#xff1a; 2019.08.01&#xff1a; 林轩田机器学习技法--Matrix Factorization &#xff1a;https://redstonewill.com/783/ [ 收获 ]&#xff1a;先假设 有用户特征向量&#xff08;维度为d表示用户对d种特性的不同喜爱程度&#xff09;、有电影特征&#xff08…

Redis系列教程(五):Redis哨兵、复制、集群的设计原理,以及区别

前一篇文章高并发架构系列&#xff1a;Redis为什么是单线程、及高并发快的3大原因详解谈了Redis高并发快的3个原因&#xff0c;本篇主要谈Redis的高可用&#xff0c;两篇合起来就可以把redis的高并发和高可用搞清楚了。 谈到Redis服务器的高可用&#xff0c;如何保证备份的机器…

老刘说NLP:焦虑被大肆贩卖下的自然语言处理学习思考

一、话题的由来 近期&#xff0c;有不少朋友来信&#xff0c;来询问如何看待自然语言处理这个方向&#xff0c;表示对当前的状态很不满&#xff0c;希望能够尽快地找到一个合适的方向、快速的学习方式&#xff0c;提升自己的自然语言处理能力&#xff0c;从而找到一个算法岗位的…