赞
踩
前言
数据结构知识点总结文档:排序算法。该文档总结了9种经典的、或有着广泛使用的排序算法。
目前最常见的排序算法有十种,这十种算法可进一步分为:
比较类排序: 通过比较元素大小来决定相互之间的次序,由于复杂度不能突破 n log ( n ) n\log(n) nlog(n),因此也被称为非线性时间排序。
非比较类排序: 不通过比较元素大小来决定次序,可突破比较类排序的时间下限,以线性时间运行,因此也被称作线性时间排序
比较类排序 | 非比较类排序 |
---|---|
冒泡排序 快速排序 插入排序 希尔排序 选择排序 归并排序 堆排序 | 计数排序 桶排序 基数排序 |
排序算法 | 平均复杂度 | 最坏复杂度 | 最优复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳定 |
希尔排序 | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 不稳定 |
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
堆排序 | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( 1 ) O(1) O(1) | 不稳定 |
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳定 |
快速排序 | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n 2 ) O(n^2) O(n2) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | 不稳定 |
归并排序 | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n log ( n ) ) O(n\log(n)) O(nlog(n)) | O ( n ) O(n) O(n) | 稳定 |
计数排序 | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | 稳定 |
桶排序 | O ( n + k ) O(n+k) O(n+k) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n + k ) O(n+k) O(n+k) | 稳定 |
基数排序 | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n + k ) O(n+k) O(n+k) | 稳定 |
插入排序是一种简单直观的排序算法,它的原理就像我们在打扑克时对手牌进行排序一样,算法从前往后的扫描整个数组,扫描指针前的数组是已排序好的, 指针后的数组是未排序的,指针每访问到一个未排序数组中的元素,就在已排序数组中扫描一次,找到合适的位置后,把元素插入其中。具体的步骤可分为:
由于插入排序首先要遍历一次整个数组,在访问每一个元素时,又要从后往前遍历一次已排序数组。因此插入排序的平均复杂度为 O ( n 2 ) O(n^2) O(n2)
适用场景: 适用于数据量不大,对算法的稳定性有要求,且数据局部或者整体有序的情况。
#include <vector> using namespace std; void InsertionSort(vector<double>& data) { for (int i = 1; i < data.size(); i++) { double key = data[i]; int j = i-1; while(j >= 0 && data[j] > key) { data[j+1] = data[j]; j -= 1; } data[j+1] = key; } return; }
插入排序的思路很简单,就是不管数据如何分布,依然要对每个元素都一步一步进行比较,移动,插入。当数据是呈完全倒序分布时,最小的元素要归位到数组的前端就会很费劲,而这也是导致插入排序复杂度高的原因之一。
希尔排序在1959年由Shell发明。希尔排序本质上是插入排序经过改进后的高效版本,也被称作最小增量排序,是第一个突破 O ( n 2 ) O(n^2) O(n2) 复杂度的算法。
希尔排序在插入排序的基础上,采用了跳跃式分组的策略。通过预先设定好的增量,把整个数组分割为若干组,再单独对每一个分组进行插入排序,随后逐步缩小增量,使每个分组所包含的元素越来越多,直到增量为1时,此时分组即为数组本身,即对整个数组进行正常的插入排序。
站在上帝视角来看,希尔排序通过分组排序,从宏观层面上使整个数组逐步趋向有序,小的元素基本在前,大的元素基本在后,到最后对整个数组进行插入排序时,对其中的大部份元素只需进行微调即可,不会涉及过多的数据移动。因此相对于插入排序有着更好的复杂度。
希尔具体步骤为:
适用场景: 如果使用已知最好的步长序列,希尔排序比直接插入排序要快,甚至在小数组中比快速排序和堆排序还快,但在涉及大量数据时希尔排序还是不如快排。
#include <vector> using namespace std; void InsertionSortWithIncrement(vector<double>& data, int k, int begin) { for (int i = begin+k; i < data.size(); i+=k) { double key = data[i]; int j = i-k; while(j >= 0 && data[j] > key) { data[j+k] = data[j]; j -= k; } data[j+k] = key; } return; } void ShellSort(vector<double>& data) { int k = data.size() / 2; while(k > 1) { for (int begin = 0; begin < k; begin++) { InsertionSortWithIncrement(data, k, begin); } k /= 2; } InsertionSortWithIncrement(data, 1, 0); return; }
归并排序是利用分治思想(Divide-and-Conquer)实现排序的算法。分治,本质上是把总问题通过递归分成若干个小问题,并对各个小问题求解,最后将求解后的小问题修补成总问题,从而得到总问题的解。
归并排序的具体步骤为:
适用场景: 适用于数据量较大,并且要求排序稳定的情况,但需要注意内存空间的开销。
#include <vector> using namespace std; void Merge(vector<double>& data, int left, int mid, int right) { vector<double> LSubsequence(data.begin()+left, data.begin()+mid+2); // Extract left subsequence to buffer vector<double> RSubsequence(data.begin()+mid+1, data.begin()+right+2); // Extract right subsequence to buffer LSubsequence[LSubsequence.size()-1] = INT_MAX; // In case the iterator out of boundary RSubsequence[RSubsequence.size()-1] = INT_MAX; // In case the iterator out of boundary int i = 0, j = 0; for (int k = left; k < right+1; k++) { // Fill ordered elements back to origin sequence if (LSubsequence[i] <= RSubsequence[j]) { data[k] = LSubsequence[i]; i++; } else { data[k] = RSubsequence[j]; j++; } } return; } void MergeSort(vector<double>& data, int left, int right) { if (left < right) { int mid = (left + right) / 2; MergeSort(data, left, mid); MergeSort(data, mid+1, right); Merge(data, left, mid, right); } return; } void MergeSort(vector<double>& data) { MergeSort(data, 0, data.size()-1); return; }
选择排序是一种简单直观的排序算法,算法的思路是通过逐一在数组的未排序区找到其中的最小的元素,将其移动到已排序区中的末尾,一次类推,直至未排序区为空,从而完成排序
选择排序的具体步骤为:
适用场景: 由于是循环地选出待排序数据中的最小或最大值,插入到有序队列中,相对于冒泡排序减少了交换的次数。适用于当数据量不大,且对稳定性没有要求的场景。
#include <vector> using namespace std; void Swap(vector<double>& data, int indexA, int indexB) { double temp = data[indexA]; data[indexA] = data[indexB]; data[indexB] = temp; return; } int FindMinIndex(vector<double>& data, int begin) { double min = data[begin]; int minIndex = begin; while (begin != data.size()) { if (data[begin] <= min) { minIndex = begin; min = data[begin]; } begin ++; } return minIndex; } void SelectionSort(vector<double>& data) { for (int i = 0; i < data.size(); i++) { int minIndex = FindMinIndex(data, i); Swap(data, i, minIndex); } return; }
堆排序是利用了堆(heap)这种数据结构所设计出来的排序算法,因此要理解堆排序,首先就要理解堆。
堆本身是二叉树中的一种。堆具有以下性质:堆中任意一个节点的数值都要比其左右子节点大,这种堆被称作大顶堆(Max-Heap);堆中任意一个节点的数值都要比其左右子节点小,这种堆被称作小顶堆(Min-Heap)。
此外,堆可以通过数组的形式来储存。可发现,对于堆中任意一个节点,若其索引值为 n n n,其左右子节点的索引值分别为 2 n + 1 2n+1 2n+1 与 2 n + 2 2n+2 2n+2,其父节点的索引值为 ⌊ ( n − 1 ) / 2 ⌋ \lfloor (n-1)/2 \rfloor ⌊(n−1)/2⌋,如下图所示:
Max-Heap 50
/ \
45 40
/ \ / \ ------> array: 50 | 45 | 40 | 20 | 25 | 35 | 30 | 10 | 15
20 25 35 30 index: 0 1 2 3 4 5 6 7 8
/ \
10 15
堆排序的具体步骤为:
适用场景: 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。适用于数据量大,
#include <vector> using namespace std; void Swap(vector<double>& data, int indexA, int indexB) { double temp = data[indexA]; data[indexA] = data[indexB]; data[indexB] = temp; return; } /* Maintain Heap Properties */ void MaxHeapify(vector<double>& data, int node, int heapSize) { int lChild = 2*node + 1; int rChild = 2*node + 2; int MaxChildIndex = node; if (lChild <= heapSize && data[lChild] > data[MaxChildIndex]) MaxChildIndex = lChild; if (rChild <= heapSize && data[rChild] > data[MaxChildIndex]) MaxChildIndex = rChild; if (MaxChildIndex != node) { Swap(data, node, MaxChildIndex); MaxHeapify(data, MaxChildIndex, heapSize); } return; } void HeapSort(vector<double>& data) { int heapSize = data.size()-1; for (int i = data.size()/2-1; i >= 0; i--) { // Build Max-Heap MaxHeapify(data, i, heapSize); } for (int i = data.size()-1; i > 0; i--) { Swap(data, 0, i); heapSize --; MaxHeapify(data, 0, heapSize); } }
冒泡排序是一种简单的排序算法。它重复地扫描要排序的数组,一次比较两个元素,如果它们的顺序错误就把它们交换过来,直到无需再进行交换,即该数组已经排序完成。由于较小的元素会经由交换慢慢“浮”到数组的前端,因此该算法被称为冒泡排序。
冒泡排序的具体步骤为:
适用场景: 此排序算法适用于数据量量不大,对稳定性有要求,且数据基本有序的情况下。
#include <vector> using namespace std; void Swap(vector<double>& data, int indexA, int indexB) { double temp = data[indexA]; data[indexA] = data[indexB]; data[indexB] = temp; return; } void BubbleSort(vector<double>& data) { int length = data.size(); for (int i = 0; i < length; i++) { for (int j = 0; j < length-i-1; j++) { if (data[j] > data[j+1]) Swap(data, j, j+1); } } return; }
冒泡排序的思路是通过逐一交换元素,从而使数组达到有序。然而逐一的交换需要耗费大量的时间,导致冒泡排序的复杂度达到了 O ( n 2 ) O(n^2) O(n2) 之久。快速排序沿用了冒泡排序交换元素的思想,同时能够极大的优化其复杂度,因此快速排序在计算机领域中有着广泛的运用。
快速排序的具体步骤是:
通过与 “标杆” 进行比较的排序方式存在一个隐患,那即是若标杆的数值很极端,比如恰好等于整个数组中的最大值,或最小值。那么指针扫描完整个数组也无法完成进行一次交换,导致算力被浪费。在最坏的情况下为数组以倒序排列,这种情况下快速排序需要重复地扫描整个数组,因此时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
针对这种情况的一种优化方式是 “三数取中法(Mid-3)",即先用数组初始位置的元素,中间位置的元素,以及末尾位置的元素作比较,取三者的中间值者作为标杆,再进行快速排序。
适用场景: 此排序算法的效率在序列越乱的时候,效率越高。在数据有序时,会退化成冒泡排序。
优化方法: 当待排序序列的长度分割到一定大小后,使用插入排序;或是在一次分割结束后,可以把与pivot相等的元素聚集在一起,下一次分割时,不必再对与pivot相等的元素分割。
额外应用: 求数组中第k小的数。将数组中某一个元素m作为划分依据。若m前面的元素个数大于k,则第k小的数一定在m前面的元素中,这时我们只需要继续在m前面的元素中找第k小的数;若m前面的元素小于k,则第k小的数一定在m后面的元素中,这时我们只需要在m后面的元素中找第k小的数;
#include <vector> using namespace std; void Swap(vector<double>& data, int indexA, int indexB) { double temp = data[indexA]; data[indexA] = data[indexB]; data[indexB] = temp; return; } void Mid3(vector<double>& data, int begin, int end) { int mid = (begin + end) / 2; if (data[begin] > data[mid]) // Compare the first and second element Swap(data, begin, mid); if (data[mid] > data[end]) // Compare the second and third element Swap(data, mid, end); if (data[begin] <= data[mid]) // Compare the first and third element Swap(data, begin, mid); return; } void QuickSort(vector<double>& data, int begin, int end) { if (begin == end) // Directly return if there are only one element return; if (end - begin + 1 >= 3) // Apply mid-3 strategy if sequence size >= 3 Mid3(data, begin, end); int pivot = data[begin]; int lpointer = begin; int rpointer = end; while(lpointer != rpointer) { while (rpointer > lpointer && data[rpointer] > pivot) rpointer --; while (lpointer < rpointer && data[lpointer] <= pivot) lpointer ++; Swap(data, lpointer, rpointer); } Swap(data, begin, lpointer); if (begin < lpointer) QuickSort(data, begin, lpointer-1); if (end > lpointer) QuickSort(data, lpointer+1, end); return; } void QuickSort(vector<double>& data) { QuickSort(data, 0, data.size()-1); return; }
计数排序在1954年由 Harold H. Seward 发明,算法采取了空间换时间的策略,核心思路是通过额外的内存空间记录数组元素的信息,并利用这些信息对数组进行调整,从而使数组达到有序状态。
计数排序的具体步骤是:
原始 index: 0 1 2 3 4 5 6 7 8 9
数组 value: 0 2 5 3 7 9 10 3 7 6
-------------------------------------------------------------
计数 index: 0 1 2 3 4 5 6 7 8 9 10
数组 value: 0 0 0 0 0 0 0 0 0 0 0
-------------------------------------------------------------
完成 index: 0 1 2 3 4 5 6 7 8 9 10
计数后 value: 1 0 1 2 0 1 1 2 0 1 1
-------------------------------------------------------------
结果 index: 0 1 2 3 4 5 6 7 8 9
数组 value: 0 2 3 3 5 6 7 7 9 10
上述的算法存在一个隐患,若待排序的数组为 [ 105 , 103 , 102 , 104 , 101 ] [105, 103, 102, 104, 101] [105,103,102,104,101],算法会创建出一个大小为 106 106 106 的数组。但实际需要排序的元素只有5个,那么 [ 0 , 100 ] [0, 100] [0,100] 的内存空间就都浪费了。
针对这种情况,一种优化策略是除了找到数组的最大值以外,还要找到数组的最小值。令 c o u n t count count 数组的大小为 m a x − m i n + 1 max-min+1 max−min+1。那么初始数组的元素值与 c o u n t count count 数组中的索引值的对应关系为: i n d e x c o u n t = v a l u e d a t a − m i n index_{count} = value_{data}-min indexcount=valuedata−min; c o u n t count count 数组的索引值与 r e s u l t result result 数组的元素值的对应关系为 v a l u e r e s u l t = i n d e x c o u n t + m i n value_{result} = index_{count} + min valueresult=indexcount+min
原始 index: 0 1 2 3 4 5
数组 value: 107 103 107 105 101 102
------------------------------------------------
完成 index: 0 1 2 3 4 5 6
计数后 value: 1 1 1 0 1 0 2
------------------------------------------------
结果 index: 0 1 2 3 4 5
数组 value: 101 102 103 105 107 107
#include <vector> using namespace std; void CountingSort(vector<double>& data) { int max = INT_MIN; int min = INT_MAX; for (auto value : data) { // Finding the maximum and minimum value of the sequence if (value > max) max = value; if (value < min) min = value; } vector<double> count(max-min+1, 0); // Creating counting sequence for (auto value : data) { count[value-min] += 1; } int j = 0; for (int i = 0; i < count.size(); i++) { while (count[i] > 0) { data[j] = i + min; count[i] -= 1; j += 1; } } }
桶排序是计数排序的扩展版本,算法的思路是利用映射函数,将数据分到有限数量的子数组或链表里(即我们所说的桶),每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。最后,依照顺序把非空桶中的元素逐个放回到原序列中,从而使数组达到有序状态。
计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素。因此桶排序需要尽量保证元素经过映射后的分布是均匀的,否则当所有数据集中在同一个桶中时,桶排序就会丧失其复杂度优势。
array: 78 17 39 26 72 94 21 12 23 68
-----
0 | / |
1 | | ---> 12 ---> 17
2 | | ---> 21 ---> 23 ---> 26
bucket: 3 | | ---> 39
4 | / |
5 | / |
6 | | ---> 68
7 | | ---> 72 ---> 78
8 | / |
9 | | ---> 94
-----
#include <vector> using namespace std; void BucketSort(vector<double>& data){ int max = INT_MIN; int min = INT_MAX; for (auto value : data) { // Finding the maximum and minimum value of the sequence if (value > max) max = value; if (value < min) min = value; } int bucketNum = (max - min) / data.size() + 1; // Calculating bucket number vector<double> bucket; vector<vector<double>> bucketMap(bucketNum, bucket); // Initialize buckets for (auto value : data) { int index = (value - min) / data.size(); int j = 0; while (j < bucketMap[index].size()) { // Put elements into buckets and sort the buckets if (bucketMap[index][j] > value) break; j++; } bucketMap[index].insert(bucketMap[index].begin()+j, value); } int j = 0; for (int m = 0; m < bucketNum; m++) { // Extract data from buckets to form ordered sequence for (int n = 0; n < bucketMap[m].size(); n++) { data[j] = bucketMap[m][n]; j++; } } }
Thomas, H., Charles, E., Ronald, L., Clifford, S. (2009). Introduction to Algorithms (3rd Edition)
一像素. (2017). 十大经典排序算法. Retrieved from https://www.cnblogs.com/onepixel/articles/7674659.html
原创博客,不得转载、抄袭
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。